好用到飞起!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界面深色模式和主题化开发》

《手把手教你搭建一个灰度发布环境》

相关文章
相关标签/搜索