Skip to content

Commit

Permalink
feat(editor): Create a command to apply all auto-fixes for the curren…
Browse files Browse the repository at this point in the history
…t 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.
  • Loading branch information
nrayburn-tech authored Dec 13, 2024
1 parent b24a636 commit 38b1c2e
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 11 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 41 additions & 8 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InitializeResult> {
Expand All @@ -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,
}))
Expand Down Expand Up @@ -278,12 +284,24 @@ impl LanguageServer for Backend {

async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
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<CodeActionOrCommand> = 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::<Vec<_>>();
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<String> = 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() {
Expand All @@ -292,14 +310,17 @@ impl LanguageServer for Backend {
}
}

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),
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)]
Expand Down Expand Up @@ -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))
}
}

Expand Down Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
79 changes: 76 additions & 3 deletions editors/vscode/client/extension.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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`,
}
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit 38b1c2e

Please sign in to comment.