Edited at

Reading Vim (Vimのソースコードを読んでみよう)

More than 1 year has passed since last update.

こんにちは、Vimのpluginひとつも書いたことない初心者です。

この記事はQiita Vim (その2) Advent Calendar 2016 6日目、Vimのソースコードを読もうという記事です。

Vimにもっと近づきたいという思いで書きました(適当)。

オープンソースのソースを読むとかは抵抗があるなんていう人に読んでもらえると嬉しいです。


課題

みなさんは<ESC>はなにで入力していますか?私は<C-j>を使っています。

そうです、マッピングしてるのです。

よく使われているマッピングのような気がしますが、ひとつ問題があるのです。

ノーマルモード時に:押した時のあの状態(コマンドラインモードというらしい)から<ESC>を押すとキャンセルしてノーマルモードに戻りますが、<C-j>を押すとコマンドを実行してしまうのです。

調べてみると、マッピングはちゃんとできているようですし、もともとのマッピングを消そうとunmapしてもダメみたいです。

<C-j>は完全な<ESC>にはなれないんでしょうか?

それではこれを題材にVimのソースを私がはじめて見てみていった様子を心の声と共に紹介しましょう。

ソースを初めて読むというのがテーマなのでまとまりよりも時系列を優先して日記のように書きました。

(今回はバージョンはあまり関係なさそうなのでソースはわかりやすくv8.0.0000のものにしました。)



Vimのソースコードをとってくる

VimのソースはGithubに上がっているみたい。

% ls

appveyor.yml farsi/ nsis/ README.md src/ vimtutor.bat
configure* Filelist pixmaps/ README.txt tools/ vimtutor.com*
CONTRIBUTING.md Makefile READMEdir/ runtime/ uninstal.txt

ふむ。

取っ掛かりがほしいので今わかっている情報といえばコマンドラインというモードで起こるってこと。


情報収集

とりあえずsrcに移動して、おもむろにgrep。

% cd src

% grep -r command-line
screen.c: * 1. Add the cmdwin_type for the command-line window
screen.c: * 1. Add the cmdwin_type for the command-line window
screen.c: /* account for first command-line character in rightleft mode */
INSTALLpc.txt:This contains the command-line tools (compiler, linker, CRT headers,
normal.c:#define NV_NCW 0x200 /* not allowed in command-line window */
normal.c: * "q:", "q/", "q?": edit command-line in command-line window.
gui_gtk_x11.c: * Parse the GUI related command-line arguments. Any arguments used are
gui_w32.c: * Parse the GUI related command-line arguments. Any arguments used are
gui_x11.c: * Parse the GUI related command-line arguments. Any arguments used are
main.c: * Also used to handle commands in the command-line window, until the window
main.c: int cmdwin, /* TRUE when working in the command-line window */
ui.c: * command-line and in the cmdline window.
ui.c: /* A click outside the command-line window: Use modeless
globals.h:EXTERN char_u e_cmdwin[] INIT(= N_("E11: Invalid in command-line window; <CR> executes, CTRL-C quits"));
term.c: * command-line, and for building the "Ambiguous mapping..." error message.
term.c: int expmap) /* TRUE when expanding mappings on command-line */
buffer.c: /* don't read in if there are files on the command-line or if writing: */
keymap.h: , KE_CMDWIN /* open command-line window from Command-line Mode */
ex_getln.c: * Used by CTRL-R command in command-line mode.
ex_getln.c: /* only expansion for ':', '>' and '=' command-lines */
ex_getln.c: * Get the current command-line type.
ex_getln.c: /* Create a window for the command-line buffer. */
ex_getln.c: /* Create the command-line buffer empty. */
ex_getln.c: /* Replace the empty last line with the current command-line and put the
ex_getln.c: /* Avoid command-line window first character being concealed. */
po/fi.po:msgid "E11: Invalid in command-line window; <CR> executes, CTRL-C quits"
po/zh_CN.po:msgid "E11: Invalid in command-line window; <CR> executes, CTRL-C quits"
[...]

poファイルは多分翻訳とかなので実質出てきたのは数十行。

cmdlineという表記も見つかったぞ

% grep -r cmdline | wc -l

836

800行か、むー

% grep -r "command line" | grep -v po/ | wc -l 

263

ふむ。

このぐらいの量なら目grepで。

この中で気になるとこいくつか発見。

% grep -r "command line" | grep -v po/

[..]
ex_getln.c: * Get an Ex command line for the ":" command.
[..]
README.txt:parsing of the ":" command line and calls do_one_cmd() for each separate
[..]

てかREADMEのなかにポイこと書いてんな?

COMMAND-LINE MODE

When typing a ":", normal_cmd() will call getcmdline() to obtain a line with
an Ex command. getcmdline() contains a loop that will handle each typed
character. It returns when hitting <CR> or <Esc> or some other character that
ends the command line mode.

getcmdline()の中に入力された文字を受け取るループがあると。これだ。


ソースにダイブ

grepしてみると定義はex_getln.c L164にあるみたい。

Vimで開いてforで検索。

ex_getln.c L364

    /*

* Collect the command string, handling editing keys.
*/

for (;;)
{
redir_off = TRUE; /* Don't redirect the typed command.
Repeated, because a ":redir" inside

あったあった。

    /* Get a character.  Ignore K_IGNORE, it should not do anything, such

* as stop completion. */

do
{
c = safe_vgetc();
} while (c == K_IGNORE);

cに入力された文字が入る感じだろう。

気になるところまで読み飛ばしていく。

ex_getln.c L787

    if (c == '\n' || c == '\r' || c == K_KENTER || (c == ESC

&& (!KeyTyped || vim_strchr(p_cpo, CPO_ESC) != NULL)))
{
/* In Ex mode a backslash escapes a newline. */
if (exmode_active
&& c != ESC
&& ccline.cmdpos == ccline.cmdlen
&& ccline.cmdpos > 0
&& ccline.cmdbuff[ccline.cmdpos - 1] == '\\')
{
if (c == K_KENTER)
c = '\n';
}
else
{
gotesc = FALSE; /* Might have typed ESC previously, don't
truncate the cmdline now. */

if (ccheck_abbr(c + ABBR_OFF))
goto cmdline_changed;
if (!cmd_silent)
{
windgoto(msg_row, 0);
out_flush();
}
break;
}
}

ここでなんか改行とかになってた場合は確定する処理をやってるぽいな。

ex_getln.c L1158

    case ESC:   /* get here if p_wc != ESC or when ESC typed twice */

case Ctrl_C:
/* In exmode it doesn't make sense to return. Except when
* ":normal" runs out of characters. */

if (exmode_active
&& (ex_normal_busy == 0 || typebuf.tb_len > 0))
goto cmdline_not_changed;

gotesc = TRUE; /* will free ccline.cmdbuff after
putting it in history */

goto returncmd; /* back to cmd mode */

コメントにあるp_wcというのは、調べてみると、

% grep -r p_wc

option.h:EXTERN long p_wc; /* 'wildchar' */

option.hに定義されていて、オプションのwildcharを保持している変数ぽい。

vimのオプションはp_って名前でexternされてる変数がいっぱいあるみたい。

wildcharが<ESC>だったらこれ以前に処理されるみたいだけどデフォルトでは<Tab>だね。

なのでここがキャンセルの処理っぽい。

で上のK_KENTERとかESCCtrl_Cは、それぞれgrepで調べてまとめると、

keymap.h:#define K_KENTER       TERMCAP2KEY('K', 'A')   /* keypad Enter */

ascii.h:#define ESC '\033'
ascii.h:#define ESC '\x27'
ascii.h:#define Ctrl_C 3
ascii.h:#define Ctrl_C 0x03

ESCとか2回あるんですけどってascii.hを関係ある部分だけに省略するとこんな感じに。

[..]

/*
* Definitions of various common control characters.
* For EBCDIC we have to use different values.
*/

#ifndef EBCDIC

/* IF_EB(ASCII_constant, EBCDIC_constant) */
[..]
#define ESC '\033'
[..]
#define Ctrl_C 3
[..]
#else

/* EBCDIC */

[..]
#define ESC '\x27'
#define Ctrl_C 0x03
[..]
#endif /* defined EBCDIC */
[..]

まぁなんかEBCDICとかいう文字セット用の定数でまぁどっちにしろ定数。


さらに深みへ

そうすると入力された文字を受け取るc = safe_vgetc();にあったsafe_vgetc()の挙動を調べないとな。

getchar.c L1793

/*

* Like vgetc(), but never return a NUL when called recursively, get a key
* directly from the user (ignoring typeahead).
*/

int
safe_vgetc(void)
{
int c;

c = vgetc();
if (c == NUL)
c = get_keystroke();
return c;
}

はい。vgetc()へ。

getchar.c L1552

/*

* Get the next input character.
* Can return a special key or a multi-byte character.
* Can return NUL when called recursively, use safe_vgetc() if that's not
* wanted.
* This translates escaped K_SPECIAL and CSI bytes to a K_SPECIAL or CSI byte.
* Collects the bytes of a multibyte character into the whole character.
* Returns the modifiers in the global "mod_mask".
*/

int
vgetc(void)
{

気になるところを探しながら眺めてみよう。

    /*

* If a character was put back with vungetc, it was already processed.
* Return it directly.
*/

if (old_char != -1)
{

ふーんvungetcね、まぁ普通はold_charは入ってないんでしょ。

    }

else
{
mod_mask = 0x0;
last_recorded_len = 0;
for (;;) /* this is done twice if there are modifiers */
{
int did_inc = FALSE;

if (mod_mask) /* no mapping after modifier has been read */
{
++no_mapping;
++allow_keys;
did_inc = TRUE; /* mod_mask may change value */
}
c = vgetorpeek(TRUE);

またcに入力を入れてるんだな?

しかしまた関数出てきたな。

        /* a keypad or special function key was not mapped, use it like

* its ASCII equivalent */

switch (c)
{
case K_KPLUS: c = '+'; break;
case K_KMINUS: c = '-'; break;

キーと文字の対応してるとこがあったけど今回関係あるキーはない。

    return c;

}

そのままcが返るみたい。

じゃあcに入ってたvgetorpeek()は?


ついに真髄へ?

getchar.c L1909

/*

* get a character:
* 1. from the stuffbuffer
* This is used for abbreviated commands like "D" -> "d$".
* Also used to redo a command for ".".
* 2. from the typeahead buffer
* Stores text obtained previously but not used yet.
* Also stores the result of mappings.
* Also used for the ":normal" command.
* 3. from the user
* This may do a blocking wait if "advance" is TRUE.
*
* if "advance" is TRUE (vgetc()):
* Really get the character.
* KeyTyped is set to TRUE in the case the user typed the key.
* KeyStuffed is TRUE if the character comes from the stuff buffer.
* if "advance" is FALSE (vpeekc()):
* just look whether there is a character available.
*
* When "no_mapping" is zero, checks for mappings in the current mode.
* Only returns one byte (of a multi-byte character).
* K_SPECIAL and CSI may be escaped, need to get two more bytes then.
*/

static int
vgetorpeek(int advance)
{

うーん長そう、でもここが本質っぽいのでがんばろう。

advanceはバッファーをすすめるかどうかで、呼び出し元ではTRUEになってた。

/*

* get a character: 1. from the stuffbuffer
*/

if (typeahead_char != 0)
{
c = typeahead_char;
if (advance)
typeahead_char = 0;
}
else
c = read_readbuffers(advance);

またcに読み込んでるな?

あ タイポ見つけた(lだけ小文字) getchar.c L2012

         * needed for CTRL-W CTRl-] to open a fold, for example. */

read_readbuffersは…

getchar.c L360

/*

* Get one byte from the read buffers. Use readbuf1 one first, use readbuf2
* if that one is empty.
* If advance == TRUE go to the next char.
* No translation is done K_SPECIAL and CSI are escaped.
*/

static int
read_readbuffers(int advance)
{
int c;

c = read_readbuf(&readbuf1, advance);
if (c == NUL)
c = read_readbuf(&readbuf2, advance);
return c;
}

バッファから読みこむだけのものっぽい

vgetorpeek()に戻って先を読んでいく。

getchar.c L2020

        /*

* Loop until we either find a matching mapped key, or we
* are sure that it is not a mapped key.
* If a mapped key sequence is found we go back to the start to
* try re-mapping.
*/

for (;;)
{

あー、ここでマッピング処理してるんだね!(感動)

noremapじゃないと再起マッピングするからね。

マッピングの途中だった場合(<C-w>押したあととか)はここのループで止まってるんだね、なんか嬉しい。

ちなみに止まってる時に<C-c>とかが来るとgot_int(interruptの意)がTRUEになってキャンセルされるみたい。

この中は結構興味深いところがあって、コメントがいっぱいあるので探索するの面白かった。

超長いけど。

いや、長すぎてよくわからん。


気づき

さらっと全体を眺めたけどあまり手がかりがない…

と、気になるコメントを発見。

getchar.c L2735

            /* When 'insertmode' is set, ESC just beeps in Insert

* mode. Use CTRL-L to make edit() return.
* For the command line only CTRL-C always breaks it.
* For the cmdline window: Alternate between ESC and
* CTRL-C: ESC for most situations and CTRL-C to close the
* cmdline window. */

if (p_im && (State & INSERT))
c = Ctrl_L;
else if ((State & CMDLINE)
#ifdef FEAT_CMDWIN
|| (cmdwin_type > 0 && tc == ESC)
#endif
)
c = Ctrl_C;
else
c = ESC;
#ifdef FEAT_CMDWIN
tc = c;
#endif
break;

ここの処理自体が関係あるかはわからないんだけど、コマンドラインではCTRL-Cだけが常にブレイクするって書いてあるのが引っかかる。

ESCは常にはブレイクしないのか。

ここでもう一回コマンドラインモードでのコマンド確定処理のところを見てみる。

ex_getln.c L787

    if (c == '\n' || c == '\r' || c == K_KENTER || (c == ESC

&& (!KeyTyped || vim_strchr(p_cpo, CPO_ESC) != NULL)))
{

あ、確かにESCでも、なになに?

KeyTypedじゃないかまたはp_cpoCPO_ESCと一緒かどうか見てるのかこれは?

とりあえずKeyTypedを。

grepしてみるとさっきまで見てたgetchar.cでも何回か設定されてるみたい。

改めて見てみると、vgetorpeek()の説明にも書いてあった。

if "advance" is TRUE (vgetc()):

Really get the character.
KeyTyped is set to TRUE in the case the user typed the key.
KeyStuffed is TRUE if the character comes from the stuff buffer.

ふむ、ユーザがそのキーをタイプしたかどうか。

と、つまりまとめると、コマンドラインモードではマッピングでESCになるとKeyTypedがFALSEになり、改行と同じ処理になる。

そういうことか。

ちなみにここでセットされてるみたい。

やっぱりマッピングされてるとFALSEになる感じ。

getchar.c L2369

                  /* When there was a matching mapping and no

* termcode could be replaced after another one,
* use that mapping (loop around). If there was
* no mapping use the character from the
* typeahead buffer right here. */

if (mp == NULL)
{
/*
* get a character: 2. from the typeahead buffer
*/

c = typebuf.tb_buf[typebuf.tb_off] & 255;
if (advance) /* remove chars from tb_buf */
{
cmd_silent = (typebuf.tb_silent > 0);
if (typebuf.tb_maplen > 0)
KeyTyped = FALSE;
else
{
KeyTyped = TRUE;
/* write char to script file(s) */
gotchars(typebuf.tb_buf
+ typebuf.tb_off, 1);
}
KeyNoremap = typebuf.tb_noremap[
typebuf.tb_off];
del_typebuf(1, 0);
}
break; /* got character, break for loop */
}

<C-j>はnewlineの意味があるからそれでコマンドが確定されてるものだと思ってたんだけど。

実験してみた。

cnoremap <C-j> <C-c>

cnoremap i <ESC>

おお!!直接<C-c>にマップすればキャンセルされる!

そして<C-j>以外のキーでも<ESC>にマップすればお構い無くコマンド実行されることがわかった!!

めでたい (まる2日かかったけど)


わかったこと


  • コマンドラインモードでは<ESC>にマッピングされたものはどんなものでも確定処理になる。


  • <C-c>はマッピングの結果であっても、確定する処理には入らない。


  • <ESC>へのマッピングの新常識(私が知らなかっただけか)

noremap  <C-j> <ESC>

inoremap <C-j> <ESC>
cnoremap <C-j> <C-c>



まとめ

はい、おつかれさまでした。

完全に見切り出発だったのでオチまで行くか心配でしたがなんとか綺麗に終わることができたと思います。

Vimに限らないですが、英語さえわかればこのようにソースを読んでいけるぞということが伝わったらいいと思います。

間違っているところもあると思うのでつっこみなどぜひ教えていただければと思います。