乐趣区

关于javascript:好用到飞起VSCode插件DevUIHelper设计开发全攻略三

DevUI 是一支兼具设计视角和工程视角的团队,服务于华为云 DevCloud 平台和华为外部数个中后盾零碎,服务于设计师和前端工程师。
官方网站:devui.design
Ng 组件库:ng-devui(欢送 Star)
官网交换群:增加 DevUI 小助手(微信号:devui-official)
DevUIHelper 插件:DevUIHelper-LSP(欢送 Star)

引言

嗨,咱们是 DevUIHelper 的开发团队。明天,让咱们聊一下咱们插件开发中利用的一些技术计划。这些教训兴许对您的插件开发有帮忙,让咱们开始吧!

综述

因为咱们的插件运行在 VSCode 上,咱们应用了一些 VSCode 提供的能力,例如 LSP 协定,补全、悬停提醒接口等。同时,插件的性能由多个独立性能的模块进行实现。接下来咱们依据模块别离进行介绍。

LSP 协定

VSCode 对代码补全插件大体提供了本地与 LSP 两种计划。本地的插件补全能够间接利用 VSCode 供的一些能力,但 LSP 协定为跨编辑器应用提供了可能,思考到咱们的插件将来可能不仅仅运行在 VSCode 平台上,咱们最终抉择了 LSP 协定。

LSP 协定的愿景,多个 IDE 应用同一套补全提醒零碎。

createConnection API

LSP 协定是基于 客户端 - 服务器 模式的,所以应用 LSP 协定的第一步,便是发明一个客户端与服务器的链接,这时,你须要在服务器端输出这样一段代码:

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

这样你就创立了一个默认规定的 LSP 连贯。然而仅有服务器显然是不行的,倡议通过微软官网提供的 LSP 种子我的项目开始进行创作。同时,VSCode 还提供了大量不同场景下的种子我的项目。
上面的介绍将基于 DevUIHelper-LSP 我的项目,倡议配合代码食用。顺便求一波 star~。

DConnection

因为间接应用 vscode 提供的 API 会导致代码十分扩散不易浏览,DConnection 对于 VSCode 提供的连贯进行的一次封装,这样,你能够不便的对所有的性能函数进行治理:

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 界面深色模式和主题化开发》

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

退出移动版