Skip to content

Commit 9f9e0e5

Browse files
committed
refactor(language_server): move code actions into own file (#10479)
1 parent d4687e7 commit 9f9e0e5

File tree

3 files changed

+212
-166
lines changed

3 files changed

+212
-166
lines changed

crates/oxc_language_server/src/capabilities.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ use tower_lsp_server::lsp_types::{
55
WorkspaceServerCapabilities,
66
};
77

8-
use crate::commands::LSP_COMMANDS;
9-
10-
pub const CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC: CodeActionKind =
11-
CodeActionKind::new("source.fixAll.oxc");
8+
use crate::{code_actions::CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, commands::LSP_COMMANDS};
129

1310
#[derive(Clone)]
1411
pub struct Capabilities {
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use tower_lsp_server::lsp_types::{
2+
CodeAction, CodeActionKind, Diagnostic, NumberOrString, Position, Range, TextEdit, Uri,
3+
WorkspaceEdit,
4+
};
5+
6+
use crate::linter::error_with_position::DiagnosticReport;
7+
8+
pub const CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC: CodeActionKind =
9+
CodeActionKind::new("source.fixAll.oxc");
10+
11+
// TODO: Would be better if we had exact rule name from the diagnostic instead of having to parse it.
12+
fn get_rule_name(diagnostic: &Diagnostic) -> Option<String> {
13+
if let Some(NumberOrString::String(code)) = &diagnostic.code {
14+
let open_paren = code.chars().position(|c| c == '(')?;
15+
let close_paren = code.chars().position(|c| c == ')')?;
16+
17+
return Some(code[(open_paren + 1)..close_paren].to_string());
18+
}
19+
20+
None
21+
}
22+
23+
pub fn apply_fix_code_action(report: &DiagnosticReport, uri: &Uri) -> Option<CodeAction> {
24+
let Some(fixed_content) = &report.fixed_content else {
25+
return None;
26+
};
27+
28+
// 1) Use `fixed_content.message` if it exists
29+
// 2) Try to parse the report diagnostic message
30+
// 3) Fallback to "Fix this problem"
31+
let title = match fixed_content.message.clone() {
32+
Some(msg) => msg,
33+
None => {
34+
if let Some(code) = report.diagnostic.message.split(':').next() {
35+
format!("Fix this {code} problem")
36+
} else {
37+
"Fix this problem".to_string()
38+
}
39+
}
40+
};
41+
42+
Some(CodeAction {
43+
title,
44+
kind: Some(CodeActionKind::QUICKFIX),
45+
is_preferred: Some(true),
46+
edit: Some(WorkspaceEdit {
47+
#[expect(clippy::disallowed_types)]
48+
changes: Some(std::collections::HashMap::from([(
49+
uri.clone(),
50+
vec![TextEdit { range: fixed_content.range, new_text: fixed_content.code.clone() }],
51+
)])),
52+
..WorkspaceEdit::default()
53+
}),
54+
disabled: None,
55+
data: None,
56+
diagnostics: None,
57+
command: None,
58+
})
59+
}
60+
61+
pub fn apply_all_fix_code_action<'a>(
62+
reports: impl Iterator<Item = &'a DiagnosticReport>,
63+
uri: &Uri,
64+
) -> Option<CodeAction> {
65+
let mut quick_fixes: Vec<TextEdit> = vec![];
66+
67+
for report in reports {
68+
if let Some(fixed_content) = &report.fixed_content {
69+
// when source.fixAll.oxc we collect all changes at ones
70+
// and return them as one workspace edit.
71+
// it is possible that one fix will change the range for the next fix
72+
// see oxc-project/oxc#10422
73+
quick_fixes.push(TextEdit {
74+
range: fixed_content.range,
75+
new_text: fixed_content.code.clone(),
76+
});
77+
}
78+
}
79+
80+
if quick_fixes.is_empty() {
81+
return None;
82+
}
83+
84+
Some(CodeAction {
85+
title: "quick fix".to_string(),
86+
kind: Some(CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC),
87+
is_preferred: Some(true),
88+
edit: Some(WorkspaceEdit {
89+
#[expect(clippy::disallowed_types)]
90+
changes: Some(std::collections::HashMap::from([(uri.clone(), quick_fixes)])),
91+
..WorkspaceEdit::default()
92+
}),
93+
disabled: None,
94+
data: None,
95+
diagnostics: None,
96+
command: None,
97+
})
98+
}
99+
100+
pub fn ignore_this_line_code_action(report: &DiagnosticReport, uri: &Uri) -> CodeAction {
101+
let rule_name = get_rule_name(&report.diagnostic);
102+
103+
// TODO: This CodeAction doesn't support disabling multiple rules by name for a given line.
104+
// To do that, we need to read `report.diagnostic.range.start.line` and check if a disable comment already exists.
105+
// If it does, it needs to be appended to instead of a completely new line inserted.
106+
CodeAction {
107+
title: rule_name.as_ref().map_or_else(
108+
|| "Disable oxlint for this line".into(),
109+
|s| format!("Disable {s} for this line"),
110+
),
111+
kind: Some(CodeActionKind::QUICKFIX),
112+
is_preferred: Some(false),
113+
edit: Some(WorkspaceEdit {
114+
#[expect(clippy::disallowed_types)]
115+
changes: Some(std::collections::HashMap::from([(
116+
uri.clone(),
117+
vec![TextEdit {
118+
range: Range {
119+
start: Position {
120+
line: report.diagnostic.range.start.line,
121+
// TODO: character should be set to match the first non-whitespace character in the source text to match the existing indentation.
122+
character: 0,
123+
},
124+
end: Position {
125+
line: report.diagnostic.range.start.line,
126+
// TODO: character should be set to match the first non-whitespace character in the source text to match the existing indentation.
127+
character: 0,
128+
},
129+
},
130+
new_text: rule_name.as_ref().map_or_else(
131+
|| "// eslint-disable-next-line\n".into(),
132+
|s| format!("// eslint-disable-next-line {s}\n"),
133+
),
134+
}],
135+
)])),
136+
..WorkspaceEdit::default()
137+
}),
138+
disabled: None,
139+
data: None,
140+
diagnostics: None,
141+
command: None,
142+
}
143+
}
144+
145+
pub fn ignore_this_rule_code_action(report: &DiagnosticReport, uri: &Uri) -> CodeAction {
146+
let rule_name = get_rule_name(&report.diagnostic);
147+
148+
CodeAction {
149+
title: rule_name.as_ref().map_or_else(
150+
|| "Disable oxlint for this file".into(),
151+
|s| format!("Disable {s} for this file"),
152+
),
153+
kind: Some(CodeActionKind::QUICKFIX),
154+
is_preferred: Some(false),
155+
edit: Some(WorkspaceEdit {
156+
#[expect(clippy::disallowed_types)]
157+
changes: Some(std::collections::HashMap::from([(
158+
uri.clone(),
159+
vec![TextEdit {
160+
range: Range {
161+
start: Position { line: 0, character: 0 },
162+
end: Position { line: 0, character: 0 },
163+
},
164+
new_text: rule_name.as_ref().map_or_else(
165+
|| "// eslint-disable\n".into(),
166+
|s| format!("// eslint-disable {s}\n"),
167+
),
168+
}],
169+
)])),
170+
..WorkspaceEdit::default()
171+
}),
172+
disabled: None,
173+
data: None,
174+
diagnostics: None,
175+
command: None,
176+
}
177+
}

0 commit comments

Comments
 (0)