jimx
2021 年1 月 21 日 13:19
1
发现Guile虽然各种库少得一只手就能数得过来但是居然有一个专门用来模拟emacs的功能(keymap, minibuffer等)的包Emacsy (这就是GNU钦定扩展脚本语言吗)。试着在它的基础上加上UI之类的写了一个简单的emacs当作学习Guile和emacs的实现细节
Guile Scheme和Elisp一些不同的地方:
Guile没有buffer-local变量。Emacsy的方案是给buffer加一个local variables的alist然后用(local-var 'name)的形式来访问。个人觉得这种方法比elisp好,因为哪些变量是local的可以一眼看出来,不用像elisp一样到处找make-variable-buffer-local
Elisp的变量和函数有不同的命名空间,Guile共用同一个命名空间(Lisp-1和Lisp-2)。比如elisp可以同时有一个minor-mode的变量记录开关状态和一个同名的函数来打开/关闭这个mode,但Guile就不行
Elisp的string可以有text-property。这个Guile应该也没法做到
Guile自带面向对象系统(goops)写起UI来很方便。比如可以把各种view(minibuffer-view, buffer-view, tree-view)定义成派生类然后分别实现各自的draw方法,不用像emacs那样把所有东西都挤到buffer里
另外觉得emacs buffer的数据结构(gap buffer)虽然操作起来简单但是对UI相当不友好。比如goto-line只能用从头开始re-search-forward找换行符的方法来实现,这样拖动鼠标选文本的时候要不停地根据鼠标的位置计算所在的行号然后用goto-line把point移过去,就会很卡顿。同样的还有line-number-at-pos之类的,在大文件里巨慢
总之个人来说还是挺喜欢Guile的,部分是因为我在用emacs之前就学了racket,另外就是用起来很简单扔到C程序里基本即插即用。不好的地方就是包实在太少了
10 个赞
cireu
2021 年1 月 21 日 13:48
2
用goto-char+forward-line实现,goto-line是为了交互操作定制的。
This function is usually the wrong thing to use in a Lisp program.
What you probably want instead is something like:
(goto-char (point-min))
(forward-line (1- N))
If at all possible, an even better solution is to use char counts
rather than line counts.
jimx
2021 年1 月 21 日 14:06
3
对,是goto-char+forward-line,但是forward-line也是用re-search-forward实现的。不知道emacs有没有做什么优化可以快速完成line和point之间的转换
只在輸入和編輯文字的時候計算 buffer 內容,在只是拖動光標的時候只記下光標位置。另外除了 search forward 也应该支持 search backward。
另外只是用 C 寫个在 char buffer 里查找換行符的 subrountine 其實是很快的,完全不會有影響體驗。
我觉得編輯器,尤其是用 gap buffer 的編輯器的設計典範是 TECO,实现简洁而定制性强大,很多 Emacs 的功能設計也是出自 TECO。
cireu
2021 年1 月 21 日 14:49
5
forward-line是C实现的函数,位于
If N is omitted or nil, move point 1 character backward.
Depending on the bidirectional context, the movement may be to the
right or to the left on the screen. This is in contrast with
\\[left-char], which see. */)
(Lisp_Object n)
{
return move_point (n, 0);
}
DEFUN ("forward-line", Fforward_line, Sforward_line, 0, 1, "^p",
doc: /* Move N lines forward (backward if N is negative).
Precisely, if point is on line I, move to the start of line I + N
\("start of line" in the logical order).
If there isn't room, go as far as possible (no error).
Interactively, N is the numeric prefix argument and defaults to 1.
Returns the count of lines left to move. If moving forward,
that is N minus number of lines moved; if backward, N plus number
moved.
其中一个重要的辅助函数(scan_newline_from_point)位于
If we find COUNT instances. we position after (always after,
even if scanning backwards) the COUNTth match.
If we don't find COUNT instances before reaching the end of the
buffer (or the beginning, if scanning backwards), we position at
the limit we bumped up against.
If ALLOW_QUIT, check for quitting. That's good to do
except in special cases. */
void
scan_newline (ptrdiff_t start, ptrdiff_t start_byte,
ptrdiff_t limit, ptrdiff_t limit_byte,
ptrdiff_t count, bool allow_quit)
{
ptrdiff_t charpos, bytepos, counted;
charpos = find_newline (start, start_byte, limit, limit_byte,
count, &counted, &bytepos, allow_quit);
if (counted != count)
并未看到任何正则相关的内容,实际上该实现是逐个逐个字符查找,使用region cache缓存不含linebreak的区域来加速。
因此你的观点
是不符合现实的。
如果你不局限于gap buffer(Emacsy的buffer允许混入任意内容来做buffer inner content),可以用piece table+缓存linebreak的方案,参见VSCode的buffer实现。可阅读 【译】重新实现 Text Buffer - MacPlay
jimx
2021 年1 月 21 日 15:06
6
学习了,看来这个region cache是关键。只是emacsy有的东西实现的比较粗糙,比如buffer的实现用是guile自己的(ice-9 gap-buffer),唯一给的查找api就是re-search-forward,想当然以为emacs也是用re来做的。emacsy甚至连move-beginning-of-line都是用re-search-backward来实现的,然后re-search-backward就是从头开始re-search-forward直到point前面的最后一个。后面自己改成逐个字符查找也是快了很多
不是 emacsy 实现的糙,它就是对标的 readline,使用場景就是 repl,自然没什么复杂的功能