組み込みプログラミング言語
Visual Studio Code は、プログラミング言語に豊富な言語機能を提供します。言語サーバー拡張機能ガイドで読んだように、任意のプログラミング言語をサポートするために言語サーバーを作成できます。ただし、組み込み言語に対してそのようなサポートを有効にするには、より多くの労力が必要です。
今日、次のような組み込み言語が増加しています。
- HTML 内の JavaScript と CSS
- JavaScript 内の JSX
- テンプレート言語 (例: Vue、Handlebars、Razor) の補間
- PHP 内の HTML
このガイドでは、組み込み言語の言語機能の実装に焦点を当てます。組み込み言語のシンタックス ハイライトの提供に関心がある場合は、シンタックス ハイライト ガイドで情報を見つけることができます。
このガイドには、このような言語サーバーを構築するための 2 つのアプローチを説明する 2 つのサンプルが含まれています: Language Services と Request Forwarding です。両方のサンプルをレビューし、それぞれのアプローチの長所と短所についてまとめます。
両方のサンプルのソース コードは、次の場所にあります。
構築する組み込み言語サーバーは次のとおりです。

両方のサンプルでは、説明のために新しい言語 html1 を提供しています。.html1 ファイルを作成し、次の機能をテストできます。
- HTML タグの補完
<style>タグ内の CSS の補完- CSS の診断 (Language Services サンプルのみ)
Language Services
言語サービスは、単一言語のプログラマティック言語機能を実装するライブラリです。言語サーバーは、組み込み言語を処理するために言語サービスを組み込むことができます。
VS Code の HTML サポートの概要を次に示します。
- 組み込みのhtml 拡張機能は、HTML のシンタックス ハイライトと言語構成のみを提供します。
- 組み込みのhtml-language-features 拡張機能には、HTML のプログラマティック言語機能を提供する HTML 言語サーバーが含まれています。
- HTML 言語サーバーは、HTML をサポートするためにvscode-html-languageserviceを使用します。
- CSS 言語サーバーは、HTML 内の CSS をサポートするためにvscode-css-languageserviceを使用します。
HTML 言語サーバーは HTML ドキュメントを分析し、それを言語領域に分割し、対応する言語サービスを使用して言語サーバー要求を処理します。
例
<|での自動補完要求の場合、HTML 言語サーバーは HTML 言語サービスを使用して HTML 補完を提供します。<style>.foo { | }</style>での自動補完要求の場合、HTML 言語サーバーは CSS 言語サービスを使用して CSS 補完を提供します。
HTML と CSS の自動補完、および CSS の診断エラーを実装する HTML 言語サーバーの簡略版であるlsp-embedded-language-serviceサンプルを調べましょう。
Language Services サンプル
注: このサンプルは、プログラマティック言語機能トピックと言語サーバー拡張機能ガイドの知識があることを前提としています。コードは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 領域に収まると判断します。
- 言語クライアントは、
embedded-content://css/<FILE_URI>.cssのような新しい URI を構築します。 - 言語クライアントは、
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
);
}
}
};
潜在的な問題
組み込み言語サーバーを実装する際に、多くの問題に遭遇しました。まだ完璧な解決策はありませんが、これらの問題に遭遇する可能性が高いため、注意喚起しておきたいと思います。
言語機能の実装の難しさ
一般的に、言語領域の境界を越えて機能する言語機能は実装が困難です。たとえば、自動補完やホバーコンテンツは、組み込みコンテンツの言語を検出し、組み込みコンテンツに基づいて応答を計算できるため、実装が簡単です。ただし、書式設定や名前変更などの言語機能には、特別な処理が必要になる場合があります。書式設定の場合、単一のドキュメント内の複数の領域のインデントと書式設定設定を処理する必要があります。名前変更の場合、異なるドキュメント内の異なる領域で機能させるのは困難な場合があります。
Language Services はステートフルで組み込みが難しい
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 の言語サーバーが</を含む結果を返す場合、それは<\/にエスケープされるべきです。
まとめ
どちらのアプローチにも長所と短所があります。
Language Service
- + 言語サーバーとユーザーエクスペリエンスを完全に制御できる。
- + 他の言語サーバーに依存しない。すべてのコードは1つのリポジトリにある。
- + 言語サーバーは、すべてのLSP準拠のコードエディターで再利用できる。
- - 他の言語で記述された言語サービスを組み込むのが難しい場合がある。
- - 言語サービスの依存関係から新機能を取得するために継続的なメンテナンスが必要。
要求転送
- + 言語サーバーの言語で記述されていない言語サービスを組み込む際の問題を回避できる (例: C# をサポートするために Razor 言語サーバーに C# コンパイラを組み込む)。
- + 他の言語サービスからアップストリームの新機能を取得するためにメンテナンスは不要。
- - 診断エラーでは機能しない。VS Code API は、診断を「プル」(要求) できる診断プロバイダーをサポートしていない。
- - 制御の欠如により、他の言語サーバーに状態を共有するのが難しい。
- - クロス言語機能の実装が難しい場合がある (例:
<div class="foo">が存在する場合に.fooの CSS 補完を提供する)。
全体として、言語サービスを埋め込むことで言語サーバーを構築することをお勧めします。このアプローチにより、ユーザーエクスペリエンスをより細かく制御でき、サーバーは LSP 準拠の任意のエディターで再利用できるためです。ただし、組み込みコンテンツがコンテキストや言語サーバーの状態なしで簡単に処理できる単純なユースケースの場合、または Node.js ライブラリのバンドルが問題となる場合は、リクエスト転送アプローチを検討できます。