豆皮粉儿们,咱们又见面啦!今天咱们来自字节跳动的“虫二”和“「锕」”二位同窗带来了智能IDE系列文章的第一篇 —— SQL编辑器。豆皮粉儿们,赶忙来丰富本身的知识吧!javascript
做者们: 虫二 &「锕」 来源: 原创css
IDE 自己是个集不少复杂功能在一块儿的应用,当你想开发一个IDE的时候,你至少须要关注html
- 代码编辑器层(这部分在本文中我称为Editor层):语法高亮、智能提示&补全、语法诊断、文档悬浮、格式化...
- 工做目录(Workspace)
- 扩展层(Extension)
- 运行调试层(Debug)
- 环境配置 (Environment)
- 上线部署层(Publish),若是你正在作一个Cloud IDE, 这一层就是一个必备的能力,如何让用户在Web端便可实现“编辑-调试-部署”一条线,而且保证调试阶段的环境配置和部署阶段相同。
- 版本管理(Version)
本文主要介绍的只是以上冰山一角中的Editor层的内容,经过本文但愿给正在进行相关学习的同窗有些许启发,本文中每一个过程不会详细解释背后技术实现原理,背后原理将在后续文章进行介绍。 若是你正好在作一个SQL Editor, 本文能够做为一个不错的参考。前端
本文的适用对象:java
DSL
(领域专用语言)语言来简化开发的语言, 须要高亮、提示特有的语法抛开目前已有的Editor组件,用原生html来实现高亮
例如,以Monaco 的一个例子展开 看原生如何实现
这是一段日志内容高亮规则是 日期:绿色、notice: 黄色、error: 红色、info: 灰色
node
语法高亮关键的步骤是词法分析, 分词的目的是将用户输入字符串分割成一个个的词 (token), token 就是不可再进一步分割的一串字符,分析过程须要扫描源代码, 扫描的方法有直接扫描和正则表达式扫描[1];
用于作分析的函数称为词法分析器
上面的案例,用正则简单粗暴实现以下,不具备任何参考意义,若是想实现复杂的分词,你应该寻找相似 flex or ANTLR这样的工具:git
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Highlight</title>
<style> .custom-info { color: #808080 } .custom-error { color: #ff0000; font-style: bold; } .custom-notice { color: #FFA500; } .custom-date { color: #008800; } </style>
</head>
<body>
<div id="log-editor">
</div>
<script> const tokenizer = { root: [ [/\[error.*/, "custom-error"], [/\[notice.*/, "custom-notice"], [/\[info.*/, "custom-info"], [/\[[a-zA-Z 0-9:]+\]/, "custom-date"], ] } const highlight = (str) => { return tokenizer.root.reduce((pre, current) => { return pre.replace(current[0], (m) => { return `<span class="${current[1]}">${m}</span>` }); }, str); }; const log = ` [Sun Mar 7 16:02:00 2004] [notice] Apache/1.3.29 (Unix) configured -- resuming normal operations [Sun Mar 7 16:02:00 2004] [info] Server built: Feb 27 2004 13:56:37 [Sun Mar 7 16:02:00 2004] [notice] Accept mutex: sysvsem (Default: sysvsem) [Sun Mar 7 16:05:49 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed [Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome [Sun Mar 7 21:20:14 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed ` const innerHtml = log.split('\n').reduce((pre, current) => { return pre + `<div class="line">${highlight(current)}</div>`; }, '') window.addEventListener('DOMContentLoaded', () => { const wrapper = document.querySelector('#log-editor') wrapper.innerHTML = innerHtml; }) </script>
</body>
</html>
复制代码
粗暴的用一个textarea 伪代码实现简单的智能提示
例如,仍是以Monaco 的一个例子展开github
<script> const suggestion = [ { label: '"lodash"', documentation: "The Lodash library exported as Node.js modules.", insertText: '"lodash": "*"', range: range }, { label: '"express"', documentation: "Fast, unopinionated, minimalist web framework", insertText: '"express": "*"', range: range } ]; const getSuggestion = (value) => { // TODO 这里 这个详细过程详见文章底部 SQL Language Server 智能提示的过程解析 const result = parser(value) return result; } window.addEventListener('DOMContentLoaded', () => { const wrapper = document.querySelector('#editor texteara') wrapper.addEventListener('change', (event) => { // 根据当前鼠标所在的位置计算出 const position = {lineNumbers: 1, columns}; const value = event.target.value; const suggestion = getSuggestion(value, position); // 建立DOM List框到输入的位置 }) }) </script>
复制代码
Editor支持高亮须要两个过程web
Editor容许你本身register一个语言id, 你须要根据token格式,编写本身的rules最终实现高亮。正则表达式
然而,多数的Javascript Editor在支持智能提示上却不尽人意。
CodeMirror & Ace 须要监听change 事件来处理
editor.on('change', changeListener);
复制代码
Monaco Editor在这方面作的比较前沿,容许你使用使用register provider 来注册语言特性,而且处理好了返回值的UI显示,对于使用者,不须要再单独定义UI。
例如 setMonarchTokensProvider
注册一个语言,详情 registerCompletionItemProvider
注册智能提示、 registerHoverProvider
注册悬浮文档,当你处理语法解析时候,若是你不用下面的方式则须要用js 来实现一套语言的解析
从上面能够看出即便是使用同一种语言(这里我都用的javascript), 只是Editor不一样而已, 实现智能提示也是须要针对单独的Editor去实现, 实际上不一样语言的IDE更是须要为每一个IDE都实现一遍 JavaScript 语言的智能提示。
如何为不一样的IDE,提供一套通用的语言服务?
例如: Javascript 语言的server只须要有一套便可让多个IDE去使用, 这里就必需要推荐下VScode 的LSP协议(想快读的能够阅读以前写的一篇学习文章)[2], 这个协议规定了IDE和语言server之间使用规范中定义的参数格式进行通讯, 协议底层交互是JSON-PRC(无状态的远程过程调用协议),在 IDE 的Client端和Server端通讯的形式能够是socket, 也能够是HTTP,甚至能够是stdio。
下面以SQL 语言为案例,说明编辑器和 SQL Language Server之间如何交互 这里我在Client和Server端创建了一个Web Socket 链接
initialize
初始化消息, 消息中params.capabilities 规定了Client端支持的能力, 好比补全此时Server 端在接受到初始化请求后,须要发送当前语言支持的能力, 例如语言支持 documentFormattingProvider(格式化)、hoverProvider(文档悬浮)、definitionProvider(跳转定义)、completionProvider(补全) 、codeActionProvider;
若是语言不支持格式化, 就不在capabilities
中返回documentFormattingProvider,client就不会显示格式化的菜单。
textDocument/didOpen
消息, 消息体以下, 会标记当前语言、源代码、uri(能够是个文件地址,也能够是个虚拟的地址,具体视Server的实现而定)textDocument/didChange
消息, 服务端决定是否处理这个消息, 一样相似open的动做,这个案例中服务端会在输入过程当中诊断语法错误,response和open 返回相同textDocument/completion
消息
Server接受消息后会发送须要补全的内容,Server在内部作一系列的分析后给出须要补全内容
好比针对用户输入的 select * from a
Server须要补全库名, 当用户输入select * from aaa.
时须要补全aaa库下面的表
这里看到 Server 响应的内容中有的会 id 字段, 该id就是Client 发送的id, Server经过此来标记响应哪一个事件,Client会根据此处理对应请求的事件 缘由是有些行为会多很短期内屡次触发, Client能够单独取消某次事件 也会有写请求体和响应体中没有id的状况, 那会经过method 决定事件类型
textDocument/hover
事件, Server 根据Client发送的当前鼠标的位置计算出当前单词在抽象语法树的位置,返回对应文档
Language Server 须要作的,是实现 LSP 定义的功能的一个子集。这里以最为核心的智能提示为例,其须要作的事情有两步
这个过程最关键的点在第二步,如何根据一段代码和其中的一个位置给出一系列智能提示。固然不少语言有现成的自动补全轮子,好比 Python 的 jedi。这里以 SQL 为例:简单来讲,咱们须要对一串 SQL 作词法分析和语法分析,以理解接下来能够写的代码是什么。这里的词法分析和语法分析,其实正是编译原理里编译器的“前端”的前半部分:词法分析是将代码切分红一个个词(Token),语法分析是对 Token 序列进行一系列定义的计算,以构建特定的数据结构。通常编译器进行语法分析后获得的产物是一颗抽象语法树(AST),并基于此继续进行语义分析并优化。一个标准SQL的AST树以下结构:
不过要实现一个智能提示,光有 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
表的全部字段,并放入提示的列表固然这只是一个示例,能够按需增长解析结果中的内容,比较典型的有提示的优先级等。而具体如何将这些规则们变成一个可用的词法+语法分析器,其实因为编译器前端的发展已经很成熟了,有不少工具(parser generator)能够完成这项任务,而不须要咱们对着规则手写代码逻辑,例如antlr、bison/yacc & lex 等。
关于这部分推荐阅读参考文档[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