テキストエディタ実装技術 ■ 行エディタ これまで、テキストエディタを実装するためのクラスについて説明してきたが、それらが実際のエディタでどう使われるのかイメージできない読者も少なくないと思われる。そこで本稿では簡単な行エディタの実装を示してみる。 行エディタは、現在のエディタのようにカーソルキーやスクロールバーでバッファ内自由に移動することは出来ず、テキストの表示・修正が行単位で行われるエディタのことである。1980年頃までは一般的に使われていた。ここでは、以下のような仕様の行エディタ vv を作成する。 ■ 行エディタ vv の仕様 vv を起動すると、コマンド入力を求めるプロンプト(:)が表示される。ユーザは簡単な文字列指定によるコマンドを入力し、[Enter] を押すと入力したコマンドが実行される。たとえば、r test.txt [Enter] と入力すると、"text.txt" というファイルをオープンし、エディットバッファに読み込む。ファイルを読み込んでも、画面には何も表示されない。バッファの内容を画面に表示するには {行番号1}, {行番号2}p [Enter] と入力しなくてはいけない。たとえば 1,10p [Enter] と入力すると1行目から10行目までの内容が表示される。テキストを入力する場合は a [Enter] または i [Enter] と入力し、続けて挿入テキストを入力していく。入力を終了する場合は [Enter] に続けて . [Enter] と入力する。行の削除は {行番号}d [Enter] とする。指定された行番号の行が削除される。{行番号1}, {行番号2}d [Enter] と指定すると行番号1から行番号2までの行が削除される。行内の文字を修正する場合は s/{before}/{after}/ [Enter] とする。文字列 {befor} が {after} に置換される。 このような行エディタは現代的なスクリーンエディタしか使ったことのない人にとってはおそろしく初期学習コストが高く、使い勝手が悪く、生産性が低いように思えるかもしれない。事実その通りなのだが、貧弱な環境でも動作するし、実装も楽である。古き良き時代はこのような行エディタしかなかったのだ。 なお、この vv は Unix の行エディタ ed/ex のサブセットになっている。vv を使いこなせれば、Emacs どころか vi さえ使えず ed/ex しかない環境にであっても大丈夫だ(筆者が昔使っていたシステムはハードウェアリセットを行うと、ed/ex しか使えなかった覚えがある)。 ■ 行エディタの構造 行エディタは、1行のコマンドを受け付け、それを解釈、実行する、という処理を繰り返す。C/C++ で書けば以下のようになる。 ## column for(;;) { 1行入力を受け付ける; 構文解析する; コマンドを実行する; } ## endcolumn コマンド実行は、表示や削除などコマンド固有の処理を行う。構文解析はそのための前処理で、1,10 とか % などの処理範囲を指定する行番号の解析を行う。処理範囲行番号指定はほとんどのコマンドで有効なので、コマンド処理に先立って行うのが合理的である。 ■ 1行入力 1行入力処理は簡単である。入力バッファを char g_buf[1024]; とか宣言し、fgets(g_buf, sizeof(g_buf), stdin) をコールすると良い。gets はバッファ長チェックを行ず危険なので、fgets() を使う方が良い。ただし、fgets は改行コードが最後に付いたままなので、コーディング中に忘れないようにしないといけない。 ■ コマンド構文解析 構文解析を実現するにはいろいろな方法があるが、本稿では構文解析クラス CCmdParser を導入することにする。クラスの宣言は以下のようになる。 ## column class CCmdParser { uchar m_cmd; uchar m_status; int m_crntLine; // カレント行番号( . で参照される) int m_line1; // 処理対象開始行番号、通常は 1..N、ただし 0 はダミー行を表す int m_line2; // 処理対象終了行番号 CEditEngine *m_ee; // エディットエンジンへのポインタ cchar *m_ptr; // パース位置へのポインタ public: CCmdParser(CEditEngine*); ~CCmdParser(); void init(); uchar doParse(cchar*); // コマンドテキストを構文解析し、結果を返す uchar getCmd() const { return m_cmd; }; uchar getStatus() const { return m_status; }; int getLine1() const { return m_line1; }; int getLine2() const { return m_line2; }; void setCrntLine(int cl) { m_crntLine = cl; }; CString getArgText() const; protected: bool noRange() const { return m_line2 == -1; }; bool notSetLine1() const { return m_line1 == -1; }; void skipSpace(); uchar prsCommand(); uchar prsRange(); uchar prsTerm(int&); uchar prsPrimeTerm(int&); }; ## endcolumn