超硬核的智能 IDE 系列 -- SQL编辑器

豆皮粉儿们,咱们又见面啦!今天咱们来自字节跳动的“虫二”和“「锕」”二位同窗带来了智能IDE系列文章的第一篇 —— SQL编辑器。豆皮粉儿们,赶忙来丰富本身的知识吧!javascript

302a901b-2a85-4caa-b16b-aa2d533170ff.gif

做者们: 虫二 &「锕」 来源: 原创css

前言

IDE 自己是个集不少复杂功能在一块儿的应用,当你想开发一个IDE的时候,你至少须要关注html

  1. 代码编辑器层(这部分在本文中我称为Editor层):语法高亮、智能提示&补全、语法诊断、文档悬浮、格式化...
  2. 工做目录(Workspace)
  3. 扩展层(Extension)
  4. 运行调试层(Debug)
  5. 环境配置 (Environment)
  6. 上线部署层(Publish),若是你正在作一个Cloud IDE, 这一层就是一个必备的能力,如何让用户在Web端便可实现“编辑-调试-部署”一条线,而且保证调试阶段的环境配置和部署阶段相同。
  7. 版本管理(Version)

本文主要介绍的只是以上冰山一角中的Editor层的内容,经过本文但愿给正在进行相关学习的同窗有些许启发,本文中每一个过程不会详细解释背后技术实现原理,背后原理将在后续文章进行介绍。 若是你正好在作一个SQL Editor, 本文能够做为一个不错的参考。前端

本文的适用对象:java

  • 你正在实现一个本身独有的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须要作哪些事情

从零开始

抛开目前已有的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是如何作的

Editor支持高亮须要两个过程web

  1. 根据语法将文本解析成符号和做用域
  2. 根据生成的做用域映射到对应的颜色和样式

Editor容许你本身register一个语言id, 你须要根据token格式,编写本身的rules最终实现高亮。正则表达式

然而,多数的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 链接

  1. 初始化: Editor打开以前 Client 会向 Server发送initialize初始化消息, 消息中params.capabilities 规定了Client端支持的能力, 好比补全

此时Server 端在接受到初始化请求后,须要发送当前语言支持的能力, 例如语言支持 documentFormattingProvider(格式化)、hoverProvider(文档悬浮)、definitionProvider(跳转定义)、completionProvider(补全) 、codeActionProvider;
若是语言不支持格式化, 就不在capabilities中返回documentFormattingProvider,client就不会显示格式化的菜单。

  1. 打开事件: Editor打开后 Client 会向Server发送textDocument/didOpen消息, 消息体以下, 会标记当前语言、源代码、uri(能够是个文件地址,也能够是个虚拟的地址,具体视Server的实现而定)

  1. change事件: 用户输入代码时,Client 会向Server发送textDocument/didChange消息, 服务端决定是否处理这个消息, 一样相似open的动做,这个案例中服务端会在输入过程当中诊断语法错误,response和open 返回相同

  1. Server 也能够主动向 Client 推送事件,我这里的案例是服务端会主动发送diagnostics事件,在打开或change后发送语法诊断的结果, 诊断返回的内容是错误的文字所在位置,和错误提示,以下range 是起始和结束位置, message是消息内容

  1. 补全事件: 在输入的过程当中Client 也会向Server发送textDocument/completion消息


Server接受消息后会发送须要补全的内容,Server在内部作一系列的分析后给出须要补全内容
好比针对用户输入的 select * from a Server须要补全库名, 当用户输入select * from aaa. 时须要补全aaa库下面的表

这里看到 Server 响应的内容中有的会 id 字段, 该id就是Client 发送的id, Server经过此来标记响应哪一个事件,Client会根据此处理对应请求的事件 缘由是有些行为会多很短期内屡次触发, Client能够单独取消某次事件 也会有写请求体和响应体中没有id的状况, 那会经过method 决定事件类型

  1. Hover文档: 鼠标悬浮单词时Client会向Server发送textDocument/hover事件, Server 根据Client发送的当前鼠标的位置计算出当前单词在抽象语法树的位置,返回对应文档

Language Server 智能提示

Language Server 须要作的,是实现 LSP 定义的功能的一个子集。这里以最为核心的智能提示为例,其须要作的事情有两步

  • 第一步当你和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图中你看到的下拉列表的内容框。

这个过程最关键的点在第二步,如何根据一段代码和其中的一个位置给出一系列智能提示。固然不少语言有现成的自动补全轮子,好比 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"
    }
}
复制代码

这样咱们能够经过其中的属性来得出咱们提示的列表,具体的操做以下

  1. keywords列表中的内容所有进入提示的列表中
  2. functions字段为true,咱们将已知的函数列表全都塞进提示的列表中
  3. 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

相关文章
相关标签/搜索