diff --git a/crates/oxc_language_server/src/backend.rs b/crates/oxc_language_server/src/backend.rs index 0928a186d5685..b5249a0f8135e 100644 --- a/crates/oxc_language_server/src/backend.rs +++ b/crates/oxc_language_server/src/backend.rs @@ -18,8 +18,9 @@ use tower_lsp_server::{ }; use crate::{ - ConcurrentHashMap, capabilities::Capabilities, file_system::LSPFileSystem, - options::WorkspaceOption, worker::WorkspaceWorker, + ConcurrentHashMap, ToolBuilder, capabilities::Capabilities, file_system::LSPFileSystem, + formatter::ServerFormatterBuilder, linter::ServerLinterBuilder, options::WorkspaceOption, + worker::WorkspaceWorker, }; /// The Backend implements the LanguageServer trait to handle LSP requests and notifications. @@ -40,6 +41,8 @@ use crate::{ pub struct Backend { // The LSP client to communicate with the editor or IDE. client: Client, + // The available tool builders to create tools like linters and formatters. + tool_builders: Vec>, // Each Workspace has it own worker with Linter (and in the future the formatter). // We must respect each program inside with its own root folder // and can not use shared programmes across multiple workspaces. @@ -125,7 +128,7 @@ impl LanguageServer for Backend { .map(|workspace_options| workspace_options.options.clone()) .unwrap_or_default(); - worker.start_worker(option.clone()).await; + worker.start_worker(option.clone(), &self.tool_builders).await; } } @@ -185,7 +188,7 @@ impl LanguageServer for Backend { for (index, worker) in needed_configurations.values().enumerate() { let configuration = configurations.get(index).unwrap_or(&serde_json::Value::Null); - worker.start_worker(configuration.clone()).await; + worker.start_worker(configuration.clone(), &self.tool_builders).await; } } @@ -422,7 +425,7 @@ impl LanguageServer for Backend { let worker = WorkspaceWorker::new(folder.uri.clone()); // get the configuration from the response and init the linter let options = configurations.get(index).unwrap_or(&serde_json::Value::Null); - worker.start_worker(options.clone()).await; + worker.start_worker(options.clone(), &self.tool_builders).await; added_registrations.extend(worker.init_watchers().await); workers.push(worker); @@ -432,7 +435,7 @@ impl LanguageServer for Backend { for folder in params.event.added { let worker = WorkspaceWorker::new(folder.uri); // use default options - worker.start_worker(serde_json::Value::Null).await; + worker.start_worker(serde_json::Value::Null, &self.tool_builders).await; workers.push(worker); } } @@ -609,8 +612,11 @@ impl Backend { /// It also holds the capabilities of the language server and an in-memory file system. /// The client is used to communicate with the LSP client. pub fn new(client: Client) -> Self { + let tools: Vec> = + vec![Box::new(ServerFormatterBuilder), Box::new(ServerLinterBuilder)]; Self { client, + tool_builders: tools, workspace_workers: Arc::new(RwLock::new(vec![])), capabilities: OnceCell::new(), file_system: Arc::new(RwLock::new(LSPFileSystem::default())), diff --git a/crates/oxc_language_server/src/formatter/server_formatter.rs b/crates/oxc_language_server/src/formatter/server_formatter.rs index 4ef3e81ac503e..5f125b957b22f 100644 --- a/crates/oxc_language_server/src/formatter/server_formatter.rs +++ b/crates/oxc_language_server/src/formatter/server_formatter.rs @@ -19,13 +19,10 @@ use crate::{ utils::normalize_path, }; -pub struct ServerFormatterBuilder { - root_uri: Uri, - options: LSPFormatOptions, -} +pub struct ServerFormatterBuilder; -impl ToolBuilder for ServerFormatterBuilder { - fn new(root_uri: Uri, options: serde_json::Value) -> Self { +impl ServerFormatterBuilder { + pub fn build(root_uri: &Uri, options: serde_json::Value) -> ServerFormatter { let options = match serde_json::from_value::(options) { Ok(opts) => opts, Err(err) => { @@ -35,22 +32,24 @@ impl ToolBuilder for ServerFormatterBuilder { LSPFormatOptions::default() } }; - Self { root_uri, options } - } - - fn build(&self) -> ServerFormatter { - if self.options.experimental { + if options.experimental { debug!("experimental formatter enabled"); } - let root_path = self.root_uri.to_file_path().unwrap(); + let root_path = root_uri.to_file_path().unwrap(); ServerFormatter::new( - Self::get_format_options(&root_path, self.options.config_path.as_ref()), - self.options.experimental, + Self::get_format_options(&root_path, options.config_path.as_ref()), + options.experimental, ) } } +impl ToolBuilder for ServerFormatterBuilder { + fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box { + Box::new(ServerFormatterBuilder::build(root_uri, options)) + } +} + impl ServerFormatterBuilder { fn get_format_options(root_path: &Path, config_path: Option<&String>) -> FormatOptions { let oxfmtrc = if let Some(config) = Self::search_config_file(root_path, config_path) { @@ -144,8 +143,7 @@ impl Tool for ServerFormatter { }; } - let new_formatter = - ServerFormatterBuilder::new(root_uri.clone(), new_options_json.clone()).build(); + let new_formatter = ServerFormatterBuilder::build(root_uri, new_options_json.clone()); let watch_patterns = new_formatter.get_watcher_patterns(new_options_json); ToolRestartChanges { tool: Some(Box::new(new_formatter)), @@ -192,7 +190,7 @@ impl Tool for ServerFormatter { // TODO: Check if the changed file is actually a config file - let new_formatter = ServerFormatterBuilder::new(root_uri.clone(), options).build(); + let new_formatter = ServerFormatterBuilder::build(root_uri, options); ToolRestartChanges { tool: Some(Box::new(new_formatter)), diff --git a/crates/oxc_language_server/src/formatter/tester.rs b/crates/oxc_language_server/src/formatter/tester.rs index c99480d843457..219d5c552e6d4 100644 --- a/crates/oxc_language_server/src/formatter/tester.rs +++ b/crates/oxc_language_server/src/formatter/tester.rs @@ -7,7 +7,7 @@ use tower_lsp_server::{ use crate::{ formatter::server_formatter::{ServerFormatter, ServerFormatterBuilder}, - tool::{Tool, ToolBuilder}, + tool::Tool, }; /// Given a file path relative to the crate root directory, return the absolute path of the file. @@ -62,7 +62,7 @@ impl Tester<'_> { .join(self.relative_root_dir); let uri = Uri::from_file_path(absolute_path).expect("could not convert current dir to uri"); - ServerFormatterBuilder::new(uri, self.options.clone()).build() + ServerFormatterBuilder::build(&uri, self.options.clone()) } pub fn format_and_snapshot_single_file(&self, relative_file_path: &str) { diff --git a/crates/oxc_language_server/src/linter/server_linter.rs b/crates/oxc_language_server/src/linter/server_linter.rs index 7847e4481717f..d7493d5292c02 100644 --- a/crates/oxc_language_server/src/linter/server_linter.rs +++ b/crates/oxc_language_server/src/linter/server_linter.rs @@ -33,13 +33,12 @@ use crate::{ utils::normalize_path, }; -pub struct ServerLinterBuilder { - root_uri: Uri, - options: LSPLintOptions, -} +pub struct ServerLinterBuilder; -impl ToolBuilder for ServerLinterBuilder { - fn new(root_uri: Uri, options: serde_json::Value) -> Self { +impl ServerLinterBuilder { + /// # Panics + /// Panics if the root URI cannot be converted to a file path. + pub fn build(root_uri: &Uri, options: serde_json::Value) -> ServerLinter { let options = match serde_json::from_value::(options) { Ok(opts) => opts, Err(e) => { @@ -49,17 +48,11 @@ impl ToolBuilder for ServerLinterBuilder { LSPLintOptions::default() } }; - Self { root_uri, options } - } - - /// # Panics - /// Panics if the root URI cannot be converted to a file path. - fn build(&self) -> ServerLinter { - let root_path = self.root_uri.to_file_path().unwrap(); + let root_path = root_uri.to_file_path().unwrap(); let mut nested_ignore_patterns = Vec::new(); let (nested_configs, mut extended_paths) = - Self::create_nested_configs(&root_path, &self.options, &mut nested_ignore_patterns); - let config_path = self.options.config_path.as_ref().map_or(LINT_CONFIG_FILE, |v| v); + Self::create_nested_configs(&root_path, &options, &mut nested_ignore_patterns); + let config_path = options.config_path.as_ref().map_or(LINT_CONFIG_FILE, |v| v); let config = normalize_path(root_path.join(config_path)); let oxlintrc = if config.try_exists().is_ok_and(|exists| exists) { if let Ok(oxlintrc) = Oxlintrc::from_file(&config) { @@ -84,8 +77,8 @@ impl ToolBuilder for ServerLinterBuilder { .unwrap_or_default(); // TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality. - let use_nested_config = self.options.use_nested_configs(); - let fix_kind = FixKind::from(self.options.fix_kind.clone()); + let use_nested_config = options.use_nested_configs(); + let fix_kind = FixKind::from(options.fix_kind.clone()); let use_cross_module = config_builder.plugins().has_import() || (use_nested_config @@ -99,7 +92,7 @@ impl ToolBuilder for ServerLinterBuilder { let lint_options = LintOptions { fix: fix_kind, - report_unused_directive: match self.options.unused_disable_directives { + report_unused_directive: match options.unused_disable_directives { UnusedDisableDirectives::Allow => None, // or AllowWarnDeny::Allow, should be the same? UnusedDisableDirectives::Warn => Some(AllowWarnDeny::Warn), UnusedDisableDirectives::Deny => Some(AllowWarnDeny::Deny), @@ -125,10 +118,10 @@ impl ToolBuilder for ServerLinterBuilder { config_store, &IsolatedLintHandlerOptions { use_cross_module, - type_aware: self.options.type_aware, - fix_kind: FixKind::from(self.options.fix_kind.clone()), + type_aware: options.type_aware, + fix_kind: FixKind::from(options.fix_kind.clone()), root_path: root_path.to_path_buf(), - tsconfig_path: self.options.ts_config_path.as_ref().map(|path| { + tsconfig_path: options.ts_config_path.as_ref().map(|path| { let path = Path::new(path).to_path_buf(); if path.is_relative() { root_path.join(path) } else { path } }), @@ -136,7 +129,7 @@ impl ToolBuilder for ServerLinterBuilder { ); ServerLinter::new( - self.options.run, + options.run, root_path.to_path_buf(), isolated_linter, LintIgnoreMatcher::new(&base_patterns, &root_path, nested_ignore_patterns), @@ -146,6 +139,12 @@ impl ToolBuilder for ServerLinterBuilder { } } +impl ToolBuilder for ServerLinterBuilder { + fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box { + Box::new(ServerLinterBuilder::build(root_uri, options)) + } +} + impl ServerLinterBuilder { /// Searches inside root_uri recursively for the default oxlint config files /// and insert them inside the nested configuration @@ -291,8 +290,7 @@ impl Tool for ServerLinter { // get the cached files before refreshing the linter, and revalidate them after let cached_files = self.get_cached_files_of_diagnostics(); - let new_linter = - ServerLinterBuilder::new(root_uri.clone(), new_options_json.clone()).build(); + let new_linter = ServerLinterBuilder::build(root_uri, new_options_json.clone()); let diagnostics = Some(new_linter.revalidate_diagnostics(cached_files)); let patterns = { @@ -346,7 +344,7 @@ impl Tool for ServerLinter { options: serde_json::Value, ) -> ToolRestartChanges { // TODO: Check if the changed file is actually a config file (including extended paths) - let new_linter = ServerLinterBuilder::new(root_uri.clone(), options).build(); + let new_linter = ServerLinterBuilder::build(root_uri, options); // get the cached files before refreshing the linter, and revalidate them after let cached_files = self.get_cached_files_of_diagnostics(); diff --git a/crates/oxc_language_server/src/linter/tester.rs b/crates/oxc_language_server/src/linter/tester.rs index d90d6ccce9395..f942258ac6cbd 100644 --- a/crates/oxc_language_server/src/linter/tester.rs +++ b/crates/oxc_language_server/src/linter/tester.rs @@ -10,7 +10,7 @@ use tower_lsp_server::{ use crate::{ linter::{ServerLinterBuilder, server_linter::ServerLinter}, - tool::{Tool, ToolBuilder}, + tool::Tool, }; /// Given a file path relative to the crate root directory, return the absolute path of the file. @@ -171,7 +171,7 @@ impl Tester<'_> { .join(self.relative_root_dir); let uri = Uri::from_file_path(absolute_path).expect("could not convert current dir to uri"); - ServerLinterBuilder::new(uri, self.options.clone()).build() + ServerLinterBuilder::build(&uri, self.options.clone()) } /// Given a relative file path (relative to `oxc_language_server` crate root), run the linter diff --git a/crates/oxc_language_server/src/tool.rs b/crates/oxc_language_server/src/tool.rs index 55315147c9e9d..5b9fb1455202e 100644 --- a/crates/oxc_language_server/src/tool.rs +++ b/crates/oxc_language_server/src/tool.rs @@ -6,9 +6,8 @@ use tower_lsp_server::{ }, }; -pub trait ToolBuilder { - fn new(root_uri: Uri, options: serde_json::Value) -> Self; - fn build(&self) -> T; +pub trait ToolBuilder: Send + Sync { + fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box; } pub trait Tool: Send + Sync { diff --git a/crates/oxc_language_server/src/worker.rs b/crates/oxc_language_server/src/worker.rs index 518d2e41b624f..4823a1bed889d 100644 --- a/crates/oxc_language_server/src/worker.rs +++ b/crates/oxc_language_server/src/worker.rs @@ -11,11 +11,7 @@ use tower_lsp_server::{ }, }; -use crate::{ - formatter::ServerFormatterBuilder, - linter::ServerLinterBuilder, - tool::{Tool, ToolBuilder}, -}; +use crate::tool::{Tool, ToolBuilder}; /// A worker that manages the individual tools for a specific workspace /// and reports back the results to the [`Backend`](crate::backend::Backend). @@ -60,11 +56,9 @@ impl WorkspaceWorker { /// Start all programs (linter, formatter) for the worker. /// This should be called after the client has sent the workspace configuration. - pub async fn start_worker(&self, options: serde_json::Value) { - *self.tools.write().await = vec![ - Box::new(ServerLinterBuilder::new(self.root_uri.clone(), options.clone()).build()), - Box::new(ServerFormatterBuilder::new(self.root_uri.clone(), options.clone()).build()), - ]; + pub async fn start_worker(&self, options: serde_json::Value, tools: &[Box]) { + *self.tools.write().await = + tools.iter().map(|tool| tool.build_boxed(&self.root_uri, options.clone())).collect(); *self.options.lock().await = Some(options); } @@ -418,7 +412,10 @@ mod test_watchers { }, }; - use crate::worker::WorkspaceWorker; + use crate::{ + ToolBuilder, formatter::ServerFormatterBuilder, linter::ServerLinterBuilder, + worker::WorkspaceWorker, + }; struct Tester { pub worker: WorkspaceWorker, @@ -443,7 +440,10 @@ mod test_watchers { options: serde_json::Value, ) -> WorkspaceWorker { let worker = WorkspaceWorker::new(absolute_path); - worker.start_worker(options).await; + let tools: Vec> = + vec![Box::new(ServerLinterBuilder), Box::new(ServerFormatterBuilder)]; + + worker.start_worker(options, &tools).await; worker }