嵌入式编程语言
Visual Studio Code 为编程语言提供了丰富的语言功能。正如您在语言服务器扩展指南中所读,您可以编写语言服务器来支持任何编程语言。但是,为嵌入式语言启用此类支持需要更多工作。
如今,越来越多的嵌入式语言,例如
- HTML 中的 JavaScript 和 CSS
- JavaScript 中的 JSX
- 模板语言中的插值,例如 Vue、Handlebars 和 Razor
- PHP 中的 HTML
本指南侧重于为嵌入式语言实现语言功能。如果您有兴趣为嵌入式语言提供语法高亮,您可以在语法高亮指南中找到信息。
本指南包含两个示例,它们说明了构建此类语言服务器的两种方法:语言服务和请求转发。我们将回顾这两个示例,并总结每种方法的优缺点。
两个示例的源代码可以在以下位置找到
以下是我们将要构建的嵌入式语言服务器
两个示例都为了说明目的贡献了一种新的语言,html1
。您可以创建一个名为.html1
的文件并测试以下功能
- HTML 标签的代码补全
<style>
标签中 CSS 的代码补全- CSS 的诊断(仅在语言服务示例中)
语言服务
语言服务是一个库,它为单一语言实现编程语言功能。语言服务器可以嵌入语言服务来处理嵌入式语言。
以下是 VS Code 的 HTML 支持概述
- 内置的html 扩展只为 HTML 提供语法高亮和语言配置。
- 内置的html-language-features 扩展包含一个 HTML 语言服务器,为 HTML 提供编程语言功能。
- HTML 语言服务器使用vscode-html-languageservice来支持 HTML。
- CSS 语言服务器使用vscode-css-languageservice来支持 HTML 中的 CSS。
HTML 语言服务器分析 HTML 文档,将其分解为语言区域,并使用相应的语言服务来处理语言服务器请求。
例如
- 对于在
<|
处的自动完成请求,HTML 语言服务器使用 HTML 语言服务来提供 HTML 代码补全。 - 对于在
<style>.foo { | }</style>
处的自动完成请求,HTML 语言服务器使用 CSS 语言服务来提供 CSS 代码补全。
让我们检查一下lsp-embedded-language-service示例,这是 HTML 语言服务器的简化版本,它实现了 HTML 和 CSS 的自动完成,以及 CSS 的诊断错误。
语言服务示例
注意:本示例假设您了解编程语言功能主题和语言服务器扩展指南。代码建立在lsp-sample之上。
源代码可在microsoft/vscode-extension-samples中找到。
与lsp-sample相比,客户端代码相同。
如上所述,服务器将文档分解为不同的语言区域来处理嵌入式内容。
以下是一个简单的示例
<div></div>
<style>.foo { }</style>
在这种情况下,服务器检测到<style>
标签,并将.foo { }
标记为 CSS 区域。
在特定位置进行自动完成请求时,服务器使用以下逻辑来计算响应
- 如果位置落在任何区域内
- 使用带有区域语言的虚拟文档进行处理,同时将所有其他区域替换为空格
- 如果位置落在任何区域之外
- 使用 HTML 中的虚拟文档进行处理,同时将所有区域替换为空格
例如,当在此位置进行自动完成时
<div></div>
<style>.foo { | }</style>
服务器确定位置在区域内,并计算出以下内容的虚拟 CSS 文档(█ 代表空格)
███████████
███████.foo { | }████████
然后,服务器使用vscode-css-languageservice
来分析此文档并计算代码补全项列表。由于内容现在不包含任何 HTML,因此 CSS 语言服务可以毫无问题地处理它。通过将所有非 CSS 内容替换为空格,我们无需手动偏移位置。
处理代码补全请求的服务器代码
connection.onCompletion(async (textDocumentPosition, token) => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return null;
}
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
if (!mode || !mode.doComplete) {
return CompletionList.create();
}
const doComplete = mode.doComplete!;
return doComplete(document, textDocumentPosition.position);
});
负责处理落在 CSS 区域内的所有语言服务器请求的 CSS 模式
export function getCSSMode(
cssLanguageService: CSSLanguageService,
documentRegions: LanguageModelCache<HTMLDocumentRegions>
): LanguageMode {
return {
getId() {
return 'css';
},
doComplete(document: TextDocument, position: Position) {
// Get virtual CSS document, with all non-CSS code replaced with whitespace
const embedded = documentRegions.get(document).getEmbeddedDocument('css');
// Compute a response with vscode-css-languageservice
const stylesheet = cssLanguageService.parseStylesheet(embedded);
return cssLanguageService.doComplete(embedded, position, stylesheet);
}
};
}
这是一种简单有效的方法来处理嵌入式语言。但是,这种方法也有一些缺点
- 您必须不断更新语言服务器依赖的语言服务。
- 包含并非用与语言服务器相同语言编写的语言服务可能具有挑战性。例如,用 PHP 编写的 PHP 语言服务器会发现包含用 TypeScript 编写的
vscode-css-languageservice
很麻烦。
我们现在将介绍请求转发,它可以解决上述问题。
请求转发
简而言之,请求转发的工作方式类似于语言服务。请求转发方法也接收语言服务器请求,计算虚拟内容并计算响应。
主要区别在于
- 语言服务方法使用库来计算语言服务器响应,而请求转发将请求发送回 VS Code 以使用已激活并为嵌入式语言注册了代码补全提供程序的扩展。
以下再次是一个简单的示例
<div></div>
<style>.foo { | }</style>
自动完成按以下方式进行
- 语言客户端使用
workspace.registerTextDocumentContentProvider
为embedded-content
文档注册一个虚拟文本文档提供程序。 - 语言客户端拦截
<FILE_URI>
的代码补全请求。 - 语言客户端确定请求位置落在 CSS 区域内。
- 语言客户端构造一个新的 URI,例如
embedded-content://css/<FILE_URI>.css
。 - 然后语言客户端调用
commands.executeCommand('vscode.executeCompletionItemProvider', ...)
- VS Code 的 CSS 语言服务器响应此提供程序请求。
- 虚拟文本文档提供程序为 CSS 语言服务器提供虚拟内容,其中所有非 CSS 代码都被替换为空格。
- 语言客户端从 VS Code 接收响应并将其作为响应发送。
使用这种方法,即使我们的代码不包含任何理解 CSS 的库,我们也能计算出 CSS 自动完成。随着 VS Code 更新其 CSS 语言服务器,我们无需更新代码即可获得最新的 CSS 语言支持。
现在让我们回顾一下示例代码。
请求转发示例
注意:本示例假设您了解编程语言功能主题和语言服务器扩展指南。代码建立在lsp-sample之上。
源代码可在microsoft/vscode-extension-samples中找到。
维护文档 URI 与它们的虚拟文档之间的映射,并在相应的请求时提供它们
const virtualDocumentContents = new Map<string, string>();
workspace.registerTextDocumentContentProvider('embedded-content', {
provideTextDocumentContent: uri => {
// Remove leading `/` and ending `.css` to get original URI
const originalUri = uri.path.slice(1).slice(0, -4);
const decodedUri = decodeURIComponent(originalUri);
return virtualDocumentContents.get(decodedUri);
}
});
通过使用语言客户端的middleware
选项,我们拦截代码补全请求
let clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'html' }],
middleware: {
provideCompletionItem: async (document, position, context, token, next) => {
// If not in `<style>`, do not perform request forwarding
if (
!isInsideStyleRegion(
htmlLanguageService,
document.getText(),
document.offsetAt(position)
)
) {
return await next(document, position, context, token);
}
const originalUri = document.uri.toString(true);
virtualDocumentContents.set(
originalUri,
getCSSVirtualContent(htmlLanguageService, document.getText())
);
const vdocUriString = `embedded-content://css/${encodeURIComponent(originalUri)}.css`;
const vdocUri = Uri.parse(vdocUriString);
return await commands.executeCommand<CompletionList>(
'vscode.executeCompletionItemProvider',
vdocUri,
position,
context.triggerCharacter
);
}
}
};
潜在问题
在实现嵌入式语言服务器时,我们遇到了许多问题。虽然我们还没有找到完美的解决方案,但我们想提醒您注意,您也可能会遇到这些问题。
难以实现语言功能
通常,跨语言区域边界工作的语言功能更难实现。例如,自动完成或悬停内容很容易实现,因为您可以检测嵌入式内容的语言并根据嵌入式内容计算响应。但是,格式化或重命名等语言功能可能需要特殊处理。在格式化的情况下,您需要处理单个文档内多个区域的缩进和格式化程序设置。对于重命名,跨不同文档中的不同区域使其工作可能具有挑战性。
语言服务可能是有状态的,并且难以嵌入
VS Code 的 HTML 支持提供了 HTML、CSS 和 JavaScript 的语言功能。尽管 HTML 和 CSS 语言服务是非状态的,但为 JavaScript 语言功能提供支持的 TypeScript 服务器是有状态的。我们只在 HTML 文档中提供基本的 JavaScript 支持,因为难以向 TypeScript 通知项目的当前状态。例如,如果你包含一个指向 CDN 上的 `lodash` 库的 `<script>` 标签,你将不会在 `<script>` 标签内获得 `_.` 的自动补全功能。
编码和解码
文档的主要语言可能具有与其嵌入语言不同的编码或转义规则。例如,根据 HTML 规范,此 HTML 文档是无效的。
<SCRIPT type="text/javascript">
document.write ("<EM>This won't work</EM>")
</SCRIPT>
在这种情况下,如果嵌入 JavaScript 的语言服务器返回包含 `</` 的结果,则应将其转义为 `<\/`。
结论
两种方法都有其优缺点。
语言服务
- + 完全控制语言服务器和用户体验。
- + 不依赖于其他语言服务器。所有代码都在一个仓库中。
- + 语言服务器可以在所有 支持 LSP 的代码编辑器 中重复使用。
- - 嵌入用其他语言编写的语言服务可能很困难。
- - 需要持续维护以获得来自语言服务依赖项的新功能。
请求转发
- + 避免嵌入未用语言服务器语言编写的语言服务的问题(例如,在 Razor 语言服务器中嵌入 C# 编译器以支持 C#)。
- + 无需维护即可从其他语言服务获取上游的新功能。
- - 不适用于诊断错误。VS Code API 不支持可以“拉取”(请求)诊断信息的诊断提供程序。
- - 由于缺乏控制,难以将状态共享给其他语言服务器。
- - 跨语言功能可能难以实现(例如,当存在 `<div class="foo">` 时,为 `.foo` 提供 CSS 自动补全功能)。
总的来说,我们建议通过嵌入语言服务来构建语言服务器,因为这种方法可以让你更好地控制用户体验,并且服务器可以重复用于任何支持 LSP 的编辑器。但是,如果你有一个简单的用例,其中嵌入内容可以轻松地无需上下文或语言服务器状态即可处理,或者如果将 Node.js 库捆绑在一起对你来说是一个问题,你可以考虑请求转发方法。