嵌入式编程语言
Visual Studio Code 为编程语言提供了丰富的语言功能。正如你在 语言服务器扩展指南中所读到的,你可以编写语言服务器来支持任何编程语言。然而,为嵌入式语言启用这种支持需要更多的努力。
今天,嵌入式语言的数量在不断增加,例如:
- HTML中的JavaScript和CSS
- JavaScript中的JSX
- 在模板语言(例如Vue、Handlebars和Razor)中的插值。
- HTML 在 PHP 中
本指南重点介绍嵌入式语言的语法功能的实现。如果您对为嵌入式语言提供语法高亮感兴趣,可以在语法高亮指南中找到相关信息。
本指南包括两个示例,分别展示了两种构建语言服务器的方法:语言服务和请求转发。我们将审查这两个示例,并总结每种方法的优缺点。
两个示例的源代码可以在以下位置找到:
这是我们将会构建的嵌入式语言服务器:

两个样本都贡献了一种新语言,html1,为了说明。你可以创建一个文件.html1并测试以下功能:
- HTML标签的完成情况
- CSS 完成
<style>Tab - 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和CSS自动补全以及CSS诊断错误的HTML语言服务器的简化版本。
语言服务示例
注意:此示例假定对编程语言特性和语言服务器扩展指南的了解。代码在此基础上构建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```plaintext
= documentRegions.get(document).getEmbeddedDocument('css');
// 计算一个响应与 vscode-css-languageservice
const stylesheet = cssLanguageService.parseStylesheet(embedded);
return cssLanguageService.doComplete(embedded, position, stylesheet);
}
};
}
```
这是一个处理嵌入式语言的简单而有效的方法。然而,这个方法有一些缺点:
- 你必须不断更新你的语言服务器所依赖的语言服务。
- 将非同一种语言编写的语言服务包含在内可能会具有挑战性。例如,一个用PHP编写的PHP语言服务器会发现包含
vscode-css-languageservice用TypeScript编写的。
现在我们将讨论 请求转发,这将解决上述问题。
请求转发
简而言之,请求转发与语言服务的工作方式类似。请求转发的方法同样接收语言服务器的请求,计算虚拟内容,并计算响应。
主要差异是:
- 当语言服务方法使用库来计算语言服务器响应时,请求转发将请求发送回VS Code,以使用已激活并为嵌入式语言注册了完成提供程序的扩展。
这是一个简单的例子:
<div</div>
<style>.foo { | }</style>
自动完成是这样进行的:
- 语言客户端注册了一个虚拟文本文档提供者用于
嵌入内容文档使用工作区注册文本文档内容提供者输入:. - 语言客户端劫持了完成请求用于
<文件路径>输入:. - 语言客户端确定请求位置属于CSS区域。
- 语言客户端构建一个新的URI,例如
嵌入式内容://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和其虚拟文档之间的映射,并为相应的请求提供这些映射:
常量 虚拟文档内容 = 新的 Map<字符串, 字符串>();
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(解码的URI);
}
});
通过使用中间件选项为语言客户端,我们劫持自动完成请求:
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())
);
```plaintext
const vdocUriString = `embedded-content://css/${encodeURIComponent(originalUri)}.css`;
const vdocUri = Uri.parse(vdocUriString);
return await commands.executeCommand
}
潜在问题
在实现嵌入式语言服务器时,我们遇到了许多问题。尽管我们还没有完美的解决方案,但鉴于您可能会遇到这些问题,我们希望提前告知您。
难以实现的语言特性
通常,跨越语言区域边界的语言功能更难实现。例如,自动补全或悬停内容很容易实现,因为你可以检测嵌入内容的语言,并根据嵌入内容计算响应。然而,格式化或重命名等语言功能可能需要特殊处理。在格式化的情况下,你需要处理单个文档中多个区域的缩进和格式化设置。对于重命名,使其在不同文档的不同区域之间工作可能会很具有挑战性。
语言服务可以是状态化的,并且难以嵌入
VS Code 的 HTML 支持提供了 HTML、CSS 和 JavaScript 语言特性。尽管 HTML 和 CSS 语言服务是非状态化的,但为 JavaScript 语言特性提供动力的 TypeScript 服务器是状态化的。由于难以向 TypeScript 通知项目的状态,我们在 HTML 文档中仅提供基本的 JavaScript 支持。例如,如果您包含一个<脚本>标签指向Lodash库托管在CDN上,你将无法获得输入:_.完成内部<脚本>标签。
编码和解码
文档的主要语言可能与其嵌入的语言有不同的编码或转义规则。例如,根据HTML规范,这个HTML文档是无效的:
<SCRIPT type="text/javascript">
document.write ("<EM>这不会起作用</EM>")
</SCRIPT>
在这种情况下,如果嵌入式JavaScript的语言服务器返回的结果包含</,它应该被转义为</输入:.
结论
两种方法都有其优点和缺点。
语言服务:
- + 完全控制语言服务器和用户体验。
- + 不依赖于其他语言服务器。所有代码都在一个仓库中。
- + 语言服务器可以在所有 LSP兼容的代码编辑器中重用。
- - 可能难以嵌入用其他语言编写的语言服务。
- - 需要持续维护以从语言服务依赖项中获取新功能。
请求转发:
- + 避免嵌入非语言服务器语言的的语言服务(例如,在Razor语言服务器中嵌入C#编译器以支持C#)。
- + 不需要维护即可从其他语言服务中获取新的上游功能。
- - 不适用于诊断错误。VS Code API 不支持可以“拉取”(请求)诊断的诊断提供程序。
- - 由于缺乏控制,很难将状态共享到其他语言服务器。
- - 跨语言功能可能难以实现(例如,为CSS提供自动完成)
.foo当存在)。
总体而言,我们建议通过嵌入语言服务来构建语言服务器,因为这种方法可以让你更多地控制用户体验,并且服务器可以为任何符合LSP的编辑器重复使用。然而,如果你有一个简单的用例,嵌入的内容可以轻松处理,无需上下文或语言服务器状态,或者如果你在捆绑Node.js库时遇到问题,你可以考虑请求转发方法。