あらゆる言語を使いこなす存在、テキストエディタ
最近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行目以降ですね。
Content
はJSON-RPC
を使用します。( jsonrpc.org )
(つまりフォーマットは固定です。 Header
でContent-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
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
]
}
]
}
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
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()
が発生します。
ログも書き込まれていますね。エディタからのリクエストを受け取れていることがわかります。
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が飛んでくることで始まります。
とりあえずこれを処理しないと以降の実装ができないのでサクッとやってしまいます。
まずはサーバコードを拡張しやすいように書き直します。
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
を用意します。
import json
def exec(request) -> str:
return json.dumps({
'jsonrpc': '2.0',
'id':int(request["id"]),
'result':{
'capabilities':{ }
}
})
先程も見たようにInitialize
ではサーバはcapabilities
でサポートしている機能をエディタに知らせる必要があります。
ここではとりあえず空にしておいて、順次追加していきましょう。
この時点で Initialize Request
は受け入れることができるはずです。もう一度 F5
で動作確認してみます。
ログファイルからもInitialize
関連の動作が確認できますね。
<<<< 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が飛んでくるようになります。
import json
def exec(request) -> str:
return json.dumps({
'jsonrpc': '2.0',
'id':int(request["id"]),
'result':{
'capabilities':{
'hoverProvider':True
}
}
})
サーバにこのReqeustをさばく箇所を追加して、メソッドもはやします。
match request["method"]:
case "initialize":
response = initialize.exec(request)
case "textDocument/hover":
response = hover.exec(request)
case _:
pass
self.response(response)
import hover
も忘れないでください。
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を実装し動作させることができました。
このサーバに追加していくことで、任意の機能を追加できます。
( 真面目に実装しようとすると、LSPがUTF16を標準にしているため(Locationなどの位置情報オブジェクトの扱いが)まあまあ大変です。Microsoft...なぜUTF16なんだ。 )
もう少ししっかりしたハンズオンがやってみたい方は、記事中でも紹介したこちらがおすすめです。
この中ではサーバ側もNode.js
を使って実装していますが、今回書いた設定とpythonコードを参考にすれば、どの言語にも翻訳できると思います。
(エンコーディングの都合でNode.js
を使うのが一番楽だとは思うのですが。。。)
弊社では、経験の有無を問わず、社員やインターン生の採用を行っています。
興味のある方はこちらをご覧ください。