刚开始写这个编辑器的时候,我是毫无思路的,就是完全不知道如何下手,后来去翻了一下 CodeMirror, ACEditor, VSCode 这些优秀编辑器的代码,但是我没有全看,因为我要我的编辑器大部分都是我自己想出来的,只有我想不到的时候才去看。 首先我们需要一个数据结构来储存我们的文本,为什么要用数据结构而不直接用 string,是因为编辑器需要大量的增删查改操作,而当一个文本很大的时候,string 是不够快的,因为 string 的增删查改操作的时间复杂度基本上都是 O(n),还不够快。
数据结构 Data structure
一开始我打算用的是 Rope,是因为看上这个数据结构足够快,后来弃用了,是因为它和我的编辑器的 View 部分的构想不太一样,很难融合在一起,另外就是它本身自己也比较难实现,所以我就用了很简单的 chains of lines 来实现了,就是用一个数组,里面存的是每一行的内容。废话不多说先上代码
class TextModel {
protected _lines : LineModel[];
constructor(_string: string) {
// ctor
}
// other methods
}
class LineModel {
protected _number : number;
protected _text : string;
constructor(_num : number, _t : string) {
// ctor
}
// other methods
}
数据结构就是这么简单,就是用数组把每一行的内容都存起来存起来。在这里我用了一个 LineModel 的类来储存,是因为我还要实现一些别的方法,比如最典型的增删操作。当然我们实际上要实现的数据结构的操作不止那么多,至少要把insert, delete, replace这几个操作都实现了才行。这样我们文中所有的字符都可以用一个对象 Position 来表示,就是行+位移
export interface Position {
line : number;
offset : number;
}
而一段文字,也就是我们说的 Range,或者说选区(Selection)则可以用两个Position 来表示:
export interface Range {
begin: Position;
end: Position;
}
想想我们要做编辑器要做的操作
1.插入(Insert): 在文本内的一个位置(Position)插入一段文字(string)
2.删除(Delete): 删除掉一段范围(Range)内的文字
3.替换(Replace): 把一段范围(Range)内的文字替换为一段新的文字(string)
展现 Presentation
下面讲讲如何展现(presentation),就是如何通过 HTML DOM 操作把数据结构里面的数据展现出来,我想大家都已经想到了,一个 LineModel 对应一行,一个父 DOM 包含着每一行的 DOM
看图可知,MDE 里面每一行其实就是一个 <p>,所以其实没什么神秘的东西,不过就是加上了行号,还有 Syntax Highlighting 而已(这个我们后面会说)。怎么把上面提到的 TextModel 编程 HTML DOM 元素了,自己写个遍历器遍历一遍就好了,在这里不推荐自己拼接 HTML 字符然后用 innerHTML 更新,这样一来效率低下,二来需要手动过滤字符,三来不方便我们后续的更新。
遍历一遍 TextModel,然后用 document.createElement 好了,这里你可能需要一个方便的工具类
function elem(elemName : string, className?: string, props?: any) {
let _elm = document.createElement(elemName);
if (className)
_elm.setAttribute("class", className);
if (props && typeof props === "object") {
for (let key in props) {
_elm.setAttribute(key, props[key]);
}
}
return _elm;
}
具体的实现大家可以参考下列几个文件,不过因为已经实现了 Syntax Highlighting,可能现在的版本已经很复杂了,初学的话看可能有点压力
到现在为止,你可能已经知道怎么把 TextModel 里面的内容展现出来了,但是做一个编辑器,仅仅这样还是不够的,下一张讲讲如何更新视图。