Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions crates/oxc_language_server/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ use tower_lsp_server::{
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams, DocumentFormattingParams, ExecuteCommandParams,
InitializeParams, InitializeResult, InitializedParams, Registration, ServerInfo, TextEdit,
Uri,
InitializeParams, InitializeResult, InitializedParams, ServerInfo, TextEdit, Uri,
},
};

use crate::{
ConcurrentHashMap, ToolBuilder, capabilities::Capabilities, file_system::LSPFileSystem,
options::WorkspaceOption, worker::WorkspaceWorker,
ConcurrentHashMap, ToolBuilder,
capabilities::{Capabilities, server_capabilities},
file_system::LSPFileSystem,
options::WorkspaceOption,
worker::WorkspaceWorker,
};

/// The Backend implements the LanguageServer trait to handle LSP requests and notifications.
Expand Down Expand Up @@ -133,7 +135,12 @@ impl LanguageServer for Backend {

*self.workspace_workers.write().await = workers;

self.capabilities.set(capabilities.clone()).map_err(|err| {
let mut server_capabilities = server_capabilities();
for tool_builder in &self.tool_builders {
tool_builder.server_capabilities(&mut server_capabilities);
}

self.capabilities.set(capabilities).map_err(|err| {
let message = match err {
SetError::AlreadyInitializedError(_) => {
"capabilities are already initialized".into()
Expand All @@ -150,7 +157,7 @@ impl LanguageServer for Backend {
version: Some(server_version.to_string()),
}),
offset_encoding: None,
capabilities: capabilities.server_capabilities(&self.tool_builders),
capabilities: server_capabilities,
})
}

Expand Down Expand Up @@ -200,14 +207,6 @@ impl LanguageServer for Backend {
}
}

if capabilities.dynamic_formatting {
registrations.push(Registration {
id: "dynamic-formatting".to_string(),
method: "textDocument/formatting".to_string(),
register_options: None,
});
}

if registrations.is_empty() {
return;
}
Expand Down
211 changes: 21 additions & 190 deletions crates/oxc_language_server/src/capabilities.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
use tower_lsp_server::lsp_types::{
ClientCapabilities, CodeActionKind, CodeActionOptions, CodeActionProviderCapability,
ExecuteCommandOptions, OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability,
ClientCapabilities, OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability,
TextDocumentSyncKind, TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
};

use crate::ToolBuilder;

#[derive(Clone, Default)]
pub struct Capabilities {
pub code_action_provider: bool,
pub workspace_apply_edit: bool,
pub workspace_execute_command: bool,
pub workspace_configuration: bool,
pub dynamic_watchers: bool,
pub dynamic_formatting: 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.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());
let workspace_configuration = value
.workspace
.as_ref()
Expand All @@ -46,193 +31,39 @@ impl From<ClientCapabilities> for Capabilities {
})
});

Self {
code_action_provider,
workspace_apply_edit,
workspace_execute_command,
workspace_configuration,
dynamic_watchers,
dynamic_formatting,
}
Self { workspace_apply_edit, workspace_configuration, dynamic_watchers, dynamic_formatting }
}
}

impl Capabilities {
pub fn server_capabilities(&self, tools: &[Box<dyn ToolBuilder>]) -> ServerCapabilities {
let code_action_kinds: Vec<CodeActionKind> =
tools.iter().flat_map(|tool| tool.provided_code_action_kinds()).collect();

let commands: Vec<String> =
tools.iter().flat_map(|tool| tool.provided_commands()).collect();

ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
change: Some(TextDocumentSyncKind::FULL),
open_close: Some(true),
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
include_text: Some(false),
})),
..Default::default()
},
)),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations: None,
pub fn server_capabilities() -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
change: Some(TextDocumentSyncKind::FULL),
open_close: Some(true),
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
include_text: Some(false),
})),
..Default::default()
})),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
code_action_provider: if self.code_action_provider && !code_action_kinds.is_empty() {
Some(CodeActionProviderCapability::Options(CodeActionOptions {
code_action_kinds: Some(code_action_kinds),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
resolve_provider: None,
}))
} else {
None
},
execute_command_provider: if self.workspace_execute_command && !commands.is_empty() {
Some(ExecuteCommandOptions { commands, ..Default::default() })
} else {
None
},
// the server supports formatting, but it will tell the client if he enabled the setting
document_formatting_provider: None,
..ServerCapabilities::default()
}
file_operations: None,
}),
..ServerCapabilities::default()
}
}

#[cfg(test)]
mod test {
use tower_lsp_server::lsp_types::{
ClientCapabilities, CodeActionClientCapabilities, CodeActionKindLiteralSupport,
CodeActionLiteralSupport, DidChangeWatchedFilesClientCapabilities,
DynamicRegistrationClientCapabilities, TextDocumentClientCapabilities,
WorkspaceClientCapabilities,
ClientCapabilities, DidChangeWatchedFilesClientCapabilities, WorkspaceClientCapabilities,
};

use super::Capabilities;

#[test]
fn test_code_action_provider_vscode() {
let client_capabilities = ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
code_action: Some(CodeActionClientCapabilities {
code_action_literal_support: Some(CodeActionLiteralSupport {
code_action_kind: CodeActionKindLiteralSupport {
// this is from build (see help, about):
// Version: 1.95.3 (user setup)
// Commit: f1a4fb101478ce6ec82fe9627c43efbf9e98c813
value_set: vec![
#[expect(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);
}

#[test]
fn test_code_action_provider_intellij() {
let client_capabilities = ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
code_action: Some(CodeActionClientCapabilities {
code_action_literal_support: Some(CodeActionLiteralSupport {
code_action_kind: CodeActionKindLiteralSupport {
// this is from build (see help, about):
// Build #IU-243.22562.145, built on December 8, 2024
value_set: vec![
"quickfix".into(),
#[expect(clippy::manual_string_new)]
"".into(),
"source".into(),
"refactor".into(),
],
},
}),
..CodeActionClientCapabilities::default()
}),
..TextDocumentClientCapabilities::default()
}),
..ClientCapabilities::default()
};

let capabilities = Capabilities::from(client_capabilities);

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![
#[expect(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 {
Expand Down
23 changes: 22 additions & 1 deletion crates/oxc_language_server/src/formatter/server_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use oxc_formatter::{
use oxc_parser::Parser;
use tower_lsp_server::{
UriExt,
lsp_types::{Pattern, Position, Range, TextEdit, Uri},
lsp_types::{Pattern, Position, Range, ServerCapabilities, TextEdit, Uri},
};

use crate::{
Expand Down Expand Up @@ -67,6 +67,10 @@ impl ServerFormatterBuilder {
}

impl ToolBuilder for ServerFormatterBuilder {
fn server_capabilities(&self, capabilities: &mut ServerCapabilities) {
capabilities.document_formatting_provider =
Some(tower_lsp_server::lsp_types::OneOf::Left(true));
}
fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box<dyn Tool> {
Box::new(ServerFormatterBuilder::build(root_uri, options))
}
Expand Down Expand Up @@ -380,6 +384,23 @@ fn load_ignore_paths(cwd: &Path) -> Vec<PathBuf> {
.collect::<Vec<_>>()
}

#[cfg(test)]
mod tests_builder {
use crate::{ServerFormatterBuilder, ToolBuilder};

#[test]
fn test_server_capabilities() {
use tower_lsp_server::lsp_types::{OneOf, ServerCapabilities};

let builder = ServerFormatterBuilder;
let mut capabilities = ServerCapabilities::default();

builder.server_capabilities(&mut capabilities);

assert_eq!(capabilities.document_formatting_provider, Some(OneOf::Left(true)));
}
}

#[cfg(test)]
mod tests {
use serde_json::json;
Expand Down
Loading
Loading