読者です 読者をやめる 読者になる 読者になる

Vimにnanoみたいなコマンドチートシートを表示した話

※このブログ記事は、とある工学部の学生実験『大規模ソフトウェアを手探る』の成果発表を兼ねたものです。

※3人のチームメンバーが同時に書き殴ったものなので、文体とか情緒とかが不安定ですがお許し下さい。

※文中に出てくるソースコードの行は多少ズレがあるので参考までにとどめておいてください。

 

 

はじめに

一年と少し前、情報系の学科に内定していた私は、コーディングへのやる気に満ち溢れていました。

でも……

 

VimEmacsも難しくない? 殺害でしょ

 

こうして私はatomしか使えない人間になっていったのでした……

※これは実験班の中の一人だけです。

 

せめて小指に優しいVimだけでも使えるようになりたい…

でもコマンド覚えられない…

Vimもコマンドのカンペを表示してくれればいいのに…

 

そう、

nanoのように

 

f:id:ReruTanizaki:20161206084541p:plain

 

ということで、

 

 

やったこと

OSSであるVimのコードを改変し、画面下部にnanoのようなコマンド一覧(カンペ)を表示させてみた。

(あくまでOSSをいじる実験なので、Vim scriptで作れるのでは????というツッコミは無しで)

完成品はこんな感じ。

 

f:id:ReruTanizaki:20161206084605p:plain

 

モード毎に別のコマンドが表示されるようにしました。

 

f:id:ReruTanizaki:20161206084726p:plain

 

以下、試行錯誤の過程も含め、完成までの道のりを書き連ねていきます。

 

 

ビルドまで

まず、http://www.vim.orgから最新版のVim(2016/10/27現在 Version8.0)をダウンロード。

デバッグシンボルを埋め込むべく、以下のようにconfigureを実行してみる。

 

$ CFLAGS=’-g -O0’ ./configure --prefix=/path/to/install_dir

 

環境変数CFLAGSをセットしてconfigureすると、それがgccのオプションとして渡る、というのは多くのconfigureスクリプトのお約束である。と、僕はこの実験で初めて知りました。

(ちなみに、-gはデバッグシンボル埋め込み、-O0はプログラムの実行順序をソース通りにするための最適化解除。)

 

そして

 

$ make

$ make install

 

した後、ビルドし終わったVimを意気揚々とgdbで立ち上げてみる。

 

cd /path/to/install_dir/bin

gdb ./vim

 

すると…

 

gdb「no debugging symbols found」

 

ダメでした。

 

 

Googleで色々調べてみたところ、Vimのヘルプの中にあるデバッグの手順書(debug.txt)がヒットし、そこに正しいやり方が書いてあった。

 

1. Compile Vim with the "-g" option (there is a line in the src/Makefile for

   this, which you can uncomment).  Also make sure "strip" is disabled (do not

   install it, or use the line "STRIP = /bin/true").

 

どうやらVimのconfigureスクリプトに-gオプションを渡すだけでは駄目で、Makefileの設定も変えないといけない仕様らしい。

そこで、Makefileの該当箇所をuncommentしてみた(赤字)。

シェルスクリプトの知識が皆無だった我々は、/bin/trueが「必ず1を返すプログラム」だと思いこんでいたので、「あれ、逆じゃね???」と10分くらい無駄に悩んでしまいました。)

 

src/Makefile:1133

### Program to run on installed binary. Use the second one to disable strip.        

#STRIP = strip

#STRIP = /bin/true #これを外した

 

するとちゃんとデバッグシンボルが埋め込まれた。やったぜ。

 

OSSを改造する時は、まず開発者向けのヘルプを探して読んでみるべきだった。(まなび)

 

(ちなみに上の手順書debug.txtはVimからは:help debug.txtとか:help debug-vimとかで読めます。)

 

 

手探り開始…と思いきや

ようやくデバッグシンボルを埋め込めたので、

gdbVimを立ち上げ、普通にrunして、nextを連打していくと…

途中で端末の画面全体にVimの画面が描画され、gdbが操作不能になった。

このままでは、起動プロセスより先に進めない。

 

仕方ないので、Vimを起動してからgdbをアタッチすることにする。

 

$ gdb -p <vim’s pid>

 

またはCUIGUIソースコードがある程度共有されていることを利用して、

 

$ gdb ./vim

(gdb) r -f -g

 

とすることで、GUIVimを使って探っても良い。

-gはVimGUIで起動するオプション、-fはGUIデバッグ時に子プロセスを追ってくれるオプションである(GUIVimでは親プロセスは子プロセスを作り、すぐにexitしてしまう)。

詳しくは、ソースファイルと同じディレクトリに入っているsrc/README.txtに書かれている(これも開発者向けの手順書で、各ソースファイルの概観とかも書いてある)。

 

 

目標・方針決め

Vimの内部を探索する準備が整い、楽しくなってきたところだが、こんな巨大なプログラムを行き当たりばったりで手探るのは無謀なので、何を調べねばならないかアタリをつける必要がある。

 

(余談だが、どれくらい巨大なのかちょっと気になったので、数えてみた。

全てのソースファイルとヘッダファイルの行数を合計すると…

 

$ cd src/

$ wc *.c *.h

 (略)

422210 1385196 10559908  total

 

ざっと42万行という結果が出た。この膨大?な量のコードが、Vimの高機能性・マルチプラットフォーム性を実現しているのだろう(多分))

 

さて、追加したい機能は

  • 画面下方に任意の文字列(ここでは、コマンドのカンペ)を表示する
  • モード毎に↑の表示を切り替える

である。

既存の機能で、これに似たような処理を行っているものはないだろうか?

もしあれば、そのコードを見ると改変の手がかりが得られるかもしれない。

 

Vimでは、挿入モードに入った時やビジュアルモード中に、コマンドラインの中にこんな表示が出る。

 

 

f:id:ReruTanizaki:20161206084953p:plain

 

これは、システムがここに何らかの文字列を表示しているに他ならなさそうである。

おまけに「モードを切り替えた瞬間に書いている」と思われるから、とりあえず周辺を引っ掛けてデバッグするのが容易そうだ。

ということで、まずここの振る舞いを理解して、流用できそうか判断することにした。

 

最初から流用狙いで良いのかという気もするが、この実験的にはきちんと手探って流用できそうなものは流用するほうが正しそうなので良しとする。

 

 

どうやって見つけるのか?

INSERTとかVISUALとか、どっかに文字列リテラルで書いてありそうなので、ソースコード全体に対してgrepを行った。

ところが”INSERT”だと数が多すぎるので、”(insert)”(←挿入モードでCtrl-Oを押すと出て来るやつ)で検索してみると

 

$ grep -Irn "(insert)" ./* | grep -v ".po" #.po(言語ファイル)は除く

./edit.c:3716:                    edit_submode = (char_u *)_(" (insert) Scroll (^E/^Y)");

./edit.c:8703:                /* Execute the key in (insert) Select mode. */

./option.c:8087:                clear_cmdline = TRUE;        /* remove "(insert)" */

./screen.c:10159:                    MSG_PUTS_ATTR(_(" (insert)"), attr);

 

この中で一番それっぽい./screen.c:10159付近を見てみると……

 

f:id:ReruTanizaki:20161206085019p:plain

 

showmode()とかいう、直球の名前の関数がありました。

 

なんかモードごとにmessage(と、ソースコードのコメントでは呼ばれている)を出しているので、これは使えそう。

ここにブレイクポイントを立ててデバッグしてみる。

 

これによって得られた有用そうな理解は、以下のごとし。

  • showmode関数はモードごとに条件分岐してメッセージを出せるが、その「現在のモード」はグローバル変数Stateで管理されている。
  • ノーマルモードに変更される(=メッセージが何も表示されない)ときも、呼び出されてはいる。
  • メッセージを書くのは、msg_puts_attr関数である。
  • メッセージが出ている場所は、ソースコード中ではcommand lineとかcmd_lineとか呼ばれている。

 

これだけわかれば、なんか行けそうな気がしてきたぞ(ほんとに?)

 

目標は

このためには、大雑把に言って、

(デフォルトだとコマンドライン1行しかないので、カンペを表示するには狭すぎる。)

 

  • メッセージを出す位置を指定する方法を探す
  • コマンドラインを使うモード(コロン : に続くコマンドを入力している時)でもきちんと表示されているままにする。

などが必要そうである。

 

 

コマンドラインの拡張

さて、コマンドラインにカンペを表示するために、コマンドラインの行数を増やすことが目標の一つとなった。

(コマンドラインの位置自体をずらしてスペースを作る方法も考えたが、色々上手く行かなくて挫折した。)

Vimにはもともとコマンドラインの行数を増やすオプション

        set cmdheight={number}

というものがある。

”cmdheight”がキーワードであろう、ということでgrepをかけてみると

 

$ grep -Irn "cmdheight" ./* | grep -v ".po"

                ・・・

./option.h:413:EXTERN long        p_ch;                /* 'cmdheight' */

        ・・・

 

を発見。どうやらp_chがコマンドラインの高さを示すグローバル変数らしい。

コマンドラインの高さはデフォルトで1なので”p_ch = 1”でgrepしてみる。

 

$ grep -Irn "p_ch = 1" ./*

./option.c:8762:            p_ch = 1;

./window.c:5554:        p_ch = 1;

 

それぞれソースコードを見てみると、

 

option.c:8759

if (p_ch < 1)

{

    errmsg = e_positive;

            p_ch = 1;

}

 

window.c:5553

if (p_ch < 1)

    p_ch = 1;

 

となっている。

これらはp_chが初期化された時に実行されたわけではなく、更新された時に実行され、p_chに0以下の値が代入された場合に有効な値に直す処理をしているようだ。

 

更にoption.c内のp_chが更新された直後のコードを少し見渡していると、

 

option.c:8774

        command_height();

 

とcommand_height関数なる怪しい関数が目に入った。

この関数の定義元へ飛んでみると

 

window.c:5869

/*

 * command_height: called whenever p_ch has been changed

 */

    void

command_height(void)

{

        ・・・

 

おそらくp_chが変更された時の種々の面倒くさいことをやっている関数と思われる。

この関数を頭に入れておいて、ここからコードに変更を加えていくことにした。

 

まずoption.cとwindow.cの”p_ch=1;”を含むif文を抜けた直後に

 

option.c:8759

if (p_ch < 1)

{

    errmsg = e_positive;

            p_ch = 1;

}

#if 1

p_ch+=3;

#endif

 

window.c:5553

if (p_ch < 1)

    p_ch = 1;

#if 1

p_ch+=3;

#endif

 

と”p_ch += 3;”を書き加えた。

その結果、例えば”set cmdheight=2”などを実行した時にコマンドラインの行数が5(=2+3)行となり拡張することができた。

しかし起動時の行数はデフォルトの1行のままであった。

 

そこでgdbでp_chをwatchして初期化されるタイミングを探してみることに。

 

(gdb) watch p_ch

        Hardware watchpoint 1: p_ch

(gdb) r -f -g

                ・・・

        Hardware watchpoint 1: p_chset_option_default (opt_idx=0x2c, opt_flags=0x0, compatible=0x1)

    at option.c:3628

 

ということでoption.c:3628あたりを見てみると

 

option.c:3626

*(long *)varp = (long)(long_i)options[opt_idx].def_val[dvi];

 

うん、明らかに面倒くさそう。

 

ということで、初期化時点でp_ch+=3することは諦め、初期化終了後で画面描画される前にp_ch+=3することに。

具体的にはvim_main2関数内、main_loop関数を呼び出す直前に以下を追加。

 

main.c:874

#if 1

if(p_ch == 1){

    p_ch += 3;

    if (p_ch > Rows - min_rows() + 1)

        p_ch = Rows - min_rows() + 1;

 

    command_height();

}

#endif

 

これで起動時からコマンドラインを3行分拡張することができた。

 

 

 

文字列表示

今までの手探りでコマンドラインに文字列を表示する関数はmsg_puts_attr関数であることが分かった。

他に考えなければいけないことは

  • msg_puts_attr関数はどのようにして文字列を出力する場所を決めているのか
  • コマンド一覧の表示をどのタイミングで実行するか

である。

 

前者に関しては幸いにもshowmode関数をじっくり読んでいくことでmsg_pos_mode関数なる位置合わせ用の関数を発見することができた。

msg_pos_mode関数の定義は以下の通りである。

 

screen.c:10254

/*

 * Position for a mode message.

 */

        static void

msg_pos_mode(void)

{

        msg_col = 0;

        msg_row = Rows - 1;

}

 

ここからmsg_puts_attr関数の出力位置はmsg_colとmsg_rowの二つのグローバル変数で定められていることが推測できる。

 

次にコマンド一覧の表示をどのタイミングで実行するかであるが、基本的にはshowmode関数と同時に(つまり、一番手っ取り早くはshowmode関数の中で)行えば十分なように思われる。

ただしここで問題なのが、ノーマルモードとコマンドラインモード(ノーマルモードでコロンを押した状態)、そしてサーチモード(ノーマルモードでスラッシュまたはハテナを押した状態をここでは便宜上こう呼ぶことにする)の時である。

これらのモードの時はコマンドラインにINSERTやVISUALといったモード表示がなされていないため、showmode関数が呼ばれているのか不明なのだ。

そこでgdbでshowmode関数にブレークポイントを立てて実行してみたところ、

  • ノーマルモードに移行するときはshowmode関数が呼ばれる
  • コマンドラインモード、サーチモードに移行するときはshowmode関数が呼ばれない

ということが分かった。

 

オリジナルではノーマルモードではshowmode関数を呼んでも何も表示せずに終了してしまうので、まずはここで何か文字列を表示させてみることにした。

ここで「現在のモード」がグローバル変数Stateで管理されていることを思い出して、まずはshowmode関数内でノーマルモードの場合の条件分岐を書いてみる。

例えばインサートモードの場合の条件分岐は

 

screen.c:10150

                ・・・

        else if (State & INSERT)

                ・・・

 

などと書かれている。

ここでINSERTの定義元にジャンプすると

 

vim.h:628

#define NORMAL            0x01    /* Normal mode, command expected */

#define VISUAL            0x02    /* Visual mode - use get_real_state() */

#define OP_PENDING    0x04    /* Normal mode, operator is pending - use

                                   get_real_state() */

#define CMDLINE            0x08    /* Editing command line */

#define INSERT            0x10    /* Insert mode */

#define LANGMAP            0x20    /* Language mapping, can be combined with

                                   INSERT and CMDLINE */

 

を見つけることができる。これによりノーマルモードは

        State & NORMAL

で条件分岐できそうだということが分かる(まぁ大体予想通り)。

これらの知見をもとに、試しにshowmode関数を以下のようにしてみる(赤字部分を追加)。

 

screen.c:10048

        do_mode = ((p_smd && msg_silent == 0)

            && ((State & INSERT)

#if 1

        || (State & NORMAL)

#endif

            || restart_edit

            || VIsual_active));

 

screen.c:10156

                ・・・

                    MSG_PUTS_ATTR(_(" INSERT"), attr);

            }

#if 1

                else if (State & NORMAL && !VIsual_active)

        {

                MSG_PUTS_ATTR(_(“NORMAL”), attr);

        }

#endif

            else if (restart_edit == 'I')

                ・・・

 

これでノーマルモードの時にもshowmode関数でモード表示をさせることができた。

 

あとはコマンドラインモードとサーチモードである。

とりあえず愚直にコロンの入力をキャッチしている関数を見つけるために”colon”でgrepをかけてみる。

 

$grep -Irn colon | grep -v “.po”

・・・

normal.c:1964:     op_colon(oap);

normal.c:1988:    op_colon(oap);    /* use external command */

normal.c:2151:op_colon(oparg_T *oap)

normal.c:5292:nv_colon(cmdarg_T *cap)

 

結構たくさん出力されてきたが、最後のnv_colonが怪しいとにらんでgdbでブレイクポイントを立てて実行してみると、確かにコマンドラインモードに入ったときにnv_colon関数が呼ばれることが確認できた。

同じ要領でサーチモードに入ったときにはnv_search関数が呼ばれることも分かった。

 

gdbでこれらの関数内を進んでみると、コマンドラインモードとサーチモードのときはどちらもStateがCMDLINEになっていることが分かる。

よってコマンドラインモードとサーチモードはStateを用いた条件分岐が不可能であることが判明した。

 

以上よりチートシート表示のタイミングは

  • showmode関数内の条件分岐はState以外にも色々やってて再現するのが大変なので、showmode関数が呼ばれるモードではこの関数内で”INSERT”や”VISUAL”を表示させるのと同時にコマンドも表示させる。
  • それ以外のコロンモード、サーチモードではStateによる条件分岐ができないので、それぞれnv_colon関数、nv_serch関数の中で直接コマンドを表示させる。

とすることにした。

またコマンド表示は拡張したコマンドラインの下3行に表示させることにした(最終的な表示内容はは後述)。

 

ただしnv_colon関数とnv_serch関数では呼ばれたときと文字が一文字入力される毎にコマンドラインのカーソル位置(コマンドラインモード時はコマンドラインの最上段にカーソルが移動する)以下を全てクリアする関数が呼ばれているようで、コマンドを表示させてもうまく表示されなかった。

このコマンドラインをクリアする関数は、実は既にshowmode関数の手探りによりmsg_clr_eos_force関数なるもの(msg_clr_cmdline関数内で呼ばれている)を発見していたので、それをもとにmsg_clr_eos関数(中でmsg_clr_eos_forceを呼んでいる)だということが割と容易に判明した。

 

このmsg_clr_eos関数でうまいことコマンドを表示した場所はクリアしない設定にできないか考えたところ、結局新しい関数msg_clr_cmdsを作って、nv_colon関数とnv_search関数内ではmsg_clr_eos関数の代わりにこれを呼び出すことにした。

具体的には以下のような関数を作った。(作ったといってもほとんどmsg_clr_eos関数、msg_clr_eos_force関数のパクリ)

 

message.c:3049

#if 1

/*

 * Clear from current message position to end of cmdline.

 * Note: msg_col is not updated, so we remember the end of the message

 * for msg_check().

 */

        void

msg_clr_cmds_force(void)

{

        if (msg_use_printf())

        {

    if (full_screen)    /* only when termcap codes are valid */

    {

            if (*T_CD)

            out_str(T_CD);    /* clear to end of display */

            else if (*T_CE)

            out_str(T_CE);    /* clear to end of line */

    }

        }

        else

        {

#ifdef FEAT_RIGHTLEFT

    if (cmdmsg_rl)

    {

            screen_fill(msg_row, msg_row + 1, 0, msg_col + 1, ' ', ' ', 0);

    }

    else

#endif

    {

            screen_fill(msg_row, msg_row + 1, msg_col, (int)Columns,

                                                             ' ', ' ', 0);

    }

        }

}

 

/* Clear from current message position to end of cmdline.

 * Skip this when ":silent" was used, no need to clear for redirection.

 */

        void

msg_clr_cmds(void)

{

        if (msg_silent == 0)

    msg_clr_cmds_force();

}

#endif

 

作った関数のプロトタイプ宣言は他の関数のプロトタイプ宣言に従ってsrc/proto/message.pro内で行った。(message.poはproto.hでインクルードされ、proto.hはvim.hでインクルードされている。うーん、ややこしい!)

 

proto/message.pro:58

        ・・・

void msg_clr_eos(void);

void msg_clr_eos_force(void);

#if 1

void msg_clr_cmds(void);

void msg_clr_cmds_force(void);

#endif

void msg_clr_cmdline(void);

int msg_end(void);

        ・・・

 

この関数の呼び出しは以下の3箇所で行う。

 

ex_getln.c:3368(redrawcmd関数内)

#if 1

    msg_clr_cmds();

#else

    msg_clr_eos();

#endif

        ・・・

#if 1

    msg_clr_cmds();

#else

    msg_clr_eos();

#endif

 

ex_getln.c:3457(godocmdlline関数内)

        if (clr)                    /* clear the bottom line(s) */

#if 1

    msg_clr_cmds();

#else

    msg_clr_eos();            /* will reset clear_cmdline */

#endif

 

        windgoto(cmdline_row, 0);

 

こうすることによりコロンモード・サーチモードでも表示させた内容が消えることはなくなった。

 

ここまでで文字列表示は大体完成したが、ただ表示させるだけでは読みにくく感じたので、文字にハイライトを設定してみることにした。

色々遊んでいるうちにmsg_puts_attr関数は第2引数で文字列の色や背景色を変更できることが判明した。

使える色は

 

vim.h:1362

/*

 * Values for index in highlight_attr[].

 * When making changes, also update HL_FLAGS below!  And update the default

 * value of 'highlight' in option.c.

 */

typedef enum

{

        HLF_8 = 0            /* Meta & special keys listed with ":map", text that is

                       displayed different from what it is */

        , HLF_EOB            /* after the last line in the buffer */

        , HLF_AT            /* @ characters at end of screen, characters that

                       don't really exist in the text */

        , HLF_D            /* directories in CTRL-D listing */

                ・・・

 

のように列挙されていることが分かったので

        :source $VIMRUNTIME/syntax/hitest.vim

で見られる色見本と見合わせて、今回は

msg_puts_attr(“hogehoge”, hl_attr(HLF_PSI));

とすることで、まぁまぁそれっぽい表示にすることができた。

 

以上で文字列表示はとりあえず完成。

 

 

表示内容を設定可能にした

さて、ここまでで、キーバインディングソースコード内にハードコーディングして表示することには一応成功した。

 

が、今のままでは使い勝手が悪い。

 

Vimキーバインドは数多く、コマンドラインのスペースは拡張したとはいえ限られているから、覚えたキーバインドと新しいキーバインドを(少なくともソースコードを書き換えてビルドしなおすよりは簡単な手段で)入れ替えられるようにすべきであろう。

初心者がいつまで経っても初心者では意味がないのである。

 

そこで、キーバインドを外部の設定ファイルから読み込めるようにする。

 

例のごとく、似ている機能を探して参考にしよう、という発想で、Vimの標準的な設定ファイルであるvimrcを読み込んでいるプロセスをまず追うことにした。

 

すると、次のような呼ばれ方をしていることがわかった。

 

main() at main.c:415
-> vim_main2() at main.c:439
-> source_startup_scripts() at main.c:2941

 -> do_source() at ex_cmds.c

 -> mch_fopen()  at macros.h  

 

大まかに説明すると、

 

初期化プロセスの中でスクリプトを読み込むsource_startup_scripts()が呼ばれており、

その中で、do_source()という、コマンドを解釈する関数が呼ばれている。

(vimrcの内容がvimのコマンドの羅列であることを考えると当然の結果といえる。)

さらにその中でmch_fopen()という関数が呼ばれていて、これはmacro.hというマクロを定義するヘッダファイルの中でfopen()やopen()に定義されている(プリプロセッサによって条件分岐がなされている)。

 

まあ要するに、vimrcは「読まれている」のではなく、「実行されている」のであった。

 

キーバインドは、それでは困るわけだ。

読んだ内容をどこかに保持しておいて、モードに応じて使用せねばならない。

 

そういうわけなので、ここに関してはかなり独自にコーディングしなければなさそうである。

 

といっても、これは極々基礎的な、ファイルを読み込むだけのソースコードを書けば良かったので、大したことではない。

結局Vimも、最終的にはopen()かfopen()を呼んでる(それはそう)んだし、我々も、Vim内部で定義されてるよくわからん関数を使おうとか考えるのをやめて、素直にfopenを使って書くことにした。

 

以下は取り急ぎ作ったカンペのテキストファイルを読み込む関数である。

 

globals.h:159

#if 1

EXTERN char_u msg_kbd[5][20][60];

#endif

 

main.c:3091

#if 1

/*

 * Read files of keybindings help.

 * normal/commandline/insert/visual/search

 */

    static void

read_keybindings_file(void){

 

  FILE* kbdf;

  char_u* fname[5] = {

    "$HOME/.vimkbd_nml",

    "$HOME/.vimkbd_cmd",

    "$HOME/.vimkbd_ins",

    "$HOME/.vimkbd_vis",

    "$HOME/.vimkbd_sch"

  };

  int i;

  for(i=0; i<5; i++){

    char_u* fname_exp = expand_env_save(fname[i]);

    if((kbdf = fopen(fname_exp,  "r")) != NULL){

      int j = 0;

      while(fgets(msg_kbd[i][j], 60, kbdf)!=NULL && j<20){

        int len = strlen(msg_kbd[i][j])-1;

        if ( msg_kbd[i][j][len] == '\n' )

          msg_kbd[i][j][len] = '\0';

        j++;

      }

      fclose(kbdf);

      vim_free(fname_exp);

    }

    else{

      fprintf(stderr, "Error: there are no %s file.\n", fname[i]);

    }

  }

}

#endif

 

仕様としては以下のような感じである。

  • msg_kbdをカンペの項目を格納するグローバル変数(globals.hで宣言)として、showmode関数でも呼び出せるようにしておく。
  • 下の5つの設定ファイルをホームディレクトリから探してmsg_kbdに読み込む。

~/.vimkbd_nml :ノーマルモードの時に表示するコマンド

~/.vimkbd_vis :ヴィジュアルモードの時に表示するコマンド

~/.vimkbd_ins :挿入モードの時に表示するコマンド

~/.vimkbd_sch :検索モードの時に表示するコマンド

~/.vimkbd_cmd :コマンド入力時の時に表示するコマンド

  • これらの設定ファイルは、1行に1項目で、表示したい項目を列挙している。

 

これを、先程も触れたvimrcとかを読み込んで実行している関数source_startup_scripts()の次に置いてみる。

 

main.c:

vim_main2()

{

    source_startup_scripts(&params);

#if 1

    read_keybindings_file();

#endif

}

 

Ubuntu14.04LTSのGNOME Terminalと、Pantheon Terminalでは問題なく表示されることを確認した。

Macでは標準の端末とiTerm2を試してみたところ、共にモード表示とカンペが重なる現象が見られるが大体動いた。

Windows?知らんな。

 

実のところ、最後に表示を整形する仕事が残っているが、これは案外容易といえる(いえるだけで実際はさにあらずだったが)。

なぜなら、ここまで見てきたように、行と列を表す変数の多くがグローバルなので、何も考えずに使えるからである。

最終的に、showmode関数などには以下のようなメッセージ表示部が出来た。

 

screen.c:10157(一例)

                ・・・

        MSG_PUTS_ATTR(_(" INSERT"), attr);

#if 1

        ++msg_row;

        msg_col = 0;

        int i=0;

        while(msg_row<Rows){

          int k = (msg_row==Rows-1)?2:2;

          while((strlen(msg_kbd[2][i])>0) && (msg_col+strlen(msg_kbd[2][i])+k<Columns)) {

            /*

            msg_puts_attr(msg_kbd[2][i++], attr);

            MSG_PUTS_ATTR(_("; "),attr);

            */

            msg_puts_attr(msg_kbd[2][i++], hl_attr(HLF_PSI));

            MSG_PUTS_ATTR(_("  "), attr);

          }

          ++msg_row;

          msg_col = 0;

        }

        msg_col = Columns-1;

#endif

                ・・・

 

これは挿入モードの時の例であり、ノーマルモード、ビジュアルモードの表示部もそれぞれの条件分岐の中で同様に書けばよい。

 

コロンモード、検索モードに関してはnv_colon関数、nv_serch関数内の適当な位置に、例えば

 

normal.c:5317(nv_colon関数内)

                ・・・

        /* When typing, don't type below an old message */

    if (KeyTyped)

            compute_cmdrow();

 

    old_p_im = p_im;

 

#if 1

  int attr;

  msg_col = 0;

  msg_row = Rows - 3;

  attr = hl_attr(HLF_CM);

  msg_clr_eos();

 

  int i=0;

  while(msg_row<Rows){

        int k = (msg_row==Rows-1)?2:2;

        while((strlen(msg_kbd[1][i])>0) && (msg_col+strlen(msg_kbd[1][i])+k<Columns)) {

          msg_puts_attr(msg_kbd[1][i++], hl_attr(HLF_PSI));

          MSG_PUTS_ATTR(_("  "), attr);

        }

        ++msg_row;

        msg_col = 0;

  }

  msg_col = Columns-1;

#endif

 

    /* get a command line and execute it */

    cmd_result = do_cmdline(NULL, getexline, NULL,

                            cap->oap->op_type != OP_NOP ? DOCMD_KEEPLINE : 0);

                ・・・

 

 

normal.c:6251(nv_search関数内)

        static void

nv_search(cmdarg_T *cap)

{

        oparg_T    *oap = cap->oap;

        pos_T    save_cursor = curwin->w_cursor;

#if 1

          int attr;

          msg_col = 0;

          msg_row = Rows - 3;

          attr = hl_attr(HLF_CM);

          msg_clr_eos();

          int i=0;

          while(msg_row<Rows){

            int k = (msg_row==Rows-1)?15:2;

            while((strlen(msg_kbd[4][i])>0) && (msg_col+strlen(msg_kbd[4][i])+k<Columns)) {        

                      msg_puts_attr(msg_kbd[4][i++], hl_attr(HLF_PSI));

                      MSG_PUTS_ATTR(_("  "), attr);

            }

            ++msg_row;

            msg_col = 0;

          }

          msg_col = Columns-1;

#endif

        if (cap->cmdchar == '?' && cap->oap->op_type == OP_ROT13)

        {

                ・・・

 

などと挿入してやればよい。

 

はい、まぁ、一応これで完成。

 

 

作ってみて

今回、作業のほとんどをVimを通して行い、結局頻出コマンドは結構覚えてしまったので、作成したこの機能は我々には不必要になってしまった……

 

などということは当然なく、

 

現在もガリゴリとチートシート設定ファイルを書き換えて使用している。

Vimはデフォルトの振る舞いがそもそも複雑で、初心者から抜け出すまでにそれなりの時間を要してしまう。(これはemacsもそんなに変わらないだろう。)

使いながら、段階的にキーバインドを覚えていくという点において、vimtutorとは違うものを作れたのではないかと自負しているが、どちらが学習効果が高いか実際のところはわからない。

 

まあ、こんなところで、この記事は終わりである。