前言
在上一期的博客中,我们深入探讨了如何为嵌入式语言实现语法高亮,从而为代码添加了色彩和样式的展示。然而,在那篇博客中,尽管我们取得了显著的进展,但在代码编写过程中仍然缺乏智能提示的功能。本期将紧接着上一期的内容,继续探讨如何为我们的语言插件添加代码智能提示,进一步提升开发体验。
代码智能提示的重要性
代码智能提示是现代代码编辑器不可或缺的功能之一。它可以极大地提高编码效率,减少开发者在查找函数名、属性名等信息时的时间和精力。通过智能提示,编辑器能够根据上下文分析,向开发者推荐可能的代码选项,从而在编码过程中提供实时的建议和补全。
在前一篇博客中,我们已经建立了一个基础的语法高亮插件,但在实际开发中,光靠高亮还远远不够。在大型项目中,函数、变量、属性等可能有着繁多的命名,有时甚至难以记忆。这时,代码智能提示的作用就凸显出来了,它可以帮助我们快速定位需要的代码元素,提供相应的选项,使得编码更加准确和流畅。
语言服务
语言服务器是一种独立的进程,它提供了与特定编程语言或技术相关的功能,如代码分析、智能提示、代码补全、语法检查等。VSCode的语言服务器框架允许开发者为任何编程语言或技术创建独立的语言服务器,然后将其与编辑器进行集成。 VSCode的语言服务架构基于客户端-服务器模型。这个模型中,编辑器本身充当客户端,而语言服务器则充当服务器。编辑器与语言服务器之间通过标准的通信协议进行交互,从而实现代码分析和智能提示等功能。
主要的通信协议是Language Server Protocol(LSP),它是一种用于客户端和服务器之间进行通信的协议,用于请求代码分析、获取智能提示建议、错误检查等操作。这种分离的架构使得编辑器与语言服务器之间的协作更加松耦合,同时也方便了语言服务器的开发与维护。
每一种语言都需要相应的语言服务,我们插件的目的就是需要在ts中@component
中template
中嵌入html语言,也就是相应的需求嵌入html语言服务
语言服务是一种架构模式,用于将编程语言相关的功能从编辑器的前端分离出来,形成独立的服务。它由两部分组成:语言客户端和语言服务端。
- 语言客户端:通常就是编辑器本身,它负责与用户进行交互,接收用户的输入,并将请求发送给语言服务端。在我们的情境中,编辑器即为VSCode。
- 语言服务端:它是一个独立的进程,负责执行实际的语言分析、智能提示、代码补全等功能。语言服务端会维护一个完整的代码分析环境,以便更好地理解和处理代码。
语言客户端
js
import * as vscode from 'vscode';
import * as path from 'path';
import * as lsp from 'vscode-languageclient/node';
import { isInsideInlineTemplateRegion } from './embedded_support';
export class VgLanguageClient implements vscode.Disposable {
// 语言客户端实例
private client: lsp.LanguageClient | null = null;
// 用于管理资源释放
private readonly disposables: vscode.Disposable[] = [];
// 存储虚拟文档的内容
private readonly virtualDocumentContents = new Map<string, string>();
// 语言服务的名称
private readonly name = 'vg Language Service';
// 语言客户端选项
private readonly clientOptions: lsp.LanguageClientOptions;
constructor(private readonly context: vscode.ExtensionContext) {
// 注册虚拟文档提供者
vscode.workspace.registerTextDocumentContentProvider('vg-embedded-content', {
provideTextDocumentContent: uri => {
return this.virtualDocumentContents.get(uri.toString());
}
});
// 配置语言客户端选项
this.clientOptions = {
// 哪些文件类型要处理
documentSelector: [
{ scheme: 'file', language: 'html' },
{ scheme: 'file', language: 'typescript' },
],
middleware: {
// 自动完成中间件
provideCompletionItem: async (
document: vscode.TextDocument, position: vscode.Position,
context: vscode.CompletionContext, token: vscode.CancellationToken,
next: lsp.ProvideCompletionItemsSignature) => {
// 确保在template中
if (!(await this.isInVgProject(document)) ||
!isInsideInlineTemplateRegion(document, position)) {
return;
}
if (document.languageId === 'typescript') {
const vdocUri = this.createVirtualHtmlDoc(document);
const htmlProviderCompletionsPromise =
vscode.commands.executeCommand<vscode.CompletionList>(
'vscode.executeCompletionItemProvider', vdocUri, position,
context.triggerCharacter);
const htmlProviderCompletions = await htmlProviderCompletionsPromise;
return htmlProviderCompletions?.items ?? []
}
return [];
},
}
}
}
private createVirtualHtmlDoc(document: vscode.TextDocument): vscode.Uri {
// 真实文件路径
const originalUri = document.uri.toString();
const vdocUri = vscode.Uri.file(encodeURIComponent(originalUri) + '.html')
// authority: 'html' 用于标识这个虚拟文档的内容类型
.with({ scheme: 'vg-embedded-content', authority: 'html' });
// 将虚拟 HTML 文档的 URI 作为键,将原始文档的文本内容作为值
this.virtualDocumentContents.set(vdocUri.toString(), document.getText());
// 返回创建的虚拟 HTML 文档的 URI
return vdocUri;
}
// 判断是否在项目中
private async isInVgProject(doc: vscode.TextDocument): Promise<boolean> {
// 需要更具体的逻辑判断
return true
}
async stop(): Promise<void> {
if (this.client === null) {
return;
}
await this.client.stop();
this.outputChannel.clear();
this.dispose();
this.client = null;
this.virtualDocumentContents.clear();
}
async start(): Promise<void> {
if (this.client !== null) {
throw new Error(`An existing client is running. Call stop() first.`);
}
const serverModule = this.context.asAbsolutePath(
path.join('server', 'out', 'server.js')
);
const serverOptions: lsp.ServerOptions = {
run: { module: serverModule, transport: lsp.TransportKind.ipc },
debug: {
module: serverModule,
transport: lsp.TransportKind.ipc,
}
};
this.client = new lsp.LanguageClient(
'vg',
this.name,
serverOptions,
this.clientOptions
);
this.disposables.push(this.client.start());
}
dispose() {
for (let d = this.disposables.pop(); d !== undefined; d = this.disposables.pop()) {
d.dispose();
}
}
}
let client: VgLanguageClient;
// 插件激活函数
export function activate(context: ExtensionContext) {
client = new VgLanguageClient(context);
client.start().then();
}
// 插件停用函数
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
请求转发与语言服务通信
在上述代码中,我们创建了一个名为VgLanguageClient
的类,用于管理语言客户端的功能。在这个类的构造函数中,我们首先通过registerTextDocumentContentProvider
方法注册了一个文本文档内容提供者。这个提供者用于在虚拟文档中展示嵌入式语言的内容。
接着,我们定义了clientOptions
对象,其中包含了各种中间件函数。在这些函数中,我们实现了智能提示的功能。例如,我们通过provideCompletionItem
函数,在用户输入时判断当前位置是否在嵌入式模板中,然后转发请求给语言服务端
*
创建虚拟文档
为了实现智能提示,我们需要创建一个虚拟的HTML文档,用于存放嵌入式模板的内容。在createVirtualHtmlDoc
函数中,我们将原始的ts文件路径编码后与.html
拼接,得到虚拟文档的URI。然后,我们将虚拟文档的URI作为键,将原始文档的内容作为值,存储在virtualDocumentContents
中。
*
请求智能提示建议
当用户在编辑器中输入时,我们通过调用executeCompletionItemProvider
命令来请求智能提示建议。这个命令将会触发我们在provideCompletionItem
函数中定义的逻辑。在这个逻辑中,我们判断当前位置是否在嵌入式模板中,如果是,则构造虚拟HTML文档的URI并发送请求给VSCode的内置智能提示提供者。
语言服务端
在前文中,我们已经详细介绍了语言客户端的实现,包括如何通过请求转发与语言服务端通信,以及如何在语言客户端中实现智能提示功能。本章节将会深入探讨语言服务端的实现,该部分将负责处理智能提示请求,并提供相应的补全建议
js
import * as lsp from 'vscode-languageserver/node';
import { getLanguageService } from 'vscode-html-languageservice';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { TextDocuments } from 'vscode-languageserver';
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// 获取HTML语言服务
const htmlLanguageService = getLanguageService();
export class Service {
private readonly connection: lsp.Connection;
constructor() {
// 创建连接
this.connection = lsp.createConnection(lsp.ProposedFeatures.all);
this.addProtocolHandlers(this.connection);
documents.listen(this.connection);
}
private addProtocolHandlers(conn: lsp.Connection) {
conn.onInitialize(p => this.onInitialize(p));
// conn.onHover(p => this.onHover(p));
// 这个处理函数提供了初始补全项列表
conn.onCompletion(p => this.onCompletion(p));
// 这个函数为补全列表的选中项提供了更多信息
// conn.onCompletionResolve(p => this.onCompletionResolve(p));
}
private onInitialize(_: lsp.InitializeParams): lsp.InitializeResult {
const result: lsp.InitializeResult = {
capabilities: {
// 增量式文本文档同步
textDocumentSync: lsp.TextDocumentSyncKind.Incremental,
// 告诉客户端该服务器支持代码完成。
completionProvider: {
resolveProvider: true,
/**
* 是指代码补全触发字符(trigger characters)的列表。
* 这些字符指的是在你在编辑器中输入代码时,
* 如果输入了其中一个字符,编辑器会发起代码补全请求
*/
triggerCharacters: ['<', '.', '*', '[', '(', '$', '|']
}
}
};
return result
}
// 补全处理函数
private onCompletion(params: lsp.CompletionParams): lsp.CompletionList | null {
const document = documents.get(params.textDocument.uri);
if (!document) {
return null;
}
return htmlLanguageService.doComplete(
document,
params.position,
htmlLanguageService.parseHTMLDocument(document)
);
}
listen(): void {
this.connection.listen();
}
}
function main() {
const server = new Service()
server.listen()
}
main()
- 获取HTML语言服务实例,用于处理HTML文档。
- 定义了
Service
类,用于管理语言服务端。 - 在构造函数中,创建了一个连接
connection
,并通过addProtocolHandlers
函数添加事件处理函数。 onInitialize
函数用于初始化语言服务端,在这里设置了文本文档同步方式以及补全提供者的相关设置。onCompletion
函数处理补全请求,获取文档并调用HTML语言服务的补全函数,最后返回补全建议列表,这部分目前使用的是vscode-html-languageservice
服务,对于我们开发的面向对象插件,这样还远远不够,需要自定义。listen
函数用于监听连接,启动语言服务端。- 在代码末尾,我们定义了
main
函数,用于创建并启动Service
实例,并通过调用listen
函数开始监听连接。
通过以上的实现,我们在语言服务端中成功地实现了补全请求的逻辑,为嵌入式语言提供了智能提示功能。整个过程中,语言服务端与客户端之间通过协议通信,提供了无缝的用户体验
运行效果
总结
在本文中,我们深入探讨了如何通过语言服务实现嵌入式 HTML 语言服务的智能提示功能。通过阐述实际的代码实现,我们展示了在 Visual Studio Code 中如何使用扩展开发技术,将智能提示应用于特定类型的代码,提高开发效率和代码质量。
我们首先介绍了语言服务的概念,解释了它在扩展开发中的重要作用,以及它是如何通过客户端和服务端之间的通信实现智能提示等功能的。随后,我们详细讲解了如何在语言客户端和语言服务端中分别实现智能提示功能的核心代码逻辑。我们着重探讨了如何创建虚拟 HTML 文档以支持嵌入式语言中的智能提示需求,并介绍了与 VSCode 内置智能提示提供者交互的过程。
本文的内容,强调了案例的简单性和实用性,将来会将面向对象的 VSCode 插件完整代码上传到 GitHub 。如果您希望深入了解如何开发面向对象的 VSCode 插件,可以关注我们的 GitHub 仓库:github.com/chengdongha...,以获取更多详细的代码和文档。
通过本文的学习,您将能够更好地理解语言服务的应用和开发流程,为自己的扩展开发项目增添更多可能性。无论是提供智能提示、代码分析还是其他功能,语言服务都为您的扩展插件带来了更高的灵活性和定制性。希望本文能为您的扩展开发之路提供有益的指引和启示。