智能 IDE 系列 -- SQL编辑器
智能 IDE 系列 -- SQL编辑器
豆皮粉儿们,我们又见面啦!今天我们来自字节跳动的“虫二”和“「锕」”二位同学带来了智能IDE系列文章的第一篇 —— SQL编辑器。豆皮粉儿们,赶紧来丰富自己的知识吧!
作者们: 虫二 &「锕」前言 IDE 本身是个集很多复杂功能在一起的应用,当你想开发一个IDE的时候,你至少需要关注
来源: 原创
本文主要介绍的只是以上冰山一角中的Editor层的内容,通过本文希望给正在进行相关学习的同学有些许启发,本文中每个过程不会详细解释背后技术实现原理,背后原理将在后续文章进行介绍。
- 代码编辑器层(这部分在本文中我称为Editor层):语法高亮、智能提示&补全、语法诊断、文档悬浮、格式化...
- 工作目录(Workspace)
- 扩展层(Extension)
- 运行调试层(Debug)
- 环境配置 (Environment)
- 上线部署层(Publish),如果你正在做一个Cloud IDE, 这一层就是一个必备的能力,如何让用户在Web端即可实现“编辑-调试-部署”一条线,并且保证调试阶段的环境配置和部署阶段相同。
- 版本管理(Version)
如果你正好在做一个SQL Editor, 本文可以作为一个不错的参考。
本文的适用对象:
- 你正在实现一个自己独有的Editor, 需要让Editor能实现上述1的能力,这个Editor 我认为可以是传统意义上的输入形式的Editor, 也可以是针对很多表单项填写or下拉选择的Editor,甚至于还可以是GUI 页面编辑器,其实我们只需要将语法高亮、智能提示这些在概念上做一个转换。
- 在你的应用(未必是IDE)中需要为用户提供代码编辑的能力
- 你正在使用一门
DSL
(领域专用语言)语言来简化开发的语言, 需要高亮、提示特有的语法 - 自研一个IDE or Cloud IDE
- 从原生Web html开始解读如果做一段代码的高亮、提示
- 开源Editor如何实现
- LSP的诞生
- 开源Editor组件如何与LSP对接, SQL Editor案例
- SQL Language Server
- 总结, 想要实现一个智能 Editor需要做哪些事情
例如,以Monaco 的一个例子展开 看原生如何实现
这是一段日志内容高亮规则是 日期:绿色、notice: 黄色、error: 红色、info: 灰色
语法高亮关键的步骤是词法分析, 分词的目的是将用户输入字符串分割成一个个的词 (token), token 就是不可再进一步分割的一串字符,分析过程需要扫描源代码, 扫描的方法有直接扫描和正则表达式扫描[1];
用于做分析的函数称为词法分析器
上面的案例,用正则简单粗暴实现如下,不具有任何参考意义,如果想实现复杂的分词,你应该寻找类似 flex or ANTLR这样的工具:
Highlight - 锐客网 .custom-info { color: #808080 }
.custom-error { color: #ff0000;
font-style: bold;
}
.custom-notice { color: #FFA500;
}
.custom-date { color: #008800;
}
粗暴的用一个textarea 伪代码实现简单的智能提示
例如,还是以Monaco 的一个例子展开
开源Editor是如何做的 Editor支持高亮需要两个过程
- 根据语法将文本解析成符号和作用域
- 根据生成的作用域映射到对应的颜色和样式
然而,多数的Javascript Editor在支持智能提示上却不尽人意。
CodeMirror & Ace 需要监听change 事件来处理
editor.on('change', changeListener);
Monaco Editor在这方面做的比较前沿,允许你使用使用register provider 来注册语言特性,并且处理好了返回值的UI显示,对于使用者,不需要再单独定义UI。
例如
setMonarchTokensProvider
注册一个语言,详情registerCompletionItemProvider
注册智能提示、registerHoverProvider
注册悬浮文档,当你处理语法解析时候,如果你不用下面的方式则需要用js 来实现一套语言的解析LSP的诞生 从上面可以看出即使是使用同一种语言(这里我都用的javascript), 只是Editor不同而已, 实现智能提示也是需要针对单独的Editor去实现, 实际上不同语言的IDE更是需要为每个IDE都实现一遍 JavaScript 语言的智能提示。
如何为不同的IDE,提供一套通用的语言服务?
例如: Javascript 语言的server只需要有一套即可让多个IDE去使用, 这里就必须要推荐下VScode 的LSP协议(想快读的可以阅读之前写的一篇学习文章)[2], 这个协议规定了IDE和语言server之间使用规范中定义的参数格式进行通信, 协议底层交互是JSON-PRC(无状态的远程过程调用协议),在 IDE 的Client端和Server端通信的形式可以是socket, 也可以是HTTP,甚至可以是stdio。
Editor 如何与LS交互
下面以SQL 语言为案例,说明编辑器和 SQL Language Server之间如何交互
这里我在Client和Server端建立了一个Web Socket 连接
- 初始化: Editor打开之前 Client 会向 Server发送
initialize
初始化消息, 消息中params.capabilities 规定了Client端支持的能力, 比如补全
如果语言不支持格式化, 就不在
capabilities
中返回documentFormattingProvider,client就不会显示格式化的菜单。- 打开事件: Editor打开后 Client 会向Server发送
textDocument/didOpen
消息, 消息体如下, 会标记当前语言、源代码、uri(可以是个文件地址,也可以是个虚拟的地址,具体视Server的实现而定) - change事件: 用户输入代码时,Client 会向Server发送
textDocument/didChange
消息, 服务端决定是否处理这个消息, 同样类似open的动作,这个案例中服务端会在输入过程中诊断语法错误,response和open 返回相同 - Server 也可以主动向 Client 推送事件,我这里的案例是服务端会主动发送diagnostics事件,在打开或change后发送语法诊断的结果, 诊断返回的内容是错误的文字所在位置,和错误提示,如下range 是起始和结束位置, message是消息内容
- 补全事件: 在输入的过程中Client 也会向Server发送
textDocument/completion
消息
Server接受消息后会发送需要补全的内容,Server在内部做一系列的分析后给出需要补全内容
比如针对用户输入的select * from a
Server需要补全库名, 当用户输入select * from aaa.
时需要补全aaa库下面的表
这里看到 Server 响应的内容中有的会 id 字段, 该id就是Client 发送的id, Server通过此来标记响应哪个事件,Client会根据此处理对应请求的事件 原因是有些行为会多很短时间内多次触发, Client可以单独取消某次事件
也会有写请求体和响应体中没有id的情况, 那会通过method 决定事件类型 - Hover文档: 鼠标悬浮单词时Client会向Server发送
textDocument/hover
事件, Server 根据Client发送的当前鼠标的位置计算出当前单词在抽象语法树的位置,返回对应文档
- 第一步当你和Editor正在交互的时候,这个时候对于Editor就是内容在change 的过程,Server 需要维护这个正在change的代码“文件”,以便在需要智能提示的时候使用。这里的实现,如果 LS 和 Editor 在同一台电脑上,大可肆意使用文件系统;如果他们分离,就需要根据change事件中的 uri 和内容来更新,并刷新到 LS 的存储中;根据 LS 声明的 capacity,每次change事件可以传递全量或增量的内容。
- 第二步当Editor意识到此处需要一个智能提示(LS 会声明一个 triggerCharacter 使 Editor 知晓在哪些字符后需要智能提示),会发送 completion 事件到 LS,其中包含当前光标所在的位置(比如VScode 提供的位置就是lineNumbers 行, column 列 都是从1开始)。由这个位置和第一步所存储代码的内容LS会进行一系列的语法分析,返回所有可以提示出来的内容,给用户展现出来,正如在上面GIF图中你看到的下拉列表的内容框。
不过要实现一个智能提示,光有 AST 是不够的。首先我们需要能够支持解析正在编辑中的 SQL 代码,其次我们要将解析 SQL 的结果转换为智能提示结果。也就是说,我们需要定义详细到编辑时的语法规则,并定义语法解析时的行为使其产物携带更多对补全有用的信息。例如,我们用
|
代表光标,并有如下的 SQL 等待补全SELECT | FROM some_table;
我们知道,正常来说这里需要补全
*
或者是some_table
表下的字段,当然也可能是函数,或者是DISTINCT
。所以在解析上面这段 SQL(注意这里是带着光标去解析的)后我们想要一个这样的数据结构{
"AST": {...},
"keywords": ["*", "DISTINCT"],
"columns": true,
"functions": true,
"source": {
"table": "some_table"
}
}
这样我们可以通过其中的属性来得出我们提示的列表,具体的操作如下
keywords
列表中的内容全部进入提示的列表中functions
字段为true
,我们将已知的函数列表全都塞进提示的列表中columns
字段为true
,结合source
字段得知我们需要拉取some_table
表的所有字段,并放入提示的列表
关于这部分推荐阅读参考文档[1]总结 实现一套语言的智能化,Server层你需要实现一个Language Server,这个Server可以用任何编程语言来写,vscode 提供一个符合LSP规范的包供开发者使用
vscode-languageserver
[3]; 如果你正在为js开发者提供一个语言服务,可以参考
typescript-language-server
[4];Editor层,如果你用的是Monaco Editor 你可以在
monaco-languageclient
[5]的基础上来改造你想要的语言能力;如果你用的是CodeMirror或者Ace可以参考
lsp-editor-adapter
[6];
参考文档
[1]词法分析
[2]LSP协议
[3]vscode-languageserver
[4]typescript-language-server
[5]monaco-languageclient
[6]lsp-editor-adapter
The End 【智能 IDE 系列 -- SQL编辑器】
文章图片
推荐阅读
- 【欢喜是你·三宅系列①】⑶
- 你不可不知的真相系列之科学
- 人脸识别|【人脸识别系列】| 实现自动化妆
- 人工智能|干货!人体姿态估计与运动预测
- IDEA|IDEA 创建工程
- 2018-06-13金句系列7(金句结构-改编古现代诗词)
- Unity和Android通信系列文章2——扩展UnityPlayerActivity
- 乡野村趣系列之烧仙草
- Java内存泄漏分析系列之二(jstack生成的Thread|Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析)
- 15、IDEA学习系列之其他设置(生成javadoc、缓存和索引的清理等)