iコマンドしか無くカーソル移動も出来ないのは、仕切り板しかないおせちのようだ。スカスカの某おせち以上に寂しい。 隙間を料理で埋めるがごとく、h j k l コマンドを実装してみることにする。
h j k l コマンドは、カーソルを ← ↓ ↑ → 方向に移動する、vi で最も基本的なコマンドだ。
何故このような割付なのかと言うと、カーソル移動は最も頻繁に使用するコマンドなので、
最も打鍵しやすいキーに割り付けたのだと筆者は考えている。
試してみるとわかるが、Ctrl + E, Ctrl + S, Ctrl + D, Ctrl + X で ↑、←、→、↓ のダイアモンドカーソルや、
Ctrl + B, Ctrl + F, Ctrl + P, Ctrl + N で ← → ↑ ↓ の Emacs 割付よりもはるかに押しやすい。
カーソルキーを素直に使えばいいじゃないかと思う人もいるかもしれないが、
vi が開発された時代のキーボードにはカーソルキーは存在しなかったのだ。
それにカーソルキーを押すには右手をホームポジションからかなり移動しなくてはいけない。
打鍵速度が大幅に低下するので、カーソルキーを押すなど真のvi使いには耐え難いことだ。いわんやマウスなど・・・
vi のほとんどのコマンドはコマンドの前に繰り返し回数を指定できる。
9h と入力すると9文字分左に移動する。9回 h を打鍵するより7回も打鍵数が少なくてすむ。素晴らしい。j k l コマンドも同様だ。
繰り返し回数を自然に指定できるのは挿入モードとコマンドモードが分かれている利点である。モードが無いエディタには無理な芸当だ。
ただ、実際に9hなどとすることはめったにない。そもそも目的地まで何文字あるかなと文字数を数えるのは時間がかかるので本末転倒だ。
カーソルを行内で大きく移動したい場合は、
単語単位(bBeEwW)・行頭行末移動(0^$)・対応括弧移動(%)・行内検索(fFtT;,)などのコマンドを使うのが普通だ。
さて実装だが、ViEditView でキーイベントが発生すると、ViEngine のイベントフィルターがコールされるので、 そこでコマンド解析を行い、h j k l の場合は ViEditView のカーソル移動メソッドを直接コールする、という方針で行く。
カーソル移動メソッドは、とりあえず以下のようにすれば宣言・実装できる。
1: class ViEditView : public QPlainTextEdit 2: { 3: ..... 4: public: 5: void moveCursor(QTextCursor::MoveOperation); 6: ..... 7: };
1: void ViEditView::moveCursor(QTextCursor::MoveOperation mv) 2: { 3: QTextCursor cur = textCursor(); 4: cur.movePosition(mv); 5: setTextCursor(cur); 6: }
QPlainTextEdit には textCursor() というメソッドがあり、QTextCursor オブジェクトを返す。 QTextCursor には movePosition(QTextCursor::MoveOperation) というカーソル移動メソッドがあるので、 それをコールしてカーソル移動し、setTextCursor(const QTextCursor &) メソッドでカーソルを設定することでカーソル移動ができる。 QTextCursor::MoveOperation には文書先頭・末尾移動など様々な種類がある。 詳細はヘルプを参照のこと。
基本的にはこれでいいのだが、movePosition() は vi を考慮していないので、h l で改行位置に移動したり、 前後の行に移動可能だったりしてしまう。 これでは vi 原理主義者の方々には到底納得していただけないので、なんとしても修正しなくてはいけない。 しかし、この修正作業はちょいと面倒そうなので後で行うことにする。
vi コマンド解析・ディスパッチの方は以下のようにでもすればおk。
1: bool ViEngine::cmdModeKeyPressEvent(QKeyEvent *event) 2: { 3: QString text = event->text(); 4: if( text.isEmpty() ) return false; 5: switch( text[0].unicode() ) { 6: case 'i': 7: setMode(INSERT); // i が押されたら挿入モードへ 8: return true; 9: case 'h': 10: m_editor->moveCursor(QTextCursor::Left); 11: return true; 12: case ' ': 13: case 'l': 14: m_editor->moveCursor(QTextCursor::Right); 15: return true; 16: case 'k': 17: m_editor->moveCursor(QTextCursor::Up); 18: return true; 19: case 'j': 20: m_editor->moveCursor(QTextCursor::Down); 21: return true; 22: } 23: return false; 24: }
高度なことは何もしていない。キーイベントの文字を取り出し、switch 文で分岐し、 h j k l だった場合はそれぞれの方向を引数に指定して ViEditView::eCursor(QTextCursor::MoveOperation mv) をコールしているだけだ。
h j k l でカーソル移動できるようになったので、これで少しは vi エディターらしくなったのではないだろうか。
次にコマンドの繰り返し回数をサポートすることとする。
QTextCursor::MoveOperation() は、不幸なことに vi 的な移動オプションは備えていないが、
幸いなことに、引数で繰り返し回数を指定できる。
なので、ViEditView::moveCursor() も引数に繰り返し回数を取ることにする。
1: class ViEditView : public QPlainTextEdit 2: { 3: ..... 4: public: 5: void moveCursor(QTextCursor::MoveOperation, int = 1); 6: ..... 7: };
1: void ViEditView::moveCursor(QTextCursor::MoveOperation mv, int n) 2: { 3: QTextCursor cur = textCursor(); 4: cur.movePosition(mv, QTextCursor::MoveAnchor, n); 5: setTextCursor(cur); 6: }
次に ViEngine の方。繰り返し回数を数値で指定された場合の処理を追加する。 int m_repeatCount は繰り返し回数を保持するメンバ変数で、0 に初期化されるものとする。
ソースコードは以下のようになる。
1: class ViEngine : public QObject 2: { 3: ..... 4: protected: 5: int repeatCount() const { return !m_repeatCount ? 1 : m_repeatCount; } 6: 7: private: 8: int m_repeatCount; 9: };
1: bool ViEngine::cmdModeKeyPressEvent(QKeyEvent *event) 2: { 3: QString text = event->text(); 4: if( text.isEmpty() ) return false; 5: bool rc = true; 6: ushort ch = text[0].unicode(); 7: if( ch == '0' && m_repeatCount != 0 || ch >= '1' && ch <= '9' ) { 8: m_repeatCount = m_repeatCount * 10 + (ch - '0'); 9: return true; 10: } 11: switch( ch ) { 12: case 'i': 13: setMode(INSERT); // i が押されたら挿入モードへ 14: break; 15: case 'h': 16: m_editor->moveCursor(QTextCursor::Left, repeatCount()); 17: break; 18: case ' ': 19: case 'l': 20: m_editor->moveCursor(QTextCursor::Right, repeatCount()); 21: break; 22: case 'k': 23: m_editor->moveCursor(QTextCursor::Up, repeatCount()); 24: break; 25: case 'j': 26: m_editor->moveCursor(QTextCursor::Down, repeatCount()); 27: break; 28: default: 29: rc = false; 30: break; 31: } 32: m_repeatCount = 0; 33: return rc; 34: }
本稿のソースコードは以下からDLできます。※ VS2008 でプロジェクトを作成しています。
http://vivi.dyndns.org/dist2/qvi-004.zip