From 38b1c2ec652b76abd54b4149b38da3d0d0ad1181 Mon Sep 17 00:00:00 2001 From: Nicholas Rayburn <52075362+nrayburn-tech@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:46:05 -0600 Subject: [PATCH] feat(editor): Create a command to apply all auto-fixes for the current active text editor (#7672) Ref #7456 - Creates a custom action in the language server that applies all auto-fixes for a given file. - Updates VS Code to use the custom action with a command to apply fixes for the currently active text editor. --- crates/oxc_language_server/README.md | 2 + crates/oxc_language_server/src/main.rs | 49 +++++++++++++--- editors/vscode/README.md | 3 + editors/vscode/client/extension.ts | 79 +++++++++++++++++++++++++- editors/vscode/package.json | 5 ++ 5 files changed, 127 insertions(+), 11 deletions(-) diff --git a/crates/oxc_language_server/README.md b/crates/oxc_language_server/README.md index 8e03c90d0cd6b..da9bdff34d297 100644 --- a/crates/oxc_language_server/README.md +++ b/crates/oxc_language_server/README.md @@ -10,6 +10,8 @@ This crate provides an [LSP](https://microsoft.github.io/language-server-protoco - File Operations: `false` - [Code Actions Provider](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind): - `quickfix` + - `source.fixAll.oxc`, behaves the same as `quickfix` only used when the `CodeActionContext#only` contains + `source.fixAll.oxc`. ## Supported LSP Specifications from Server diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index 8594bfc70ddec..993707c0b933b 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -87,6 +87,9 @@ enum SyntheticRunLevel { OnType, } +const CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC: CodeActionKind = + CodeActionKind::new("source.fixAll.oxc"); + #[tower_lsp::async_trait] impl LanguageServer for Backend { async fn initialize(&self, params: InitializeParams) -> Result { @@ -111,7 +114,10 @@ impl LanguageServer for Backend { }) }) { Some(CodeActionProviderCapability::Options(CodeActionOptions { - code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]), + code_action_kinds: Some(vec![ + CodeActionKind::QUICKFIX, + CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, + ]), work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None }, resolve_provider: None, })) @@ -278,12 +284,24 @@ impl LanguageServer for Backend { async fn code_action(&self, params: CodeActionParams) -> Result> { let uri = params.text_document.uri; + let is_source_fix_all_oxc = params + .context + .only + .is_some_and(|only| only.contains(&CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC)); + let mut code_actions_vec: Vec = vec![]; 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) { + let reports = value + .iter() + .filter(|r| { + r.diagnostic.range == params.range + || range_includes(params.range, r.diagnostic.range) + }) + .collect::>(); + for report in reports { // TODO: Would be better if we had exact rule name from the diagnostic instead of having to parse it. let mut rule_name: Option = None; - if let Some(NumberOrString::String(code)) = report.clone().diagnostic.code { + if let Some(NumberOrString::String(code)) = &report.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() { @@ -292,14 +310,17 @@ impl LanguageServer for Backend { } } - let mut code_actions_vec: Vec = 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), + kind: Some(if is_source_fix_all_oxc { + CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC + } else { + CodeActionKind::QUICKFIX + }), is_preferred: Some(true), edit: Some(WorkspaceEdit { #[expect(clippy::disallowed_types)] @@ -391,12 +412,14 @@ impl LanguageServer for Backend { diagnostics: None, command: None, })); - - return Ok(Some(code_actions_vec)); } } - Ok(None) + if code_actions_vec.is_empty() { + return Ok(None); + } + + Ok(Some(code_actions_vec)) } } @@ -578,3 +601,13 @@ async fn main() { Server::new(stdin, stdout, socket).serve(service).await; } + +fn range_includes(range: Range, to_include: Range) -> bool { + if range.start >= to_include.start { + return false; + } + if range.end <= to_include.end { + return false; + } + true +} diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 8bee6d1f994ee..021d3d4db02c5 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -16,3 +16,6 @@ This is the linter for Oxc. The currently supported features are listed below. - Highlighting for warnings or errors identified by Oxlint - Quick fixes to fix a warning or error when possible - JSON schema validation for supported Oxlint configuration files (does not include ESLint configuration files) +- Command to fix all auto-fixable content within the current text editor. +- Support for `source.fixAll.oxc` as a code action provider. Configure this in your settings `editor.codeActionsOnSave` + to automatically apply fixes when saving the file. diff --git a/editors/vscode/client/extension.ts b/editors/vscode/client/extension.ts index 0e53d47ac226f..896f2107b5e2d 100644 --- a/editors/vscode/client/extension.ts +++ b/editors/vscode/client/extension.ts @@ -1,8 +1,25 @@ import { promises as fsPromises } from 'node:fs'; -import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, ThemeColor, window, workspace } from 'vscode'; +import { + CodeAction, + Command, + commands, + ExtensionContext, + StatusBarAlignment, + StatusBarItem, + ThemeColor, + window, + workspace, +} from 'vscode'; -import { MessageType, ShowMessageNotification } from 'vscode-languageclient'; +import { + CodeActionRequest, + CodeActionTriggerKind, + MessageType, + Position, + Range, + ShowMessageNotification, +} from 'vscode-languageclient'; import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; @@ -15,7 +32,7 @@ const commandPrefix = 'oxc'; const enum OxcCommands { RestartServer = `${commandPrefix}.restartServer`, - ApplyAllFixes = `${commandPrefix}.applyAllFixes`, + ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`, ShowOutputChannel = `${commandPrefix}.showOutputChannel`, ToggleEnable = `${commandPrefix}.toggleEnable`, } @@ -62,7 +79,63 @@ export async function activate(context: ExtensionContext) { }, ); + const applyAllFixesFile = commands.registerCommand( + OxcCommands.ApplyAllFixesFile, + async () => { + if (!client) { + window.showErrorMessage('oxc client not found'); + return; + } + const textEditor = window.activeTextEditor; + if (!textEditor) { + window.showErrorMessage('active text editor not found'); + return; + } + + const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1); + const codeActionResult = await client.sendRequest(CodeActionRequest.type, { + textDocument: { + uri: textEditor.document.uri.toString(), + }, + range: Range.create(Position.create(0, 0), lastLine.range.end), + context: { + diagnostics: [], + only: [], + triggerKind: CodeActionTriggerKind.Invoked, + }, + }); + const commandsOrCodeActions = await client.protocol2CodeConverter.asCodeActionResult(codeActionResult || []); + + await Promise.all( + commandsOrCodeActions + .map(async (codeActionOrCommand) => { + // Commands are always applied. Regardless of whether it's a Command or CodeAction#command. + if (isCommand(codeActionOrCommand)) { + await commands.executeCommand(codeActionOrCommand.command, codeActionOrCommand.arguments); + } else { + // Only preferred edits are applied + // LSP states edits must be run first, then commands + if (codeActionOrCommand.edit && codeActionOrCommand.isPreferred) { + await workspace.applyEdit(codeActionOrCommand.edit); + } + if (codeActionOrCommand.command) { + await commands.executeCommand( + codeActionOrCommand.command.command, + codeActionOrCommand.command.arguments, + ); + } + } + }), + ); + + function isCommand(codeActionOrCommand: CodeAction | Command): codeActionOrCommand is Command { + return typeof codeActionOrCommand.command === 'string'; + } + }, + ); + context.subscriptions.push( + applyAllFixesFile, restartCommand, showOutputCommand, toggleEnable, diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 2c6bd887f9d6a..cf8d1e14c1b48 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -52,6 +52,11 @@ "command": "oxc.showOutputChannel", "title": "Show Output Channel", "category": "Oxc" + }, + { + "command": "oxc.applyAllFixesFile", + "title": "Fix all auto-fixable problems (file)", + "category": "Oxc" } ], "configuration": {