LoginSignup
96
55

More than 3 years have passed since last update.

Language Server Protocol開発チュートリアル

Last updated at Posted at 2019-12-21

はじめに

プログラマの仕事道具といえばソースコードエディタ(以下,エディタ)です.エディタ開発はプログラマの能力を最大化するための有効な手段と言えます.本記事はエディタ開発の第一歩して拡張機能,またはプラグインの開発を行います.特にEclipse登場以降のプログラミングエディタの多くは拡張機能の集合体として設計されているため,拡張機能開発の経験はエディタ本体の開発にも応用可能です.

本記事ではLanguage Server Protocol (以下,LSP)を用いたエディタの拡張機能開発を行います.LSPとは,コード補完や,変数参照,スタイル修正といった機能実装をあらゆるエディタ/IDE へ提供するプロトコルです.本記事ではVisual Studio Codeを用いて拡張機能開発を行いますが,作成した機能はVimEmacsでも使えます.

一応公式チュートリアルもありますが,拡張機能の開発経験を前提とした高度な内容なため,ここではその内容を簡単にした例でエディタ開発を学んでいきます.

注意: 本記事はLanguage Server Protocolの開発方法を紹介する記事であり,利用方法については他に詳しいサイトを記事終盤で紹介しています 

対象読者

  • エディタ開発に興味がある人
  • エディタの構造を知りたい人
  • 色んなエディタをいじってみたい人

もしこの記事の内容が難しすぎる場合,以下のリンクから基礎的な学習を行えます.

この記事ではTypeScriptを利用していますが,あまり高度な機能は利用しないため,JavaScript,もしくは他の言語にふれたことがあれば大丈夫です.

今回行うこと

行わないこと

  • 言語依存の機能(補完, 参照)開発
  • リモート環境・マルチルートワークスペースを考慮した機能開発

開発環境

Hello World

コンピュータープログラミングにおける古来の伝統に従い,最初に作るアプリケーションは「Hello World」を表示するプログラムにしましょう.

簡素なテンプレートを用意しましたのでそれをクローンしてVS Codeで開きましょう.

git clone https://github.com/Ikuyadeu/vscode-language-server-template.git
code vscode-language-server-template

ビルド

1: ターミナルを起動
[表示] -> [統合ターミナル]
または,Control + ` (Macなら ^ + `)

2: 以下のコマンドを実行し,必要なパッケージのインストールを行います.

npm install

3: F5 キーを入力することで拡張機能をインストールしたVS Codeを立ち上げます.
4: 開いたエディタ上で.txtファイル,もしくは.mdファイルを開いてみましょう.
画像ではtest.txtの1行目に波線を表示させ,その上にマウスを置くとHello Worldと表示させています.
Screen Shot 2019-12-22 at 0.27.56.png

ファイル構成

以下がVS CodeでLSPを実装するときの一般的なファイルの構成です.
本記事ではサーバー側のメインであるserver/src/server.tsを編集していきます.

.
├── client               // Language Serverのクライアントサイド
│   ├── src
│   │   └── extension.ts // クライアント側拡張機能のソースコード
│   └── package.json     // クライアント側として利用するパッケージ情報
│
├── server               // Language Serverのサーバーサイド
│   │── src
│   │   └── server.ts    // Language Serverのメインソースコード
│   │── package.json     // クライアント側として利用するパッケージ情報
│   └── README.md        // サーバーとしてのREADME.md (説明書)
│
├── package.json         // VS Codeプラグインとしてのパッケージ情報 
└── README.md            // VS CodeプラグインとしてのREADME.md(説明書)

ソースコード解説

サーバー側のソースコード
server/src/server.ts
"use strict";

import {
    CodeActionKind,
    createConnection,
    Diagnostic,
    DiagnosticSeverity,
    Range,
    TextDocuments,
    TextDocumentSyncKind,
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";

namespace CommandIDs {
    export const fix = "sample.fix";
}
// Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features.
const connection = createConnection();
connection.console.info(`Sample server running in node ${process.version}`);
let documents!: TextDocuments<TextDocument>;

// 初期化
connection.onInitialize(() => {
    documents = new TextDocuments(TextDocument);
    setupDocumentsListeners();

    // サーバー全体の機能を記述する
    return {
        capabilities: {
            // ファイルを保存したときや変更,閉じたときに実行
            textDocumentSync: {
                openClose: true,
                change: TextDocumentSyncKind.Incremental,
                willSaveWaitUntil: false,
                save: {
                    includeText: false,
                },
            },
            // 自動修正を実装よてい
            codeActionProvider: {
                codeActionKinds: [CodeActionKind.QuickFix],
            },
            executeCommandProvider: {
                commands: [CommandIDs.fix],
            },
        },
    };
});

/**
 * ソースコードの一行目に対して警告を表示する
 */
function validate(doc: TextDocument) {
    // 警告などファイルの状態を管理するリスト
    const diagnostics: Diagnostic[] = [];
    // 警告を出す範囲(今回は0行目の0文字目から0行目の最終文字目まで)
    const range: Range = {start: {line: 0, character: 0},
                          end: {line: 0, character: Number.MAX_VALUE}};
    // 警告を追加,引数は順番に範囲,メッセージ,警告強度,警告id,警告のソース
    diagnostics.push(Diagnostic.create(range, "Hello world", DiagnosticSeverity.Warning, "", "sample"));
    connection.sendDiagnostics({ uri: doc.uri, diagnostics });
}

function setupDocumentsListeners() {
    documents.listen(connection);

    // ファイルを開いたとき,変更したときにファイルを検証し,警告を表示する
    documents.onDidOpen((event) => {
        validate(event.document);
    });

    documents.onDidChangeContent((change) => {
        validate(change.document);
    });

    // ファイルを閉じたとき,警告表示を消す
    documents.onDidClose((close) => {
        connection.sendDiagnostics({ uri: close.document.uri, diagnostics: []});
    });

}

// Listen on the connection
connection.listen();


クライアント側のソースコード
client/src/extension.ts
"use strict";

import * as path from "path";
import { ExtensionContext, window as Window } from "vscode";
import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions, TransportKind } from "vscode-languageclient";

// 拡張機能を立ち上げたときに呼び出す関数
export function activate(context: ExtensionContext): void {
    const serverModule = context.asAbsolutePath(path.join("server", "out", "server.js"));
    const debugOptions = { execArgv: ["--nolazy", "--inspect=6009"], cwd: process.cwd() };
    const serverOptions: ServerOptions = {
        run: { module: serverModule, transport: TransportKind.ipc, options: { cwd: process.cwd() } },
        debug: {
            module: serverModule,
            transport: TransportKind.ipc,
            options: debugOptions,
        },
    };
    // 対象とする言語.今回は.txtファイルと.mdファイル
    const clientOptions: LanguageClientOptions = {
        documentSelector: [
            {
                scheme: "file",
                language: "plaintext",
            },
            {
                scheme: "file",
                language: "markdown",
            }],
        diagnosticCollectionName: "sample",
        revealOutputChannelOn: RevealOutputChannelOn.Never,
    };

    let client: LanguageClient;
    try {
        client = new LanguageClient("Sample LSP Server", serverOptions, clientOptions);
    } catch (err) {
        Window.showErrorMessage("The extension couldn't be started. See the output channel for details.");

        return;
    }
    client.registerProposedFeatures();

    context.subscriptions.push(
        client.start(),
    );
}

警告機能の実装

次に,大文字に対して警告文を出す実装を行います.
サーバーのソースコードserver/src/server.tsの関数validateを以下の通りに書き換えてみましょう.

server/src/server.ts
/**
 * 大文字に対して警告を表示する
 */
function validate(doc: TextDocument) {
    // 2つ以上並んでいるアルファベット大文字を検出
    const text = doc.getText();
    // 検出するための正規表現 (正規表現テスト: https://regex101.com/r/wXZbr9/1)
    const pattern = /\b[A-Z]{2,}\b/g;
    let m: RegExpExecArray | null;

    // 警告などの状態を管理するリスト
    const diagnostics: Diagnostic[] = [];
    // 正規表現に引っかかった文字列すべてを対象にする
    while ((m = pattern.exec(text)) !== null) {
        // 対象の位置から正規表現に引っかかった文字列までを対象にする
        const range: Range = {start: doc.positionAt(m.index),
                              end: doc.positionAt(m.index + m[0].length),
        };
        // 警告内容を作成,上から範囲,メッセージ,重要度,ID,警告原因
        const diagnostic: Diagnostic = Diagnostic.create(
            range,
            `${m[0]} is all uppercase.`,
            DiagnosticSeverity.Warning,
            "",
            "sample",
        );
        // 警告リストに警告内容を追加
        diagnostics.push(diagnostic);
    }

    // Send the computed diagnostics to VSCode.
    connection.sendDiagnostics({ uri: doc.uri, diagnostics });
}

もう一度F5キーで実行し以下のファイルを作ってみましょう

README.md
TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY shost. ANY OS. Open Source.

ファイル中の大文字ANYOSに対して以下のように波線で警告が出るはずです.

Screen Shot 2019-12-21 at 18.32.41.png

自動修正機能の実装

警告だけに終わらず,自動的に修正する機能を実装していきます.今回は大文字を小文字にしていきます.

引き続き,server/src/server.tsを編集します.import以下と関数setupDocumentsListenersを以下のコードに編集してみましょう.ここまでの過程で,つまづいた場合は出来上がったものcompletedブランチとして用意してありますのでそちらをご利用ください.

import文
import {
    CodeAction,
    CodeActionKind,
    createConnection,
    Diagnostic,
    DiagnosticSeverity,
    Range,
    TextDocumentEdit,
    TextDocuments,
    TextDocumentSyncKind,
    TextEdit,
} from "vscode-languageserver";
関数setupDocumentsListeners
function setupDocumentsListeners() {
    documents.listen(connection);

    documents.onDidOpen((event) => {
        validate(event.document);
    });

    documents.onDidChangeContent((change) => {
        validate(change.document);
    });

    documents.onDidClose((close) => {
        connection.sendDiagnostics({ uri: close.document.uri, diagnostics: []});
    });

    // Code Actionを追加する
    connection.onCodeAction((params) => {
        // sampleから生成した渓谷のみを対象とする
        const diagnostics = params.context.diagnostics.filter((diag) => diag.source === "sample");
        // 対象ファイルを取得する
        const textDocument = documents.get(params.textDocument.uri);
        if (textDocument === undefined || diagnostics.length === 0) {
            return [];
        }
        const codeActions: CodeAction[] = [];
        // 各警告に対してアクションを生成する
        diagnostics.forEach((diag) => {
            // アクションの目的
            const title = "Fix to lower case";
            // 警告範囲の文字列取得
            const originalText = textDocument.getText(diag.range);
            // 該当箇所を小文字に変更
            const edits = [TextEdit.replace(diag.range, originalText.toLowerCase())];
            const editPattern = { documentChanges: [
                TextDocumentEdit.create({uri: textDocument.uri,
                                         version: textDocument.version},
                                        edits)] };
            // コードアクションを生成
            const fixAction = CodeAction.create(title,
                                                editPattern,
                                                CodeActionKind.QuickFix);
            // コードアクションと警告を関連付ける
            fixAction.diagnostics = [diag];
            codeActions.push(fixAction);
        });

        return codeActions;
    });

}

警告に電球マークが付き,該当箇所を小文字に修正します.
Screen Shot 2019-12-21 at 18.37.01.png

公開方法

以下はそれぞれ各リンクを参照ください.

VS Code 拡張機能の公開

公式ガイド(英語)
日本語記事

まとめると以下のコマンドです.

vsce login
vsce publish

Language Serverの公開

npmパッケージとして公開します.

公式ガイド(英語)
日本語記事

まとめると以下のコマンドです.

cd server
npm login
npm publish

他エディタでもLSPを利用

以下のコマンドでLanguage Serverのnpmパッケージをインストールすることで他のエディタでも開発したLSPを利用できます.

npm install -g {公開したLanguage Server}

エディタごとの利用方法リンク

発展

公式が提供しているLSPの実装例は以下のとおりです.
更に新しい機能を作りたい場合はこれらを参考に発展させていきましょう.

サンプル 内容
LSP Sample 小文字のアルファベットを見つけると注意する.本記事の内容に加え,設定ファイルへのアクセスなども行う
LSP Multi Root Server Sample マルチルートワークスペースごとにLSPを立ち上げる,LSP Sampleの拡張版
LSP Log Streaming Sample LSP実行時にログを出力する, LSP Sampleの拡張版
LSP UI Example ファイルの1行目に文字の挿入を行うアクションを追加する

より高度な開発を体験するには各言語サーバーのOSSに参加するのが一番だと思います.

参考文献

宣伝

  • GitHubアプリ作成チュートリアルの翻訳記事を公開しています.本記事と合わせて読むことでオフライン・オンライン両方で開発を効率化できます.
  • LSPを使ったVS Codeの拡張機能を公開しています.本記事で紹介したソースコードの発展版となります.よければ使ってみてください.
96
55
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
96
55