作者|柳干
Any application that can be written in JavaScript, will eventually be written in JavaScript. 任何可以用 JavaScript 来写的应用,最终都将用 JavaScript 来写
Monaco Editor 是一个浏览器端的代码编辑器库,同时它也是 VS Code 所使用的编辑器。Monaco Editor 可以看作是一个编辑器控件,只提供了基础的编辑器与语言相关的接口,可以被用于任何基于 Web 技术构建的项目中,而 VS Code 包含了文件管理、版本控制、插件等功能,是一款桌面软件。Monaco Editor 的 GitHub 仓库中不包含任何实际功能代码,因为其源代码与 VS Code 在同一个仓库,只是在版本发布时会构建出独立的编辑器代码。
考古
大部分人也许会认为 Monaco Editor 是从 VS Code 开源出来的库,也即先有 VS Code ,在此基础上抽离并开源了 Monaco Editor,而实际上正好相反,Monaco 的历史比 VS Code 更早一些。
Monaco Editor 由 Erich Gamma 在苏黎世带领的团队所开发的(Erich Gamma 就是《设计模式》一书的作者之一),关于 Monaco Editor 可以追溯到 2011 年,最早的 Monaco 是被广泛用于微软内部及外部一些 Web 产品的编辑器控件,在这篇2013年的博客 「A rich new JavaScript code editor spreading to several Microsoft web sites」 中介绍了一些在使用 Monaco 的微软产品,包括 Sky Drive、Azure、TypeScript 等站点都有 Monaco 的身影。而更为人所熟知的是早期的 Visual Studio Online 。VS Online 是 2013 年就已经上线运营的产品,界面与较老版本的 VS Code 非常类似,可以说 VS Code 是将 VS Online 搬到了桌面端,而新的 Github Codespaces 又将其搬到了 Web 端。你可以在这篇博客「A look at the new Visual Studio Online "Monaco" code editor」中了解 VS Online 所提供的能力。另外还有一系列的视频教程「Visual Studio Online "Monaco"」详细介绍相关的特性,这时已经可以看出一些设计被一直沿用至今(例如界面布局、版本控制、输出面板、终端等)。
VS Online 当时的代号是 Monaco,在新的 VS Online (2019 年,基于 VS Code Web 版本)短暂的运营了一段时间以后,目前已经改名叫做 GitHub Codes paces。
下图为现在的 GitHub Codespaces
基础部件
Monaco Editor 的核心功能与组件和 VS Code 基本一致,它们基本上都包含下图中这些小部件。
- 行号
- Overlay Widget,可以渲染任意的内容小部件,能选择放置在顶部、底部或编辑器中间。例如编辑器内的查找框即是一个 Overlay Widget
- ViewLine,每一行都表示一个 ViewLine
- Decorations 装饰块,可以指定某个位置的代码块以何种样式呈现,例如修改其背景色、前景色等
- Content Widget,与 Overlay Widget 类似,但可以基于行、列指定其位置。例如自动补全的列表框就是一个 Content Widget
- View Zone, 与 Overlay、Content Widget 不同,它可以插入到特定的行之间将其撑开。例如在上图中88行与89行之间的查找引用窗口
除了这些标配的部件之外,Monaco 还支持通过 Decorations API 来添加更多的装饰,例如版本控制中为修改过的行号添加一个色块,又或者在调试状态下显示断点信息等。
语言支持
Monaco 本身只是一个文本编辑器,对于不同编程语言的基础支持(即高亮)则需要通过注册语言规则的方式来实现,Monaco 默认使用了名为 Monarch 的高亮系统(而 VS Code 使用的则是 Textmate ),Monarch 规定了语言需要包含的关键字、类型、操作符,以及 Tokenizer。Tokenizer 是一组正则表达式,表示以何种规则来识别这些关键字以及括号、注释块,Monarch 将会依照正则表达式的配置来匹配每个 Token,并为之渲染对应的主题色。
// Create your own language definition here // You can safely look at other samples without losing modifications. // Modifications are not saved on browser refresh/close though -- copy often! return { // Set defaultToken to invalid to see what you do not tokenize yet // defaultToken: 'invalid', keywords: [ 'abstract', 'continue', 'for', 'new', 'switch', 'assert', 'goto', 'do', 'if', 'private', 'this', 'break', 'protected', 'throw', 'else', 'public', 'enum', 'return', 'catch', 'try', 'interface', 'static', 'class2', 'finally', 'const', 'super', 'while', 'true', 'false' ], typeKeywords: [ 'boolean', 'double', 'byte', 'int', 'short', 'char', 'void', 'long', 'float' ], operators: [ '=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=', '&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%', '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=', '%=', '<<=', '>>=', '>>>=' ], // we include these common regular expressions symbols: /[=><!~?:&|+\-*\/\^%]+/, // C# style strings escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, // The main tokenizer for our languages tokenizer: { root: [ // identifiers and keywords [/[a-z_$][\w$]*/, { cases: { '@typeKeywords': 'keyword', '@keywords': 'keyword', '@default': 'identifier' } }], [/[A-Z][\w\$]*/, 'type.identifier' ], // to show class names nicely // whitespace { include: '@whitespace' }, // delimiters and operators [/[{}()\[\]]/, '@brackets'], [/[<>](?!@symbols)/, '@brackets'], [/@symbols/, { cases: { '@operators': 'operator', '@default' : '' } } ], // @ annotations. // As an example, we emit a debugging log message on these tokens. // Note: message are supressed during the first load -- change some lines to see them. [/@\s*[a-zA-Z_\$][\w\$]*/, { token: 'annotation', log: 'annotation token: $0' }], // numbers [/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'], [/0[xX][0-9a-fA-F]+/, 'number.hex'], [/\d+/, 'number'], // delimiter: after number because of .\d floats [/[;,.]/, 'delimiter'], // strings [/"([^"\\]|\\.)*$/, 'string.invalid' ], // non-teminated string [/"/, { token: 'string.quote', bracket: '@open', next: '@string' } ], // characters [/'[^\\']'/, 'string'], [/(')(@escapes)(')/, ['string','string.escape','string']], [/'/, 'string.invalid'] ], comment: [ [/[^\/*]+/, 'comment' ], [/\/\*/, 'comment', '@push' ], // nested comment ["\\*/", 'comment', '@pop' ], [/[\/*]/, 'comment' ] ], string: [ [/[^\\"]+/, 'string'], [/@escapes/, 'string.escape'], [/\\./, 'string.escape.invalid'], [/"/, { token: 'string.quote', bracket: '@close', next: '@pop' } ] ], whitespace: [ [/[ \t\r\n]+/, 'white'], [/\/\*/, 'comment', '@comment' ], [/\/\/.*$/, 'comment'], ], }, };
这个例子中注册了类似 C# 风格的语言,在编辑器中的高亮结果则是这样
Monaco 默认的方案与 VS Code 不同,这主要是因为 Textmate 的方案依赖 Oniguruma 这款正则引擎,而在 WebAssembly 大热以后,VS Code 团队推出了 wasm 版本,这使得 Monaco 也可以使用与 VS Code 一致的高亮方案 (同时社区也早有人做了移植,可以参考 monaco-editor-textmate)。
对于高级的语言特性支持,Monaco 也提供了遵循 LSP 标准的 API,例如自动补全、鼠标悬停、查找引用、定义跳转等常见功能都可以通过注册语言服务器来实现,而 Monaco 本身自带了包括 TypeScirpt/JavaScript、CSS、HTML 的语言服务支持。