基于 Monaco Editor & LSP 打造智能 IDE

6,027 阅读6分钟

1. 背景介绍

1.1 业务背景

基于公司产品的特征——大数据平台,数据开发工程师在使用产品开发的过程中存在很多代码编辑的使用场景。对比现有 codemirror编辑器,存在很多不满足现有产品的业务需求。

  • 编辑器功能:缺少查找缩略图注释快捷键右键菜单等功能。
  • 语言功能:存在关键字提示不完整缺少内置依赖(包)提示缺少token补全缺少代码格式化等问题。

2. 技术方案

2.1 Monaco Editor

Monaco Editor 是微软开源的基于 VS Code 的代码编辑器,运行在浏览器环境中。目前很多浏览器上的 "云编辑器" 都是基于 monaco-editor

Monaco Editor涵盖了编辑器的基础功能,例如代码注释,查找,缩略图,右键菜单等

💡 问题:虽然提供30多种内置语言,但是每一种语言都需要单独去定义语法和代码提示等功能,开发🔧和维护🔨成本高

2.2 LSP(Language Server Protocol)

LSP(Language Server Protocol) 语言服务协议,此协议定义了在编辑器或IDE与语言服务器之间使用的通信规范,该语言服务器提供了例如自动补全,转到定义,查找所有引用等的功能

2.2.1 LSP优势

Language Server Protocol主要就是解决市面上很多编辑器(m)和很多语言(n)共存的问题,编辑器只需要管基于LSP和语言服务器通信即可,把编辑器行为和语言特性用一种通用的方式解耦。

通过LSP,编辑器插件复杂度由原来的 m * n 转换为了 m + n

市面上很多比较常用的编辑器用的都是lsp去实现,例如:VSCodeSublime

2.2.2 LSP实现的功能

Lsp几乎满足了我们对编辑器的所有要求

  • 语法(Syntax)
  • 语法高亮(Syntax Highlighting)
  • 自动格式化(Automatic Formatting)
  • 自动补全(Autocomplete)
  • 工具提示(Tooltips)
  • 内联诊断(Inline Diagnostics)
  • 跳转到定义(Jump to Definition)
  • 项目内查找引用(Find References in Project)
  • 高级文本和符号搜索(Advanced Text and Symbol Search)
  • ……

2.2.3 LSP通信

语言服务器会作为单独的进程运行,同时编辑器使用基于JSON-RPC的语言协议与服务器进行通信

下面是在日常编辑会话期间,编辑器和语言服务器的通信方式的示例

当用户在编辑器中触发某种操作,编辑器会通知语言服务器,并将相应的信息带给语言服务器,服务器接收到请求进行响应。

2.2.4JSON-RPC

JSON-RPC是基于json的跨语言远程调用协议,LSP协议定义了在开发工具和语言服务器之间使用 JSON-RPC 发送的消息的格式。

特点:

  • 文本传输数据小
  • 便于调试扩展

请求格式:

{
   "jsonrpc": 2.0, // 定义JSON-RPC版本
   "id": 1, // 调用标识符
   "method": "textDocument/didOpen",   // 调用的方法名
   "params": {    // 方法传入的参数
     ...
   }
}

响应格式:

{
   "jsonrpc": 2.0, // 定义JSON-RPC版本
   "id": 1, //   // 调用标识符
   "result": "Hello JSON-RPC",   // 方法返回值
   "error": null  // 调用时错误
}

3. 项目实践

3.1 项目方案

基于业务场景,确定使用 Monaco Editor + Language Server Protocol

首先会有一个languageclient的包,可以更方便的让编辑器和语言服务器进行通信,他会做一些初始化的设置,包括把通信数据转成jsonrpc格式的数据结构,因为是运行在web端的编辑器,所以需要借助websocket的服务来作为一个通信的桥梁,每当我们新开一个编辑器,就会建立一个websocket的连接,并告知当前需要的是哪种语言服务器,然后websocket服务和语言服务器通过线程之间数据流的方式进行通信

3.2项目进展

整个项目分为以下四个步骤去进行开发

3.2.1 实现Websocket服务

新建一个语言服务的项目,实现Websocket服务,并且代理转发不同语言服务

langservers:
  python:
    - pyls
  sql:
    - /usr/src/app/simba-language-server/hive-sql/hive-sql
    - -config
    - ./sqlConfig.yml
  shell:
    - bash-language-server
    - start

3.2.2实现语言服务(Python/Shell/SQL...)

虽然lsp官方提供的语言服务可以直接使用,但是每一种语言服务的提供商不同,语言服务的稳定性和质量会受其影响,而且,每一种语言服务实现的语言也不同,实施过程中会有很多问题

SQL为例,我们在lsp官网提供的SQL语言服务的基础上,根据业务需要,进行了相应的源码修改和拓展

  • 修复了原有语言服务的bug
  • SQL代码提示(关键字、函数、表、字段)
  • 丰富了语法解析功能
  • 修改了数据库解析方式
  • 自定义传参

以下是sql语言服务的部分事件类型

3.2.3实现前端编辑器组件

  1. 安装依赖
"monaco-editor": "=0.19.3",
"monaco-languageclient": "file:lib/monaco-languageclient", //定制化需要,修改了源码,存在本地
"monaco-themes": "^0.3.3",
"vscode-json-languageservice": "^3.4.11",
"vscode-jsonrpc": "^5.0.0",
"vscode-languageclient": "file:lib/vscode-languageclient",
"vscode-languageserver": "^6.0.0",
"vscode-languageserver-protocol": "3.15.3",
"vscode-uri": "^2.1.1",
"vscode-ws-jsonrpc": "^0.2.0",
  1. 创建monaco实例
monaco.editor.create(html对象, options)
  1. 设置属性和方法,可参考monaco官网
automaticLayout: true, // 自动布局
readOnly: false, // 是否为只读模式
contextmenu: false, // 上下文菜单
minimap: {
  enabled: false, // 代码缩略图
},
lightbulb: {
   enabled: false, // 快速修复功能
},
// 获取代码
editor.getModel().getValue()
// 光标位置
editor.getPosition().lineNumber;
editor.getPosition().column;
// 快捷键
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_F, () => {})
  1. 处理业务逻辑
// props参数确实
// 动态监听代码的改变,双向数据绑定
editor.onDidChangeModelContent()
// 手动触发action
editor.trigger('1', 'editor.action.formatDocument')

3.2.4部署语言服务

3.3开发过程中注意点

3.3.1主题的配置

// 设置主题
monaco.editor.defineTheme( )
monaco.editor.setTheme( )

主题json文件

可以查看背景色和token类型

"colors": {
  "editor.foreground": "#839496",
  "editor.background": "#1E2337",
  "editor.selectionBackground": "#4356A1",
  "editor.lineHighlightBackground": "#2D334C",
  "editorCursor.foreground": "#819090",
  "editorWhitespace.foreground": "#073642"
}
{
  "foreground": "586e75",
  "token": "comment"
},
{
  "foreground": "2aa198",
  "token": "string"
},

如何查看token类型

monaco官网,按F1或鼠标右键点击Command Palette,点击Developer: Inspect Tokens,鼠标点击代码

3.3.2自定义语言

自定义语言的功能可以分为本地和远程(LSP

SQL为例,以下为本地自定义语言功能的代码实现

// 注册语言
    monaco.languages.register({
        id: 'mysql',
    });
// 代码提示
monaco.languages.registerCompletionItemProvider('mysql', {
  provideCompletionItems: function (model, position) {
    var word = model.getWordUntilPosition(position);
    var range = {
      startLineNumber: position.lineNumber,
      endLineNumber: position.lineNumber,
      startColumn: word.startColumn,
      endColumn: word.endColumn
    };
    return {
      // SQL关键字(本地文件)、token代码补全
      suggestions: createDependencyProposals(range, editor, word)
    };
  }
});

远程LSP自定义语言功能代码实现

// 注册语言
monaco.languages.register({
        id: 'mysql',
    });

// 建立连接,创建LSP client
const webSocket = createWebSocket(`${address}/mysql`);
listen({
  webSocket,
  onConnection: connection => {
    // create and start the language client
    console.log("onConnection!sql")
    const languageClient = createLanguageClient(connection);
    const disposable = languageClient.start();
    connection.onClose(() => {
      disposable.dispose()
    });
  },
});