Repository navigation
chore(lsp): migrate from tower-lsp to tower-lsp-server 0.23 - #877
Merged
Merged
Conversation
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
Three ways a client request could break the server or the document state.
### `pgls.executeStatement` without arguments panicked
The handler read `params.arguments[0]` and `params.arguments[1]` directly, so `workspace/executeCommand` with no arguments panicked in the handler. Malformed arguments came back as an internal error. Both are now `InvalidParams` errors with a message that says which argument is wrong.
### Documents of other schemes shared the workspace document of a file
`Session::file_path` mapped every URI to its path, whatever the scheme, and the workspace keys documents by path. So:
- VS Code's `git:/workspace/a.sql?{"ref":"HEAD"}` (the left side of a diff) mapped to `/workspace/a.sql`. `didOpen` on it kept the existing document, and `didClose` removed the document of the real file. The next edit of the file failed with "not found", and its diagnostics were never published again.
- Every cell of a notebook (`vscode-notebook-cell:/nb.ipynb#W0...`, `#W1...`) mapped to the same path.
- `untitled:Untitled-1` became the relative path `Untitled-1`, and `ntitled-1` on Windows, where `to_file_path` strips the first character.
Only `file:` URIs map to file paths now. Other schemes are keyed by their whole URI, e.g. `untitled:Untitled-1`, which never equals a file's path and differs between notebook cells.
I gave them paths of their own rather than skipping them because untitled buffers and notebook cells are real editing surfaces, and people do write SQL in them. With their own key they keep diagnostics, completions and hover without touching any file's document. The trade-off is that path-based settings (`files.include`/`ignore`, the migrations directory) don't match them, which is right since they are not files. A git diff's old side is linted like any other buffer, which matches what VS Code does for other languages. The WASM worker's own `documentSelector` (`{ language: 'sql' }`) is unchanged.
### Out-of-range positions failed the whole request
`LineIndex::offset` returned `None` for a line past the end, and a column past the end of a line ran into the next line or past the end of the text. Some clients send the line after the last one as the end of a range:
```
textDocument/inlayHint, range (0,0)-(2,0) on a two-line document without a final newline
-> Internal error: position Position { line: 2, character: 0 } is out of range
```
Positions from the client are now clamped (`LineIndex::offset_clamped`): a column past the end of its line means the end of the line, as the LSP specifies, and a line past the end means the end of the text. This applies to every handler that converts client positions (`from_lsp::offset`). The internal `LineIndex::offset` used for diffs stays exact. A huge UTF-16 column no longer overflows in `wide_to_utf8_col`. Inlay hints that can't be converted are left out with a warning instead of failing the response.
### Tests
- `execute_statement_without_arguments_is_invalid`: no arguments, and arguments of the wrong type, both get `InvalidParams`.
- `closing_a_document_of_another_scheme_keeps_the_file`: open the file, open and close its `git:` version, edit the file. Diagnostics for the edit are published.
- `test_inlay_hints_utf16_and_out_of_range`: hints after an emoji land on the right UTF-16 columns, and a range that ends at the end of the line, past it, or on the line after the last one gets the same hints.
- Unit tests: `document_path` for `file:` (percent-decoding, drive colon), `git:`, notebook cells and `untitled:`, and clamping in UTF-16 including `u32::MAX`.
### Not covered
`fluent_uri` (since #877) rejects URIs with unencoded spaces, so such a request fails to deserialize before it reaches the server. VS Code always encodes them, and fixing it would need a change in `ls-types`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #446.
Why
tower-lsphas been unmaintained since August 2024. It also panics when the server sends a response after the client has disconnected (ebkalderon/tower-lsp#413, reported in #446). The panic happens in its transport:Once writing to the client fails,
print_outputstops and drops the receiving end ofresponses_tx. The next request to finish then fails to send its response and lands inunreachable!().The community fork
tower-lsp-serveris maintained and is what Biome'sbiome_lsp(where our LSP layer comes from) uses. In 0.23.0 the same line forwards into the channel without theunreachable!(), and a failed send just ends the stream:The fork also fixed a related panic in 0.21.1: "Do no panic when receiving a response whose request has been cancelled". I pinned 0.23.0 rather than the 0.24 RC, because the RC switches the types crate again.
What changed
The change is mechanical and limited to
pgls_lsp. Only that crate depended ontower-lsp;pgls_wasmkeeps its ownlsp-types.tower-lsp = "0.20.0"→tower-lsp-server = "0.23.0". Thetowerdev-dependency used by the integration tests goes from 0.4 to 0.5 to match, which drops tower 0.4 from the lockfile.tower_lsp::lsp_types→tower_lsp_server::ls_types, andtower_lsp::→tower_lsp_server::.#[tower_lsp::async_trait]is gone.LanguageServernow uses async fn in traits.Url→Uri.Uriwrapsfluent_urirather than theurlcrate:to_file_path()now returnsOption<Cow<Path>>instead ofResult<PathBuf, ()>. The match arms inSession::file_path,Session::base_path,load_workspace_settingsanddid_change_watched_filesare adapted, and the fallbacks are unchanged.Uridoesn't implementDisplay, so tracing fields and log messages useuri.as_str(). That prints the same string as before.Url::parse→Uri::from_str, for diagnostic code-description links and in the tests.InitializeResult::offset_encoding: Noneis skipped when serialized, so the wire output is the same. The tests also setInitializeParams::work_done_progress_params.pgls/shutdown,pgls/set_configuration_overrides, and the workspace methods registered throughworkspace_method!) are registered through the sameLspService::build(..).custom_method(..)API, which is unchanged.Behaviour is meant to be identical. There are no feature changes.
Tests
client_disconnecting_mid_request_does_not_panictest incrates/pgls_lsp/tests/server.rs. It runs the server over in-memory pipes, drops the client's read end, sends a batch of requests, and checks thatservereturns without panicking. I ran the same scenario in a scratch crate against tower-lsp 0.20, and it reproduces the panic:panicked at tower-lsp-0.20.0/src/transport.rs:120:60: internal error: entered unreachable code.pgls_lsptests pass against the docker DB, and so do allpgls_clitests (including the daemon / lsp-proxy ones).cargo clippy --all-targets --all-featuresis clean.Risks
url::Urlnormalised URIs it parsed (scheme case, percent-encoding of unsafe characters, and so on).fluent_urikeeps the client's string as is and rejects invalid URIs. Documents are keyed by the URI the client sends, and responses now echo that exact string, so a client that sends consistent URIs (every mainstream client does) sees no difference. A client that sends URIs that aren't RFC 3986 valid would now get a deserialization error whereurlwould have repaired them.Uri::to_file_pathbuilds Windows paths with/separators (z:/workspace/...instead ofz:\workspace\...).std::pathtreats both the same, and Biome runs the same conversion on Windows.lsp_types/Urlrenames.