CodeReading: EmacsLisp "abbrev"

Kazuki Ohta, 2005/01/28


前回はdefsubr関数を見た。次はどこが良いかなーと思ってソースコードを彷徨っていたのだが、src/abbrev.cが規模的にも流れ的にも丁度良いんじゃないかなという印象を受けた。Emacsのコア部分とは少しはずれるが読んでみようと思う。 Abbrevは予め登録しておいた「省略形」を元に単語を展開する機能である。動的略称展開(dabbrev)と静的略称展開(abbrev)が有るが、src/abbrev.cでは静的な方、つまり自分で省略形を登録する形式の略称展開を扱っている。dabbrevはlisp/dabbrev.elで実装されているので興味が有る方は是非読んで頂きたい。 全部を解説するのもダルいので、今回は略称テーブルの作成(Fmake_abbrev_table)・クリア(Fclear_abbrev_table)・略称の追加(Fdefine_abbrev)・略称の展開(Sexpand_abbrev)を見ていく事にする。

ではmake_abbrev_tableから始める。
DEFUN ("make-abbrev-table", Fmake_abbrev_table, Smake_abbrev_table, 0, 0, 0,
       doc: /* Create a new, empty abbrev table object.  */)
     ()
{
  /* The value 59 is arbitrary chosen prime number.  */
  return Fmake_vector (make_number (59), make_number (0));
}
ただのベクターです。"arbitrary chosen prime number"なのは何故だろう?遊び心かなぁ?とか思いつつ次のclear_abbrev_tableへ。
DEFUN ("clear-abbrev-table", Fclear_abbrev_table, Sclear_abbrev_table, 1, 1, 0,
       doc: /* Undefine all abbrevs in abbrev table TABLE, leaving it empty.  */)
     (table)
     Lisp_Object table;
{
  int i, size;

  CHECK_VECTOR (table);
  size = XVECTOR (table)->size;
  abbrevs_changed = 1;
  for (i = 0; i < size; i++)
    XVECTOR (table)->contents[i] = make_number (0);
  return Qnil;
}
ベクタをmake_number(0)で初期化してますね。まぁこんな所です。次はdefine-abbrev。結構長くなります。
DEFUN ("define-abbrev", Fdefine_abbrev, Sdefine_abbrev, 3, 6, 0,
       doc: /* Define an abbrev in TABLE named NAME, to expand to EXPANSION and call HOOK.
NAME must be a string.
EXPANSION should usually be a string.
To undefine an abbrev, define it with EXPANSION = nil.
If HOOK is non-nil, it should be a function of no arguments;
it is called after EXPANSION is inserted.
If EXPANSION is not a string, the abbrev is a special one,
 which does not expand in the usual way but only runs HOOK.

COUNT, if specified, gives the initial value for the abbrev's
usage-count, which is incremented each time the abbrev is used.
\(The default is zero.)

SYSTEM-FLAG, if non-nil, says that this is a "system" abbreviation
which should not be saved in the user's abbreviation file.  */)
     (table, name, expansion, hook, count, system_flag)
     Lisp_Object table, name, expansion, hook, count, system_flag;
{
  Lisp_Object sym, oexp, ohook, tem;
  CHECK_VECTOR (table);
  CHECK_STRING (name);

  if (NILP (count))
    count = make_number (0);
  else
    CHECK_NUMBER (count);

  sym = Fintern (name, table);

  oexp = SYMBOL_VALUE (sym);
  ohook = XSYMBOL (sym)->function;
  if (!((EQ (oexp, expansion)
	 || (STRINGP (oexp) && STRINGP (expansion)
	     && (tem = Fstring_equal (oexp, expansion), !NILP (tem))))
	&&
	(EQ (ohook, hook)
	 || (tem = Fequal (ohook, hook), !NILP (tem))))
      && NILP (system_flag))
    abbrevs_changed = 1;

  Fset (sym, expansion);
  Ffset (sym, hook);

  if (! NILP (system_flag))
    Fsetplist (sym, list4 (Qcount, count, Qsystem_type, system_flag));
  else
    Fsetplist (sym, count);

  return name;
}
DEFUNを見ると、この関数は3 - 6個の引数を取る。取るべき動作はコメントに詳細に書かれている通りだ。うーん、コメント情報量が多くて良い感じだ。

まずは、countがNILかどうかを見る。countが与えられていない場合はNILとなっているからだ。countはコメントにも有るようにUsage Countという事なので、展開する際にcountによって重み付けでもするのだろう。NILで無い場合はCHECK_NUMBERで数字かどうかチェックしている。

そして、Finternでシンボルをtableからinternする。この辺りが前回の話と絡んでくる。次にシンボルのVALUEと、hookを取り出して来る。その次のif文は多少ややこしいが、(1)oxp と expantion が違う場合 (2)ohookとhookが違う場合 (3)system_flagがNILの場合にabbrebs_changedフラグを1にするというものだ。

そして、expansion・hook・countをそれぞれFset, Ffset, Fsetplistでsymにセットしている。この辺りのシンボルに関するSetter & Getterもいずれ詳しく調べなければなるまい。

さて最後はexpand_abbrevだが、こいつが結構長い。200行ぐらい有る。ただし、マルチバイト処理やバッファー操作等勉強になる部分が有りそうなので、頑張って読む事にする。掲載は小分けにするが、それだと流れが見えにくいので、エディタ等を立ち上げてそちらと見比べながら読んだ方が良いと思われる。
/* Expand the word before point, if it is an abbrev.
  Returns 1 if an expansion is done. */

DEFUN ("expand-abbrev", Fexpand_abbrev, Sexpand_abbrev, 0, 0, "",
       doc: /* Expand the abbrev before point, if there is an abbrev there.
Effective when explicitly called even when `abbrev-mode' is nil.
Returns the abbrev symbol, if expansion took place.  */)
     ()
{
  register char *buffer, *p;
  int wordstart, wordend;
  register int wordstart_byte, wordend_byte, idx, idx_byte;
  int whitecnt;
  int uccount = 0, lccount = 0;
  register Lisp_Object sym;
  Lisp_Object expansion, hook, tem;
  Lisp_Object value;
  int multibyte = ! NILP (current_buffer->enable_multibyte_characters);

  value = Qnil;

  Frun_hooks (1, &Qpre_abbrev_expand_hook);
expand-abbrev関数は0個の引数を取る。カーソルの前の文字をみて、それがabbrevならばabbrevを消去して代わりに展開した文字列を挿入する。遂にバッファ操作が絡んで来る。さて、始める。

current_buffer->enable_multibyte_charactersでマルチバイト処理を行うかどうかのフラグを見ている。最初にQpre_abbrev_expand_hookに登録されたフック関数を実行する。
  wordstart = 0;
  if (!(BUFFERP (Vabbrev_start_location_buffer)
	&& XBUFFER (Vabbrev_start_location_buffer) == current_buffer))
    Vabbrev_start_location = Qnil;
  if (!NILP (Vabbrev_start_location))
    {
      tem = Vabbrev_start_location;
      CHECK_NUMBER_COERCE_MARKER (tem);
      wordstart = XINT (tem);
      Vabbrev_start_location = Qnil;
      if (wordstart < BEGV || wordstart > ZV)
	wordstart = 0;
      if (wordstart && wordstart != ZV)
	{
	  wordstart_byte = CHAR_TO_BYTE (wordstart);
	  if (FETCH_BYTE (wordstart_byte) == '-')
	    del_range (wordstart, wordstart + 1);
	}
    }
  if (!wordstart)
    wordstart = scan_words (PT, -1);

  if (!wordstart)
    return value;
最初のif文では、Vabbrev_start_location_bufferの妥当性を確かめている。current_bufferと一致しなければQnilに設定する。

Vabbrev_start_location_bufferがQnilで無ければ次のif文の中身が実行される。この部分にはBEGV, ZV, PTといった初めて見るシンボルがいくつか出てくるので、先にそれを調べてみる。src/buffer.hでは次のように定義されている。
/* Position of beginning of accessible range of buffer.  */
#define BEGV (current_buffer->begv)
#define BEGV_BYTE (current_buffer->begv_byte)

/* Position of point in buffer.  The "+ 0" makes this
   not an l-value, so you can't assign to it.  Use SET_PT instead.  */
#define PT (current_buffer->pt + 0)
#define PT_BYTE (current_buffer->pt_byte + 0)

/* Position of end of accessible range of buffer.  */
#define ZV (current_buffer->zv)
#define ZV_BYTE (current_buffer->zv_byte)

/* Position of end of buffer.  */
#define Z (current_buffer->text->z)
#define Z_BYTE (current_buffer->text->z_byte)
バッファの属性にアクセスする為のマクロだったという訳だ。PTで使用されている + 0テクニックもなかなか上手い。これはlvalueとして使用出来ないように、つまり代入出来ないようにする為のテクニックだ。

ここを調べておけば結構すらすら読める。まずはstart_locationの値をword_startへと取り出す。もしword_startがBEGV(編集可能領域の開始位置)より小さかったり、ZV(編集可能領域の終端位置)より大きかったりしたらそれは明らかに不正な値なので、0にセットする。

wordstart_byteには文字では無くバイト単位での位置を入れる。おそらくマルチバイト処理の為だ。その次は開始位置の'-'を消去しているが、何の為かはちょっと分からない。

さて、3つ目のif文。word_startが0の場合はscan_wordsでもう一度走査。scan_wordsはsrc/syntax.cで定義されている。第一引数の場所から、第二引数の数だけ単語を探して来るという関数だ。-1という事は、後戻りして1単語探すという操作になっている。scan_wordsしてもまだ0だった場合はvalue(Qnil)を返す。
  wordstart_byte = CHAR_TO_BYTE (wordstart);
  wordend = scan_words (wordstart, 1);
  if (!wordend)
    return value;

  if (wordend > PT)
    wordend = PT;

  wordend_byte = CHAR_TO_BYTE (wordend);
  whitecnt = PT - wordend;
  if (wordend <= wordstart)
    return value;
あれ?またCHAR_TO_BYTEしているな。後で一元管理した方が良くないか?うーん、謎。

さて、戻る。scan_wordsでwordstartから1語分進んだwordendを探す。0だったらvalue(Qnil)を返す。PTより大きかったらPTに戻す。これで、カーソル部分の前の文字の先頭から、カーソル部分 or 1単語の最後地点の値が得られた。whitecntはカーソルとwordendの間にある空白の数を保持する。
  0  1  2  3  4  5  6  7
  a  b  b  r  e  v  
まとめてみる。カーソルが6の位置に有る場合は、(word_start, word_end, whitecnt) = (0, 6, 0)となる。カーソルが3の位置にある場合は(0, 3, 0)、カーソルが7の位置にある場合は(0, 6, 1)となる。

次にwordend <= wordstartをチェックしているが、これはもうちょっと上でやった方が良いんじゃないかな。さて、次に行こう。
  p = buffer = (char *) alloca (wordend_byte - wordstart_byte);

  for (idx = wordstart, idx_byte = wordstart_byte; idx < wordend; )
    {
      register int c;

      if (multibyte)
	{
	  FETCH_CHAR_ADVANCE (c, idx, idx_byte);
	}
      else
	{
	  c = FETCH_BYTE (idx_byte);
	  idx++, idx_byte++;
	}

      if (UPPERCASEP (c))
	c = DOWNCASE (c), uccount++;
      else if (! NOCASEP (c))
	lccount++;
      if (multibyte)
	p += CHAR_STRING (c, p);
      else
	*p++ = c;
    }
まずは、wordstart_byteからwordend_byteまでの文字を入れるだけのバッファーを確保する。次のfor文の中では一文字一文字順番に見ていき、大文字を小文字にしながら確保したバッファへ文字をコピーしている。マルチバイト処理が入っているのもポイント。NOCASEPやUPPERCASEPはsrc/lisp.hで以下のように定義されている。
/* 1 if CH is upper case.  */

#define UPPERCASEP(CH) (DOWNCASE (CH) != (CH))

/* 1 if CH is neither upper nor lower case.  */

#define NOCASEP(CH) (UPCASE1 (CH) == (CH))
上記ループで用いられているuccountとlccountは、それぞれ"大文字の数"、"大文字でも小文字でも無い文字の数"を表している。さて、次。
  if (VECTORP (current_buffer->abbrev_table))
    sym = oblookup (current_buffer->abbrev_table, buffer,
                    wordend - wordstart, p - buffer);
  else
    XSETFASTINT (sym, 0);

  if (INTEGERP (sym) || NILP (SYMBOL_VALUE (sym)))
    sym = oblookup (Vglobal_abbrev_table, buffer,
                    wordend - wordstart, p - buffer);
  if (INTEGERP (sym) || NILP (SYMBOL_VALUE (sym)))
    return value;
この部分が実際にテーブルを走査している部分だ。前回使用したoblookupを用いて、(1)まずはcurrent_buffer->abbrev_tableの走査 (2)無ければVglobal_abbrev_tableの走査という処理になっている。見付からなければvalue (Qnil)を返す。
  if (INTERACTIVE && !EQ (minibuf_window, selected_window))
    {
      /* Add an undo boundary, in case we are doing this for                                                                                  
         a self-inserting command which has avoided making one so far.  */
      SET_PT (wordend);
      Fundo_boundary ();
    }
うーん、undo処理を行っているのか?よー分からん。次。
  Vlast_abbrev_text
    = Fbuffer_substring (make_number (wordstart), make_number (wordend));

  /* Now sym is the abbrev symbol.  */
  Vlast_abbrev = sym;
  value = sym;
  last_abbrev_point = wordstart;

  /* Increment use count.  */
  if (INTEGERP (XSYMBOL (sym)->plist))
    XSETINT (XSYMBOL (sym)->plist,
             XINT (XSYMBOL (sym)->plist) + 1);
  else if (INTEGERP (tem = Fget (sym, Qcount)))
    Fput (sym, Qcount, make_number (XINT (tem) + 1));
さて、この時点ではもうabbrevに対応するsymが見付かっている。まずはVlast_abbrev_textにwordstartからwordendまでを切り出した文字列をセットする。次にVlast_abbrev, valueにもsymをセット。

その次のブロックは良く分からないが、コメント通りなんだろう。plistとかはその内調べよう。
  /* If this abbrev has an expansion, delete the abbrev
     and insert the expansion.  */
  expansion = SYMBOL_VALUE (sym);
  if (STRINGP (expansion))
    {
      SET_PT (wordstart);

      insert_from_string (expansion, 0, 0, SCHARS (expansion),
			  SBYTES (expansion), 1);
      del_range_both (PT, PT_BYTE,
		      wordend + (PT - wordstart),
		      wordend_byte + (PT_BYTE - wordstart_byte),
		      1);

      SET_PT (PT + whitecnt);

      if (uccount && !lccount)
	{
	  /* Abbrev was all caps */
	  /* If expansion is multiple words, normally capitalize each word */
	  /* This used to be if (!... && ... >= ...) Fcapitalize; else Fupcase
	     but Megatest 68000 compiler can't handle that */
	  if (!abbrev_all_caps)
	    if (scan_words (PT, -1) > scan_words (wordstart, 1))
	      {
		Fupcase_initials_region (make_number (wordstart),
					 make_number (PT));
		goto caped;
	      }
	  /* If expansion is one word, or if user says so, upcase it all. */
	  Fupcase_region (make_number (wordstart), make_number (PT));
	caped: ;
	}
      else if (uccount)
	{
	  /* Abbrev included some caps.  Cap first initial of expansion */
	  int pos = wordstart_byte;

	  /* Find the initial.  */
	  while (pos < PT_BYTE
		 && SYNTAX (*BUF_BYTE_ADDRESS (current_buffer, pos)) != Sword)
	    pos++;

	  /* Change just that.  */
	  pos = BYTE_TO_CHAR (pos);
	  Fupcase_initials_region (make_number (pos), make_number (pos + 1));
	}
    }
最初の行でsymから置き換え後の文字列expansionを取得する。ここで文字列かどうかチェック。

まず、SET_PTで現在Pointの位置をwordstartに設定する。そこにinsert_from_stringでexpansionを挿入する。そして、空白の分だけPTを増やし、それをSETする。これで文字列置き換えは完了した。以下はuccountやlccountに関する処理だ。あんまり本質じゃないし、追っても仕方ないのでパス。
  hook = XSYMBOL (sym)->function;
  if (!NILP (hook))
    {
      Lisp_Object expanded, prop;

      /* If the abbrev has a hook function, run it.  */
      expanded = call0 (hook);

      /* In addition, if the hook function is a symbol with
	 a non-nil `no-self-insert' property, let the value it returned
	 specify whether we consider that an expansion took place.  If
	 it returns nil, no expansion has been done.  */

      if (SYMBOLP (hook)
	  && NILP (expanded)
	  && (prop = Fget (hook, intern ("no-self-insert")),
	      !NILP (prop)))
	value = Qnil;
    }

  return value;
次はhookの実行。hookがNILで無ければそのhookをcall0(つまり引数無し)で実行する。その後は、"no-self-insert"というプロパティを処理しているようだ。

最後にvalueを返して終了。

今回はバッファ周りの処理と、少しだけマルチバイト文字の処理が出てきた。結構楽しかったな。


[ return ]