Chapter 09

Semantic Tokens

takl
takl
2020.12.22に更新

次は Semantic Tokens を実装します。

Semantic Tokens というのは要するにソースコードの装飾機能です。しかし Language Server Protocol では色やフォントを直接指定しません。「このトークンは variable ですよ」や「このトークンは number ですよ」など、トークンの意味をやりとりします。エディタによって使える装飾機能は違いますので、トークンの意味をやりとりするのは理にかなっているでしょう。

エディタにより使えるトークンの種類は異なっています。というわけでここでも capabilitiesをチェックしなければなりません。使えるトークンの種類は initialize メッセージの params.capabilities.textDocument.semantiTokens.tokenTypes にあります。

また、 Language Server によっても使えるトークンの種類は異ります。というわけで Language Server が使えるトークンの種類も Language Client に教えてあげなければなりません。これは response の result.capabilities.semanticTokensProvider で指定します。今回は手抜きのために Client から送られてきた tokenTypes をそのまま使います。

ところで、 Semantic Tokens はいわゆる普通の JSON ではなく、数字にエンコードされた JSON で送られます。一般的にはかなりアレな設計なのですが、 Semantic Tokens の情報は大きいのでそうなったようです。そしてトークンの種類は tokenTypes のインデクスで指定されます。つまり tokenTypes[ "comment", "variable", "number" ] だったら comment0, variable1number2 になります。今回のコードでは変数 tokenTypeToIndex でこの変換を行えるようにしておきます。

requestTable["initialize"] = (msg) => {
    const capabilities = {
        textDocumentSync: 1
    };

    if (msg.params && msg.params.capabilities) {
        if (msg.params.capabilities.textDocument && msg.params.capabilities.textDocument.publishDiagnostics) {
            publishDiagnosticsCapable = true;
        }
        if (msg.params.capabilities.textDocument && msg.params.capabilities.textDocument.semanticTokens && msg.params.capabilities.textDocument.semanticTokens.tokenTypes) {
            const tokenTypes = msg.params.capabilities.textDocument.semanticTokens.tokenTypes;
            for (const i in tokenTypes) {
                tokenTypeToIndex[tokenTypes[i]] = i;
            }
            capabilities.semanticTokensProvider = {
                legend: {
                    tokenTypes,
                    tokenModifiers: [] // 今回は省略
                },
                range: false, // textDocument/semanticTokens/range を無効にする
                full: true    // textDocument/semanticTokens/full を有効にする
            }
        }
    }

    sendMessage({ jsonrpc: "2.0", id: msg.id, result: { capabilities } });
}

初期化が正しく行えれば、Language Client が色を付けたくなったときに textDocument/semanticTokens/full が飛んできます。responseとしてエンコードされたトークン列を返せば色がつきます。トークン列の正確なエンコード方法は仕様書をじっくり読んで頂くとして、次のコードでエンコードできます。

requestTable["textDocument/semanticTokens/full"] = (msg) => {
    const uri = msg.params.textDocument.uri;
    const data = [];
    let line = 0;
    let character = 0;

    for (const token of buffers[uri].tokens) {
        if (token.kind in tokenTypeToIndex) {
            let d_line;
            let d_char;
            if (token.location.range.start.line === line) {
                d_line = 0;
                d_char = token.location.range.start.character - character;
            } else {
                d_line = token.location.range.start.line - line;
                d_char = token.location.range.start.character;
            }
            line = token.location.range.start.line;
            character = token.location.range.start.character;

            data.push(d_line, d_char, token.text.length, tokenTypeToIndex[token.kind], 0);
        }
    }

    sendMessage({ jsonrpc: "2.0", id: msg.id, result: { data } })
}

うまく行けば次のように色がつきます。

* ここまでのソースコード *