Skip to content

Commit f4662a9

Browse files
marekvospelautofix-ci[bot]
andauthoredFeb 4, 2025··
feat(oxc_language_server): implement oxc.fixAll workspace command (#8858)
This pull request focuses on optimizing the implementation of the `oxlint.applyAllFixesFile` vscode command by adding the command interface to the LSP instead of requesting all code actions and executing the preferred ones. This PR contains an abstraction above the LSP workspace commands, the `oxc.fixAll` command itself and minor changes to the vscode extension. Since the `workspace/executeCommand` handler is new, I've created an abstraction to register the commands and parse their arguments. While it isn't necessary, I feel like it makes future additions to the LS easier. I tested the command in both vscode and neovim, doubt it will be hard to use this in the zed / intellij projects. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent d6d80f7 commit f4662a9

File tree

6 files changed

+248
-67
lines changed

6 files changed

+248
-67
lines changed
 

‎crates/oxc_language_server/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ workspace = true
1818

1919
[[bin]]
2020
name = "oxc_language_server"
21-
test = false
21+
test = true
2222
doctest = false
2323

2424
[dependencies]

‎crates/oxc_language_server/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This crate provides an [LSP](https://microsoft.github.io/language-server-protoco
88
- Workspace
99
- [Workspace Folders](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceFoldersServerCapabilities): `true`
1010
- File Operations: `false`
11+
- [Workspace commands](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand)
12+
- `oxc.fixAll`, requires `{ uri: URL }` as command argument. Does safe fixes in `uri` file.
1113
- [Code Actions Provider](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind):
1214
- `quickfix`
1315
- `source.fixAll.oxc`, behaves the same as `quickfix` only used when the `CodeActionContext#only` contains
@@ -32,6 +34,10 @@ The server will revalidate or reset the diagnostics for all open files and send
3234
The server expects this request when the oxlint configuration is changed.
3335
The server will revalidate the diagnostics for all open files and send one or more [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) requests to the client.
3436

37+
#### [workspace/executeCommand](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand)
38+
39+
Executes a [Command](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand) if it exists. See [Server Capabilities](#server-capabilities)
40+
3541
### TextDocument
3642

3743
#### [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen)

‎crates/oxc_language_server/src/capabilities.rs

+96-8
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,48 @@
11
use tower_lsp::lsp_types::{
2-
ClientCapabilities, CodeActionKind, CodeActionOptions, CodeActionProviderCapability, OneOf,
3-
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, WorkDoneProgressOptions,
4-
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
2+
ClientCapabilities, CodeActionKind, CodeActionOptions, CodeActionProviderCapability,
3+
ExecuteCommandOptions, OneOf, ServerCapabilities, TextDocumentSyncCapability,
4+
TextDocumentSyncKind, WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities,
5+
WorkspaceServerCapabilities,
56
};
67

8+
use crate::commands::LSP_COMMANDS;
9+
710
pub const CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC: CodeActionKind =
811
CodeActionKind::new("source.fixAll.oxc");
912

13+
#[derive(Clone)]
1014
pub struct Capabilities {
1115
pub code_action_provider: bool,
16+
pub workspace_apply_edit: bool,
17+
pub workspace_execute_command: bool,
1218
}
1319

1420
impl From<ClientCapabilities> for Capabilities {
1521
fn from(value: ClientCapabilities) -> Self {
1622
// check if the client support some code action literal support
17-
let code_action_provider = value.text_document.is_some_and(|capability| {
18-
capability.code_action.is_some_and(|code_action| {
19-
code_action.code_action_literal_support.is_some_and(|literal_support| {
23+
let code_action_provider = value.text_document.as_ref().is_some_and(|capability| {
24+
capability.code_action.as_ref().is_some_and(|code_action| {
25+
code_action.code_action_literal_support.as_ref().is_some_and(|literal_support| {
2026
!literal_support.code_action_kind.value_set.is_empty()
2127
})
2228
})
2329
});
30+
let workspace_apply_edit =
31+
value.workspace.as_ref().is_some_and(|workspace| workspace.apply_edit.is_some());
32+
let workspace_execute_command =
33+
value.workspace.as_ref().is_some_and(|workspace| workspace.execute_command.is_some());
2434

25-
Self { code_action_provider }
35+
Self { code_action_provider, workspace_apply_edit, workspace_execute_command }
2636
}
2737
}
2838

2939
impl From<Capabilities> for ServerCapabilities {
3040
fn from(value: Capabilities) -> Self {
41+
let commands = LSP_COMMANDS
42+
.iter()
43+
.filter_map(|c| if c.available(value.clone()) { Some(c.command_id()) } else { None })
44+
.collect();
45+
3146
Self {
3247
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
3348
workspace: Some(WorkspaceServerCapabilities {
@@ -51,6 +66,11 @@ impl From<Capabilities> for ServerCapabilities {
5166
} else {
5267
None
5368
},
69+
execute_command_provider: if value.workspace_execute_command {
70+
Some(ExecuteCommandOptions { commands, ..Default::default() })
71+
} else {
72+
None
73+
},
5474
..ServerCapabilities::default()
5575
}
5676
}
@@ -60,7 +80,8 @@ impl From<Capabilities> for ServerCapabilities {
6080
mod test {
6181
use tower_lsp::lsp_types::{
6282
ClientCapabilities, CodeActionClientCapabilities, CodeActionKindLiteralSupport,
63-
CodeActionLiteralSupport, TextDocumentClientCapabilities,
83+
CodeActionLiteralSupport, DynamicRegistrationClientCapabilities,
84+
TextDocumentClientCapabilities, WorkspaceClientCapabilities,
6485
};
6586

6687
use super::Capabilities;
@@ -129,4 +150,71 @@ mod test {
129150

130151
assert!(capabilities.code_action_provider);
131152
}
153+
154+
#[test]
155+
fn test_code_action_provider_nvim() {
156+
let client_capabilities = ClientCapabilities {
157+
text_document: Some(TextDocumentClientCapabilities {
158+
code_action: Some(CodeActionClientCapabilities {
159+
code_action_literal_support: Some(CodeActionLiteralSupport {
160+
code_action_kind: CodeActionKindLiteralSupport {
161+
// nvim 0.10.3
162+
value_set: vec![
163+
#[allow(clippy::manual_string_new)]
164+
"".into(),
165+
"quickfix".into(),
166+
"refactor".into(),
167+
"refactor.extract".into(),
168+
"refactor.inline".into(),
169+
"refactor.rewrite".into(),
170+
"source".into(),
171+
"source.organizeImports".into(),
172+
],
173+
},
174+
}),
175+
..CodeActionClientCapabilities::default()
176+
}),
177+
..TextDocumentClientCapabilities::default()
178+
}),
179+
..ClientCapabilities::default()
180+
};
181+
182+
let capabilities = Capabilities::from(client_capabilities);
183+
184+
assert!(capabilities.code_action_provider);
185+
}
186+
187+
// This tests code, intellij and neovim (at least nvim 0.10.0+), as they all support dynamic registration.
188+
#[test]
189+
fn test_workspace_execute_command() {
190+
let client_capabilities = ClientCapabilities {
191+
workspace: Some(WorkspaceClientCapabilities {
192+
execute_command: Some(DynamicRegistrationClientCapabilities {
193+
dynamic_registration: Some(true),
194+
}),
195+
..WorkspaceClientCapabilities::default()
196+
}),
197+
..ClientCapabilities::default()
198+
};
199+
200+
let capabilities = Capabilities::from(client_capabilities);
201+
202+
assert!(capabilities.workspace_execute_command);
203+
}
204+
205+
#[test]
206+
fn test_workspace_edit_nvim() {
207+
let client_capabilities = ClientCapabilities {
208+
workspace: Some(WorkspaceClientCapabilities {
209+
// Nvim 0.10.3
210+
apply_edit: Some(true),
211+
..WorkspaceClientCapabilities::default()
212+
}),
213+
..ClientCapabilities::default()
214+
};
215+
216+
let capabilities = Capabilities::from(client_capabilities);
217+
218+
assert!(capabilities.workspace_apply_edit);
219+
}
132220
}
+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use log::error;
2+
use serde::Deserialize;
3+
use tower_lsp::{
4+
jsonrpc::{self, Error},
5+
lsp_types::{
6+
request::ApplyWorkspaceEdit, ApplyWorkspaceEditParams, TextEdit, Url, WorkspaceEdit,
7+
},
8+
};
9+
10+
use crate::{capabilities::Capabilities, Backend};
11+
12+
pub const LSP_COMMANDS: [WorkspaceCommands; 1] = [WorkspaceCommands::FixAll(FixAllCommand)];
13+
14+
pub trait WorkspaceCommand {
15+
fn command_id(&self) -> String;
16+
fn available(&self, cap: Capabilities) -> bool;
17+
type CommandArgs<'a>: serde::Deserialize<'a>;
18+
async fn execute(
19+
&self,
20+
backend: &Backend,
21+
args: Self::CommandArgs<'_>,
22+
) -> jsonrpc::Result<Option<serde_json::Value>>;
23+
}
24+
25+
pub enum WorkspaceCommands {
26+
FixAll(FixAllCommand),
27+
}
28+
29+
impl WorkspaceCommands {
30+
pub fn command_id(&self) -> String {
31+
match self {
32+
WorkspaceCommands::FixAll(c) => c.command_id(),
33+
}
34+
}
35+
pub fn available(&self, cap: Capabilities) -> bool {
36+
match self {
37+
WorkspaceCommands::FixAll(c) => c.available(cap),
38+
}
39+
}
40+
pub async fn execute(
41+
&self,
42+
backend: &Backend,
43+
args: Vec<serde_json::Value>,
44+
) -> jsonrpc::Result<Option<serde_json::Value>> {
45+
match self {
46+
WorkspaceCommands::FixAll(c) => {
47+
let arg: Result<
48+
<FixAllCommand as WorkspaceCommand>::CommandArgs<'_>,
49+
serde_json::Error,
50+
> = serde_json::from_value(serde_json::Value::Array(args));
51+
if let Err(e) = arg {
52+
error!("Invalid args passed to {:?}: {e}", c.command_id());
53+
return Err(Error::invalid_request());
54+
}
55+
let arg = arg.unwrap();
56+
57+
c.execute(backend, arg).await
58+
}
59+
}
60+
}
61+
}
62+
63+
pub struct FixAllCommand;
64+
65+
#[derive(Deserialize)]
66+
pub struct FixAllCommandArg {
67+
uri: String,
68+
}
69+
70+
impl WorkspaceCommand for FixAllCommand {
71+
fn command_id(&self) -> String {
72+
"oxc.fixAll".into()
73+
}
74+
fn available(&self, cap: Capabilities) -> bool {
75+
cap.workspace_apply_edit
76+
}
77+
type CommandArgs<'a> = (FixAllCommandArg,);
78+
79+
async fn execute(
80+
&self,
81+
backend: &Backend,
82+
args: Self::CommandArgs<'_>,
83+
) -> jsonrpc::Result<Option<serde_json::Value>> {
84+
let url = Url::parse(&args.0.uri);
85+
if let Err(e) = url {
86+
error!("Invalid uri passed to {:?}: {e}", self.command_id());
87+
return Err(Error::invalid_request());
88+
}
89+
let url = url.unwrap();
90+
91+
let mut edits = vec![];
92+
if let Some(value) = backend.diagnostics_report_map.get(&url.to_string()) {
93+
for report in value.iter() {
94+
if let Some(fixed) = &report.fixed_content {
95+
edits.push(TextEdit { range: fixed.range, new_text: fixed.code.clone() });
96+
}
97+
}
98+
let _ = backend
99+
.client
100+
.send_request::<ApplyWorkspaceEdit>(ApplyWorkspaceEditParams {
101+
label: Some(match edits.len() {
102+
1 => "Oxlint: 1 fix applied".into(),
103+
n => format!("Oxlint: {n} fixes applied"),
104+
}),
105+
edit: WorkspaceEdit {
106+
#[expect(clippy::disallowed_types)]
107+
changes: Some(std::collections::HashMap::from([(url, edits)])),
108+
..WorkspaceEdit::default()
109+
},
110+
})
111+
.await;
112+
}
113+
114+
Ok(None)
115+
}
116+
}

‎crates/oxc_language_server/src/main.rs

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{fmt::Debug, path::PathBuf, str::FromStr};
22

3+
use commands::LSP_COMMANDS;
34
use dashmap::DashMap;
45
use futures::future::join_all;
56
use globset::Glob;
@@ -14,8 +15,9 @@ use tower_lsp::{
1415
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
1516
ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
1617
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
17-
DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
18-
NumberOrString, Position, Range, ServerInfo, TextEdit, Url, WorkspaceEdit,
18+
DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult,
19+
InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit, Url,
20+
WorkspaceEdit,
1921
},
2022
Client, LanguageServer, LspService, Server,
2123
};
@@ -27,6 +29,7 @@ use crate::linter::error_with_position::DiagnosticReport;
2729
use crate::linter::server_linter::ServerLinter;
2830

2931
mod capabilities;
32+
mod commands;
3033
mod linter;
3134

3235
type FxDashMap<K, V> = DashMap<K, V, FxBuildHasher>;
@@ -386,6 +389,18 @@ impl LanguageServer for Backend {
386389

387390
Ok(Some(code_actions_vec))
388391
}
392+
393+
async fn execute_command(
394+
&self,
395+
params: ExecuteCommandParams,
396+
) -> Result<Option<serde_json::Value>> {
397+
let command = LSP_COMMANDS.iter().find(|c| c.command_id() == params.command);
398+
399+
return match command {
400+
Some(c) => c.execute(self, params.arguments).await,
401+
None => Err(Error::invalid_request()),
402+
};
403+
}
389404
}
390405

391406
impl Backend {

‎editors/vscode/client/extension.ts

+12-56
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
11
import { promises as fsPromises } from 'node:fs';
22

3-
import {
4-
CodeAction,
5-
Command,
6-
commands,
7-
ExtensionContext,
8-
StatusBarAlignment,
9-
StatusBarItem,
10-
ThemeColor,
11-
window,
12-
workspace,
13-
} from 'vscode';
3+
import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, ThemeColor, window, workspace } from 'vscode';
144

15-
import {
16-
CodeActionRequest,
17-
CodeActionTriggerKind,
18-
MessageType,
19-
Position,
20-
Range,
21-
ShowMessageNotification,
22-
} from 'vscode-languageclient';
5+
import { ExecuteCommandRequest, MessageType, ShowMessageNotification } from 'vscode-languageclient';
236

247
import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
258

@@ -37,6 +20,10 @@ const enum OxcCommands {
3720
ToggleEnable = `${commandPrefix}.toggleEnable`,
3821
}
3922

23+
const enum LspCommands {
24+
FixAll = 'oxc.fixAll',
25+
}
26+
4027
let client: LanguageClient;
4128

4229
let myStatusBarItem: StatusBarItem;
@@ -92,45 +79,14 @@ export async function activate(context: ExtensionContext) {
9279
return;
9380
}
9481

95-
const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
96-
const codeActionResult = await client.sendRequest(CodeActionRequest.type, {
97-
textDocument: {
82+
const params = {
83+
command: LspCommands.FixAll,
84+
arguments: [{
9885
uri: textEditor.document.uri.toString(),
99-
},
100-
range: Range.create(Position.create(0, 0), lastLine.range.end),
101-
context: {
102-
diagnostics: [],
103-
only: [],
104-
triggerKind: CodeActionTriggerKind.Invoked,
105-
},
106-
});
107-
const commandsOrCodeActions = await client.protocol2CodeConverter.asCodeActionResult(codeActionResult || []);
108-
109-
await Promise.all(
110-
commandsOrCodeActions
111-
.map(async (codeActionOrCommand) => {
112-
// Commands are always applied. Regardless of whether it's a Command or CodeAction#command.
113-
if (isCommand(codeActionOrCommand)) {
114-
await commands.executeCommand(codeActionOrCommand.command, codeActionOrCommand.arguments);
115-
} else {
116-
// Only preferred edits are applied
117-
// LSP states edits must be run first, then commands
118-
if (codeActionOrCommand.edit && codeActionOrCommand.isPreferred) {
119-
await workspace.applyEdit(codeActionOrCommand.edit);
120-
}
121-
if (codeActionOrCommand.command) {
122-
await commands.executeCommand(
123-
codeActionOrCommand.command.command,
124-
codeActionOrCommand.command.arguments,
125-
);
126-
}
127-
}
128-
}),
129-
);
86+
}],
87+
};
13088

131-
function isCommand(codeActionOrCommand: CodeAction | Command): codeActionOrCommand is Command {
132-
return typeof codeActionOrCommand.command === 'string';
133-
}
89+
await client.sendRequest(ExecuteCommandRequest.type, params);
13490
},
13591
);
13692

0 commit comments

Comments
 (0)
Please sign in to comment.