demo思路
- 需求:語法高亮本質(zhì)是把源文件中的關(guān)鍵字等具有語法意義的特殊字符序列渲染出來。
- 思路:
- 從源文件中把關(guān)鍵字識別出來
- 如何渲染識別出來的高亮部分
- 解決方案
- 去識別關(guān)鍵字:直接基于正則掃描 (目前眾包的實現(xiàn)) / 基于AST直接渲染
- 基于 html element 的方案
基于 svg 的方案
基于 類似 ace editor的編輯器組件,開箱即用,只需要傳遞需要渲染的文本和高亮規(guī)則
高亮
語法高亮由兩部分工作組成:
- 根據(jù)語法將文本解析成符號和作用域
- 然后根據(jù)這份作用域映射應(yīng)用對應(yīng)的顏色和樣式
語法高亮其實是有兩種實現(xiàn)方案,
- 一種是基于正則,原文直接匹配,匹配的結(jié)果直接替換成富文本(例如帶樣式的html標簽),最終會得到一個關(guān)鍵字被高亮的富文本。
- 第二種,源文件調(diào)用paser 處理成AST,然后用AST去渲染,生成富文本。
第一種方案更適合于語法簡單,不包含上下文關(guān)鍵字的情況,例如在c#中這類情況 add , group這類情況在非linq的上下文,是不應(yīng)該被渲染成關(guān)鍵字。
第二種方案可以完美解決這種情況,AST中包含了上下文信息,有助于判斷是否應(yīng)該是關(guān)鍵字的情況
AST
AST(Abstract Syntax Tree 抽象語法樹) , 它是源代碼語法結(jié)構(gòu)的一種抽象標表示,它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個節(jié)點都表示源代碼中的一種結(jié)構(gòu)。
.用處
- 最初是為了實現(xiàn)某種編程語言的編輯器所需要的語法中介
- 編輯器的錯誤提示,代碼高亮,自動補全,
- eslint等對代碼風(fēng)格和格式的檢查
- webpack通過babel轉(zhuǎn)譯js語法
AST 如何生成
編譯器執(zhí)行的第一步是讀取文件中的字符流,然后通過詞法分析生成 token,之后再通過語法分析( Parser )生成 AST,最后生成機器碼執(zhí)行。
- 分詞:將整個代碼字符串分割成最小語法單元數(shù)組
- 語法分析:在分詞基礎(chǔ)上建立分析語法單元之間的關(guān)系
詞法分析
詞法分析:也稱之為掃描(scanner),簡單來說就是調(diào)用 next() 方法,一個一個字母的來讀取字符,然后與定義好的關(guān)鍵字符做比較,生成對應(yīng)的Token。Token 是一個不可分割的最小單元:
詞法分析器里,每個關(guān)鍵字是一個 Token ,每個標識符是一個 Token,每個操作符是一個 Token,每個標點符號也都是一個 Token。除此之外,還會過濾掉源程序中的注釋和空白字符(換行符、空格、制表符等。)
最終,整個代碼將被分割進一個tokens列表(或者說一維數(shù)組)。
語法分析
語法分析會將詞法分析出來的 Token 轉(zhuǎn)化成有語法含義的抽象語法樹結(jié)構(gòu)。同時,驗證語法,語法如果有錯的話,拋出語法錯誤。
demo 演示
LSP( LSP (Language Server Protocol))
LSP 是 微軟為解決 IDE 語言服務(wù)和調(diào)試適配器 M x N 問題, 傳統(tǒng)的每個 IDE 都要自行開發(fā)一套某個語言的語言服務(wù)程序和調(diào)試適配器, 而這些語言服務(wù)程序都使用不同的接口, 完全無法復(fù)用, 造成各大 IDE 開發(fā)成本過高的問題.
通俗的講就是語言服務(wù)單獨運行在一個進程里,通過 JSON RPC 作為協(xié)議與客戶端通信,為其提供如跳轉(zhuǎn)定義、自動補全等通用語言功能,例如 ts 的類型檢查、類型跳轉(zhuǎn)、自動補全等都需要有對應(yīng)的 ts 語言服務(wù)端實現(xiàn)并與 Client 端通信。

使用語言服務(wù)器協(xié)議的語言服務(wù)器。它的實現(xiàn)方式如下:
- 一個為JS同時提供語言客戶端和語言服務(wù)器的插件
- 語言客戶端就像普通插件一樣,運行于Node.js插件主機環(huán)境中。這個插件激活后,會啟動另一個進程——語言服務(wù)器,然后兩者通過語言服務(wù)器協(xié)議進行通信。
- 你懸停到JS代碼上
- VS Code通知語言客戶端
- 語言客戶端向語言服務(wù)器發(fā)起請求,索要懸停的返回結(jié)果,最后再送回給VS Code
- VS Code將結(jié)果展示在懸浮框中
這個過程可能看起來有些復(fù)雜,但是這么做主要有兩個好處:
- 語言服務(wù)器可以用任何語言實現(xiàn)
- 語言服務(wù)器可以被多個編輯器重用,提供更加智能的編輯體驗
language server
Language Server翻譯為“語言服務(wù)器”,并不是說它真的是一個服務(wù)器,而是它把語言相關(guān)的特性和功能從IDE中解耦出來,作為一個獨立的程序單獨運行,提供了例如引用查詢(Find All References)等功能的具體實現(xiàn),Client是編輯器或IDE,例如Atom、VScode等。
更加確切的解釋是,Language Server是某語言的Language Server Protocol具體實現(xiàn)。
vscode
Visual Studio Code(簡稱VSCode) 是開源免費的IDE編輯器,原本是微軟內(nèi)部使用的云編輯器(Monaco)。
git倉庫地址: https://github.com/microsoft/vscode
通過Eletron集成了桌面應(yīng)用,可以跨平臺使用,開發(fā)語言主要采用微軟自家的TypeScript。 整個項目結(jié)構(gòu)比較清晰,方便閱讀代碼理解。成為了最流行跨平臺的桌面IDE應(yīng)用
微軟希望VSCode在保持核心輕量級的基礎(chǔ)上,增加項目支持,智能感知,編譯調(diào)試。
- TypeScript是一種由微軟開發(fā)的自由和開源的編程語言。它是JavaScript的一個超集,而且本質(zhì)上向這個語言添加了可選的靜態(tài)類型和基于類的面向?qū)ο缶幊?/li>
├── build # gulp編譯構(gòu)建腳本
├── extensions # 內(nèi)置插件
├── product.json # App meta信息
├── resources # 平臺相關(guān)靜態(tài)資源
├── scripts # 工具腳本,開發(fā)/測試
├── src # 源碼目錄
└── typings # 函數(shù)語法補全定義
└── vs
├── base # 通用工具/協(xié)議和UI庫
│ ├── browser # 基礎(chǔ)UI組件,DOM操作
│ ├── common # diff描述,markdown解析器,worker協(xié)議,各種工具函數(shù)
│ ├── node # Node工具函數(shù)
│ ├── parts # IPC協(xié)議(Electron、Node),quickopen、tree組件
│ ├── test # base單測用例
│ └── worker # Worker factory和main Worker(運行IDE Core:Monaco)
├── code # VSCode主運行窗口
├── editor # IDE代碼編輯器
| ├── browser # 代碼編輯器核心
| ├── common # 代碼編輯器核心
| ├── contrib # vscode 與獨立 IDE共享的代碼
| └── standalone # 獨立 IDE 獨有的代碼
├── platform # 支持注入服務(wù)和平臺相關(guān)基礎(chǔ)服務(wù)(文件、剪切板、窗體、狀態(tài)欄)
├── workbench # 工作區(qū)UI布局,功能主界面
│ ├── api #
│ ├── browser #
│ ├── common #
│ ├── contrib #
│ ├── electron-browser #
│ ├── services #
│ └── test #
├── css.build.js # 用于插件構(gòu)建的CSS loader
├── css.js # CSS loader
├── editor # 對接IDE Core(讀取編輯/交互狀態(tài)),提供命令、上下文菜單、hover、snippet等支持
├── loader.js # AMD loader(用于異步加載AMD模塊)
├── nls.build.js # 用于插件構(gòu)建的NLS loader
└── nls.js # NLS(National Language Support)多語言loader
核心層
- base: 提供通用服務(wù)和構(gòu)建用戶界面
- platform: 注入服務(wù)和基礎(chǔ)服務(wù)代碼
- editor: 微軟Monaco編輯器,也可獨立運行使用
- wrokbench: 配合Monaco并且給viewlets提供框架:如:瀏覽器狀態(tài)欄,菜單欄利用electron實現(xiàn)桌面程序
核心環(huán)境
整個項目完全使用typescript實現(xiàn),electron中運行主進程和渲染進程,使用的api有所不同,所以在core中每個目錄組織也是按照使用的api來安排, 運行的環(huán)境分為幾類:
- common: 只使用javascritp api的代碼,能在任何環(huán)境下運行
- browser: 瀏覽器api, 如操作dom; 可以調(diào)用common
- node: 需要使用node的api,比如文件io操作
- electron-brower: 渲染進程api, 可以調(diào)用common, brower, node, 依賴electron renderer-process API
- electron-main: 主進程api, 可以調(diào)用: common, node 依賴于electron main-process AP
vscode事件分發(fā)
src/vs/base/common/event.ts
程序中常見使用once方法進行事件綁定, 給定一個事件,返回一個只觸發(fā)一次的事件,放在匿名函數(shù)返回
export function once<T>(event: Event<T>): Event<T> {
return (listener, thisArgs = null, disposables?) => {
// 設(shè)置次變量,防止事件重復(fù)觸發(fā)造成事件污染
let didFire = false;
let result: IDisposable;
result = event(e => {
if (didFire) {
return;
} else if (result) {
result.dispose();
} else {
didFire = true;
}
return listener.call(thisArgs, e);
}, null, disposables);
if (didFire) {
result.dispose();
}
return result;
};
}
循環(huán)派發(fā)了所有注冊的事件, 事件會存儲到一個事件隊列,通過fire方法觸發(fā)事件
private _deliveryQueue?: LinkedList<[Listener, T]>;//事件存儲隊列
fire(event: T): void {
if (this._listeners) {
// 將所有事件傳入 delivery queue
// 內(nèi)部/嵌套方式通過emit發(fā)出.
// this調(diào)用事件驅(qū)動
if (!this._deliveryQueue) {
this._deliveryQueue = new LinkedList();
}
for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) {
this._deliveryQueue.push([e.value, event]);
}
while (this._deliveryQueue.size > 0) {
const [listener, event] = this._deliveryQueue.shift()!;
try {
if (typeof listener === 'function') {
listener.call(undefined, event);
} else {
listener[0].call(listener[1], event);
}
} catch (e) {
onUnexpectedError(e);
}
}
}
}