LoginSignup
35

posted at

updated at

Organization

エディタの裏からプログラマを操る†闇の技術† LSP

あらゆる言語を使いこなす存在、テキストエディタ

最近EmacsよりもVSCodeの使用頻度が増えてきていて切なさを感じているソフトウェアエンジニアです。

コーディング業務でとてもお世話になっているのがテキストエディタですね。時には優しく時には厳しく叱責してくれる。理想の先輩の様な存在がテキストエディタです (トゥンク)。

どんな言語でも詳しいし何でも知ってて凄いなーと思っていたのですが、どうやらこの機能、テキストエディタのものではない様です。

エディタは言語ごとのサーバとやりとりしている

最近のエディタは言語ごとに解析機能を提供しているサーバとやりとりしていて、これをLanguage Serverというようです。

例えばGo言語で作業している時、裏側ではgolspと呼ばれるサーバが走っていて、エディタは作業で発生するイベントごとにこのサーバとやりとりを行い解析結果を画面上に表示しています。

マウスホバーしたときはこんな感じです。

completion, go to definition, lint なども全てこのサーバの提供している機能です
(lint は入れた記憶のある人も多いからイメージつくはず)

VSCodeでは各言語の拡張機能が呼び出すクライアント、emacsならlsp-mode,eglotなどが図で言う editor_lsp_client に当たります。

Language Server Protocol

そしてこのサーバとのやりとり、情報交換の手順を規定しているのが Language Server Protocol(LSP)です。
LSPはオープンなプロトコルで、Microsoftによって策定され現在も同チーム主導でコントリビュートが続いています。

Protocolを定めておくことで、言語ごとのスマート機能を備えたサーバが各エディタとやりとりできるようになります。
サーバもエディタの共通インタフェースとしてProtocolに準拠することで、よりスマート機能の向上にリソースを割くことができるようになります。

自作Language Serverを作ってみよう

エディタの機能拡張として色々なアイディアが浮かんで、プログラマとしてはワクワクしますね。

特定機能のスマートを自作のサーバで提供したり...

(2022/12/14現在 ChatGPT はAPIを提供していません)

既存のサーバの間にproxyすることで一部機能を拡張したり

色々できそうです(大変そうですが)。

ということで、LSPについて調べながら最低限動作する自前のLanguageServerを作ってみます。

余談ですが、MicrosoftはGithubを買収した時点でCopilotのLSPとの連携を考えてたと思っています。もしかしたらLSPをオープンにした時点(買収前)でCopilotローンチの計画まですべて社内では決定していたかもしれません。

Copilotもローンチされた2022年12月現在、以下のすべてがMicrosoftで完結しています。

  • エディタ (VSCode)
  • エディタスイートのプロトコル(LSP)
  • Gitリポジトリ (GitHub)
  • AIコーディング支援 (Copilot)

恐るべし大企業。

LSP の基礎知識

UTF16 という誰得なエンコーディングを指定している割に親切なドキュメントが用意されているので、改めてこれをまとめていきます。

LSPは言語ごとのスマート機能とエディタなどのツールが通信する方法を標準化するためのオープンなプロトコルです。
プロセス間通信を可能にするプロトコルを介してやりとりができるようになることで、1つの機能サーバが複数の開発ツールで再利用でき、言語サーバとツール(エディタなど)共に開発リソースを効率化することができます。

LSPはJSON-RPC形式でRequest/Responseをやりとりする Server-Client 式で定められています。
エディタでイベントが発生するたびにRequest<==>Responseのやり取りを行い、その結果をUIとして描画します。

Request/Responseのメッセージフォーマットを抑えてから、機能ごとのprotocolを見てみましょう。

メッセージフォーマット

LSPでやり取りされるメッセージ(Request/Response)は全て、\r\n区切りでHeader+Contentの形式を取ります。
例えばこんな感じ。

Content-Length: 4556\r\n
\r\n
{
	"jsonrpc": "2.0",
	"id": 1,
	"method": "textDocument/didOpen",
	"params": {
		...
	}
}

この例の1行目はHeaderです。

フィールド名
Content-Length integer(number) 必須. Content部分のbyte長.
Content-Type string Optional. デフォルトはapplication/vscode-jsonrpc; charset=utf-8

Header\r\nで区切られ、Header終了時にContentとも\r\nで区切られています。 httpと同じやつですね。

次にContentです。例だと3行目以降ですね。

ContentJSON-RPCを使用します。( jsonrpc.org )
(つまりフォーマットは固定です。 HeaderContent-Typeを指定できるようになっているのは一応余白を残しておいた程度だと思われます。)

基本的なオブジェクト

Contentの中でははコードやリソースの情報をやり取りする必要があるのですが、そのJSON表現として共通するオブジェクトが存在します。
全部見るのは大変なので今回の実装に使う一部を抜粋します。

まずは「基本的な」オブジェクトを紹介します。実際のメッセージはこのオブジェクトの組み合わせとして定義されています。

(公式の表記に従ってtypescriptで表現します)

Position

ドキュメント
Positionはリソースの特定の位置を表現するもので、エディタで言う「インサートカーソル」のようなものです。
Line,Offsetを0ベースで指定します。

interface Position {
	line: uinteger;
	character: uinteger;
}

Range

ドキュメント
Positionで挟むことで指定するリソースの存在範囲を表現します。

interface Range {
	start: Position;
	end: Position;
}

Location

ドキュメント
これが各メッセージの指定するリソースの場所を表現します。2つの基本型の組み合わせです。
DocumentUriはファイルパスを表すstringです。

interface Location {
	uri: DocumentUri;
	range: Range;

基本の Request/Response

Initialize Request

ドキュメント
クライアントからサーバに送信される最初のリクエスト。
オブジェクトの定義は長くなるので省略しますが、お互いのversion情報や、CapabilitiesをつかってLSPとして何の機能をサポートしているかをサーバがクライアントに知らせたりします。。

Hover Request

ドキュメント
エディタで特定の単語にマウスカーソルをホバーした際にトリガされるRequestです。
マウスカーソルの位置を特定できるPosition, DocumentUriを含み、サーバは基本的にContents:文字列で結果を返します。

テキトーな Language Server を立てる

では実際にサーバを実装してみます。

エディタからLSPで話しかけるLSP-Clientは EcmaScript on Node,
LanguageServerはポータビリティと書きやすさのバランスを取り、疑似コード適性も高い Python3 で実装します。

仕様設計

とりあえず以下を目標とします。

  • ファイルの拡張子 .abc でactivateされる
  • 単語にホバーしたら「うごいてるヨーン。」のメッセージが表示される

[クライアント]VSCodeのextension用意

まずこいつを用意します。
実装方法は以下の記事を参考にしました(実用的なハンズオンとしてはこっちのほうが網羅的で良い記事かも...)。

まず、エディタ側のlsp-clientとローカルのlsp-serverは標準入出力を介してやり取りします。

とりあえずVSCodeで起動するように以下のファイルを次の構成で用意します。

.
├── .vscode
│   └── launch.js
├── bin
│   └── server.py
├── extension.js
├── log.txt
├── package.json
└── sample.abc

それぞれのファイルは以下です。
F5を押すと拡張機能が動く形でデバッグモードを開く設定です。デバッグモードのWindowで.abc拡張子のファイルを開くと自分で書いたextension.jsonが実行されます。

この中で

  • vscode-languageclientをクライアント
  • python bin/server.py をサーバ

としてLSPメッセージをやり取りするよう設定しています。まあ適当にコピーしていただいて大丈夫です。
(server.py内のログファイルのパスは適宜変えてください。多分フルパスじゃないと動かないっぽい?)

launch.json
launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Extension",
            "type": "extensionHost",
            "request": "launch",
            "runtimeExecutable": "${execPath}",
            "args": [
                "--extensionDevelopmentPath=${workspaceFolder}"
            ]
        }
    ]
}
extension.js
extension.js
"use strict";

const vscode = require("vscode");
const languageclient = require("vscode-languageclient");

let CLI;

function activate(ctx) {
    try {
        const srvOpt = {
            command:"python",
            args:[
                ctx.extensionPath + "/bin/server.py",
            ]
        };
        const cliOpt = {
            documentSelector: [
                {
                    scheme: "file",
                    lanugage: "my-abc",
                }
            ]
        };
        CLI = new languageclient.LanguageClient("abc-mode",srvOpt,cliOpt);
        ctx.subscriptions.push(CLI.start());

    } catch(e){
        console.log(e);
        vscode.window.showErrorMessage("abc-mode couldnt be started")
    }
}

function deactivate() {
    if (CLI) return CLI.stop();
}

module.exports = {activate, deactivate} 
package.json ```package.json { "name": "my-abc", "version": "0.0.1", "author": "YOUR NAME", "license": "YOUR LICENSE", "description": "ABC language support extension with LSP.", "engines": { "vscode": "^1.52.1" }, "main": "./extension.js", "activationEvents": [ "onLanguage:my-abc" ], "contributes": { "languages": [ { "id": "my-abc", "extensions": [ ".abc" ] } ] }, "dependencies": { "vscode-languageclient": "^7.0.0" }, "private": true } ```
server.py
server.py
import sys
import json

with open('/home/hitoshi/lsp-handson/log.txt', mode='w+') as log:
    while True:
        length = 0
        for line in sys.stdin:
            kv_pair = line.split(':')
            match kv_pair[0]:
                case "Content-Length":
                    log.write(line)
                    length = int(kv_pair[1].strip())
                case "Content-Type":
                    pass # 特に何もしない
                case _:
                    log.write("\n")
                    break
        contents = sys.stdin.read(length)
        request  = json.loads(contents)
        log.write(contents) 
        log.flush()

全部揃ったら

  • npm install
  • F5押す
  • 開いたWindowで .abc拡張子のファイルを開く

で、エディタ側から送られた LSP Request Message が log.txt に書き込まれていれば成功です!。

エンコーディングの関係で、ASCII文字以外がファイルに書き込まれていると動作しません。LSPの標準がUTF16と定まっているためです。
このコードだとContents-Lengthの不整合による json.decode() が発生します。

ログも書き込まれていますね。エディタからのリクエストを受け取れていることがわかります。

log.txt
Content-Length: 4560

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":15429,"clientInfo":{"name":"Visual Studio Code","version":"1.71.2"},"locale":"ja","rootPath":null,"rootUri":null,"capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional","normalizesLineEndings":true,"changeAnnotationSupport":{"groupsOnLabel":true}},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]}},"codeLens":{"refreshSupport":true},"executeCommand":{"dynamicRegistration":true},"configuration":true,"workspaceFolders":true,"semanticTokens":{"refreshSupport":true},"fileOperations":{"dynamicRegistration":true,"didCreate":true,"didRename":true,"didDelete":true,"willCreate":true,"willRename":true,"willDelete":true}},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"codeDescriptionSupport":true,"dataSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true,"resolveSupport":{"properties":["documentation","detail","additionalTextEdits"]},"insertTextModeSupport":{"valueSet":[1,2]}},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]},"labelSupport":true},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"disabledSupport":true,"dataSupport":true,"resolveSupport":{"properties":["edit"]},"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"honorsChangeAnnotations":false},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true,"prepareSupportDefaultBehavior":1,"honorsChangeAnnotations":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"callHierarchy":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"formats":["relative"],"requests":{"range":true,"full":{"delta":true}},"multilineTokenSupport":false,"overlappingTokenSupport":false},"linkedEditingRange":{"dynamicRegistration":true}},"window":{"showMessage":{"messageActionItem":{"additionalPropertiesSupport":true}},"showDocument":{"support":true},"workDoneProgress":true},"general":{"regularExpressions":{"engine":"ECMAScript","version":"ES2020"},"markdown":{"parser":"marked","version":"1.1.0"}}},"trace":"off","workspaceFolders":null}}

[サーバ] Initialize Respones 実装

ログに書き込まれたRequestを見ると

{"jsonrpc":"2.0","id":0,

  "method":"initialize" ,

...}

と method に initializeが設定されていることがわかります。
そうです。先程見た Initialize Request ですね。

LSPサーバとのやり取りはまずクライアントからこのRequestが飛んでくることで始まります。

とりあえずこれを処理しないと以降の実装ができないのでサクッとやってしまいます。

まずはサーバコードを拡張しやすいように書き直します。

server.py
import sys
import json

import initialize

class LanguageServer:
    def __init__(self, 
        input_file  = sys.stdin,
        output_file = sys.stdout,
        log_path = '/home/hitoshi/lsp-handson/log.txt'
    ) -> None:
        self.I = input_file
        self.O = output_file
        self.log_path = log_path

    def run(self) -> None:
        with open(self.log_path, mode='w+') as log:
            self.Log = log
            while True:
                self.Log.write("\n")
                request = self.recieve_request()
                response = ""
                match request["method"]:
                    case "initialize":
                        response = initialize.exec(request)
                    case _:
                        self.Log.write("    Unknown Method: " + request["method"]+"\n")
                        self.Log.flush()
                self.respond(response)

    def recieve_request(self) -> dict:
        """Inputから Request Message を読み取って
           Contents:JSON を dict型に変換して返す"""
        length = 0
        for line in self.I:
            kv_pair = line.split(':')
            match kv_pair[0]:
                case "Content-Length":
                    length = int(kv_pair[1].strip())
                case "Content-Type":
                    pass # 特に何もしない
                case _:
                    break
        contents = sys.stdin.read(length)
        request  = json.loads(contents)
        self.Log.write("<<<< Req:<" + request["method"] + ">\n")
        self.Log.write(contents + "\n") 
        self.Log.flush()
        return request

    def respond(self, response: str) -> None:
        if len(response) == 0:
            self.Log.write(">>>> Resp: No Response Implemented\n")
            self.Log.flush()
            return
        self.Log.write(">>>> Resp:\n")
        self.Log.write(response)
        self.Log.write("\n")
        self.Log.flush()
        resp = "Content-Length: " + str(len(response)) + "\r\n\r\n" + response
        self.O.write(resp)
        self.O.flush()


server = LanguageServer()
server.run()

同じディレクトリにinitialize.pyを用意します。

initialize.py
import json

def exec(request) -> str:
    return json.dumps({
        'jsonrpc': '2.0',
        'id':int(request["id"]), 
        'result':{
            'capabilities':{ }
        }
    })

先程も見たようにInitializeではサーバはcapabilitiesでサポートしている機能をエディタに知らせる必要があります。
ここではとりあえず空にしておいて、順次追加していきましょう。

この時点で Initialize Requestは受け入れることができるはずです。もう一度 F5 で動作確認してみます。

ログファイルからもInitialize関連の動作が確認できますね。

log.txt

<<<< Req:<initialize>
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":8571,"clientInfo":{"name":"Visual Studio Code","version":"1.71.2"},"locale":"ja","rootPath":null,"rootUri":null,"capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional","normalizesLineEndings":true,"changeAnnotationSupport":{"groupsOnLabel":true}},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]}},"codeLens":{"refreshSupport":true},"executeCommand":{"dynamicRegistration":true},"configuration":true,"workspaceFolders":true,"semanticTokens":{"refreshSupport":true},"fileOperations":{"dynamicRegistration":true,"didCreate":true,"didRename":true,"didDelete":true,"willCreate":true,"willRename":true,"willDelete":true}},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"codeDescriptionSupport":true,"dataSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true,"resolveSupport":{"properties":["documentation","detail","additionalTextEdits"]},"insertTextModeSupport":{"valueSet":[1,2]}},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]},"labelSupport":true},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"disabledSupport":true,"dataSupport":true,"resolveSupport":{"properties":["edit"]},"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"honorsChangeAnnotations":false},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true,"prepareSupportDefaultBehavior":1,"honorsChangeAnnotations":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"callHierarchy":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"formats":["relative"],"requests":{"range":true,"full":{"delta":true}},"multilineTokenSupport":false,"overlappingTokenSupport":false},"linkedEditingRange":{"dynamicRegistration":true}},"window":{"showMessage":{"messageActionItem":{"additionalPropertiesSupport":true}},"showDocument":{"support":true},"workDoneProgress":true},"general":{"regularExpressions":{"engine":"ECMAScript","version":"ES2020"},"markdown":{"parser":"marked","version":"1.1.0"}}},"trace":"off","workspaceFolders":null}}
>>>> Resp:
{"jsonrpc": "2.0", "id": 0, "result": {"capabilities": {}}}

<<<< Req:<initialized>
{"jsonrpc":"2.0","method":"initialized","params":{}}
    Unknown Method: initialized
>>>> Resp: No Response Implemented

Req/Respの流れはこんな感じです。

[サーバ]Hoverの実装

ホバーをした時の型(定義)表示機能を実装してみましょう。

まずは Initialize の Response.capcabilities にホバーを含めます。
こうすることでホバーのたびにクライアントからRequestが飛んでくるようになります。

initialize.py
import json

def exec(request) -> str:
    return json.dumps({
        'jsonrpc': '2.0',
        'id':int(request["id"]), 
        'result':{
            'capabilities':{
                'hoverProvider':True
            }
        }
    })

サーバにこのReqeustをさばく箇所を追加して、メソッドもはやします。

server.py
                match request["method"]:
                    case "initialize":
                        response = initialize.exec(request)
                    case "textDocument/hover":
                        response = hover.exec(request)
                    case _:
                        pass
                self.response(response)

import hoverも忘れないでください。

hover.py
import json

def exec(request) -> str:
    file_name = request["params"]["textDocument"]["uri"]
    pos_line = request["params"]["position"]["line"]
    pos_char = request["params"]["position"]["character"]
    info = "Hover at line:" + str(pos_line) + ", char:" + str(pos_char) + "\n\nof " + file_name + "\n\n"
    return json.dumps({
        'jsonrpc':"2.0",
        'id': request["id"],
        'result':{ 
            'contents': info + '動いてるヨーン。'
        }
    })

F5で開いたWindowで、.abc拡張子のファイルでどれかの文字列をホバーしてみます。
この時、エディタでホバーを行うたびにRequestが飛んできています。ログからも観測できますね。

<<<< Req:<textDocument/hover>
{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///home/hitoshi/lsp-handson/sample.abc"},"position":{"line":5,"character":34}}}
>>>> Resp:
{"jsonrpc": "2.0", "id": 1, "result": {"contents": "Hover at line:5, char:34\n\nof file:///home/hitoshi/lsp-handson/sample.abc\n\n\u52d5\u3044\u3066\u308b\u30e8\u30fc\u30f3\u3002"}}

流れはこんな感じ。

まとめ

ということで、手元で書いたコードでホバー機能を提供するLanguageServerを実装し動作させることができました。
peek.gif

このサーバに追加していくことで、任意の機能を追加できます。
( 真面目に実装しようとすると、LSPがUTF16を標準にしているため(Locationなどの位置情報オブジェクトの扱いが)まあまあ大変です。Microsoft...なぜUTF16なんだ。 )

もう少ししっかりしたハンズオンがやってみたい方は、記事中でも紹介したこちらがおすすめです。

この中ではサーバ側もNode.jsを使って実装していますが、今回書いた設定とpythonコードを参考にすれば、どの言語にも翻訳できると思います。
(エンコーディングの都合でNode.jsを使うのが一番楽だとは思うのですが。。。)

弊社では、経験の有無を問わず、社員やインターン生の採用を行っています。

興味のある方はこちらをご覧ください。

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
What you can do with signing up
35