好用到飛起!VSCode插件DevUIHelper設計開發全攻略(三)

DevUI是一支兼具設計視角和工程視角的團隊,服務於華爲雲 DevCloud平臺和華爲內部數箇中後臺系統,服務於設計師和前端工程師。
官方網站: devui.design
Ng組件庫: ng-devui(歡迎Star)
官方交流羣:添加DevUI小助手(微信號:devui-official)
DevUIHelper插件:DevUIHelper-LSP(歡迎Star)

引言

嗨,咱們是DevUIHelper 的開發團隊。今天,讓咱們聊一下咱們插件開發中應用的一些技術方案。這些經驗也許對您的插件開發有幫助,讓咱們開始吧!html

綜述

因爲咱們的插件運行在 VSCode 上,咱們使用了一些 VSCode 提供的能力,例如 LSP 協議,補全、懸停提示接口等。同時,插件的功能由多個獨立功能的模塊進行完成。接下來咱們根據模塊分別進行介紹。前端

LSP 協議

VSCode 對代碼補全插件大致提供了本地與LSP兩種方案。本地的插件補全能夠直接應用 VSCode 供的一些能力,但 LSP 協議爲跨編輯器使用提供了可能,考慮到咱們的插件將來可能不只僅運行在VSCode平臺上,咱們最終選擇了LSP 協議。node

LSP 協議的願景,多個 IDE 使用同一套補全提示系統。

createConnection API

LSP 協議是基於 客戶端-服務器 模式的,因此使用 LSP 協議的第一步,即是創造一個客戶端與服務器的連接,這時,你須要在服務器端輸入這樣一段代碼:git

import {createConnection,} from 'vscode-languageserver';
private connection = createConnection(ProposedFeatures.all);
connection.listen()github

這樣你就建立了一個默認規則的 LSP 鏈接。可是僅有服務器顯然是不行的,建議經過微軟官方提供的 LSP 種子項目開始進行創做。同時,VSCode 還提供了大量不一樣場景下的種子項目
下面的介紹將基於DevUIHelper-LSP 項目,建議配合代碼食用。順便求一波 star~。segmentfault

DConnection

因爲直接使用vscode 提供的API 會致使代碼很是分散不易閱讀, DConnection 對於 VSCode 提供的鏈接進行的一次封裝,這樣,你能夠方便的對全部的功能函數進行管理:api

export class DConnection{數組

private connection = createConnection(ProposedFeatures.all);
...

constructor(host:Host,logger:Logger){
    ...
    this.addProtocalHandlers();
}

addProtocalHandlers(){
    this.connection.onInitialize(e=>this.onInitialze(e));
    this.connection.onInitialized(()=>this.onInitialized());
    this.connection.onDidChangeConfiguration(e=>this.onDidChangeConfiguration(e));
    this.connection.onHover(e=>this.onHover(e));
    this.connection.onCompletion(e=>this.onCompletion(e));
    this.connection.onDidOpenTextDocument(e=>this.validateTextDocument(e.textDocument.uri))
    this.host.documents.onDidChangeContent(change=>this.validateTextDocument(change.document.uri));
}   
...

}服務器

其他API的應用咱們將在對應包中進行講解微信

功能模塊

DevUIHelper 的功能主要是由多個不一樣的功能模塊實現的,如下是這些包的依賴關係,接下來咱們將自底向上進行講解

Providers

黃色的部分表明了許多 Providers 包 他們位於server/src 目錄下。
其中 CompletionProvider 經過 Dconnection.onCompletion喚醒, 應用了 onCompletion 接口, 提供了補全的能力,

onCompletion(_textDocumentPosition: TextDocumentPositionParams){

...
return this.host.completionProvider.provideCompletionItes(\_textDocumentPosition,FileType.HTML);

}

HoverProvider 經過 Dconnection.onHover 喚醒, 應用了 onHover 接口,提供了懸停提示的能力,

async onHover(_textDocumentPosition:HoverParams){

...
return this.host.hoverProvider.provideHoverInfoForHTML(\_textDocumentPosition);

}

Diagnosis 經過 經過 Dconnection.validateTextDocument 喚醒,應用了sendDiagnostics 接口提供錯誤提醒。

async validateTextDocument(uri: string) {

...
let diagnostics: Diagnostic\[\] = this.host.diagnoser.diagnose(textDocument); 
this.connection.sendDiagnostics({ uri: uri, diagnostics });

}

解析器

因爲 VSCode 的 onCompletion/onHover API 僅僅告訴了咱們一個座標, 爲了完成補全、懸停、以及報警的任務,咱們須要明白光標所在的位置意味着什麼。

export interface Position {

/\*\*
 \* 光標所在行
 \*/
line: number;
/\*\*
 \* 光標相對於行首位的位移
 \*/
character: number;

}

VSCode 的座標API,僅提供了行數與位移。

Parser

首先,咱們須要對輸入的文檔進行解析,這一部分能力由 yq-Parser 提供:

export class YQ_Parser{

...
parseTextDocument(textDocument:TextDocument,parseOption:ParseOption):ParseResult{
const uri = textDocument.uri;

// 進行詞法解析
const tokenizer = new Tokenizer(textDocument); 
const tokens = tokenizer.Tokenize();

// 創建語法樹
const treebuilder =new TreeBuilder(tokens);
return treebuilder.build();
}

}

在分析事後,咱們須要找到光標所在位置的語法樹節點,這個能力由 Hunter 提供。 例如:「光標 {line:10 character:5} 懸停在了 d-button 節點上」

export class Hunter {

...
searchTerminalAST(offset: number, uri: string): SearchResult {

// 找到分析生成的語法樹   
let \_snapShot = host.snapshotMap.get(uri);
if (!\_snapShot) { throw Error(\`this uri does not have a snapShot: ${uri}\`); }
const { root, textDocument, HTMLAstToHTMLInfoNode } = \_snapShot;
if (!root) {
    throw Error(\`Snap shot does not have this file : ${uri}, please parse it befor use it!\`);
}

// 進行深度搜索
let \_result = this.searchParser.DFS(offset, root);

//調整Node位置
return \_result ? \_result : { ast: undefined, type: SearchResultType.Null };
}

以後,咱們須要明白字符串 d-button 意味着什麼,這部分能力由 SourceLoader 提供,經過加載資源樹文件,咱們瞭解了每個AST節點對應的字符串的含義。例如 「d-button 意味着 這是一個 DevUI 組件庫的按鈕標籤」這樣,咱們就能夠把這些信息提供給使用者。

export class Architect {

// 初始化
private readonly componentRootNode = new RootNode();
private readonly directiveRootNode = new RootNode();
constructor() { }

// 加載語法樹的資源文件
build(info: Array<any>,comName:SupportComponentName): RootNode\[\] {
    ...
}

// 生成補全和懸停信息
buildCompletionItemsAndHoverInfo() {
this.componentRootNode.buildCompletionItemsAndHoverInfo();
this.directiveRootNode.buildCompletionItemsAndHoverInfo();
}

咱們須要一個對於文件變化的監視器來保證插件語法樹的分析結果一直是最新的,咱們使用了VSCode 自己提供的 document 接口,這個接口的調用也十分簡單:

public documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

...
// 當文件出現變化的時候進行的操做
this.documents.onDidChangeContent(change => {
    ...
});

DataStructor

咱們但願咱們的語法書更新是伴隨着最小的資源消耗的,這一點借鑑了迴流重繪的思想。網頁在迴流重繪的過程當中會經過只更新變化的部分(一般是變化節點樹以後的部分)儘量的減小資源消耗。 在插件的工做中,響應速度直接影響了用戶體驗,所以咱們但願局部更新語法樹。爲此咱們設計了一個比較特殊的語法樹結構,在這種結構中大量的應用了鏈表的思想,所以咱們製做了一個小型的數據結構模塊對此進行支持。

export interface LinkList<T>{

/\*\*
 \* 頭結點
 \*/
head:HeadNode;

/\*\*
 \* 長度
 \*/
length:number;

/\*\*
 \* 尾節點
 \*/
end:LinkNode<T>|undefined;

/\*\*
 \* 插入節點
 \*/
insertNode(newElement:T,node?:Node):void;

/\*\*
 \* 插入鏈表
 \*/
insetLinkList(list:LinkList<T>,node?:Node):void;

/\*\*
 \* 獲取元素
 \*/
getElement(cb?:()=>any,param?:T):T|undefined;

/\*\*
 \* 依據下標獲取元素
 \* @param num 
 \*/
get(num:number):Node|undefined;

/\*\*
 \* 轉化爲數組
 \*/
toArray():T\[\];

}

咱們但願經過這種語法樹結構更好的進行局部更新。

Cursor

在插件製做的早期咱們借鑑了許多 Angular Parser 部分的思想,指針思想即是其中之一,咱們但願經過指針進行語法樹分析,儲存錯誤和語法樹節點出現的位置。可是做爲一個組件庫的提示插件,咱們並不須要框架級別的強大能力,所以咱們製做了一個簡單版的指針模塊,如今,他爲 Parser 和 @表達式 的解析提供支撐。

MarkUpBuilder

DevUIHelper 插件使用了 MarkDown 模式的文本進行提示,可是在 LSP 中,vscode 暫時沒有提強大的markDown 語法編輯器,所以,咱們指望使用這個工具模塊文檔分段添加 文本 內容,而且使得代碼更加語義化。

export class MarkUpBuilder{

private markUpContent:MarkupContent;
constructor(content?:string){
    this.markUpContent=  {kind:MarkupKind.Markdown,value:content?content:""};
}

getMarkUpContent():MarkupContent{
    return this.markUpContent;
}

addContent(content:string){
    this.markUpContent.value+=content;
    this.markUpContent.value+='\\n\\n';
    return this;
}

addCodeBlock(type:string,content:string\[\]){
    content = content.filter(e=>e!="");
    this.markUpContent.value+= 
         \[
            '\`\`\`'+type,
             ...content,
            '\`\`\`'
        \].join('\\n');
    return this;
}

setSpecialContent(type:string,content:string){
    this.markUpContent.value='\`\`\`'+type+'\\n'+content+'\\n\`\`\`';
    return this;
}

}

結語

截止這篇文章發稿以前,DevUIHelper 已經得到了 211 次獨立下載量了,在插件剛起步的時候,咱們發現關於 VSCode 插件的中文教程與討論比較少, 咱們但願經過文章來與更多喜好插件的開發者進行交流。

若是想要更進一步的瞭解 VSCode 插件,建議參照 VSCode 插件 官方文檔, 此外,中文社區也有許多很是優秀的入門教程,例如小茗同窗的 VSCode 插件教程 、JTag 特工的 快餐式VSCode 插件教程 等。這些教程很是全面的介紹了 VSCode 的 API。

最後,祝你們使用愉快~

加入咱們

咱們是DevUI團隊,歡迎來這裏和咱們一塊兒打造優雅高效的人機設計/研發體系。招聘郵箱:muyang2@huawei.com

做者: 動次打次咚咚咚

責編: DevUI團隊

往期文章推薦

《好用到飛起!VSCode插件DevUIHelper設計開發全攻略(二)》

《Web界面深色模式和主題化開發》

《手把手教你搭建一個灰度發佈環境》

相關文章
相關標籤/搜索