Skip to content

Commit

Permalink
feat(oxc_language_server): implement oxc.fixAll workspace command (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
marekvospel and autofix-ci[bot] authored Feb 4, 2025
1 parent d6d80f7 commit f4662a9
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 67 deletions.
2 changes: 1 addition & 1 deletion crates/oxc_language_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ workspace = true

[[bin]]
name = "oxc_language_server"
test = false
test = true
doctest = false

[dependencies]
Expand Down
6 changes: 6 additions & 0 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This crate provides an [LSP](https://microsoft.github.io/language-server-protoco
- Workspace
- [Workspace Folders](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceFoldersServerCapabilities): `true`
- File Operations: `false`
- [Workspace commands](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand)
- `oxc.fixAll`, requires `{ uri: URL }` as command argument. Does safe fixes in `uri` file.
- [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
Expand All @@ -32,6 +34,10 @@ The server will revalidate or reset the diagnostics for all open files and send
The server expects this request when the oxlint configuration is changed.
The server will revalidate the diagnostics for all open files and send one or more [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) requests to the client.

#### [workspace/executeCommand](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand)

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)

### TextDocument

#### [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen)
Expand Down
104 changes: 96 additions & 8 deletions crates/oxc_language_server/src/capabilities.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
use tower_lsp::lsp_types::{
ClientCapabilities, CodeActionKind, CodeActionOptions, CodeActionProviderCapability, OneOf,
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, WorkDoneProgressOptions,
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
ClientCapabilities, CodeActionKind, CodeActionOptions, CodeActionProviderCapability,
ExecuteCommandOptions, OneOf, ServerCapabilities, TextDocumentSyncCapability,
TextDocumentSyncKind, WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities,
};

use crate::commands::LSP_COMMANDS;

pub const CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC: CodeActionKind =
CodeActionKind::new("source.fixAll.oxc");

#[derive(Clone)]
pub struct Capabilities {
pub code_action_provider: bool,
pub workspace_apply_edit: bool,
pub workspace_execute_command: bool,
}

impl From<ClientCapabilities> for Capabilities {
fn from(value: ClientCapabilities) -> Self {
// check if the client support some code action literal support
let code_action_provider = value.text_document.is_some_and(|capability| {
capability.code_action.is_some_and(|code_action| {
code_action.code_action_literal_support.is_some_and(|literal_support| {
let code_action_provider = value.text_document.as_ref().is_some_and(|capability| {
capability.code_action.as_ref().is_some_and(|code_action| {
code_action.code_action_literal_support.as_ref().is_some_and(|literal_support| {
!literal_support.code_action_kind.value_set.is_empty()
})
})
});
let workspace_apply_edit =
value.workspace.as_ref().is_some_and(|workspace| workspace.apply_edit.is_some());
let workspace_execute_command =
value.workspace.as_ref().is_some_and(|workspace| workspace.execute_command.is_some());

Self { code_action_provider }
Self { code_action_provider, workspace_apply_edit, workspace_execute_command }
}
}

impl From<Capabilities> for ServerCapabilities {
fn from(value: Capabilities) -> Self {
let commands = LSP_COMMANDS
.iter()
.filter_map(|c| if c.available(value.clone()) { Some(c.command_id()) } else { None })
.collect();

Self {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
workspace: Some(WorkspaceServerCapabilities {
Expand All @@ -51,6 +66,11 @@ impl From<Capabilities> for ServerCapabilities {
} else {
None
},
execute_command_provider: if value.workspace_execute_command {
Some(ExecuteCommandOptions { commands, ..Default::default() })
} else {
None
},
..ServerCapabilities::default()
}
}
Expand All @@ -60,7 +80,8 @@ impl From<Capabilities> for ServerCapabilities {
mod test {
use tower_lsp::lsp_types::{
ClientCapabilities, CodeActionClientCapabilities, CodeActionKindLiteralSupport,
CodeActionLiteralSupport, TextDocumentClientCapabilities,
CodeActionLiteralSupport, DynamicRegistrationClientCapabilities,
TextDocumentClientCapabilities, WorkspaceClientCapabilities,
};

use super::Capabilities;
Expand Down Expand Up @@ -129,4 +150,71 @@ mod test {

assert!(capabilities.code_action_provider);
}

#[test]
fn test_code_action_provider_nvim() {
let client_capabilities = ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
code_action: Some(CodeActionClientCapabilities {
code_action_literal_support: Some(CodeActionLiteralSupport {
code_action_kind: CodeActionKindLiteralSupport {
// nvim 0.10.3
value_set: vec![
#[allow(clippy::manual_string_new)]
"".into(),
"quickfix".into(),
"refactor".into(),
"refactor.extract".into(),
"refactor.inline".into(),
"refactor.rewrite".into(),
"source".into(),
"source.organizeImports".into(),
],
},
}),
..CodeActionClientCapabilities::default()
}),
..TextDocumentClientCapabilities::default()
}),
..ClientCapabilities::default()
};

let capabilities = Capabilities::from(client_capabilities);

assert!(capabilities.code_action_provider);
}

// This tests code, intellij and neovim (at least nvim 0.10.0+), as they all support dynamic registration.
#[test]
fn test_workspace_execute_command() {
let client_capabilities = ClientCapabilities {
workspace: Some(WorkspaceClientCapabilities {
execute_command: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
}),
..WorkspaceClientCapabilities::default()
}),
..ClientCapabilities::default()
};

let capabilities = Capabilities::from(client_capabilities);

assert!(capabilities.workspace_execute_command);
}

#[test]
fn test_workspace_edit_nvim() {
let client_capabilities = ClientCapabilities {
workspace: Some(WorkspaceClientCapabilities {
// Nvim 0.10.3
apply_edit: Some(true),
..WorkspaceClientCapabilities::default()
}),
..ClientCapabilities::default()
};

let capabilities = Capabilities::from(client_capabilities);

assert!(capabilities.workspace_apply_edit);
}
}
116 changes: 116 additions & 0 deletions crates/oxc_language_server/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use log::error;
use serde::Deserialize;
use tower_lsp::{
jsonrpc::{self, Error},
lsp_types::{
request::ApplyWorkspaceEdit, ApplyWorkspaceEditParams, TextEdit, Url, WorkspaceEdit,
},
};

use crate::{capabilities::Capabilities, Backend};

pub const LSP_COMMANDS: [WorkspaceCommands; 1] = [WorkspaceCommands::FixAll(FixAllCommand)];

pub trait WorkspaceCommand {
fn command_id(&self) -> String;
fn available(&self, cap: Capabilities) -> bool;
type CommandArgs<'a>: serde::Deserialize<'a>;
async fn execute(
&self,
backend: &Backend,
args: Self::CommandArgs<'_>,
) -> jsonrpc::Result<Option<serde_json::Value>>;
}

pub enum WorkspaceCommands {
FixAll(FixAllCommand),
}

impl WorkspaceCommands {
pub fn command_id(&self) -> String {
match self {
WorkspaceCommands::FixAll(c) => c.command_id(),
}
}
pub fn available(&self, cap: Capabilities) -> bool {
match self {
WorkspaceCommands::FixAll(c) => c.available(cap),
}
}
pub async fn execute(
&self,
backend: &Backend,
args: Vec<serde_json::Value>,
) -> jsonrpc::Result<Option<serde_json::Value>> {
match self {
WorkspaceCommands::FixAll(c) => {
let arg: Result<
<FixAllCommand as WorkspaceCommand>::CommandArgs<'_>,
serde_json::Error,
> = serde_json::from_value(serde_json::Value::Array(args));
if let Err(e) = arg {
error!("Invalid args passed to {:?}: {e}", c.command_id());
return Err(Error::invalid_request());
}
let arg = arg.unwrap();

c.execute(backend, arg).await
}
}
}
}

pub struct FixAllCommand;

#[derive(Deserialize)]
pub struct FixAllCommandArg {
uri: String,
}

impl WorkspaceCommand for FixAllCommand {
fn command_id(&self) -> String {
"oxc.fixAll".into()
}
fn available(&self, cap: Capabilities) -> bool {
cap.workspace_apply_edit
}
type CommandArgs<'a> = (FixAllCommandArg,);

async fn execute(
&self,
backend: &Backend,
args: Self::CommandArgs<'_>,
) -> jsonrpc::Result<Option<serde_json::Value>> {
let url = Url::parse(&args.0.uri);
if let Err(e) = url {
error!("Invalid uri passed to {:?}: {e}", self.command_id());
return Err(Error::invalid_request());
}
let url = url.unwrap();

let mut edits = vec![];
if let Some(value) = backend.diagnostics_report_map.get(&url.to_string()) {
for report in value.iter() {
if let Some(fixed) = &report.fixed_content {
edits.push(TextEdit { range: fixed.range, new_text: fixed.code.clone() });
}
}
let _ = backend
.client
.send_request::<ApplyWorkspaceEdit>(ApplyWorkspaceEditParams {
label: Some(match edits.len() {
1 => "Oxlint: 1 fix applied".into(),
n => format!("Oxlint: {n} fixes applied"),
}),
edit: WorkspaceEdit {
#[expect(clippy::disallowed_types)]
changes: Some(std::collections::HashMap::from([(url, edits)])),
..WorkspaceEdit::default()
},
})
.await;
}

Ok(None)
}
}
19 changes: 17 additions & 2 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{fmt::Debug, path::PathBuf, str::FromStr};

use commands::LSP_COMMANDS;
use dashmap::DashMap;
use futures::future::join_all;
use globset::Glob;
Expand All @@ -14,8 +15,9 @@ use tower_lsp::{
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
NumberOrString, Position, Range, ServerInfo, TextEdit, Url, WorkspaceEdit,
DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult,
InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit, Url,
WorkspaceEdit,
},
Client, LanguageServer, LspService, Server,
};
Expand All @@ -27,6 +29,7 @@ use crate::linter::error_with_position::DiagnosticReport;
use crate::linter::server_linter::ServerLinter;

mod capabilities;
mod commands;
mod linter;

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

Ok(Some(code_actions_vec))
}

async fn execute_command(
&self,
params: ExecuteCommandParams,
) -> Result<Option<serde_json::Value>> {
let command = LSP_COMMANDS.iter().find(|c| c.command_id() == params.command);

return match command {
Some(c) => c.execute(self, params.arguments).await,
None => Err(Error::invalid_request()),
};
}
}

impl Backend {
Expand Down
Loading

0 comments on commit f4662a9

Please sign in to comment.