zls を workspace/configuration と workspace/didChangeConfiguration に対応させたい
zig の language server の zls では、設定を zls.json というファイルで行っている。
でも、 LSP には workspace/configuration と workspace/didChangeConfiguraiotn というものがあるため、それらで設定ができるはず。
issue もあるため、ちょっとやってみたい。
サーバー側の実装をするのは初めてだから、他の言語サーバーも参考にしながら実装してみたい。
zig の勉強も兼ねてやってみる。
目的があったほうが言語は学びやすいってどっかに書いてあったし、とりあえず、読んだり書いたりしてみる。
- config.zig:zls の設定値の一覧とデフォルト値の設定
- requests.zig :リクエストや通知でクライアントから送られてくるパラメータの定義 (パースするときに使うため)
- types.zig:LSPで使用される型の定義
- main.zig:メイン処理
workspace/configuration リクエストを実装したい。
これは、サーバーからクライアントに対して送信されるリクエストで、クライアントで保持している現在の設定を取得するためのもの。
clientCapabilities は [requests.zig] の Initialize
構造体にある?
各メソッドのハンドラは method_map で定義されていた
メソッドに対応している一覧のようなものがあった
inline for ってのが使われていた
inline
ってなに??
コンパイル時にアンロールされるって書いてある
ループ展開って呼ばれるものらしい。
ループ展開
プログラムのサイズを犠牲に実行速度を最適化する
展開するからプログラムのサイズは大きくなるけど、速度は速くなるよーってことか
ループ展開の目的は、毎回の繰り返しごとに発生する「ループの終了」条件のテストを減少させる(もしくはなくす)事によって、実行速度を向上させることである
ループの展開をしておくことでループの回数を減らして、速度を上げる
workpsace/didChangeConfiguration
efm-langserver の場合、handle_workspace_did_change_configuration.go#L10-L41 に書いてあった。
JSON をオブジェクトにパースして、 config.Settings から値を取得して、 langHandler のフィールドにセットしている。
zls でもこんな感じでやればいいのかも。
workspace/didChangeConfiguration に対応するメソッドを定義して、method_map に追加する。
method_map
について
const method_map = .{ 1, 2, 3};
のように .{}
を使うことで、型を省略できる
docs
const std = @import("std");
const expect = std.testing.expect;
pub fn main() void {
var arr = .{1, 2, 3};
expect(arr[0] == 1);
expect(arr[1] == 2);
expect(arr[2] == 3);
}
method_map の要素は、以下のようなリスト
- 1つ目: LSPのメソッド名
- 2つ目:パラメータのstruct (型)
- 3つ目:ハンドラの関数
- 関数のシグニチャは
(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.パラメータ, config: Config)
- 最後の引数は、それぞレのハンドラによるかも?
- 関数のシグニチャは
method_map の要素は、数や型によって、呼ばれ方が違う。
- 数が 1 つの場合:何も呼ばれない。
- 数が2つ以上で、2つ目が
void
の場合:handler(arena, id, config)
のように呼ばれる - それ以外の場合:
handler(arena, id, request_obj, config)
のように呼ばれる。- もし、JSON で送られてきたパラメータが正しくなかった場合、エラーになる。
workspace/didChangeConfiguration は、クライアントからのパラメータを使って処理をしたい
そのため、以下のようになる?
const method_map = .{
// ...
.{ "workspace/didChangeConfiguration", requests.DidChangeConfiguration, didChangeConfigurationHandler }
};
requests.DidChangeConfiguration のパラメータはsettings
で型は any
となっているけど、実際に設定できる値を型にしておいたほうがいいかも?
って思ったけど、 vscode-languageserver-node で any ってなってたから、 anytype でいいかも。って思ったけど、 config.zig
のほうがいいかも??
std.json.Value
ってのはなんだろう。 requests.zig
の fromDynamicTreeInternal()
での分岐に使われていそう。
const config = @import("config.zig");
pub const DidChangeConfiguration = struct {
params: struct {
// settings: anytype,
settings: config,
},
};
もしくは、config.zig
の型になる?
const config = @import("config.zig");
pub const DidChangeConfiguration = struct {
params: struct {
settings: config,
},
};
↓ 以下、調査時のメモ
わかんないから、他の言語サーバーの実装を見てみる。
yamlls の場合、vscode-languageserver の DidChangeConfigurationParams
を使っているっぽい。
settingsHandlers.ts#30
vscode-languageserver の DidChangeConfigurationParams
は vscode-language-node の protocol.ts にいた
/**
* The parameters of a change configuration notification.
*/
export interface DidChangeConfigurationParams {
/**
* The actual changed settings
*/
settings: any;
}
any
でいいのかー!
rust-analyzer workspace/didChangeConfiguration
のコードを見ると、 パラメータは無視して、 workspace/configuration
で最新の値を取得するようになっている。
LSP の issue にもあった
以下の手順かな?
- クライアント から サーバーが
workspace/didChangeConfiguration
通知を受け取る - サーバーからクライアントに対して
workspace/configuration
リクエストを送信する - 返ってきた値でサーバー側の設定を更新する
もしかしたら、仕様がまだしっかりと決まっていないのかもしれない?
config.rs に知見が詰まっていそう。
ということは、以下のようになる (はず)
workspace/didChangeConfiguration
通知は、クライアントで設定が変更されたことを通知するためだけに使う。そのため、クライアントは最新の設定値を常に持っていないといけない。
サーバーは workspace/didChangeConfiguration
通知を受け取ったら、変更された最新の設定値を取得するために、 worskpace/configuration
リクエストを送信し、設定値を取得する。
ちょっと前に疑問だったことが解消されたかもしれない!
クライアント側から見たときの、「workspace/configuration リクエスト で返す値」と、「workspace/didChangeConfiguration のリクエストパラメータ settings の値」 は常に同期を取っておくべきなのか?
サーバー側が通知を受け取ったら、workspace/configuration を送信するという前提が必要になっちゃうけど、クライアントで設定値を常に更新して、workspace/configuration で返すようにすればいい。
workspace/configuration リクエスト
仕様は↓
zls にはサーバーからクライアントへのリクエストをするための実装が見当たらない。
それを実装しないといけないのかも?
まずは、types.zig に RequestMessage を追加する。
ResponseMessage は Response
、
NotificationMessage は Notification
で定義されているから、それに合わせて、 Request
で定義する。
Response は union(enum)
で定義されていた。
union(enum) はタグ付きunion と呼ばれるもの → メモ
Request は以下のようになるかな?
pub const Request = struct {
jsonrpc: []const u8 = "2.0",
id: RequestId,
method: []const u8,
params: RequestParams,
};
// もし、switch で使うなら、 union(enum) にする
pub const RequestParams = union(enum) {
ContigurationParams: ConfigurationParams,
};
pub const ConfigurationParams = struct {
items: []ConfigurationItem;
};
pub const ConfigurationItem = struct {
scopeUri: ?[]const u8,
section: ?[]const u8,
};
サーバーからクライアントへのリクエストの仕組みを実装しないといけない。
rlsの mod.ts が参考になりそう?
loop している。
/// Runs this language service.
pub fn run(mut self) -> i32 {
loop {
match self.handle_message() {
ServerStateChange::Continue => (),
ServerStateChange::Break { exit_code } => return exit_code,
}
}
}
mod.rs の handle_message() で ServerStateChange::Continue が返されている間はループする。
handle_message() の処理は、
- stdin から入力を読み取る
- 読み取ったデータを構造体?にパースする (method, id, params を持った構造体)
- method が lsp-types の Exit (ExitNotification) ならBreak (ループの終わり)
- それ以外なら、ハンドラを実行する。
Rust には lsp-types っていう crate があるのか!
すごい
ん-ー rls はなんか微妙?
rust-analyzer を見てみる
workspace/didChangeConfiguration 通知を受け取ったら、 this.send_request::...()
を実行している
send_request() は
pub(crate) fn send_request<R: lsp_types::request::Request>(
&mut self,
params: R::Params,
handler: ReqHandler,
) {
let request = self.req_queue.outgoing.register(R::METHOD.to_string(), params, handler);
self.send(request.into());
}
pub(crate) fn complete_request(&mut self, response: lsp_server::Response) {
let handler = self.req_queue.outgoing.complete(response.id.clone());
handler(self, response)
}
self.req_queue.outgoing.register()
で、なんか登録して、 send()
している。
complete_request()
を呼び出すことで、ハンドラを呼び出せるようになっている!!!
req_queue
は ReqQueue
型
pub(crate) type ReqHandler = fn(&mut GlobalState, lsp_server::Response);
pub(crate) type ReqQueue = lsp_server::ReqQueue<(String, Instant), ReqHandler>;
lsp_server は これかな?
req_queue.outgoing.register() あった
use crate::{ErrorCode, Request, RequestId, Response, ResponseError};
impl<O> Outgoing<O> {
pub fn register<P: Serialize>(&mut self, method: String, params: P, data: O) -> Request {
let id = RequestId::from(self.next_id);
self.pending.insert(id.clone(), data);
self.next_id += 1;
Request::new(id, method, params)
}
pub fn complete(&mut self, id: RequestId) -> O {
self.pending.remove(&id).unwrap()
}
}
Outgoing.pending が { requestId: handler }
となっているため、呼び出せる (はず)
でも、どこで呼んでるの??
↑の complete_request() で呼び出す。
complete_request()
を呼び出すことで、ハンドラを呼び出せるようになっている!!!
Request は以下のように定義されている
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Request {
pub id: RequestId,
pub method: String,
#[serde(default = "serde_json::Value::default")]
#[serde(skip_serializing_if = "serde_json::Value::is_null")]
pub params: serde_json::Value,
}
pub(crate) fn send_request<R: lsp_types::request::Request>(
&mut self,
params: R::Params,
handler: ReqHandler,
) {
let request = self.req_queue.outgoing.register(R::METHOD.to_string(), params, handler);
self.send(request.into());
}
の self.send() は
fn send(&mut self, message: lsp_server::Message) {
self.sender.send(message).unwrap()
}
となっている
main_loop.rs の GlobalState.handle_event()) で呼び出している。
もし、event が Response だったら、サーバーがクライアントに対してリクエストを送信して、返してきたということだから、 complere_request()
を呼び出す。 // なるほど!!
fn handle_event(&mut self, event: Event) -> Result<()> {
match event {
Event::Lsp(msg) => match msg {
lsp_server::Message::Request(req) => self.on_request(loop_start, req)?,
lsp_server::Message::Notification(not) => {
self.on_notification(not)?;
}
lsp_server::Message::Response(resp) => self.complete_request(resp),
},
...
}
...
}
この handle_event() は GlobalState.run() で呼ばれている
impl GlobalState {
fn run(mut self, inbox: Receiver<lsp_server::Message>) -> Result<()> {
...
while let Some(event) = self.next_event(&inbox) {
if let Event::Lsp(lsp_server::Message::Notification(not)) = &event {
if not.method == lsp_types::notification::Exit::METHOD {
return Ok(());
}
}
self.handle_event(event)?
}
...
}
}
zls に実装するなら
workspace/didChangeConfiguration 通知が来たら、send_request("workspace/configuration", パラメータ, ハンドラ) を呼び出す。
これにより、クライアントに向けてリクエストが送信される。
メインのループ (processJsonRpc()) で Response が来たら、その Response の id に紐づくハンドラを実行する。
ほかのリクエストに対してのハンドラと同じように、 method_map に追加でいいのかもしれない。
対応するハンドラは、 workspace/configuration リクエストを送信した時の id をキーにすればいいし。
method_map に追加する値の2つ目の要素は、 responses.Configuration
とかにしていいのかな?
union
どれか1つのフィールドのみ使用できるようにする構造体みたいなもの?
const Payload = union {
int: i32,
float: f32,
boolean: bool,
};
test "simple union" {
var payload = Payload{ .int = 1234 };
// ここでエラーになる
// int がすでに使われているため?
payload.float = 12.34;
}
もし、ほかのフィールドをアクティブにしたい場合には、再度割り当てる必要がある
const std = @import("std");
const expect = std.testing.expect;
const Payload = union {
int: i32,
float: f32,
boolean: bool,
};
test "simple union" {
var payload = Payload{ .int = 1234 };
expect(payload.int == 1234);
// 再割り当て
payload = Payload{ .float = 12.34 };
expect(payload.float == 12.34);
}
もし、 union を switch で使いたい場合、タグ付きunion (Tagged union) にする必要がある。
タグ付きunion はそのタグの型を強制します。
って書いてある。どゆこと??
const std = @import("std");
const expect = std.testing.expect;
const E = enum {
one,
two,
three,
};
const U = union(E) {
one: i32,
two: f32,
three,
};
test "coercion between unions and enums" {
var u: U = U{ .two = 12.34 };
// e の型が E だから、型強制が発生して u が E.two になる?
var e: E = u;
except(e == E.two);
const three: E = E.three;
// another_u の型が U だから、型強制が発生して three が U.three になる?
var another_u: U = three;
expect(another_u == E.three);
}
union(enum)
とすると、 enumタグの型を推測させることができる。
zls でも タグ付きunion を switch で使っていた
/// Id of a request
pub const RequestId = union(enum) {
String: []const u8,
Integer: i64,
Float: f64,
};
fn respondGeneric(id: types.RequestId, response: []const u8) !void {
const id_len = switch (id) {
.Integer => |id_val| blk: {
if (id_val == 0) break :blk 1;
var digits: usize = 1;
var value = @divTrunc(id_val, 10);
while (value != 0) : (value = @divTrunc(value, 10)) {
digits += 1;
}
break :blk digits;
},
.String => |str_val| str_val.len + 2,
else => unreachable,
};
...
}