エディタ調査
エディタ調査
Rhinocsを作るにあたり、他のエディタの挙動を調べた時のメモ。
- xyzzy-022/xyzzy: xyzzy 0.2.2 系列。有志により開発が継続中です。 昔結構読んだコードなのでxyzzyを参考にする事多し
- GNU Emacs Lispリファレンスマニュアル: GNU Emacs Lispリファレンスマニュアル emacs lispのwindowとかbufferとかは良く見る
- lem-project/lem: General-purpose editor/IDE with high expansibility in Common Lisp lemもたまに参考にする
バッファのpointとwindowのオフセット
画面にはバッファの一部が描かれる。 バッファの特定の行数の特定の文字から描かれる感じと思うが、半角-全角的な問題があるので単純に文字数とはならない。
この辺をどう扱っているかを見てみる。
コードを全部読んでいくよりも、特定の処理を追っていく事でさしあたって必要な程度の理解を目指す。
行内を右に一文字移動する
一番単純なケースとして、右に一文字移動するが次の行には行かない場合を見てみる。 とりあえずforward-charを見ていく感じか。
xyzzyならFforward_charで検索すると良さそう。これはmove.cc。
lisp
Fforward_char (lisp n)
{
Window *wp = selected_window ();
wp->w_disp_flags |= Window::WDF_GOAL_COLUMN;
return boole (wp->w_bufp->forward_char (wp->w_point,
(!n || n == Qnil) ? 1 : fixnum_value (n)));
}
Bufferのforward_charが呼ばれているが、渡しているのはwpのw_point。 w_pointとかはPoint構造体で以下みたいになっている。
struct Point
{
point_t p_point;
Chunk *p_chunk;
int p_offset;
Char ch () const;
Char &ch ();
Char prevch () const;
};
point_tは単なるlong。p_pointはチャンクのはじめの位置で、p_offsetはチャンク内のオフセットか。
Buffer::forward_charは適当に抜粋すると以下(dが正の方だけ抜き出す)。
int
Buffer::forward_char (Point &point, long ncp) const
{
long d = min (max (point.p_point + ncp, b_contents.p1),
b_contents.p2) - point.p_point;
int f = d == ncp;
Chunk *cp = point.p_chunk;
while (1)
{
int head_cp = count_code_points (cp->c_text, point.p_offset);
int rest_cp = cp->c_nchars - head_cp;
if (d <= rest_cp)
{
int cu = chunk_forward_cp (cp->c_text, cp->c_used,
point.p_offset, int (d));
point.p_offset = cu;
point.p_point += d;
if (point.p_offset == cp->c_used && cp->c_next)
{
cp = cp->c_next;
point.p_offset = 0;
}
point.p_chunk = cp;
break;
}
d -= rest_cp;
point.p_point += rest_cp;
point.p_offset = 0;
cp = cp->c_next;
assert (cp);
}
}
return f;
}
ようするに現在のチャンクの中のオフセットが何文字目かを見て、forwardの範囲が残りのチャンク内ならoffsetをずらすだけ、 そうでなければ目的のチャンクまで進んでそのチャンク内の目的の場所まで進む、とう感じか。
point自身はBufferは持って無くてWindowが持っているように見えるな。
新しくWindowを開く時に既存のpointをBufferから持ってくるのかと思っていたが、Windowから探すのか?まぁいい。
疑問なのはWindowのinvalidate的なのをやっている場所が無さそうな所。スクロールの位置なども何もしていないように見える。 つまりw_pointから毎回再計算しているという事か?
Window.hを眺めると、w_linenumとかw_columnとかを持っているので、これの更新の場所やこれを使った描画の場所を調べてみる。
disp.ccのpending_refreshがw_linenumとかの更新をしているっぽいな。 これはコマンドループで毎回呼ばれるっぽいので、Fforward_charなどを呼んだ後も毎回呼ばれると思って良さそうだ。
更新処理は抜粋して少し整理すると以下みたいになっている。
if (w_point.p_point != w_last_point)
{
w_last_point = w_point.p_point;
if (w_bufp->b_fold_columns == Buffer::FOLD_NONE)
{
w_linenum = w_bufp->point_linenum (w_point);
w_column = w_bufp->point_column (w_point);
}
else
w_linenum = w_bufp->folded_point_linenum_column (w_point, &w_column);
}
if (w_linenum < w_last_top_linenum)
w_last_top_linenum = w_linenum;
else if (w_linenum >= w_last_top_linenum + w_ech.cy)
w_last_top_linenum = w_linenum - w_ech.cy + 1;
if (w_disp_flags & WDF_GOAL_COLUMN)
w_goal_column = w_column;
更新するだけでpaint系の処理はされていないな。paintのフラグは結構複雑だが、この辺は最適化の話なので適当に描かれると思っておこう。 基本的には以下で更新される。
w_linenum = w_bufp->point_linenum (w_point);
w_column = w_bufp->point_column (w_point);
つまりWindowのw_pointを更新しておくと、イベントループで非同期にw_linenumとw_columnを更新する訳だな。
point_linenumはmove.ccにある。
long
Buffer::point_linenum (point_t goal) const
{
long linenum = 1;
point_t point = 0;
const Chunk *cp;
for (cp = b_chunkb; point + cp->c_nchars < goal; cp = cp->c_next)
{
if (cp->c_nlines == -1)
((Chunk *)cp)->c_nlines = cp->count_lines ();
linenum += cp->c_nlines;
point += cp->c_nchars;
}
int remaining_cp = int (goal - point);
int cu_end = chunk_forward_cp (cp->c_text, cp->c_used, 0, remaining_cp);
for (const Char *p = cp->c_text, *pe = p + cu_end; p < pe; p++)
if (*p == '\n')
linenum++;
return linenum;
}
チャンクを先頭からなめているが、cpごとの行数はキャッシュされるんだろうな…とコードを見たらされてないな。毎回全文字を数えるのか。 そのくらいは大した問題では無いって事かね。
pending_refreshに戻って、w_last_top_linenumの更新を見てみる。
if (w_linenum < w_last_top_linenum)
w_last_top_linenum = w_linenum;
else if (w_linenum >= w_last_top_linenum + w_ech.cy)
w_last_top_linenum = w_linenum - w_ech.cy + 1;
次のlinenumが以下の場合は更新。
- 前回表示した一番上より小さい場合
- 前回表示したlinenum+w_ech.cy+1
w_echはWindowの行数と思えばいいかな。 でもw_last_top_linenumを更新しても表示のフラグを何も立てていないな。まぁそこはどうにかしているんだろう。
これだとスクロールしていく時には一行ずつしか進まないし一番端まで行かないと動かない気がするな。まぁその辺はC-nの挙動とC-vで違えばそんなには気にならないか。
とりあえずlinenumはいいとして、columnの方は何もしてないな。 w_top_columnの更新はどうしているのだろう?
disp.ccのWindow::reframeがそれっぽいな。 これはかなり複雑だが、関係ありそうな所を抜き出すと以下みたいな感じか。
column = w_bufp->point_column (w_point);
w_column = column;
if (column < w_top_column)
w_top_column = column / hjump * hjump;
else if (column >= w_top_column + maxwidth)
w_top_column = ((column - maxwidth + hjump) / hjump * hjump);
if (column < w_top_column || column - w_top_column >= maxwidth)
w_top_column = column;
hjunmpはw_hjump_columnsから来ていて、デフォルトは8。8列くらい余裕がある感じに設定するイメージか。
左端なら左端、右端なら画面端ぶんくらい引いた場所に設定している。
ようするに
- Windowがpointを持つ
- pointからcolumnとlinenumを計算し、前回のtop_columnとtop_linenumと比較して画面内なら動かさない、画面外なら再計算した値を基準に適当に設定してpaint
という感じか。
lemの作者に質問
$key
- self-insertとかinsertをどうやって追ったらいいか?
- define-command, insertはinsert-characterでdefunでわかる(basic.lisp)
- self-insert時のキーボード入力の変数
- key-sequenceとかinput.lispにlast-read-key-sequence
- DeleteとかBackspaceとかTabとかReturnの名前など
- key.lispのnamed-key-syms
- 再描画の話
- Bufferの無いWindowはある?
- 無い。
- skk-modeとかvi-modeどうやって作ったか?
- C-x C-s とかの複数キーのキーマップの内部実装
- keymap.lispにあるとか。ハッシュか関数が入っていて、ハッシュだったら〜的に動く。ハッシュだったら一旦保存して次を促し、全部揃ったら送られる感じ。
- switch-bufferする時のpointはどこからとってる?
- bufferにも保存している
- swtich-to-bufferでインターナルは%で始まる方
- バッファが変更されてる時のwindowの振る舞い
- そもそもpointはbuffer側が全部持っている
- マーカーはバッファが持つ