mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
<!-- Thank you for contributing to Ruff! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary Add support for hover menu to ruff_server, as requested in [10595](https://github.com/astral-sh/ruff/issues/10595). Majority of new code is in hover.rs. I reused the regex from ruff-lsp's implementation. Also reused the format_rule_text function from ruff/src/commands/rule.rs Added capability registration in server.rs, and added the handler to api.rs. ## Test Plan Tested in NVIM v0.10.0-dev-2582+g2a8cef6bd, configured with lspconfig using the default options (other than cmd pointing to my test build, with options "server" and "--preview"). OS: Ubuntu 24.04, kernel 6.8.0-22. --------- Co-authored-by: Jane Lewis <me@jane.engineering>
This commit is contained in:
parent
455d22cdc8
commit
7c8c1c71a3
6 changed files with 121 additions and 0 deletions
|
@ -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 }
|
||||
|
|
|
@ -257,6 +257,7 @@ impl Server {
|
|||
},
|
||||
},
|
||||
)),
|
||||
hover_provider: Some(types::HoverProviderCapability::Simple(true)),
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
|
|
|
@ -49,6 +49,9 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
|
|||
request::FormatRange::METHOD => {
|
||||
background_request_task::<request::FormatRange>(req, BackgroundSchedule::Fmt)
|
||||
}
|
||||
request::Hover::METHOD => {
|
||||
background_request_task::<request::Hover>(req, BackgroundSchedule::Worker)
|
||||
}
|
||||
method => {
|
||||
tracing::warn!("Received request {method} which does not have a handler");
|
||||
return Task::nothing();
|
||||
|
|
|
@ -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<Vec<lsp_types::TextEdit>>;
|
||||
|
|
113
crates/ruff_server/src/server/api/requests/hover.rs
Normal file
113
crates/ruff_server/src/server/api/requests/hover.rs
Normal file
|
@ -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<lsp_types::Url> {
|
||||
std::borrow::Cow::Borrowed(¶ms.text_document_position_params.text_document.uri)
|
||||
}
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
_notifier: Notifier,
|
||||
params: types::HoverParams,
|
||||
) -> Result<Option<types::Hover>> {
|
||||
Ok(hover(&snapshot, ¶ms.text_document_position_params))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hover(
|
||||
snapshot: &DocumentSnapshot,
|
||||
position: &types::TextDocumentPositionParams,
|
||||
) -> Option<types::Hover> {
|
||||
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): )?(?P<noqa>noqa))(?::\s?(?P<codes>([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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue