怎么去实现一个简单文本编辑器?

泛旅传媒 2023-09-03 10:01 编辑:admin 144阅读

这是一个大坑... 跳坑多年仍然没出来.
The craft of text editing 讲得比较全了, 我再补充一些.

编辑器大致分为源代码编辑器和富文本两种.

源代码编辑器为了省事可以设置相同的行高方便计算, 不过现在多数支持变化行高了可以从富文本编辑器开始做.

首先, 挑一个 GUI 框架

跨平台的 GUI 通常很吸引人, 例如 Fox, FLTK, Tk, GTK+, Qt, WxWidgets 等, 大部分都有一个编辑源代码的控件, 而这个控件基本是 Scintilla 之上的包装 (再研究 Scintilla 你会发现其实各种 GUI 框架的编程模型包装都不需要, 按照 Scintilla 的设计去用就可以了). 学习下来你会发现各个 GUI 框架都自带一个特别的观感: Fox 的光标是个不可改变的巨型铁轨截面, wxWidgets 尽量模仿原生组件 (E-texteditor 就是用 wxWidgets 做的), GTK 就尽量自己画... 其编程模式实质差异并不大, 因为都是 C 和 C++ ... 最惨的是在 Windows 看着还可以, 一放到 Mac 就觉得丑爆了. 做了其他语言的绑定还是感觉在写 C 和 C++. 当然也有做得不一样的:

  • Tcl/Tk 最简洁
  • REBOL view 最 fancy
  • Paul Graham 最推崇 Arc


另一大类 GUI 框架是 XUL. 写个 XML 界面, 然后在 XML 界面上画东西. XHTML+JavaScript 就是一种 XUL 方案. Sun, 很小很柔软, 摸斯拉 等等大公司都推过自家的 XUL 方案. 然而 XML 根本就不适合人类编写, 作为 model 格式也过于巨大不好维护. 最初魔兽世界的插件也是推荐 XML 写界面然后绑定 lua 的动作, 但由于太不灵活也没有一个拖控件的界面, 所以玩家开发了 Ace 系列的 UI lib, 完全不用 XML 纯用 lua 写了. 拖拽式画界面只能骗骗小朋友, PaintCode 也比 XML 解释器性能更好, 所以现在 XUL 基本绝迹, 连直接用 HTML 写界面都不时髦了.

如果不跨平台, 用图形操作系统的 GUI 框架会更能解决很多实际问题, 性能也有保证. Win32API, MFC, ATL, WinForms, WPF, Carbon, Cocoa, CGContext, CoreText ... 就是操作系统商人心狠手辣变幻无常, 一心搞个大新闻还处处夹带私货, 一路学来也是挺累人的. 另一方面嘛 X11 这种更难学, 我就卡在了 motif ...

虽然跨平台的 GUI 框架在慢慢衰亡, 但 OpenGL 这类更接近底层硬件的图形库给人类提供了新的希望. 利用 OpenGL 的成功案例就有 Sublime Text. 我觉得 Cairo, SDL 这种半 GUI 框架的高性能图形库是比较适合的, 就是用的人少了点.

鉴于图形化界面的巨坑... 何不写个纯命令行的编辑器呢? 这时候我们有各种行编辑库可以用: readline, libedit, termcap, Antirez 的轻量 linenoise ... 再用脚本语言的话, 由于内建正则语法和一些字符串处理函数, 很容易在一两万行内写个功能齐全的编辑器解决战斗, 例如 Daikonos.

就算用 C, 如果只实现最简单的功能, 1024 行以内也是可以的: Writing an editor in less than 1000 lines of code, just for fun

纯字符界面缺点也很多, 平滑滚动没有, 动画高亮没有, 文字显示揪细点想调个 kerning 啊 ligature 啊也没办法. 那就自己做一个图形框架? Eclipse 就自掘巨坑组合 C++ 和 Awt 搞出个 SWT. 其实 Awt 和 Swing (NetBeans, IntJ 都是基于 Swing) 处理 Unicode 都有大量的坑, 我都不喜欢... 曾经有个我关注的编辑器 Redcar, 最初用 GTK 编写, 后来转成了 Swing, 然后逐渐就做不动了... jEdit 作者弃编辑器坑, 后来挖了个基于栈的语言新坑 Factor. 后来? 后来也不搞了...

现在 GUI 基本被 ES 的大流统治. 用 Web 做编辑器可以做出一些非常棒的用户体验, 现在浏览器引擎也优化得比几年前好太多. Atom, Monaco Code Editor 都是在 Web 上做的成功案例. 为了容易上手估计 ES 是首选. 缺点是某些细的 UX 不好实现, 正经的优化会花掉更多时间 (例如 Monaco 为了分析性能点连 IR Hydra 都用上了).

介绍两个 Helloworld, GUI 框架 + Scintilla 实现常见一个编辑框
基于 FxRuby 的:

require 'fox16'

include Fox

app = FXApp.new
window = FXMainWindow.new app,
  "My Editor",
  nil, nil, DECOR_ALL, 100, 100, 710, 550

sci = FXScintilla.new window,
  nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y

app.create
window.show
app.run


基于我自己写的 GUI 框架的就更简单了 (谁不年少轻狂造过几个 GUI 框架轮子?)

require 'cici'
app = Cici.app 'scintilla'
c = app.paint [600, 600], Cici::ZoomLayout
c.scintilla [500, 500]
app.message_loop


其实还有各种 GUI 框架的编辑器 hello world 都差不多, 但用框架就是跟着别人走, 很难做出更好的用户体验.

如果从更底层点的地方开始, 例如 Win32API 和 Carbon, 站稳脚跟学习图形界面编程, 前面的道路会... 更狭窄 (公司刚裁了很多桌面程序员并对 Web 产品加大投入...). 不过你理解事件模型的实现和常见优化手段以后, 就算编辑器不成功, 也可以自己写个游戏引擎玩玩嘛.

然后, 挑一个 text storage 数据结构

例如 Cocoa 就自己提供了一个 NSTextStorage, 自己造大约有几个主流选项:

  • Gap buffer: 例子有 Emacs. 很简单的数据结构, 光标前一个 buffer, 光标后一个 buffer. 能极大的减少 buffer 重新分配次数. 扩展一下变成 multi-gap buffer, 多光标编辑也很流畅.
  • Chain of lines: 例子有 TextMate. 每行一个 buffer, 一行不拆散. 对压缩的文件高亮时会比较卡. 但是可以和功能强大的正则引擎 Oniguruma 完美集成.
  • Cell buffer: 例子有 Scintilla. Cell 大小固定, 如果一行超出 Cell 的固定大小, 就分拆成多个 Cell. 用过 Scite 或者 Code::Blocks 或者 Notepad++ 等会发现, 打开大文件, 高亮都还流畅, 因为 Scintilla 的 Cell buffer 和重绘计算的效率很高. 但由于拆行, 只能集成 input driven 的功能较弱的正则引擎或者 lexer, 而这会对实现很多功能带来麻烦.
  • Zipper: Immutable 的数据结构, 如果用 Haskell 做后端会非常适合. 同时还能顺便实现树形历史.


text storage 到界面显示之前, 需要一个排版引擎. 最简单的排版引擎就是把显示区域等分成很多格子, 把等宽字符直接填到格子里 -- 但是这并不好看. 一个文本框的显示得考虑:

  • 如果当前字体包含这个字符不? 是不是从别的字体里找替代?
  • 这些字符组合起来占多宽和多高? 会不会被上面和下面的行挡住? 注意字符的组合并不等于它们的宽度之和, 你要理解 kerning, ligature, baseline 等等 type setting 概念先.
  • 当你排好看以后, 排版的效率往往就不能保证了... 优化也是很难的.


在浏览器里有一套默认的设置, 还有 font-kerning, letter-spacing 等 CSS 属性的帮助, 处理这些问题要简易很多.

text storage (文本模型) + text container (排版引擎) + text view (显示引擎) 是不是就够了呢? 你还得考虑输入法和文字方向... 这一块光看文档是不够的, 自绘的文本输入框, 连很多大厂都没把输入法兼容好... Windows 的 input method editor 和 Cocoa 的 NSTextInput 都能让你抓狂很久.

然后, 考虑一下语言模型...

很多浏览器的文本编辑框里可以用中文词为单位移动光标 (ctrl + 方向键 / opt + 方向键), 这是怎么实现的呢? 有个库叫 ICU, 里面提供了很多边界分析(分字分词分句)用的函数. 浏览器就是用它实现的. ICU 甚至提供了多语言的排版引擎.

如果你要做智能提示和自动完成... 所谓智能提示往往并没有那么智能, 不如参考 vim, 用更依赖用户主动性的设计, 把自动完成的快捷键分为 line complete, dictionary complete, omni complete 等等, 给程序一个更 specific 的指令, 它就能完成得更精准快捷.

但现在的主流是被动性编程, 编辑器/IDE 给你一堆选项, 让你挨个选... 实现这类型的智能提示, 你得写很多代码把语法/类型/先验知识编进去. 而提升智能的方式是, 分析当前的 skip-gram 中最高概率出现的词, 把它排到更优先的位置去 --- 所以先学点计算语言学, 把 word2vec 玩熟吧.

------

除了上述几个大的 design decision 以外, 还有很多很多设计和编码的 Topic 可以讲...

  • 模块和插件机制 -- 正面例子有 Atom 的插件机制, 反面例子有 Osgi...
  • 协程/线程调度后台任务
  • 动态编译和 shader
  • 集成脚本语言 -- 可以方便组合功能和实现插件. 例如 Emacs 有 elisp, Vim 有 vimscript/tcl/perl/python/ruby/mzscheme, Scite 有 lua. 执行脚本时要考虑隔离, 又不影响界面的重绘. 语言特性对编写插件影响巨大, 专门设计一个语言是有很多好处的.
  • ...


当初我只是想用趁手的编辑器写篇博客而已, 好几年过去了, 现在都还在写脚本语言... 所以请慎重跳坑...

———

2020 年补充:现在不写脚本语言了,在写编译器。还要山寨 Revery 。再次忠告请慎重跳坑。

另外,现在对文本编辑器后端,有不少成熟的 buffer 管理项目

  • 如果想做 VSCode 这类的传统编辑器,推荐使用 libvim
  • 如果想做多人协同编辑,推荐选一款 CRDT
  • 如果想做基于 web 的编辑器,可以选择像 Prosemirror 等 web based text editor 为基础
顶一下
(0)
0%
踩一下
(0)
0%