diff --git a/crates/oxc_language_server/src/linter/code_actions.rs b/crates/oxc_language_server/src/linter/code_actions.rs index d375f3e717e4d..4c9b12989eae9 100644 --- a/crates/oxc_language_server/src/linter/code_actions.rs +++ b/crates/oxc_language_server/src/linter/code_actions.rs @@ -30,12 +30,12 @@ fn fix_content_to_code_action( } } -pub fn apply_fix_code_actions(report: &LinterCodeAction, uri: &Uri) -> Vec { +pub fn apply_fix_code_actions(action: &LinterCodeAction, uri: &Uri) -> Vec { let mut code_actions = vec![]; // only the first code action is preferred let mut preferred = true; - for fixed in &report.fixed_content { + for fixed in &action.fixed_content { let action = fix_content_to_code_action(fixed, uri, preferred); preferred = false; code_actions.push(action); @@ -44,11 +44,11 @@ pub fn apply_fix_code_actions(report: &LinterCodeAction, uri: &Uri) -> Vec( - reports: impl Iterator, +pub fn apply_all_fix_code_action( + actions: impl Iterator, uri: &Uri, ) -> Option { - let quick_fixes: Vec = fix_all_text_edit(reports); + let quick_fixes: Vec = fix_all_text_edit(actions); if quick_fixes.is_empty() { return None; @@ -72,28 +72,28 @@ pub fn apply_all_fix_code_action<'a>( /// Collect all text edits from the provided diagnostic reports, which can be applied at once. /// This is useful for implementing a "fix all" code action / command that applies multiple fixes in one go. -pub fn fix_all_text_edit<'a>(reports: impl Iterator) -> Vec { +pub fn fix_all_text_edit(actions: impl Iterator) -> Vec { let mut text_edits: Vec = vec![]; - for report in reports { - if report.fixed_content.is_empty() { + for action in actions { + if action.fixed_content.is_empty() { continue; } // for a real linter fix, we expect at least 3 fixes - if report.fixed_content.len() == 2 { + if action.fixed_content.len() == 2 { debug!("Multiple fixes found, but only ignore fixes available"); #[cfg(debug_assertions)] { - debug_assert!(report.fixed_content[0].message.starts_with("Disable")); - debug_assert!(report.fixed_content[0].message.ends_with("for this line")); + debug_assert!(action.fixed_content[0].message.starts_with("Disable")); + debug_assert!(action.fixed_content[0].message.ends_with("for this line")); } continue; } // For multiple fixes, we take the first one as a representative fix. // Applying all possible fixes at once is not possible in this context. - let fixed_content = report.fixed_content.first().unwrap(); + let fixed_content = action.fixed_content.first().unwrap(); // when source.fixAll.oxc we collect all changes at ones // and return them as one workspace edit. // it is possible that one fix will change the range for the next fix diff --git a/crates/oxc_language_server/src/linter/error_with_position.rs b/crates/oxc_language_server/src/linter/error_with_position.rs index 158d107480cb8..f76ba0630ea4a 100644 --- a/crates/oxc_language_server/src/linter/error_with_position.rs +++ b/crates/oxc_language_server/src/linter/error_with_position.rs @@ -17,7 +17,7 @@ pub struct DiagnosticReport { #[derive(Debug, Clone, Default)] pub struct LinterCodeAction { - // pub range: Range, + pub range: Range, pub fixed_content: Vec, } @@ -139,10 +139,7 @@ pub fn message_to_lsp_diagnostic( if error_offset == section_offset && message.span.end == section_offset { return DiagnosticReport { diagnostic, - code_action: Some(LinterCodeAction { - // range, - fixed_content, - }), + code_action: Some(LinterCodeAction { range, fixed_content }), }; } @@ -158,10 +155,7 @@ pub fn message_to_lsp_diagnostic( let code_action = if fixed_content.is_empty() { None } else { - Some(LinterCodeAction { - // range, - fixed_content, - }) + Some(LinterCodeAction { range, fixed_content }) }; DiagnosticReport { diagnostic, code_action } diff --git a/crates/oxc_language_server/src/linter/server_linter.rs b/crates/oxc_language_server/src/linter/server_linter.rs index 6335290e50c2c..b4802360b8821 100644 --- a/crates/oxc_language_server/src/linter/server_linter.rs +++ b/crates/oxc_language_server/src/linter/server_linter.rs @@ -20,6 +20,7 @@ use oxc_linter::{ LintIgnoreMatcher, LintOptions, Oxlintrc, }; +use crate::linter::error_with_position::LinterCodeAction; use crate::{ ConcurrentHashMap, linter::{ @@ -30,7 +31,6 @@ use crate::{ }, commands::{FIX_ALL_COMMAND_ID, FixAllCommandArgs}, config_walker::ConfigWalker, - error_with_position::DiagnosticReport, isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions}, options::{LintOptions as LSPLintOptions, Run, UnusedDisableDirectives}, }, @@ -302,7 +302,7 @@ pub struct ServerLinter { ignore_matcher: LintIgnoreMatcher, gitignore_glob: Vec, extended_paths: FxHashSet, - diagnostics: Arc>>>, + code_actions: Arc>>>, } impl Tool for ServerLinter { @@ -311,9 +311,7 @@ impl Tool for ServerLinter { } fn shutdown(&self) -> ToolShutdownChanges { - ToolShutdownChanges { - uris_to_clear_diagnostics: Some(self.get_cached_files_of_diagnostics()), - } + ToolShutdownChanges { uris_to_clear_diagnostics: Some(self.get_cached_uris()) } } /// # Panics @@ -436,19 +434,17 @@ impl Tool for ServerLinter { return Ok(None); } - let value = if let Some(cached_diagnostics) = self.get_cached_diagnostics(uri) { - cached_diagnostics - } else { - let diagnostics = self.run_file(uri, None); - diagnostics.unwrap_or_default() + let actions = self.get_code_actions_for_uri(uri); + + let Some(actions) = actions else { + return Ok(None); }; - if value.is_empty() { + if actions.is_empty() { return Ok(None); } - let text_edits = - fix_all_text_edit(value.iter().filter_map(|report| report.code_action.as_ref())); + let text_edits = fix_all_text_edit(actions.into_iter()); Ok(Some(WorkspaceEdit { #[expect(clippy::disallowed_types)] @@ -464,39 +460,31 @@ impl Tool for ServerLinter { range: &Range, only_code_action_kinds: Option>, ) -> Vec { - let value = if let Some(cached_diagnostics) = self.get_cached_diagnostics(uri) { - cached_diagnostics - } else { - let diagnostics = self.run_file(uri, None); - diagnostics.unwrap_or_default() + let actions = self.get_code_actions_for_uri(uri); + + let Some(actions) = actions else { + return vec![]; }; - if value.is_empty() { + if actions.is_empty() { return vec![]; } - let reports = value - .iter() - .filter(|r| r.diagnostic.range == *range || range_overlaps(*range, r.diagnostic.range)); - + let actions = + actions.into_iter().filter(|r| r.range == *range || range_overlaps(*range, r.range)); let is_source_fix_all_oxc = only_code_action_kinds .is_some_and(|only| only.contains(&CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC)); if is_source_fix_all_oxc { - return apply_all_fix_code_action( - reports.filter_map(|report| report.code_action.as_ref()), - uri, - ) - .map_or(vec![], |code_actions| vec![CodeActionOrCommand::CodeAction(code_actions)]); + return apply_all_fix_code_action(actions, uri).map_or(vec![], |code_actions| { + vec![CodeActionOrCommand::CodeAction(code_actions)] + }); } let mut code_actions_vec: Vec = vec![]; - for report in reports { - let Some(ref code_action) = report.code_action else { - continue; - }; - let fix_actions = apply_fix_code_actions(code_action, uri); + for action in actions { + let fix_actions = apply_fix_code_actions(&action, uri); code_actions_vec.extend(fix_actions.into_iter().map(CodeActionOrCommand::CodeAction)); } @@ -508,7 +496,6 @@ impl Tool for ServerLinter { /// - If the file is lintable, but no diagnostics are found, an empty vector is returned fn run_diagnostic(&self, uri: &Uri, content: Option<&str>) -> Option> { self.run_file(uri, content) - .map(|reports| reports.into_iter().map(|report| report.diagnostic).collect()) } /// Lint a file with the current linter @@ -538,7 +525,7 @@ impl Tool for ServerLinter { } fn remove_diagnostics(&self, uri: &Uri) { - self.diagnostics.pin().remove(&uri.to_string()); + self.code_actions.pin().remove(uri); } } @@ -560,21 +547,21 @@ impl ServerLinter { ignore_matcher, gitignore_glob, extended_paths, - diagnostics: Arc::new(ConcurrentHashMap::default()), + code_actions: Arc::new(ConcurrentHashMap::default()), } } - fn get_cached_diagnostics(&self, uri: &Uri) -> Option> { - if let Some(diagnostics) = self.diagnostics.pin().get(&uri.to_string()) { - // when the uri is ignored, diagnostics is None. - // We want to return Some(vec![]), so the Worker knows there are no diagnostics for this file. - return Some(diagnostics.clone().unwrap_or_default()); - } - None + fn get_cached_uris(&self) -> Vec { + self.code_actions.pin().keys().cloned().collect() } - fn get_cached_files_of_diagnostics(&self) -> Vec { - self.diagnostics.pin().keys().filter_map(|s| Uri::from_str(s).ok()).collect() + fn get_code_actions_for_uri(&self, uri: &Uri) -> Option> { + if let Some(cached_code_actions) = self.code_actions.pin().get(uri) { + cached_code_actions.clone() + } else { + self.run_file(uri, None); + self.code_actions.pin().get(uri).and_then(std::clone::Clone::clone) + } } fn is_ignored(&self, uri: &Uri) -> bool { @@ -600,16 +587,28 @@ impl ServerLinter { } /// Lint a single file, return `None` if the file is ignored. - fn run_file(&self, uri: &Uri, content: Option<&str>) -> Option> { + fn run_file(&self, uri: &Uri, content: Option<&str>) -> Option> { if self.is_ignored(uri) { return None; } - let diagnostics = self.isolated_linter.run_single(uri, content); + let mut diagnostics = vec![]; + let mut code_actions = vec![]; + + let reports = self.isolated_linter.run_single(uri, content); + if let Some(reports) = reports { + for report in reports { + diagnostics.push(report.diagnostic); + + if let Some(code_action) = report.code_action { + code_actions.push(code_action); + } + } + } - self.diagnostics.pin().insert(uri.to_string(), diagnostics.clone()); + self.code_actions.pin().insert(uri.clone(), Some(code_actions)); - diagnostics + Some(diagnostics) } fn needs_restart(old_options: &LSPLintOptions, new_options: &LSPLintOptions) -> bool {