Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(language_server): Add code actions to disable rules for the current line or entire file #6968

Merged
merged 3 commits into from
Nov 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 102 additions & 21 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod linter;

use std::{fmt::Debug, path::PathBuf, str::FromStr};

use crate::linter::{DiagnosticReport, ServerLinter};
use dashmap::DashMap;
use futures::future::join_all;
use globset::Glob;
Expand All @@ -10,6 +11,7 @@ use log::{debug, error, info};
use oxc_linter::{FixKind, LinterBuilder, Oxlintrc};
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, OnceCell, RwLock, SetError};
use tower_lsp::lsp_types::{NumberOrString, Position, Range};
use tower_lsp::{
jsonrpc::{Error, ErrorCode, Result},
lsp_types::{
Expand All @@ -25,8 +27,6 @@ use tower_lsp::{
Client, LanguageServer, LspService, Server,
};

use crate::linter::{DiagnosticReport, ServerLinter};

struct Backend {
client: Client,
root_uri: OnceCell<Option<Url>>,
Expand Down Expand Up @@ -277,29 +277,108 @@ impl LanguageServer for Backend {
let uri = params.text_document.uri;

if let Some(value) = self.diagnostics_report_map.get(&uri.to_string()) {
if let Some(report) = value
.iter()
.find(|r| r.diagnostic.range == params.range && r.fixed_content.is_some())
{
let title =
report.diagnostic.message.split(':').next().map_or_else(
|| "Fix this problem".into(),
|s| format!("Fix this {s} problem"),
);

let fixed_content = report.fixed_content.clone().unwrap();

return Ok(Some(vec![CodeActionOrCommand::CodeAction(CodeAction {
title,
if let Some(report) = value.iter().find(|r| r.diagnostic.range == params.range) {
// TODO: Would be better if we had exact rule name from the diagnostic instead of having to parse it.
let mut rule_name: Option<String> = None;
if let Some(NumberOrString::String(code)) = report.clone().diagnostic.code {
let open_paren = code.chars().position(|c| c == '(');
let close_paren = code.chars().position(|c| c == ')');
if open_paren.is_some() && close_paren.is_some() {
rule_name =
Some(code[(open_paren.unwrap() + 1)..close_paren.unwrap()].to_string());
}
}

let mut code_actions_vec: Vec<CodeActionOrCommand> = vec![];
if let Some(fixed_content) = &report.fixed_content {
code_actions_vec.push(CodeActionOrCommand::CodeAction(CodeAction {
title: report.diagnostic.message.split(':').next().map_or_else(
|| "Fix this problem".into(),
|s| format!("Fix this {s} problem"),
),
kind: Some(CodeActionKind::QUICKFIX),
is_preferred: Some(true),
edit: Some(WorkspaceEdit {
#[expect(clippy::disallowed_types)]
changes: Some(std::collections::HashMap::from([(
uri.clone(),
vec![TextEdit {
range: fixed_content.range,
new_text: fixed_content.code.clone(),
}],
)])),
..WorkspaceEdit::default()
}),
disabled: None,
data: None,
diagnostics: None,
command: None,
}));
}

code_actions_vec.push(
// TODO: This CodeAction doesn't support disabling multiple rules by name for a given line.
// To do that, we need to read `report.diagnostic.range.start.line` and check if a disable comment already exists.
// If it does, it needs to be appended to instead of a completely new line inserted.
CodeActionOrCommand::CodeAction(CodeAction {
title: rule_name.clone().map_or_else(
|| "Disable oxlint for this line".into(),
|s| format!("Disable {s} for this line"),
),
kind: Some(CodeActionKind::QUICKFIX),
is_preferred: Some(false),
edit: Some(WorkspaceEdit {
#[expect(clippy::disallowed_types)]
changes: Some(std::collections::HashMap::from([(
uri.clone(),
vec![TextEdit {
range: Range {
nrayburn-tech marked this conversation as resolved.
Show resolved Hide resolved
start: Position {
line: report.diagnostic.range.start.line,
// TODO: character should be set to match the first non-whitespace character in the source text to match the existing indentation.
character: 0,
},
end: Position {
line: report.diagnostic.range.start.line,
// TODO: character should be set to match the first non-whitespace character in the source text to match the existing indentation.
character: 0,
},
},
new_text: rule_name.clone().map_or_else(
|| "// eslint-disable-next-line\n".into(),
nrayburn-tech marked this conversation as resolved.
Show resolved Hide resolved
nrayburn-tech marked this conversation as resolved.
Show resolved Hide resolved
|s| format!("// eslint-disable-next-line {s}\n"),
),
}],
)])),
..WorkspaceEdit::default()
}),
disabled: None,
data: None,
diagnostics: None,
command: None,
}),
);

code_actions_vec.push(CodeActionOrCommand::CodeAction(CodeAction {
title: rule_name.clone().map_or_else(
|| "Disable oxlint for this file".into(),
|s| format!("Disable {s} for this file"),
),
kind: Some(CodeActionKind::QUICKFIX),
is_preferred: Some(true),
is_preferred: Some(false),
edit: Some(WorkspaceEdit {
#[expect(clippy::disallowed_types)]
changes: Some(std::collections::HashMap::from([(
uri,
uri.clone(),
vec![TextEdit {
range: fixed_content.range,
new_text: fixed_content.code,
range: Range {
start: Position { line: 0, character: 0 },
end: Position { line: 0, character: 0 },
},
new_text: rule_name.clone().map_or_else(
|| "// eslint-disable\n".into(),
|s| format!("// eslint-disable {s}\n"),
),
}],
)])),
..WorkspaceEdit::default()
Expand All @@ -308,7 +387,9 @@ impl LanguageServer for Backend {
data: None,
diagnostics: None,
command: None,
})]));
}));

return Ok(Some(code_actions_vec));
}
}

Expand Down