豆皮粉兒們,咱們又見面啦!今天咱們來自字節跳動的「蟲二」和「「錒」」二位同窗帶來了智能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