Skip to content

feat: add language server#236

Open
lemonadern wants to merge 13 commits intofeat/linterfrom
feat/linter-language-server
Open

feat: add language server#236
lemonadern wants to merge 13 commits intofeat/linterfrom
feat/linter-language-server

Conversation

@lemonadern
Copy link
Copy Markdown
Collaborator

@lemonadern lemonadern commented Apr 13, 2026

Summary

LSP の言語サーバを実装しました。
Node-API 版にサーバ実装を追加しています。

主なライブラリとして以下を追加しています:

  • tower-lsp-server
    • Language Server 実装のため
  • ropey (Rope 構造の Rust 実装)
    • サーバ側でエディタのテキストを効率的に保持するため

言語サーバの機能

  • ドキュメントフォーマット / 範囲フォーマット
  • リント診断(open / save 時に publishDiagnostics
  • 設定解決:ワークスペースルート直下の .uroborosqlfmtrc.json / .uroborosqllintrc.json または 明示指定されたパス

制限

  • 現状では単一ワークスペースのみに対応しています

フォーマット機能について

現状の言語サーバは最低限の lint による検査とフォーマット処理を実装しています。

VS Code 上において、SQL ファイルの 1. 全文フォーマットおよび 2. 選択箇所のフォーマット はそれぞれ 1. Format Document, 2. Format Selection コマンドで実行可能になります。

一方、現状の実装では SQL 以外の言語のファイルに(埋め込まれたSQLへ)対するフォーマットは動作しません。
そちらについてはLSP のカスタムコマンドを実装して実現しようと考えています。(別 PR で対応します)

また、そのタイミングで uroborosql-fmt-napi crate で公開している runfmt()およびrunfmt_with_settings() が不要となるため削除する予定です。

Note

@lemonadern lemonadern force-pushed the feat/linter-language-server branch from fb02ca5 to 5c12d11 Compare April 17, 2026 08:59
@lemonadern lemonadern changed the title Feat/linter language server feat: add language server Apr 20, 2026
Comment thread .github/workflows/CI.yml
Comment on lines -55 to +57
# This image ships an older Cargo, so pin Rust 1.86.0 here for edition 2024 support.
rustup install 1.86.0 &&
rustup default 1.86.0 &&
# This image ships an older Cargo, so pin Rust 1.88.0 here for edition 2024 support.
rustup install 1.88.0 &&
rustup default 1.88.0 &&
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

language server クレートで利用している let chains のビルドに失敗するため 1.88.0 にアップデートしました

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

language server と関係ない差分ですが、 clippy に指摘されていたため修正しました

https://rust-lang.github.io/rust-clippy/rust-1.95.0/index.html#collapsible_match

@lemonadern lemonadern marked this pull request as ready for review April 20, 2026 08:13
@lemonadern lemonadern requested review from ppputtyo and tanzaku April 20, 2026 08:13
Add uroborosql-language-server crate implementing LSP over stdio.
Supports textDocument/formatting, textDocument/rangeFormatting, and
save-triggered lint diagnostics via uroborosql-lint.
Add run_language_server() NAPI function that starts the Rust language
server over stdio using a multi-thread tokio runtime, and export it
as runLanguageServer() from the npm package.
@lemonadern lemonadern force-pushed the feat/linter-language-server branch from b7e4066 to b276ef8 Compare April 20, 2026 10:35
Comment on lines +81 to +89
doc.version = version;
if let Some(range) = change.range {
if let Some((start, end)) = rope_range_to_char_range(&doc.rope, &range) {
doc.rope.remove(start..end);
doc.rope.insert(start, &change.text);
}
} else {
doc.rope = Rope::from_str(&change.text);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rope_range_to_char_range() が失敗してNoneを返した場合、ropeの状態と doc.version の齟齬が発生し、その場合に publish_diagnostics の診断が不正になりそうです。

以下のようにしてropeが正常に更新された場合のみversionを更新するのはいかがでしょうか?

            if let Some(range) = change.range {
                if let Some((start, end)) = rope_range_to_char_range(&doc.rope, &range) {
                    doc.rope.remove(start..end);
                    doc.rope.insert(start, &change.text);
                    doc.version = version;
                }
            } else {
                doc.rope = Rope::from_str(&change.text);
                doc.version = version;
            }

Copy link
Copy Markdown
Collaborator Author

@lemonadern lemonadern Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。 rope の更新が成功した場合のみ doc.version を更新するよう修正しました。

283a7bb

.await;
return;
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(現状はVSCodeのみ対応すると思うので優先度低です)

Neovim等のクライアントはconfigが設定されていない場合に received_config がnullになることがあるようです。

なので、以下のようにnullであればdefaultを返す処理を入れても良いかもしれないです。

        if received_config.is_null() {
            *self.client_config.write().unwrap() = ClientConfig::default();
            return;
        }

Copy link
Copy Markdown
Collaborator Author

@lemonadern lemonadern Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。フォールバックはあったほうが良いと思ったので、 received_config.is_null() の場合に ClientConfig::default() を返す処理を追加しました。

94ea5a6

Comment thread crates/uroborosql-language-server/src/configuration.rs Outdated
version: i32,
}

pub(crate) fn rope_position_to_char(rope: &Rope, position: Position) -> Option<usize> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_charで関数名が終わるとcharに変換して返すのかと思ってしまうので、char_indexのように明示したいです。
以下もあわせて変えたいです。

  1. rope_position_to_char
  2. rope_char_to_position
  3. rope_range_to_char_range

Copy link
Copy Markdown
Collaborator Author

@lemonadern lemonadern Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

それぞれ、rope_position_to_char_indexrope_char_index_to_positionrope_range_to_char_index_range に改名しました。

da3f9b6

},
server_info: Some(ServerInfo {
name: "uroborosql-language-server".into(),
version: None,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

以下のようにバージョン指定しておきませんか。
不具合報告の際にも、ユーザーが利用しているバージョンも追いやすくなるかなと。

version: Some(env!("CARGO_PKG_VERSION").into()),

Copy link
Copy Markdown
Collaborator Author

@lemonadern lemonadern Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

バージョン情報を指定するよう修正しました。

94ea5a6

Comment thread crates/uroborosql-language-server/src/server.rs Outdated

let registrations = vec![Registration {
id: "uroborosql-fmt-watcher".to_string(),
method: "workspace/didChangeWatchedFiles".to_string(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

文字列を自分で書かずに、以下のように書けるようです。
method: DidChangeWatchedFiles::METHOD.to_string(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同様に修正しました。

da3f9b6

#[serde(rename_all(serialize = "snake_case", deserialize = "camelCase"))]
pub(crate) struct ClientConfig {
pub debug: Option<bool>,
pub tab_size: Option<usize>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

フォーマッター本体で定義しているConfigと被りがあるのが気になりました。
可能ならうまく集約したいです

@lemonadern lemonadern force-pushed the feat/linter-language-server branch from 3e1bfd1 to 6977d3b Compare April 21, 2026 07:25
…sion

- handle null received_config from clients like Neovim
- replace expect() with if-let in resolve_config_path
- expose CARGO_PKG_VERSION in server_info
- ClientConfig の formatter フィールドに PartialConfig を flatten で埋め込み、フィールド重複を解消
- rename_all を廃止し alias 方式に統一(flatten との干渉を回避)
- client_config_json_explicit_only を PartialConfig の直接 serialize に簡素化
@lemonadern lemonadern force-pushed the feat/linter-language-server branch from 6977d3b to 38d385f Compare April 21, 2026 07:47
@lemonadern
Copy link
Copy Markdown
Collaborator Author

@tanzaku. config の重複解消について、以下のように変更しました。

config の変更について

uroborosql-fmt クレートに PartialConfig 構造体を新設し、language server の ClientConfig から #[serde(flatten)] で再利用する方式に変更しました。

変更前は language server が ClientConfig にフォーマッタの設定フィールドを重複定義していましたが、変更後は PartialConfig が唯一の定義元になります。

【変更前】

  • uroborosql-fmt::Config: serde あり、全フィールド確定値
  • language-server::ClientConfig: フォーマッタフィールドを重複定義

【変更後】

  • uroborosql-fmt::PartialConfig: serde あり、全フィールド Option<T>
    • PartialConfig どうしの merge()resolve() で以下の構造体を生成
  • uroborosql-fmt::Config: serde なし、全フィールド確定値(フォーマッタ内部で利用)
  • language-server::ClientConfig: PartialConfig を flatten で再利用(言語サーバで利用)

副作用

現状では、 CLI や設定ファイルで camelCase のキーを受け付けるように なりました。
これは、VS Code Extension Client の設定値を解釈するため PartialConfig で camelCase の エイリアスを(tabSize, keywordCase など) を付与しており、それを言語サーバとフォーマッタでフォーマッタで共用しているためです。

既存の動作への破壊的変更はありませんが、この変更については意見があればいただきたいです。

@lemonadern lemonadern requested review from ppputtyo and tanzaku April 21, 2026 08:24
@tanzaku
Copy link
Copy Markdown
Collaborator

tanzaku commented Apr 21, 2026

現状では、 CLI や設定ファイルで camelCase のキーを受け付けるように なりました。
これは、VS Code Extension Client の設定値を解釈するため PartialConfig で camelCase の エイリアスを(tabSize, keywordCase など) を付与しており、それを言語サーバとフォーマッタでフォーマッタで共用しているためです。

既存の動作への破壊的変更はありませんが、この変更については意見があればいただきたいです。

対応ありがとうございます。
以下の理由から、camelCaseのキーを受け付けるのは許容して良いと考えています。

  1. 本フォーマッターにおいては、そこまで厳密に仕様に沿う必要はなく、VS Code Extension Clientの設定値を解釈するときだけcamelCaseを受け付けるためにコストを払う必要はない
  2. README.mdに書かなければcamelCaseのキーを受け付けるとユーザーは気づかない、また仮にcamelCaseのキーを指定したところ実害はない

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants