エディタ調査
エディタ調査
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もたまに参考にする
なお、新しいのは上に足していきます。
エコー領域の調査
そろそろミニバッファを実装するか、という事でまず既存実装の振る舞いを調べて見た所、どうもミニバッファは入力の時だけに出来て、 表示だけのはエコー領域と言って違うっぽい事がemacs lispのマニュアルにかかれていた。
という事で今欲しいのはエコー領域なので、エコー領域を調べる
emacs lispのリファレンスのエコー領域
とりあえずemacs lispのリファレンスを読む所から。
GNU Emacs Lispリファレンスマニュアル: Display
ふむふむ、message関数で表示する、nilを渡すとクリア、か。
xyzzyのエコー領域
まずはdefun messageを検索するとmisc.lで以下が引っかかる。
(defun message (fmt &rest args)
(terpri *status-window*)
(apply #'format *status-window* fmt args)
(terpri *status-window*)
t)
terpriってなんだ?Fterpriで検索するとlprint.ccがそれっぽいか。
lisp
Fterpri (lisp stream)
{
writec_stream (output_stream (stream), '\n');
return Qnil;
}
改行をprintするだけか。
(message "hoge") を実行してみたら、ステータスバーに書かれた。おぉ、emacsとは違って別領域に書かれるのか。 こっちの方が筋が良いとは思うが、Androidで二行使うのはいまいちなのでemacsのように下の行を使う方が良さそうか。
skk-modeの変換周辺を読む
エディタじゃないけどまぁここでいいでしょう。
chrome-skkの移植はなんとなくpreeditが動いているあたりまでは来た。ただsetCompositionとかが空なのでこの辺をどうするか考えるタイミングに来た。
という事でloyaltouch/Skk-Mode: xyzzy-skk-modeのskk.lを読む。
大文字のAとかが何にバインドされているかを調べると、j-set-henkan-pointという関数らしい。
(define-key map #\A 'j-set-henkan-point)
j-set-henkan-pointで変換が始まり、j-henkanとかj-kakuteiで処理される感じか?
j-set-henkan-pointを見ると、
- j-set-henkan-point-subrを呼ぶ(ここで▽をinsertしたり変換開始のpointを覚えたりj-henkan-onをセットしたり)
- j-okuriganaや撥音などを処理
- 必要ならj-henkanを呼び出す
j-henkanを簡単に見ると
- j-change-markerする
- j-henkan-activeをtにする
- j-henkan-show-candidateを呼ぶ
- (delete-region j-henkan-start-point j-henkan-end-point)を呼ぶ
- (insert new-word)する
- kakuteなら(j-kakute new-word)する
ここまでの感じだと、chrome-skkでpreeditと呼んでいるのがだいたいj-set-henkan-pointに対応し、setCompositionやclearCompositionとconversionの処理をしているのがj-henkanっぽい。 chrome-skkの移植として参考にすべきはj-henkanのあたりか。
Rhinocs側で実装する必要がある関数を抜き出そう。
- (point)
- j-kana-start-pointとかj-henkan-start-pointとかを覚えている
- delete-backward-char
- delete-region
まずはこの辺か。
next-lineでカラムの位置を覚えておくメカニズム
行を下に移動していく時に、文字が少ない行を通ってもその次にまた文字が多い行にうつる時には前のカラムに移動していく。 そのためには移動開始時のカラムを覚えておくメカニズムが必要という事だと思う。
一方でこのカラムは例えば水平方向に移動した時などはリセットされないと変な事になる。けれどどこでリセットするべきかはそこまで自明でもない。
その辺のコードを読んでいく。
xyzzyでのgoal-column
next-lineはcmds.lに定義されている。
(defun next-line-1 (n line-mover column-mover)
(let ((goal-column (goal-column))
(moved (funcall line-mover n)))
(cond ((and next-line-add-newlines
(plusp n)
(not (eql moved n)))
(goto-char (point-max))
(insert #\LFD (- n (or moved 0)))
(set-goal-column goal-column))
(moved
(funcall column-mover goal-column)
(set-goal-column goal-column))
(t nil))))
(defun next-line (&optional (n 1))
(interactive "p")
(next-line-1 n #'forward-line #'goto-column))
goto-columnなんてのがあるのか。まぁいいとして、基本的には
- next-line-1の先頭で(goal-column)で取得して
- next-line-1の最後にset-goal-columnというのでこれを保存している
Fset_goal_columnはmove.ccにあり、以下のようにwpのw_goal_columnに保存している。
lisp
Fset_goal_column (lisp goal)
{
long n = fixnum_value (goal);
if (n < 0)
FErange_error (goal);
Window *wp = selected_window ();
if (wp->w_goal_column != n)
{
wp->w_goal_column = n;
wp->w_disp_flags |= Window::WDF_SET_GOAL_COLUMN;
}
wp->w_disp_flags &= ~Window::WDF_GOAL_COLUMN;
return Qt;
}
あとはこれをリセットしている場所をなんとなく検索して調べれば良さそうか。 ざっと見るとやっておいた方が良さそうなのは以下。
- set_buffer
- reframe(ようするにonPaintみたいなもの)でWDF_GOAL_COLUMNフラグが立ってたら
だいたいWDF_GOAL_COLUMNフラグを立てるという事にしてそう。そうか。0がgoalかどうかがわからないのでフラグが必要なのか。 そしてこれはforward_charなどで立てている。ふむふむ。
ちなみにフラグが立ってる時は、wpのpointから計算している。
lisp
Fgoal_column ()
{
Window *wp = selected_window ();
if (wp->w_disp_flags & Window::WDF_GOAL_COLUMN)
{
wp->w_goal_column = (wp->w_bufp->b_fold_columns == Buffer::FOLD_NONE
? wp->w_bufp->point_column (wp->w_point)
: wp->w_bufp->folded_point_column (wp->w_point));
wp->w_disp_flags &= ~Window::WDF_GOAL_COLUMN;
}
return make_fixnum (wp->w_goal_column);
}
lemのnext-line周辺を読む
move.lispに以下のようなコードがある。
(defun next-line-aux (n
point-column-fn
forward-line-fn
move-to-column-fn)
(if (continue-flag :next-line)
(unless (not (null (cursor-saved-column (current-point))))
(log:error "asseriton error: (not (null (cursor-saved-column (current-point))))"))
(setf (cursor-saved-column (current-point))
(funcall point-column-fn (current-point))))
(unless (prog1 (funcall forward-line-fn (current-point) n)
(funcall move-to-column-fn (current-point) (cursor-saved-column (current-point))))
(cond ((plusp n)
(move-to-end-of-buffer)
(error 'end-of-buffer :point (current-point)))
((minusp n)
(move-to-beginning-of-buffer)
(error 'beginning-of-buffer :point (current-point))))))
(define-command (next-line (:advice-classes movable-advice)) (&optional n) (:universal)
"Move the cursor to next line."
(next-line-aux n
#'point-virtual-line-column
#'move-to-next-virtual-line
#'move-to-virtual-line-column))
なんとなく見ると、cursor-saved-columnがそれっぽいか?continue-flagというのがどう立つのかいまいちわからんが。 これがfalseの時だけsetfしているように見えるが。クリアをどうしているか問題を知りたい場合はこれを誰がセットしているかを知る必要があるが、 :next-lineで検索しても引っかからず。うーん、わからん。
なお、sursor-saved-columnはcursors.lispに以下のような定義がある。
(defclass cursor (point)
((saved-column :initform nil
:accessor cursor-saved-column)
...))
バッファの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の作者に質問
- 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側が全部持っている
- マーカーはバッファが持つ