Monaco是个啥?
简单的介绍
简单来说,Monaco Editor 是一款开源的在线代码编辑器。
Monaco Editor 可以看作是一个编辑器控件,只提供了基础的编辑器与语言相关的接口,可以被用于任何基于 Web 技术构建的项目中。
Monaco Editor 之所以广为人知是因为他被用在了VS Code中。
Talk is cheap,show the code
但实际上我们点开它的仓库会发现,这个仓库是空的,只包含把所有内容打包在一起并发布npm的脚本。它的源代码和VS Code在同一个仓库下,只有在版本构建的时候才会构建独立的编辑器代码。
来看一下代码结构吧。monaco editor的源码位于vs/editor层。
├── build # gulp编译构建脚本
├── extensions # 内置插件
├── product.json # App meta信息
├── resources # 平台相关静态资源
├── scripts # 工具脚本,开发/测试
├── src # 源码目录
└── typings # 函数语法补全定义
└── vs
├── base # 通用工具/协议和UI库
│ ├── browser # 基础UI组件,DOM操作
│ ├── common # diff描述,markdown解析器,worker协议,各种工具函数
│ ├── node # Node工具函数
│ ├── parts # IPC协议(Electron、Node),quickopen、tree组件
│ ├── test # base单测用例
│ └── worker # Worker factory和main Worker(运行IDE Core:Monaco)
├── code # VSCode主运行窗口
├── editor # IDE代码编辑器
| ├── browser # 代码编辑器核心
| ├── common # 代码编辑器核心
| ├── contrib # vscode 与独立 IDE共享的代码
| └── standalone # 独立 IDE 独有的代码
├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏)
├── workbench # 工作区UI布局,功能主界面
│ ├── api #
│ ├── browser #
│ ├── common #
│ ├── contrib #
│ ├── electron-browser #
│ ├── services #
│ └── test #
├── css.build.js # 用于插件构建的CSS loader
├── css.js # CSS loader
├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持
├── loader.js # AMD loader(用于异步加载AMD模块)
├── nls.build.js # 用于插件构建的NLS loader
└── nls.js # NLS(National Language Support)多语言loader
Who's your daddy?
从源码仓库的纠葛上看就能发现,Monaco Editor与vscode有着剪不断理还乱的联系。从功能上看,与Monaco Editor相比,VS Code 作为一款桌面软件,包含了文件管理、版本控制、插件等一系列比Monaco Editor更加强大的功能。并且 Monaco Editor 作为从 VS Code 开源出来的库,大部分人应该都会认为是先有 VS Code ,再在此基础上抽离并开源了 Monaco Editor,也就是说vscode看起来应该是monaco的爸爸,但事实却正好相反,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 搬到了桌面端,(VS Online 当时的代号是 Monaco,在新的 VS Online (2019 年,基于 VS Code Web 版本)短暂的运营了一段时间以后,目前已经改名叫做 GitHub Codespaces。)而新的 Github Codespaces 又将其搬到了 Web 端。你可以在这篇博客「A look at the new Visual Studio Online "Monaco" code editor」中了解 VS Online 所提供的能力。另外还有一系列的视频教程「Visual Studio Online "Monaco"」详细介绍相关的特性,这时已经可以看出一些设计被一直沿用至今(例如界面布局、版本控制、输出面板、终端等)。
Monaco的一些基础知识
一些基础部件
- 行号
- Overlay widget。可以渲染任意内容小部件,能选择放置在顶部、底部或编辑器中间。例如编辑器内的查找框即是一个 Overlay Widget
- ViewLine,每一行都表示一个 ViewLine
- Decorations 装饰块,可以指定某个位置的代码块以何种样式呈现,例如修改其背景色、前景色等
- Content Widget,与 Overlay Widget 类似,但可以基于行、列指定其位置。例如自动补全的列表框就是一个 Content Widget
- View Zone, 与 Overlay、Content Widget 不同,它可以插入到特定的行之间将其撑开。
语言支持
Monaco 本身只是一个文本编辑器,对于不同编程语言的基础支持、则需要通过注册语言规则的方式来实现,Monaco 默认使用了 Monarch (而 VS Code 使用的则是 Textmate),Monarch 规定了语言需要包含的关键字、类型、操作符,以及 Tokenizer。Tokenizer 是一组正则表达式,表示以何种规则来识别这些关键字以及括号、注释块,Monarch 将会依照正则表达式的配置来匹配每个 Token,并为之渲染对应的主题色。
Monaco 默认的方案与 VS Code 不同,这主要是因为 Textmate 的方案依赖 Oniguruma 这款正则引擎,而在 WebAssembly 大热以后,VS Code 团队推出了 wasm 版本,这使得 Monaco 也可以使用与 VS Code 一致的高亮方案 (同时社区也早有人做了移植,可以参考 monaco-editor-textmate)。
对于高级的语言特性支持,Monaco 也提供了遵循 LSP 标准的 API,例如自动补全、鼠标悬停、查找引用、定义跳转等常见功能都可以通过注册语言服务器来实现,而 Monaco 本身自带了包括 TypeScirpt/JavaScript、CSS、HTML 的语言服务支持。
一个简单的语法高亮
我们可以先进入到官网的playground。
这是Monaco自带的js解析。
接下来,我们自己实现一个语法高亮。
//注册我们自定义的语言
monaco.languages.register({ id: 'myJavascript' });
//注册自定义语言的解析器
monaco.languages.setMonarchTokensProvider('myJavascript', {
tokenizer: {
root:[
[/\d+/,{token:"keyword"}], // 数字
[/[a-z]+/,{token:"string"}] // 小写字符串
],
}
});
//创建对应的编辑器
const editor = monaco.editor.create(document.getElementById('container'), {
value: ['function x() {', '\tconsole.log("Hello world!",1,2,3);', '}'].join('\n'),
language: 'myJavascript', // 'myJavascript'
})
在上面的栗子中,分别写了两个rule,用来匹配数字和小写字母,匹配成功后就接着执行对应的 action;在 action 中,我们使用的是Monarch中自带的 token class : keyword和string。
以下是一些稍复杂的例子,感兴趣的同学可以看一下,网页下方也有有相关文档介绍。
一个简单的代码补全
主要是通过registerCompletionItemProvider 函数实现一个简单的json自定义代码提示补全功能。
monaco.languages.registerCompletionItemProvider('json', {
provideCompletionItems: function (model, position) { // position表示当前光标的位置
// 获取第一行第一列开始到光标所在行列的text
var textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
});
// 利用text去匹配自定义的部分规则
var match = textUntilPosition.match(/"dependencies"\s*:\s*{\s*("[^"]*"\s*:\s*"[^"]*"\s*,\s*)*([^"]*)?$/);
// 未匹配上,提示内容就为空
if (!match) {
return { suggestions: [] };
}
var word = model.getWordUntilPosition(position);
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
// 如果能匹配上相应的规则,就根据当然的输入值匹配可能的补全项
return {
suggestions: createDependencyProposals(range)
};
}
});
function createDependencyProposals(range) {
// 返回一个静态的proposal列表,此处过滤由monaco editor完成
return [
{
// label, 默认情况下,也是选择此完成时插入的文本。
label: '"lodash"',
// completion item的类别,编辑器根据种类选择图标
kind: monaco.languages.CompletionItemKind.Function,
// 文档注释
documentation: 'The Lodash library exported as Node.js modules.',
// 选择补全时应插入到文档中的字符串或片段
insertText: '"lodash": "*"',
// completion item要被替换的文本范围
range: range
},
{ // snippets
label: 'list2d_basic', // 用户键入list2d_basic的任意前缀即可触发自动补全,选择该项即可触发添加代码片段
kind: monaco.languages.CompletionItemKind.Snippet,
documentation: "2D-list with built-in basic type elements",
insertText: '[[${1:0}]*${3:cols} for _ in range(${2:rows})]', // ${i:j},其中i表示按tab切换的顺序编号,j表示默认串
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range
},
{
label: '"express"',
kind: monaco.languages.CompletionItemKind.Function,
documentation: 'Fast, unopinionated, minimalist web framework',
insertText: '"express": "*"',
range: range
},
{
label: '"mkdirp"',
kind: monaco.languages.CompletionItemKind.Function,
documentation: 'Recursively mkdir, like <code>mkdir -p</code>',
insertText: '"mkdirp": "*"',
range: range
},
{
label: '"mkdhh"',
kind: monaco.languages.CompletionItemKind.Function,
documentation: 'Recursively mkdir, like <code>mkdir -p</code>',
insertText: '"mkdhh": "*"',
range: range
},
{
label: '"my-third-party-library"',
kind: monaco.languages.CompletionItemKind.Function,
documentation: 'Describe your library here',
insertText: '"${1:my-third-party-library}": "${2:1.2.3}"',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range
}
];
}
一些其他的能力
Diff操作
鼠标事件监控
滚动条
Monaco Editor的能力非常丰富,此处不能列全,仅用几个距离,感兴趣的同学可以在官网的playground中查询。
\