关于ide:智能-IDE-系列-SQL编辑器

46次阅读

共计 7188 个字符,预计需要花费 18 分钟才能阅读完成。

智能 IDE 系列 — SQL 编辑器

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

作者们:虫二 &「锕」
起源:原创

前言

IDE 自身是个集很多简单性能在一起的利用,当你想开发一个 IDE 的时候,你至多须要关注

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

本文次要介绍的只是以上冰山一角中的 Editor 层的内容,通过本文心愿给正在进行相干学习的同学有些许启发,本文中每个过程不会具体解释背地技术实现原理,背地原理将在后续文章进行介绍。
如果你正好在做一个 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 须要做哪些事件

从零开始

抛开目前已有的 Editor 组件,用原生 html 来实现高亮
例如,以 Monaco 的一个例子开展 看原生如何实现
这是一段日志内容高亮规定是 日期:绿色、notice: 黄色、error: 红色、info: 灰色

语法高亮要害的步骤是词法剖析, 分词的目标是将用户输出字符串宰割成一个个的词 (token), token 就是不可再进一步宰割的一串字符,剖析过程须要扫描源代码, 扫描的办法有间接扫描和正则表达式扫描 [1];
用于做剖析的函数称为词法分析器
下面的案例,用正则简略粗犷实现如下,不具备任何参考意义,如果想实现简单的分词,你应该寻找相似 flex or ANTLR 这样的工具:

<!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 的一个例子开展

  <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 反对高亮须要两个过程

  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 的实现而定)
  2. change 事件: 用户输出代码时,Client 会向 Server 发送 textDocument/didChange 音讯, 服务端决定是否解决这个音讯, 同样相似 open 的动作,这个案例中服务端会在输出过程中诊断语法错误,response 和 open 返回雷同
  3. Server 也能够被动向 Client 推送事件,我这里的案例是服务端会被动发送 diagnostics 事件,在关上或 change 后发送语法诊断的后果, 诊断返回的内容是谬误的文字所在位置,和谬误提醒,如下 range 是起始和完结地位, message 是音讯内容
  4. 补全事件: 在输出的过程中 Client 也会向 Server 发送 textDocument/completion 音讯

    Server 承受音讯后会发送须要补全的内容,Server 在外部做一系列的剖析后给出须要补全内容
    比方针对用户输出的 select * from a Server 须要补全库名, 当用户输出select * from aaa. 时须要补全 aaa 库上面的表

    这里看到 Server 响应的内容中有的会 id 字段, 该 id 就是 Client 发送的 id, Server 通过此来标记响应哪个事件,Client 会依据此解决对应申请的事件 起因是有些行为会多很短时间内屡次触发, Client 能够独自勾销某次事件
    也会有写申请体和响应体中没有 id 的状况, 那会通过 method 决定事件类型

  5. 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

The End

正文完
 0