言語サーバープロトコルチュートリアル:VSCodeからVimへ
公開: 2022-03-11すべての作業の主な成果物は、ほとんどの場合、プレーンテキストファイルです。 では、メモ帳を使用して作成してみませんか?
構文の強調表示と自動フォーマットは、氷山の一角にすぎません。 リンティング、コード補完、および半自動リファクタリングについてはどうですか? これらはすべて、「実際の」コードエディタを使用する非常に良い理由です。 これらは私たちの日常にとって不可欠ですが、私たちはそれらがどのように機能するかを理解していますか?
このLanguageServerProtocolチュートリアルでは、これらの質問を少し調べて、テキストエディタが機能する理由を見つけます。 最後に、VSCode、Sublime Text 3、Vimのサンプルクライアントとともに、基本的な言語サーバーを一緒に実装します。
コンパイラと言語サービス
ここでは、静的分析(それ自体が興味深いトピック)で処理される構文の強調表示と書式設定をスキップし、これらのツールから得られる主なフィードバックに焦点を当てます。 コンパイラと言語サービスの2つの主要なカテゴリがあります。
コンパイラはソースコードを取り込み、別の形式を吐き出します。 コードが言語の規則に従わない場合、コンパイラはエラーを返します。 これらは非常によく知られています。 これに伴う問題は、通常、非常に遅く、範囲が制限されていることです。 コードを作成している間に支援を提供するのはどうですか?
これが言語サービスが提供するものです。 コードベースがまだ作業中である間、それらはあなたにあなたのコードベースへの洞察を与えることができ、そしておそらくプロジェクト全体をコンパイルするよりもはるかに速くなります。
これらのサービスの範囲はさまざまです。 プロジェクト内のすべてのシンボルのリストを返すような単純なものでも、コードをリファクタリングするためのステップを返すような複雑なものでもかまいません。 これらのサービスは、コードエディタを使用する主な理由です。 コンパイルしてエラーを確認したいだけの場合は、数回のキーストロークでそれを行うことができます。 言語サービスは、より多くの洞察を非常に迅速に提供します。
プログラミングのためのテキストエディタへの賭け
特定のテキストエディタをまだ呼び出していないことに注意してください。 その理由を例を挙げて説明しましょう。
Lapineと呼ばれる新しいプログラミング言語を開発したとしましょう。 それは美しい言語であり、コンパイラは素晴らしいエルムのようなエラーメッセージを出します。 さらに、コードの補完、参照、リファクタリングのヘルプ、および診断を提供できます。
最初にサポートするコード/テキストエディタはどれですか? その後はどうですか? あなたは人々にそれを採用させるために苦戦を強いられているので、あなたはそれをできるだけ簡単にしたいのです。 間違ったエディターを選んでユーザーを見逃したくないでしょう。 コードエディタから距離を置き、専門分野である言語とその機能に焦点を当てたらどうなるでしょうか。
言語サーバー
言語サーバーを入力します。 これらは、言語クライアントと通信し、私たちが言及した洞察を提供するツールです。 仮定の状況で説明した理由により、これらはテキストエディタから独立しています。
いつものように、抽象化の別のレイヤーはまさに私たちが必要とするものです。 これらは、言語ツールとコードエディタの緊密な結合を断ち切ることを約束します。 言語作成者は機能をサーバーに一度ラップすることができ、コード/テキストエディターは小さな拡張機能を追加して自分自身をクライアントに変えることができます。 それは誰にとっても勝利です。 ただし、これを容易にするために、これらのクライアントとサーバーがどのように通信するかについて合意する必要があります。
私たちにとって幸運なことに、これは架空のものではありません。 Microsoftは、LanguageServerProtocolの定義をすでに開始しています。
ほとんどの素晴らしいアイデアと同様に、それは先見性ではなく必然的に成長しました。 多くのコードエディタは、すでにさまざまな言語機能のサポートを追加し始めていました。 一部の機能はサードパーティのツールにアウトソーシングされており、一部はエディター内で内部的に実行されています。 スケーラビリティの問題が発生し、Microsoftが主導権を握って物事を分割しました。 はい、Microsoftは、これらの機能をVSCode内に蓄積するのではなく、コードエディターから移動する道を開きました。 彼らはエディターを構築し続け、ユーザーを閉じ込めることができたかもしれませんが、彼らは彼らを解放しました。
言語サーバープロトコル
言語サーバープロトコル(LSP)は、言語ツールとエディターを分離するために2016年に定義されました。 まだ多くのVSCodeフィンガープリントがありますが、これはエディターにとらわれない方向への大きな一歩です。 プロトコルを少し調べてみましょう。
クライアントとサーバー(コードエディタや言語ツールを考えてください)は、単純なテキストメッセージで通信します。 これらのメッセージにはHTTPのようなヘッダーとJSON-RPCコンテンツがあり、クライアントまたはサーバーのいずれかから発信される可能性があります。 JSON-RPCプロトコルは、リクエスト、レスポンス、通知、およびそれらに関するいくつかの基本的なルールを定義します。 重要な機能は、非同期で動作するように設計されているため、クライアント/サーバーがメッセージを順不同である程度の並列処理で処理できることです。
つまり、JSON-RPCを使用すると、クライアントは別のプログラムにパラメーターを使用してメソッドを実行し、結果またはエラーを返すように要求できます。 LSPはこれに基づいて構築され、使用可能なメソッド、予想されるデータ構造、およびトランザクションに関するいくつかのルールを定義します。 たとえば、クライアントがサーバーを起動するときにハンドシェイクプロセスがあります。
サーバーはステートフルであり、一度に1つのクライアントのみを処理することを目的としています。 ただし、通信に明示的な制限はないため、言語サーバーはクライアントとは異なるマシンで実行できます。 ただし、実際には、リアルタイムのフィードバックにはかなり時間がかかります。 言語サーバーとクライアントは同じファイルで動作し、かなりおしゃべりです。
何を探すべきかがわかれば、LSPにはかなりの量のドキュメントがあります。 前述のように、これの多くはVSCodeのコンテキスト内で記述されていますが、アイデアの用途ははるかに広くなっています。 たとえば、プロトコル仕様はすべてTypeScriptで記述されています。 VSCodeとTypeScriptに慣れていないエクスプローラーを支援するために、ここに入門書があります。
LSPメッセージタイプ
言語サーバープロトコルで定義されているメッセージのグループは多数あります。 それらは大きく「管理」と「言語機能」に分けることができます。 管理メッセージには、クライアント/サーバーハンドシェイク、ファイルのオープン/変更などで使用されるメッセージが含まれます。重要なことに、これはクライアントとサーバーが処理する機能を共有する場所です。 確かに、異なる言語とツールは異なる機能を提供します。 これにより、段階的な採用も可能になります。 Langserver.orgは、クライアントとサーバーがサポートする必要のある6つの主要な機能を挙げており、そのうちの少なくとも1つはリストを作成するために必要です。
言語機能は、私たちが最も関心を持っているものです。これらのうち、具体的に呼び出すものが1つあります。それは、診断メッセージです。 診断は重要な機能の1つです。 ファイルを開くと、ほとんどの場合、これが実行されると想定されます。 編集者は、ファイルに問題があるかどうかを通知する必要があります。 これがLSPで発生する方法は次のとおりです。
- クライアントはファイルを開き、
textDocument/didOpen
をサーバーに送信します。 - サーバーはファイルを分析し、
textDocument/publishDiagnostics
通知を送信します。 - クライアントは結果を解析し、エディターにエラーインジケーターを表示します。
これは、言語サービスから洞察を得るための受動的な方法です。 よりアクティブな例は、カーソルの下にあるシンボルのすべての参照を見つけることです。 これは次のようになります。
- クライアントは、ファイル内の場所を指定して、
textDocument/references
をサーバーに送信します。 - サーバーはシンボルを見つけ出し、このファイルや他のファイルで参照を見つけて、リストで応答します。
- クライアントはユーザーへの参照を表示します。
ブラックリストツール
確かにLanguageServerProtocolの詳細を掘り下げることはできますが、それはクライアントの実装者に任せましょう。 エディタと言語ツールの分離のアイデアを固めるために、私たちはツールクリエーターの役割を果たします。
シンプルに保ち、新しい言語や機能を作成する代わりに、診断に固執します。 診断は最適です。診断はファイルの内容に関する単なる警告です。 リンターは診断を返します。 似たようなものを作ります。
避けたい言葉を知らせるツールを作ります。 次に、その機能をいくつかの異なるテキストエディタに提供します。
ランゲージサーバー
まず、ツール。 これを言語サーバーに焼き付けます。 簡単にするために、これはNode.jsアプリになりますが、読み取りと書き込みにストリームを使用できる任意の技術で実行できます。
これがロジックです。 いくつかのテキストが与えられると、このメソッドは、一致したブラックリストに登録された単語の配列とそれらが見つかったインデックスを返します。
const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }
それでは、サーバーにしましょう。
const { TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.listen(connection) connection.listen()
ここでは、 vscode-languageserver
を利用しています。 この名前は、VSCodeの外部でも確実に機能するため、誤解を招く恐れがあります。 これは、LSPの起源について見られる多くの「指紋」の1つです。 vscode-languageserver
は低レベルのプロトコルを処理し、ユースケースに集中できるようにします。 このスニペットは接続を開始し、それをドキュメントマネージャーに結び付けます。 クライアントがサーバーに接続すると、サーバーは、開かれているテキストドキュメントの通知を受け取りたいことをサーバーに通知します。
ここでやめることができます。 これは、無意味ではありますが、完全に機能するLSPサーバーです。 代わりに、いくつかの診断情報を使用してドキュメントの変更に対応しましょう。
documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })
最後に、変更されたドキュメント、ロジック、および診断応答の間のドットを接続します。
const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const { DiagnosticSeverity, } = require('vscode-languageserver') const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })
診断ペイロードは、関数を介してドキュメントのテキストを実行した結果であり、クライアントが期待する形式にマップされます。
このスクリプトはあなたのためにそれらすべてを作成します。

curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash
注:見知らぬ人がマシンに実行可能ファイルを追加することに不安がある場合は、ソースを確認してください。 プロジェクトを作成し、 index.js
をダウンロードし、 npm link
を使用します。
完全なサーバーソース
最終的なblacklist-server
ソースは次のとおりです。
#!/usr/bin/env node const { DiagnosticSeverity, TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results } const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', }) const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) }) documents.listen(connection) connection.listen()
言語サーバープロトコルチュートリアル:テストドライブの時間
プロジェクトがlink
されたら、トランスポートメカニズムとしてstdio
を指定して、サーバーを実行してみてください。
blacklist-server --stdio
これは、前に説明したLSPメッセージをstdio
でリッスンしています。 それらを手動で提供することもできますが、代わりにクライアントを作成しましょう。
言語クライアント:VSCode
このテクノロジーはVSCodeで始まったので、そこから始めるのが適切だと思われます。 LSPクライアントを作成し、作成したサーバーに接続する拡張機能を作成します。
VSCode拡張機能を作成するには、Yeomanと適切なジェネレーターであるgenerator-code
を使用するなど、いくつかの方法があります。 ただし、簡単にするために、必要最低限の例を見てみましょう。
ボイラープレートのクローンを作成し、その依存関係をインストールしましょう。
git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn
VSCodeでblacklist-vscode
ディレクトリを開きます。
F5キーを押して、別のVSCodeインスタンスを開始し、拡張機能をデバッグします。
最初のVSCodeインスタンスの「デバッグコンソール」に、「Look、ma。 拡張!"
これで、基本的なVSCode拡張機能がすべてのベルやホイッスルなしで機能するようになりました。 それをLSPクライアントにしましょう。 両方のVSCodeインスタンスを閉じ、 blacklist-vscode
ディレクトリ内から次のコマンドを実行します。
npm i vscode-languageclient
extension.jsを次のように置き換えます。
const { LanguageClient } = require('vscode-languageclient') module.exports = { activate(context) { const executable = { command: 'blacklist-server', args: ['--stdio'], } const serverOptions = { run: executable, debug: executable, } const clientOptions = { documentSelector: [{ scheme: 'file', language: 'plaintext', }], } const client = new LanguageClient( 'blacklist-extension-id', 'Blacklister', serverOptions, clientOptions ) context.subscriptions.push(client.start()) }, }
これは、 vscode-languageclient
パッケージを使用して、VSCode内にLSPクライアントを作成します。 vscode-languageserver
とは異なり、これはVSCodeと緊密に結合されています。 つまり、この拡張機能で行っているのは、クライアントを作成し、前の手順で作成したサーバーを使用するようにクライアントに指示することです。 VSCode拡張機能の詳細を確認すると、プレーンテキストファイルにこのLSPクライアントを使用するように指示していることがわかります。
テストドライブするには、VSCodeでblacklist-vscode
ディレクトリを開きます。 F5キーを押して別のインスタンスを開始し、拡張機能をデバッグします。
新しいVSCodeインスタンスで、プレーンテキストファイルを作成して保存します。 「foo」または「bar」と入力して、しばらく待ちます。 これらがブラックリストに登録されているという警告が表示されます。
それでおしまい! ロジックを再作成する必要はなく、クライアントとサーバーを調整するだけで済みました。
別の編集者、今回はSublime Text 3でもう一度やりましょう。プロセスは非常に似ており、少し簡単になります。
言語クライアント:Sublime Text 3
まず、ST3を開き、コマンドパレットを開きます。 エディターをLSPクライアントにするためのフレームワークが必要です。 「PackageControl:Install Package」と入力し、Enterキーを押します。 パッケージ「LSP」を見つけてインストールします。 完了すると、LSPクライアントを指定できるようになります。 多くのプリセットがありますが、それらは使用しません。 独自に作成しました。
もう一度、コマンドパレットを開きます。 「設定:LSP設定」を見つけてEnterキーを押します。 これにより、LSPパッケージの構成ファイルLSP.sublime-settings
が開きます。 カスタムクライアントを追加するには、以下の構成を使用します。
{ "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }
これは、VSCode拡張機能からはおなじみのように見えるかもしれません。 クライアントを定義し、プレーンテキストファイルで動作するように指示し、言語サーバーを指定しました。
設定を保存してから、プレーンテキストファイルを作成して保存します。 「foo」または「bar」と入力して待ちます。 繰り返しになりますが、これらがブラックリストに登録されているという警告が表示されます。 処理(エディターでのメッセージの表示方法)は異なります。 ただし、機能は同じです。 今回は、エディターにサポートを追加するためにほとんど何もしませんでした。
言語「クライアント」:Vim
この関心の分離によってテキストエディタ間で機能を簡単に共有できるとまだ確信が持てない場合は、Cocを介して同じ機能をVimに追加する手順を次に示します。
Vimを開き、 :CocConfig
と入力して、次を追加します。
"languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }
終わり。
クライアント/サーバー分離により、言語と言語サービスが繁栄します
言語サービスの責任を、それらが使用されているテキストエディタから分離することは明らかに勝利です。 これにより、言語機能の作成者は専門分野に集中でき、編集者の作成者も同じことができます。 これはかなり新しいアイデアですが、採用が広がっています。
作業の基礎ができたので、プロジェクトを見つけて、このアイデアを前進させるのに役立つかもしれません。 編集者の炎上戦争は決して終わらないでしょう、しかしそれは大丈夫です。 言語能力が特定のエディターの外に存在できる限り、あなたは好きなエディターを自由に使うことができます。
マイクロソフトゴールドパートナーとして、Toptalはマイクロソフトエキスパートのエリートネットワークです。 必要なときに必要な場所で正確に、必要な専門家と一緒に高性能のチームを構築しましょう!