From b7ce694162bcd82592926afbbbf918ed61c49bad Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 30 Apr 2025 14:06:18 -0400 Subject: [PATCH] red_knot_server: add auto-completion MVP This PR does the wiring necessary to respond to completion requests from LSP clients. As far as the actual completion results go, they are nearly about the dumbest and simplest thing we can do: we simply return a de-duplicated list of all identifiers from the current module. --- crates/red_knot_ide/src/completion.rs | 39 +++++++++++++ crates/red_knot_ide/src/lib.rs | 2 + crates/red_knot_server/src/server.rs | 3 + crates/red_knot_server/src/server/api.rs | 5 ++ .../src/server/api/requests.rs | 2 + .../src/server/api/requests/completion.rs | 58 +++++++++++++++++++ .../src/server/schedule/task.rs | 1 - 7 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 crates/red_knot_ide/src/completion.rs create mode 100644 crates/red_knot_server/src/server/api/requests/completion.rs diff --git a/crates/red_knot_ide/src/completion.rs b/crates/red_knot_ide/src/completion.rs new file mode 100644 index 0000000000..275b1fcb1e --- /dev/null +++ b/crates/red_knot_ide/src/completion.rs @@ -0,0 +1,39 @@ +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_python_ast::visitor::source_order::SourceOrderVisitor; +use ruff_python_ast::{AnyNodeRef, Identifier}; +use ruff_text_size::TextSize; + +use crate::Db; + +pub struct Completion { + pub label: String, +} + +pub fn completion(db: &dyn Db, file: File, _offset: TextSize) -> Vec { + let parsed = parsed_module(db.upcast(), file); + identifiers(parsed.syntax().into()) + .into_iter() + .map(|label| Completion { label }) + .collect() +} + +fn identifiers(node: AnyNodeRef) -> Vec { + struct Visitor { + identifiers: Vec, + } + + impl<'a> SourceOrderVisitor<'a> for Visitor { + fn visit_identifier(&mut self, id: &'a Identifier) { + self.identifiers.push(id.id.as_str().to_string()); + } + } + + let mut visitor = Visitor { + identifiers: vec![], + }; + node.visit_source_order(&mut visitor); + visitor.identifiers.sort(); + visitor.identifiers.dedup(); + visitor.identifiers +} diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs index 48f1145894..a85c0cd1fe 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/red_knot_ide/src/lib.rs @@ -1,3 +1,4 @@ +mod completion; mod db; mod find_node; mod goto; @@ -5,6 +6,7 @@ mod hover; mod inlay_hints; mod markup; +pub use completion::completion; pub use db::Db; pub use goto::goto_type_definition; pub use hover::hover; diff --git a/crates/red_knot_server/src/server.rs b/crates/red_knot_server/src/server.rs index e57d1f7bbf..3556fec9d4 100644 --- a/crates/red_knot_server/src/server.rs +++ b/crates/red_knot_server/src/server.rs @@ -227,6 +227,9 @@ impl Server { inlay_hint_provider: Some(lsp_types::OneOf::Right( InlayHintServerCapabilities::Options(InlayHintOptions::default()), )), + completion_provider: Some(lsp_types::CompletionOptions { + ..Default::default() + }), ..Default::default() } } diff --git a/crates/red_knot_server/src/server/api.rs b/crates/red_knot_server/src/server/api.rs index 478666ea61..5bfc835cc5 100644 --- a/crates/red_knot_server/src/server/api.rs +++ b/crates/red_knot_server/src/server/api.rs @@ -36,6 +36,11 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { request::InlayHintRequestHandler::METHOD => background_request_task::< request::InlayHintRequestHandler, >(req, BackgroundSchedule::Worker), + request::CompletionRequestHandler::METHOD => background_request_task::< + request::CompletionRequestHandler, + >( + req, BackgroundSchedule::LatencySensitive + ), method => { tracing::warn!("Received request {method} which does not have a handler"); diff --git a/crates/red_knot_server/src/server/api/requests.rs b/crates/red_knot_server/src/server/api/requests.rs index b6e907aa0c..bfdec09623 100644 --- a/crates/red_knot_server/src/server/api/requests.rs +++ b/crates/red_knot_server/src/server/api/requests.rs @@ -1,8 +1,10 @@ +mod completion; mod diagnostic; mod goto_type_definition; mod hover; mod inlay_hints; +pub(super) use completion::CompletionRequestHandler; pub(super) use diagnostic::DocumentDiagnosticRequestHandler; pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; pub(super) use hover::HoverRequestHandler; diff --git a/crates/red_knot_server/src/server/api/requests/completion.rs b/crates/red_knot_server/src/server/api/requests/completion.rs new file mode 100644 index 0000000000..93a1c9d8cc --- /dev/null +++ b/crates/red_knot_server/src/server/api/requests/completion.rs @@ -0,0 +1,58 @@ +use std::borrow::Cow; + +use lsp_types::request::Completion; +use lsp_types::{CompletionItem, CompletionParams, CompletionResponse, Url}; +use red_knot_ide::completion; +use red_knot_project::ProjectDatabase; +use ruff_db::source::{line_index, source_text}; + +use crate::document::PositionExt; +use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; +use crate::server::client::Notifier; +use crate::DocumentSnapshot; + +pub(crate) struct CompletionRequestHandler; + +impl RequestHandler for CompletionRequestHandler { + type RequestType = Completion; +} + +impl BackgroundDocumentRequestHandler for CompletionRequestHandler { + fn document_url(params: &CompletionParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position.text_document.uri) + } + + fn run_with_snapshot( + snapshot: DocumentSnapshot, + db: ProjectDatabase, + _notifier: Notifier, + params: CompletionParams, + ) -> crate::server::Result> { + let Some(file) = snapshot.file(&db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let source = source_text(&db, file); + let line_index = line_index(&db, file); + let offset = params.text_document_position.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + let completions = completion(&db, file, offset); + if completions.is_empty() { + return Ok(None); + } + + let items: Vec = completions + .into_iter() + .map(|comp| CompletionItem { + label: comp.label, + ..Default::default() + }) + .collect(); + let response = CompletionResponse::Array(items); + Ok(Some(response)) + } +} diff --git a/crates/red_knot_server/src/server/schedule/task.rs b/crates/red_knot_server/src/server/schedule/task.rs index b9ae6f4a59..d269c1edb5 100644 --- a/crates/red_knot_server/src/server/schedule/task.rs +++ b/crates/red_knot_server/src/server/schedule/task.rs @@ -21,7 +21,6 @@ pub(in crate::server) enum BackgroundSchedule { Fmt, /// The task should be run on the general high-priority background /// thread. Reserved for actions caused by the user typing (e.g.syntax highlighting). - #[expect(dead_code)] LatencySensitive, /// The task should be run on a regular-priority background thread. /// The default for any request that isn't in the critical path of the user typing.