こんにちは。開発プロジェクトのテックリードを務める者です。
日々の開発において、VS Codeは単なる「テキストエディタ」の枠を超え、私たちの思考スピードをそのままコードベースに定着させるための「認知の拡張デバイス」として機能しています。しかし、そのポテンシャルを標準機能のまま使い倒しているだけでは、チーム全体の生産性を頭打ちにさせてしまいます。
特に、社内独自のDSL(ドメイン特化言語)、レガシーな設定ファイル、あるいはニッチなマイナー言語を扱う際、「なぜこのファイルには補完が効かないのか」「なぜリファクタリングの網から漏れるのか」というフラストレーションに直面したことはないでしょうか。
今回は、VS Codeの裏側を支配する Language Server Protocol (LSP) の深層を紐解き、「自社専用の言語サーバー」を自作して開発体験を爆速化するアプローチを、プロの実践的知見を交えて徹底解説します。
—
1. 現場の生産性を極限まで高める:VS Code 隠れキーボードショートカット&神プラグイン
LSPの自作に入る前に、まずはVS Code自体のスループットを限界突破させるための「土台」を整えます。プロが手放せない隠しコマンドと拡張機能です。
開発スピードを3倍にする隠しキーボードショートカット (macOS / Windows)
- シンボルのグローバルスコープ・クイックジャンプ
- `Cmd + T` (macOS) / `Ctrl + T` (Windows)
- 解説: ファイル名ではなく、関数名や変数名などの「シンボル」ベースでプロジェクト全体を縦断します。LSPが正確に動いていれば、型を意識したジャンプが瞬時に可能です。
- マルチカーソルによる矩形・条件付き一括置換
- `Cmd + Option + 下矢印` / `Ctrl + Alt + 下矢印` (カーソル行追加)
- `Shift + Option + I` / `Shift + Alt + I` (選択行の末尾にすべてカーソルを配置)
- 解説: ログの整形や、生データをJSON/YAMLの配列に変換する作業が一瞬で終わります。
- フォーカスを失わずにエディタを分割・移動
- `Cmd + 1`, `Cmd + 2`, `Cmd + 3` でエディタグループ間を瞬時に移動。
導入必須の神プラグイン(選りすぐりの3選)
1. Error Lens
- 理由: エラーや警告を、行末にインラインで赤く(または黄色く)描き出します。LSPが吐き出したエラーメッセージを見落とすことが物理的に不可能になり、タイポによる手戻りが激減します。
2. GitLens — Git supercharged
- 理由: コードの各行に「誰が、いつ、何のコミットで書いたか」を幽霊文字(Blame)で表示。LSPによるコード解析と組み合わせることで、「この関数、なぜこんな実装になっているのか」の文脈を1秒で把握できます。
3. Todo Tree
- 理由: コードベース内に散らばる `TODO` や `FIXME` をツリービューで一元管理。技術的負債の放置を防ぎます。
—
2. チーム開発で絶対守るべき設定の共有化ルール
個人のマシーンだけでVS Codeが最適化されていても、チーム全体で環境がバラバラでは意味がありません。以下の2点を必ずリポジトリのルートに組み込んでください。
① `.vscode/extensions.json` (推奨拡張機能の強制)
チームメンバーがリポジトリをクローンした瞬間、必要な拡張機能を自動検知させます。
{
“recommendations”: [
“dbaeumer.vscode-eslint”,
“esbenp.prettier-vscode”,
“usernamehw.errorlens”,
“eamodio.gitlens”,
// 後述する自作LSPクライアント用のカスタム拡張機能ID
“mycompany.custom-dsl-lsp”
]
}
② `.vscode/settings.json` (プロジェクト固有の設定強制)
プロジェクトごとのフォーマッタやLSPの挙動を統一します。
{
// 保存時に自動でコード整形を実行
“editor.formatOnSave”: true,
// デフォルトのフォーマッタをPrettierに固定
“editor.defaultFormatter”: “esbenp.prettier-vonsole”,
// 特定のマイナー拡張子(例: .mycfg)を自社DSLとしてVS Codeに認識させる
“files.associations”: {
“.mycfg”: “mydsl”
},
// ワークスペース固有のLSPトレースを有効化(デバッグ用)
“mydsl.trace.server”: “verbose”
}
—
3. LSP(Language Server Protocol)のアーキテクチャと勝算
なぜLSPなのか?
かつて、N種類の言語をM種類のエディタでサポートする場合、$N \times M$ のプラグインをそれぞれ開発する必要がありました。LSPはこの複雑性を $N + M$ に削減します。
[ VS Code (Client) ] <--- JSON-RPC (標準入出力 / WebSocket) ---> [ 言語サーバー (Server) ]
- クライアント(VS Code): UIの描画、カーソル位置のトラッキング、ユーザーからの入力を受け付け、「今どこにいるか」をサーバーに通知。
- サーバー(自作スクリプト): テキスト解析(AST生成)、型推論、補完候補(Completion)の計算を行い、クライアントに返す。
この分離により、「面倒なエディタ側のUI実装」をVS Codeに丸投げし、コアロジック(補完の中身)だけに集中できるという圧倒的なメリットが生まれます。
—
4. 【実践】特定の設定ファイル専用「入力補完LSP」を自作する
ここでは、実務でよくある「独自の独自設定ファイル(拡張子: `.mycfg`)」をターゲットにし、Node.js(TypeScript)を用いて超軽量なLSPサーバーを自作する手順を解説します。
このサーバーは、`.mycfg` 内で `env:` と入力した瞬間に、自動で `[ “development”, “staging”, “production” ]` の補完候補をサジェストする機能を持ちます。
ステップ1: プロジェクトの初期化と依存関係のインストール
適当なディレクトリで作業を開始します。
mkdir my-custom-lsp && cd my-custom-lsp
npm init -y
マイクロソフト公式のLSPライブラリをインストール
npm install vscode-languageserver vscode-languageserver-textdocument
npm install -D typescript @types/node
npx tsc –init
ステップ2: サーバー本体の実装 (`src/server.ts`)
プロジェクト内に `src/server.ts` を作成し、以下のコードを記述します。各行のコメントに処理の意図を詳細に記しています。
import {
createConnection,
TextDocuments,
ProposedFeatures,
InitializeParams,
CompletionItem,
CompletionItemKind,
TextDocumentSyncKind,
InitializeResult
} from ‘vscode-languageserver/node’;
import { TextDocument } from ‘vscode-languageserver-textdocument’;
// 標準入出力(stdio)を利用してVS Codeクライアントと通信するコネクションを生成
const connection = createConnection(ProposedFeatures.all);
// 開かれているドキュメント群を管理するマネージャー
const documents: TextDocuments
connection.onInitialize((params: InitializeParams): InitializeResult => {
// クライアント(VS Code)からの初期化リクエストに対し、サーバーの能力を通知
return {
capabilities: {
// ドキュメントの変更はフルテキストで同期する
textDocumentSync: TextDocumentSyncKind.Incremental,
// 補完機能(Completion)を提供することを宣言
completionProvider: {
resolveProvider: true,
// ‘.’ が入力された時にも補完を発火させるトリガー文字の指定
triggerCharacters: [‘.’, ‘:’]
}
}
};
});
// ユーザーがコード補完(Ctrl + Space 等)を呼び出した際に実行されるハンドラ
connection.onCompletion(
(_textDocumentPosition: any): CompletionItem[] => {
// 現場の知見: 本来はここでTextDocumentのAST(抽象構文木)を解析し、
// 現在のカーソル位置がどのスコープにあるかを判定して動的に候補を絞り込みます。
return [
{
label: ‘env:development’,
kind: CompletionItemKind.Value,
data: 1,
detail: ‘開発環境用の設定を指定します’,
documentation: ‘ローカルホストおよびデバッグモードを有効にします。’
},
{
label: ‘env:staging’,
kind: CompletionItemKind.Value,
data: 2,
detail: ‘ステージング環境用の設定を指定します’,
documentation: ‘本番同等のクラウドインフラ上で動作します。’
},
{
label: ‘env:production’,
kind: CompletionItemKind.Value,
data: 3,
detail: ‘本番環境用の設定を指定します’,
documentation: ‘厳格なセキュリティポリシーと最適化が適用されます。’
}
];
}
);
// 補完アイテムの詳細情報が求められた時に呼ばれる(今回はダミーデータをそのまま返す)
connection.onCompletionResolve(
(item: CompletionItem): CompletionItem => {
return item;
}
);
// ドキュメント管理をコネクションに紐付け
documents.listen(connection);
// サーバー接続のリスナー起動
connection.listen();
ステップ3: VS Code拡張機能(クライアント)からサーバーを起動する
LSPサーバーをVS Codeからプロセスとして起動するため、拡張機能(Client)のコードを作成します。
拡張機能開発用のフォルダ構成を作成
mkdir -p client/src
cd client
npm init -y
npm install vscode-languageclient
npm install -D typescript @types/node
`client/src/extension.ts` を作成します。
import as path from ‘path’;
import { ExtensionContext } from ‘vscode’;
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
} from ‘vscode-languageclient/node’;
let client: LanguageClient;
export function activate(context: ExtensionContext) {
// サーバー側のビルド済みJSファイルのパスを特定
const serverModule = context.asAbsolutePath(
path.join(‘out’, ‘server.js’) // 便宜上、ルートのoutにビルドすると仮定
);
// デバッグ時のオプション(デバッガーをアタッチできるようにする)
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: { execArgv: [‘–nolazy’, ‘–inspect=6009’] }
}
};
// クライアント側のオプション設定
const clientOptions: LanguageClientOptions = {
// 拡張子が .mycfg のファイルに対してのみこのLSPを有効化する
documentSelector: [{ scheme: ‘file’, language: ‘mydsl’ }],
synchronize: {
// 設定ファイルの変更を監視
fileEvents: workspace.createFileSystemWatcher(‘/.clientrc’)
}
};
// LanguageClientのインスタンス化と起動
client = new LanguageClient(
‘myCustomLsp’,
‘My Custom DSL Language Server’,
serverOptions,
clientOptions
);
client.start();
}
export function deactivate(): void {
if (client) {
client.stop();
}
}
—
5. 運用上の極意:LSP開発における「沼」と回避策
自作LSPを実務のチーム開発やOSSに投入する際、多くのエンジニアがハマる「落とし穴」と、その回避策を授けます。
1. JSON-RPCの通信デバッグは「Outputビュー」を見よ
- LSPは標準入出力でJSONを喋っているため、何が起きているか分からなくなることがあります。
- VS Codeの設定で `”mydsl.trace.server”: “verbose”` を有効にし、出力パネル(Output)から `My Custom DSL Language Server` を選択すると、クライアントとサーバー間でやり取りされる全パケットが生々しく流れます。通信エラーの切り分けにおいて、これ以上の特効薬はありません。
2. 重い解析処理は「デバウンス(Debounce)」せよ
- ユーザーがタイピングするたびに `onDidChangeTextDocument` が発火し、重いAST解析を同期的に走らせると、エディタ全体がカクつきます(開発体験の最悪の殺し方です)。
- テキスト変更イベントを受け取ったら必ず `setTimeout` 等で 300ms 程度のデバウンスを挟み、タイピングの手が止まってから解析・エラー診断を行うのがプロの鉄則です。
—
おわりに:エディタを支配する者が、開発スピードを支配する
「既存のツールに機能がないなら、拡張すればいい」。
エンジニアにとって、VS Codeは完成された黒箱ではありません。LSPという標準化されたプロトコルを手に入れた私たちには、どんなマイナーな言語や複雑な設定ファイルであっても、一流のIDEと同等の開発支援環境に仕立て上げる力が備わっています。
まずは小さな設定ファイルから、あなただけの言語サーバーをスピンアップしてみてください。
コードを書く手が心地よく加速する、圧倒的な自由とスピード感がそこには待っています。