超硬核的智能 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

相關文章
相關標籤/搜索