Open31

zls を workspace/configuration と workspace/didChangeConfiguration に対応させたい

たまたまごたまたまご

zig の勉強も兼ねてやってみる。
目的があったほうが言語は学びやすいってどっかに書いてあったし、とりあえず、読んだり書いたりしてみる。

たまたまごたまたまご

workspace/configuration リクエストを実装したい。

これは、サーバーからクライアントに対して送信されるリクエストで、クライアントで保持している現在の設定を取得するためのもの。

たまたまごたまたまご

clientCapabilities は [requests.zig] の Initialize 構造体にある?


各メソッドのハンドラは method_map で定義されていた

たまたまごたまたまご

メソッドに対応している一覧のようなものがあった

main.zig#L1633-L1655

inline for ってのが使われていた

inline ってなに??
コンパイル時にアンロールされるって書いてある
ループ展開って呼ばれるものらしい。

https://ja.wikipedia.org/wiki/ループ展開

たまたまごたまたまご

ループ展開

https://ja.wikipedia.org/wiki/ループ展開

プログラムのサイズを犠牲に実行速度を最適化する

展開するからプログラムのサイズは大きくなるけど、速度は速くなるよーってことか

ループ展開の目的は、毎回の繰り返しごとに発生する「ループの終了」条件のテストを減少させる(もしくはなくす)事によって、実行速度を向上させることである

ループの展開をしておくことでループの回数を減らして、速度を上げる

たまたまごたまたまご

workpsace/didChangeConfiguration

efm-langserver の場合、handle_workspace_did_change_configuration.go#L10-L41 に書いてあった。

JSON をオブジェクトにパースして、 config.Settings から値を取得して、 langHandler のフィールドにセットしている。

zls でもこんな感じでやればいいのかも。

たまたまごたまたまご

workspace/didChangeConfiguration に対応するメソッドを定義して、method_map に追加する。

method_map について

main.zig
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.zigfromDynamicTreeInternal() での分岐に使われていそう。

requests.zig
const config = @import("config.zig");

pub const DidChangeConfiguration = struct {
    params: struct {
        // settings: anytype,
        settings: config,
    },
};

もしくは、config.zig の型になる?

requests.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 にいた

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 にもあった
https://github.com/microsoft/language-server-protocol/issues/676

以下の手順かな?

  1. クライアント から サーバーがworkspace/didChangeConfiguration 通知を受け取る
  2. サーバーからクライアントに対して workspace/configuration リクエストを送信する
  3. 返ってきた値でサーバー側の設定を更新する

もしかしたら、仕様がまだしっかりと決まっていないのかもしれない?
https://github.com/microsoft/language-server-protocol/issues/972


config.rs に知見が詰まっていそう。

https://github.com/rust-analyzer/rust-analyzer/blob/186c5c47cbfde4ae9d81dc67450c958cb6aece2c/crates/rust-analyzer/src/config.rs

たまたまごたまたまご

ということは、以下のようになる (はず)

workspace/didChangeConfiguration 通知は、クライアントで設定が変更されたことを通知するためだけに使う。そのため、クライアントは最新の設定値を常に持っていないといけない。

サーバーは workspace/didChangeConfiguration 通知を受け取ったら、変更された最新の設定値を取得するために、 worskpace/configuration リクエストを送信し、設定値を取得する。


ちょっと前に疑問だったことが解消されたかもしれない!

クライアント側から見たときの、「workspace/configuration リクエスト で返す値」と、「workspace/didChangeConfiguration のリクエストパラメータ settings の値」 は常に同期を取っておくべきなのか?

サーバー側が通知を受け取ったら、workspace/configuration を送信するという前提が必要になっちゃうけど、クライアントで設定値を常に更新して、workspace/configuration で返すようにすればいい。

たまたまごたまたまご
たまたまごたまたまご

zls にはサーバーからクライアントへのリクエストをするための実装が見当たらない。
それを実装しないといけないのかも?

まずは、types.zig に RequestMessage を追加する。
ResponseMessageResponse
NotificationMessageNotification
で定義されているから、それに合わせて、 Request で定義する。

たまたまごたまたまご

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 が参考になりそう?

https://github.com/rust-lang/rls/blob/32c0fe006dcdc13e1ca0ca31de543e4436c1299e/rls/src/server/mod.rs#L219-L227

loop している。

mod.rs
    /// Runs this language service.
    pub fn run(mut self) -> i32 {
        loop {
            match self.handle_message() {
                ServerStateChange::Continue => (),
                ServerStateChange::Break { exit_code } => return exit_code,
            }
        }
    }
たまたまごたまたまご

ん-ー rls はなんか微妙?

rust-analyzer を見てみる

workspace/didChangeConfiguration 通知を受け取ったら、 this.send_request::...() を実行している

https://github.com/rust-analyzer/rust-analyzer/blob/75371eb0fa015ba8834ae2b66cda68eba5d83874/crates/rust-analyzer/src/main_loop.rs#L659-L694

send_request() は

https://github.com/rust-analyzer/rust-analyzer/blob/75371eb0fa015ba8834ae2b66cda68eba5d83874/crates/rust-analyzer/src/global_state.rs#L216-L227

global_state.rs
    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_queueReqQueue

global_state.rs
pub(crate) type ReqHandler = fn(&mut GlobalState, lsp_server::Response);
pub(crate) type ReqQueue = lsp_server::ReqQueue<(String, Instant), ReqHandler>;

lsp_server は これかな?

https://github.com/rust-analyzer/lsp-server

req_queue.outgoing.register() あった

req_queue.rs
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 は以下のように定義されている

msg.rs
#[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,
}
たまたまごたまたまご
global_state.rs
    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() は

global_state.rs
    fn send(&mut self, message: lsp_server::Message) {
        self.sender.send(message).unwrap()
    }

となっている

たまたまごたまたまご

main_loop.rs の GlobalState.handle_event()) で呼び出している。

もし、event が Response だったら、サーバーがクライアントに対してリクエストを送信して、返してきたということだから、 complere_request() を呼び出す。 // なるほど!!

main_loop.rs
    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() で呼ばれている

main_loop.rs
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 はそのタグの型を強制します。

って書いてある。どゆこと??

https://ziglang.org/documentation/master/#Type-Coercion-unions-and-enums

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 で使っていた

types.zig
/// Id of a request
pub const RequestId = union(enum) {
    String: []const u8,
    Integer: i64,
    Float: f64,
};
main.zig
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,
    };
    ...
}