diff --git a/Cargo.lock b/Cargo.lock index 8b8f9fec2f26e..73a117e1cf857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2262,6 +2262,7 @@ dependencies = [ "libc", "lsp-server", "lsp-types", + "regex", "ruff_diagnostics", "ruff_formatter", "ruff_linter", diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 24297c970717f..a93430d6eb0d3 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -36,6 +36,7 @@ serde = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } walkdir = { workspace = true } +regex = { workspace = true } [dev-dependencies] insta = { workspace = true } diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 647e42f353eab..e9944db053c63 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -257,6 +257,7 @@ impl Server { }, }, )), + hover_provider: Some(types::HoverProviderCapability::Simple(true)), text_document_sync: Some(TextDocumentSyncCapability::Options( TextDocumentSyncOptions { open_close: Some(true), diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 81e969a5a1ac8..610774ced0171 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -48,6 +48,9 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { request::FormatRange::METHOD => { background_request_task::(req, BackgroundSchedule::Fmt) } + request::Hover::METHOD => { + background_request_task::(req, BackgroundSchedule::Worker) + } method => { tracing::warn!("Received request {method} which does not have a handler"); return Task::nothing(); diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs index 3713ef536f592..049f396f639f2 100644 --- a/crates/ruff_server/src/server/api/requests.rs +++ b/crates/ruff_server/src/server/api/requests.rs @@ -4,6 +4,7 @@ mod diagnostic; mod execute_command; mod format; mod format_range; +mod hover; use super::{ define_document_url, @@ -15,5 +16,6 @@ pub(super) use diagnostic::DocumentDiagnostic; pub(super) use execute_command::ExecuteCommand; pub(super) use format::Format; pub(super) use format_range::FormatRange; +pub(super) use hover::Hover; type FormatResponse = Option>; diff --git a/crates/ruff_server/src/server/api/requests/hover.rs b/crates/ruff_server/src/server/api/requests/hover.rs new file mode 100644 index 0000000000000..23a6c09c393b0 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/hover.rs @@ -0,0 +1,113 @@ +use crate::server::{client::Notifier, Result}; +use crate::session::DocumentSnapshot; +use lsp_types::{self as types, request as req}; +use regex::Regex; +use ruff_diagnostics::FixAvailability; +use ruff_linter::registry::{Linter, Rule, RuleNamespace}; +use ruff_source_file::OneIndexed; + +pub(crate) struct Hover; + +impl super::RequestHandler for Hover { + type RequestType = req::HoverRequest; +} + +impl super::BackgroundDocumentRequestHandler for Hover { + fn document_url(params: &types::HoverParams) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + fn run_with_snapshot( + snapshot: DocumentSnapshot, + _notifier: Notifier, + params: types::HoverParams, + ) -> Result> { + Ok(hover(&snapshot, ¶ms.text_document_position_params)) + } +} + +pub(crate) fn hover( + snapshot: &DocumentSnapshot, + position: &types::TextDocumentPositionParams, +) -> Option { + let document = snapshot.document(); + let line_number: usize = position + .position + .line + .try_into() + .expect("line number should fit within a usize"); + let line_range = document.index().line_range( + OneIndexed::from_zero_indexed(line_number), + document.contents(), + ); + + let line = &document.contents()[line_range]; + + // Get the list of codes. + let noqa_regex = Regex::new(r"(?i:# (?:(?:ruff|flake8): )?(?Pnoqa))(?::\s?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?").unwrap(); + let noqa_captures = noqa_regex.captures(line)?; + let codes_match = noqa_captures.name("codes")?; + let codes_start = codes_match.start(); + let code_regex = Regex::new(r"[A-Z]+[0-9]+").unwrap(); + let cursor: usize = position + .position + .character + .try_into() + .expect("column number should fit within a usize"); + let word = code_regex.find_iter(codes_match.as_str()).find(|code| { + cursor >= (code.start() + codes_start) && cursor < (code.end() + codes_start) + })?; + + // Get rule for the code under the cursor. + let rule = Rule::from_code(word.as_str()); + let output = if let Ok(rule) = rule { + format_rule_text(rule) + } else { + format!("{}: Rule not found", word.as_str()) + }; + + let hover = types::Hover { + contents: types::HoverContents::Markup(types::MarkupContent { + kind: types::MarkupKind::Markdown, + value: output, + }), + range: None, + }; + + Some(hover) +} + +fn format_rule_text(rule: Rule) -> String { + let mut output = String::new(); + output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); + output.push('\n'); + output.push('\n'); + + let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); + output.push_str(&format!("Derived from the **{}** linter.", linter.name())); + output.push('\n'); + output.push('\n'); + + let fix_availability = rule.fixable(); + if matches!( + fix_availability, + FixAvailability::Always | FixAvailability::Sometimes + ) { + output.push_str(&fix_availability.to_string()); + output.push('\n'); + output.push('\n'); + } + + if rule.is_preview() || rule.is_nursery() { + output.push_str(r"This rule is in preview and is not stable."); + output.push('\n'); + output.push('\n'); + } + + if let Some(explanation) = rule.explanation() { + output.push_str(explanation.trim()); + } else { + tracing::warn!("Rule {} does not have an explanation", rule.noqa_code()); + output.push_str("An issue occurred: an explanation for this rule was not found."); + } + output +}