Skip to content

Commit

Permalink
Implement hover menu support for ruff-server; Issue #10595 (#11096)
Browse files Browse the repository at this point in the history
<!--
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](#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>
  • Loading branch information
nolanking90 and snowsignal committed Apr 23, 2024
1 parent 455d22c commit 7c8c1c7
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ruff_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ impl Server {
},
},
)),
hover_provider: Some(types::HoverProviderCapability::Simple(true)),
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_server/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_server/src/server/api/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod diagnostic;
mod execute_command;
mod format;
mod format_range;
mod hover;

use super::{
define_document_url,
Expand All @@ -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 changes: 113 additions & 0 deletions crates/ruff_server/src/server/api/requests/hover.rs
Original file line number Diff line number Diff line change
@@ -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(&params.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, &params.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
}

0 comments on commit 7c8c1c7

Please sign in to comment.