Vincent Chan 的巴士站 🚉

如何实现编辑器文本语法高亮着色

最近正在尝试造一个Markdown编辑器的轮子 MDE 现在算是实现了简单的数据结构,用 chains of lines实现了,支持插入,删除,替换操作。

至于 Model 至视图(View)层面的更新就简单了,只要判断出插入、删除的那几行,去更新 DOM 里面那几行就可以了,也可以说是非常简单。

目前比较棘手的一个问题是给 Syntax Highlighting。Syntax Highlighting 是编辑器很重要的一部分。因为它需要速度非常快,必须在输入瞬间就完成,这样不会给用户发现有卡顿。而且它必须在输入瞬间完成。我想过一种做法就是,先显示没有着色的文本,然后在后台进行着色,然后再更新视图(View)。但是我觉得这样的做法就体验非常差了,用户输入的时候,文本没有被着色,而是等一段时间才有,会让人觉得非常不流畅。所以 Syntax Highlighting 必须再用户输入的瞬间完成。另外,我用很多基于Web的编辑器(Atom, Typora)都觉得不如 Native 的(Sublime Text)之类的来得快,这样就造成了体验不好,所以我写的 Syntax Highlighting 必须要快。

我的编辑器只支持 Markdown 语法,按道理来说 Markdown 语法(包含HTML)是属于有限自动机(Finite-State)语法,也就是说,只要写Lexer(Tokenizer)就好了。

如果这样想的话,就简单了,有限自动机语法直接用正则表达式(Regular Expression)做就好了,如果一行有更新,就对这一行进行重新 Tokenize,然后再更新视图(View),一行来说一不会超过 50 个字,如果是 DFA 的话,速度会很快,基本不用担心速度问题,即使是正则表达式也不会慢多少,但是这就有一个问题,就是换行的问题。

<div
id = "name"></div>

因为 Markdown 语法是兼容 HTML 的,假设,我们有一句 html,它的 tag 在第 n 行,它的 attribute在第 n+1,如果我们只对一行做正则表达式,那么第 n+1 行的 attribute 就无法感知上一行的改变了。另一方面,在上面得例子第二行上面加上<div>这样一行,下面的语法高亮都会有所不同。那是否意味着,我们每一次改变,都要对整个文本进行 tokenize 呢?当然这样肯定是不切实际的,上面说过,语法高亮必须是实时的,这样才能保证好的用户体验,但是 Syntax Highting 依然与上下文有关。这里我们可以采用 CodeMirror 的做法了: 每一行保存一个 state

我们刚才说到,Syntax Highting 需要用到上下文的信息,那么我们可以为每一行保存一个 state。当我们对当前行进行 high lighting 的时候,就可以使用前一行的 state 的。仍然用回上面的例子,第一行里面我们 tokenize 了,进入了一个state 这个 state 告诉我们这个 tag 还没有定义完,例如 state.finishTag = false 当我们第二行修改的时候,就可以利用上一行的 state,得知我们仍在一个tag里面,这样,第二行的 attribute 就可以正确着色了。第二行完成了 tag 之后 state.finishTag = true

仍然用回上面的例子,我们知道,如果某一行的 state 改变了,下面所有的内容都必须进行重新 tokenize,但是我们可以考虑下一下,第 n 行被修改了,那么下文的修改其实不需要实时进行修改,我们可以把这个工作交给后台,用别的 process 或者 thread 进行,完成以后再更新视图,我们只需要保证当前行的着色是实时的就可以了。