Skip to content

Commit e91a0fe

Browse files
authored
[red-knot] Implement basic LSP server (#12624)
## Summary This PR adds basic LSP implementation for the Red Knot project. This is basically a fork of the existing `ruff_server` crate into a `red_knot_server` crate. The following are the main differences: 1. The `Session` stores a map from workspace root to the corresponding Red Knot database (`RootDatabase`). 2. The database is initialized with the newly implemented `LSPSystem` (implementation of `System` trait) 3. The `LSPSystem` contains the server index corresponding to each workspace and an underlying OS system implementation. For certain methods, the system first checks if there's an open document in LSP system and returns the information from that. Otherwise, it falls back to the OS system to get that information. These methods are `path_metadata`, `read_to_string` and `read_to_notebook` 4. Add `as_any_mut` method for `System` **Why fork?** Forking allows us to experiment with the functionalities that are specific to Red Knot. The architecture is completely different and so the requirements for an LSP implementation are different as well. For example, Red Knot only supports a single workspace, so the LSP system needs to map the multi-workspace support to each Red Knot instance. In the end, the server code isn't too big, it will be easier to implement Red Knot specific functionality without worrying about existing server limitations and it shouldn't be difficult to port the existing server. ## Review Most of the server files hasn't been changed. I'm going to list down the files that have been changed along with highlight the specific part of the file that's changed from the existing server code. Changed files: * Red Knot CLI implementation: https://github.com/astral-sh/ruff/pull/12624/files#diff-579596339a29d3212a641232e674778c339b446de33b890c7fdad905b5eb50e1 * In https://github.com/astral-sh/ruff/pull/12624/files#diff-b9a9041a8a2bace014bf3687c3ef0512f25e0541f112fad6131b14242f408db6, server capabilities have been updated, dynamic capability registration is removed * In https://github.com/astral-sh/ruff/pull/12624/files#diff-b9a9041a8a2bace014bf3687c3ef0512f25e0541f112fad6131b14242f408db6, the API for `clear_diagnostics` now take in a `Url` instead of `DocumentQuery` as the document version doesn't matter when clearing diagnostics after a document is closed * [`did_close`](https://github.com/astral-sh/ruff/pull/12624/files#diff-9271370102a6f3be8defaca40c82485b0048731942520b491a3bdd2ee0e25493), [`did_close_notebook`](https://github.com/astral-sh/ruff/pull/12624/files#diff-96fb53ffb12c1694356e17313e4bb37b3f0931e887878b5d7c896c19ff60283b), [`did_open`](https://github.com/astral-sh/ruff/pull/12624/files#diff-60e852cf1aa771e993131cabf98eb4c467963a8328f10eccdb43b3e8f0f1fb12), [`did_open_notebook`](https://github.com/astral-sh/ruff/pull/12624/files#diff-ac356eb5e36c3b2c1c135eda9dfbcab5c12574d1cb77c71f7da8dbcfcfb2d2f1) are updated to open / close file from the corresponding Red Knot workspace * The [diagnostic handler](https://github.com/astral-sh/ruff/pull/12624/files#diff-4475f318fd0290d0292834569a7df5699debdcc0a453b411b8c3d329f1b879d9) is updated to request diagnostics from Red Knot * The [`Session::new`] method in https://github.com/astral-sh/ruff/pull/12624/files#diff-55c96201296200c1cab37c8b0407b6c733381374b94be7ae50563bfe95264e4d is updated to construct the Red Knot databases for each workspace. It also contains the `index_mut` and `MutIndexGuard` implementation * And, `LSPSystem` implementation is in https://github.com/astral-sh/ruff/pull/12624/files#diff-4ed62bd359c43b0bf1a13f04349dcd954966934bb8d544de7813f974182b489e ## Test Plan First, configure VS Code to use the `red_knot` binary 1. Build the `red_knot` binary by `cargo build` 2. Update the VS Code extension to specify the path to this binary ```json { "ruff.path": ["/path/to/ruff/target/debug/red_knot"] } ``` 3. Restart VS Code Now, open a file containing red-knot specific diagnostics, close the file and validate that diagnostics disappear.
1 parent d2c627e commit e91a0fe

File tree

44 files changed

+3912
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3912
-1
lines changed

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ruff_workspace = { path = "crates/ruff_workspace" }
3737

3838
red_knot_module_resolver = { path = "crates/red_knot_module_resolver" }
3939
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
40+
red_knot_server = { path = "crates/red_knot_server" }
4041
red_knot_workspace = { path = "crates/red_knot_workspace" }
4142

4243
aho-corasick = { version = "1.1.3" }

crates/red_knot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ license.workspace = true
1414
[dependencies]
1515
red_knot_module_resolver = { workspace = true }
1616
red_knot_workspace = { workspace = true }
17+
red_knot_server = { workspace = true }
1718

1819
ruff_db = { workspace = true, features = ["os", "cache"] }
1920

crates/red_knot/src/main.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::num::NonZeroUsize;
12
use std::sync::Mutex;
23

34
use clap::Parser;
@@ -29,6 +30,9 @@ mod cli;
2930
)]
3031
#[command(version)]
3132
struct Args {
33+
#[command(subcommand)]
34+
pub(crate) command: Command,
35+
3236
#[arg(
3337
long,
3438
help = "Changes the current working directory.",
@@ -65,6 +69,11 @@ struct Args {
6569
watch: bool,
6670
}
6771

72+
#[derive(Debug, clap::Subcommand)]
73+
pub enum Command {
74+
Server,
75+
}
76+
6877
#[allow(
6978
clippy::print_stdout,
7079
clippy::unnecessary_wraps,
@@ -73,6 +82,7 @@ struct Args {
7382
)]
7483
pub fn main() -> anyhow::Result<()> {
7584
let Args {
85+
command,
7686
current_directory,
7787
custom_typeshed_dir,
7888
extra_search_path: extra_paths,
@@ -83,6 +93,18 @@ pub fn main() -> anyhow::Result<()> {
8393

8494
let verbosity = verbosity.level();
8595
countme::enable(verbosity == Some(VerbosityLevel::Trace));
96+
97+
if matches!(command, Command::Server) {
98+
let four = NonZeroUsize::new(4).unwrap();
99+
100+
// by default, we set the number of worker threads to `num_cpus`, with a maximum of 4.
101+
let worker_threads = std::thread::available_parallelism()
102+
.unwrap_or(four)
103+
.max(four);
104+
105+
return red_knot_server::Server::new(worker_threads)?.run();
106+
}
107+
86108
setup_tracing(verbosity);
87109

88110
let cwd = if let Some(cwd) = current_directory {

crates/red_knot_module_resolver/src/db.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ pub(crate) mod tests {
7474
&self.system
7575
}
7676

77+
fn system_mut(&mut self) -> &mut dyn ruff_db::system::System {
78+
&mut self.system
79+
}
80+
7781
fn files(&self) -> &Files {
7882
&self.files
7983
}

crates/red_knot_python_semantic/src/db.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ pub(crate) mod tests {
7777
&self.system
7878
}
7979

80+
fn system_mut(&mut self) -> &mut dyn System {
81+
&mut self.system
82+
}
83+
8084
fn files(&self) -> &Files {
8185
&self.files
8286
}

crates/red_knot_server/Cargo.toml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[package]
2+
name = "red_knot_server"
3+
version = "0.0.0"
4+
publish = false
5+
authors = { workspace = true }
6+
edition = { workspace = true }
7+
rust-version = { workspace = true }
8+
homepage = { workspace = true }
9+
documentation = { workspace = true }
10+
repository = { workspace = true }
11+
license = { workspace = true }
12+
13+
[dependencies]
14+
red_knot_workspace = { workspace = true }
15+
ruff_db = { workspace = true }
16+
ruff_linter = { workspace = true }
17+
ruff_notebook = { workspace = true }
18+
ruff_python_ast = { workspace = true }
19+
ruff_source_file = { workspace = true }
20+
ruff_text_size = { workspace = true }
21+
22+
anyhow = { workspace = true }
23+
crossbeam = { workspace = true }
24+
jod-thread = { workspace = true }
25+
lsp-server = { workspace = true }
26+
lsp-types = { workspace = true }
27+
rustc-hash = { workspace = true }
28+
salsa = { workspace = true }
29+
serde = { workspace = true }
30+
serde_json = { workspace = true }
31+
shellexpand = { workspace = true }
32+
tracing = { workspace = true }
33+
tracing-subscriber = { workspace = true }
34+
35+
[dev-dependencies]
36+
37+
[target.'cfg(target_vendor = "apple")'.dependencies]
38+
libc = { workspace = true }
39+
40+
[lints]
41+
workspace = true

crates/red_knot_server/src/edit.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion.
2+
3+
mod notebook;
4+
mod range;
5+
mod text_document;
6+
7+
use lsp_types::{PositionEncodingKind, Url};
8+
pub use notebook::NotebookDocument;
9+
pub(crate) use range::RangeExt;
10+
pub(crate) use text_document::DocumentVersion;
11+
pub use text_document::TextDocument;
12+
13+
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
14+
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
15+
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
16+
pub enum PositionEncoding {
17+
/// UTF 16 is the encoding supported by all LSP clients.
18+
#[default]
19+
UTF16,
20+
21+
/// Second choice because UTF32 uses a fixed 4 byte encoding for each character (makes conversion relatively easy)
22+
UTF32,
23+
24+
/// Ruff's preferred encoding
25+
UTF8,
26+
}
27+
28+
/// A unique document ID, derived from a URL passed as part of an LSP request.
29+
/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook.
30+
#[derive(Clone, Debug)]
31+
pub enum DocumentKey {
32+
Notebook(Url),
33+
NotebookCell(Url),
34+
Text(Url),
35+
}
36+
37+
impl DocumentKey {
38+
/// Returns the URL associated with the key.
39+
pub(crate) fn url(&self) -> &Url {
40+
match self {
41+
DocumentKey::NotebookCell(url)
42+
| DocumentKey::Notebook(url)
43+
| DocumentKey::Text(url) => url,
44+
}
45+
}
46+
}
47+
48+
impl std::fmt::Display for DocumentKey {
49+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50+
match self {
51+
Self::NotebookCell(url) | Self::Notebook(url) | Self::Text(url) => url.fmt(f),
52+
}
53+
}
54+
}
55+
56+
impl From<PositionEncoding> for lsp_types::PositionEncodingKind {
57+
fn from(value: PositionEncoding) -> Self {
58+
match value {
59+
PositionEncoding::UTF8 => lsp_types::PositionEncodingKind::UTF8,
60+
PositionEncoding::UTF16 => lsp_types::PositionEncodingKind::UTF16,
61+
PositionEncoding::UTF32 => lsp_types::PositionEncodingKind::UTF32,
62+
}
63+
}
64+
}
65+
66+
impl TryFrom<&lsp_types::PositionEncodingKind> for PositionEncoding {
67+
type Error = ();
68+
69+
fn try_from(value: &PositionEncodingKind) -> Result<Self, Self::Error> {
70+
Ok(if value == &PositionEncodingKind::UTF8 {
71+
PositionEncoding::UTF8
72+
} else if value == &PositionEncodingKind::UTF16 {
73+
PositionEncoding::UTF16
74+
} else if value == &PositionEncodingKind::UTF32 {
75+
PositionEncoding::UTF32
76+
} else {
77+
return Err(());
78+
})
79+
}
80+
}

0 commit comments

Comments
 (0)