之前介绍了开发的起因, 目标, 下面将转入实际的设计过程.
初始架构
设计一个可移植的插件引擎, 有一个思路比较好:
预先定义一个抽象的目标环境(IVimHost), 这个环境中包含了一组行为明确的操作接口(即 API).
引擎通过访问这些接口与外界环境交互, 从而避免对某个具体环境的依赖.
LibNVim 的整体架构, 即是按照这个原则组织.
模块划分
程序目前有 2 个模块, 一个是 LibNVim 引擎(LibNVim), 另一个是引擎在 VS 2010 中的宿主(VsNVim, 很可耻的抄袭了一下 VsVim ;).
从道理上来讲, 设计一个跨平台系统, 在开始时就针对 2 个或更多平台同时开发比较好, 这样可以及早纠正设计中的错误, 保证设计的质量.
但是因为精力有限, 现在只能以自己常用的 VS 2010 作为目标模型.
因此必须在设计时必须小心, 尽量保证抽象的通用性, 而不要依赖某些 VS 特有的东西, 机制, 以免之后移植其他平台出现问题.
输入的解析
Vim 接收的键盘输入种类繁多, 规则也比较复杂.
所以, 把输入的解析独立出来, 交给状态机(VimKeyInputEvaluation)去处理, 是个不错的选择.
插入时输入的文本如何重现?
插入模式(ModeInsert)时的文本输入的重现, 即 '.', 如果要精确重现所有键盘动作, 不仅实现起来麻烦, 从概念模型上来说也很复杂: 比如光标跳转(如上下左右)导致编辑区域的变化, 区域内删除操作的记录, 区域边界处删除操作的处理...如此等等
没有必要做一个精确, 但是复杂, 难用的重现功能, 只要为最常见的场景, 提供一个简单的重现规则即可.
所以这里对重现能力做了一个很大的删减, 只记录从起始位置开始的, 连贯的文本输入, 包括三个特殊的编辑操作 Key.Enter, Key.Backspace, Key.Tab, 其他诸如方向键, Key.PageUp, "Ctrl+Home" 操作统统忽略, 因此大大降低了实现的难度.
插入模式时还有些地方值得注意:
1. VS 2010 中, 文本编辑器(TextEditor)有些键盘输入事件无法读取, 比如 Key.Escape, Key.Enter, Key.ArrowUp...
2. 文本输入事件的处理, 用的是 TextInput() 事件, 这个事件, 发生在文本变化之前
ModeInsert 对 VS 2010 这 2 点的依赖, 很可能会导致移植其他平台出现问题, 因此, 实现时, 需要小心避开这些地雷
Span 的引入?
编辑中, 经常会遇到一些区域性的操作, 之前都是提供参数 (from, to), 搞得比较麻烦, 最主要的原因在于, 有些操作会依赖区域的开闭集合属性(比如 "ce" 跟 "cw").
所以后来引入了区间 (Span) 概念, 同时提供了两端的开闭属性(参考 yzis ), 很好的解决了这类问题.