From b83b1bd43352384bc4c2c9b669ebb6a208310ce2 Mon Sep 17 00:00:00 2001 From: Sysix <3897725+Sysix@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:44:41 +0000 Subject: [PATCH] docs(language_server): docs for `Backend` struct (#14172) --- crates/oxc_language_server/src/backend.rs | 88 +++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/crates/oxc_language_server/src/backend.rs b/crates/oxc_language_server/src/backend.rs index c07ee8a52ea3c..18801fb957970 100644 --- a/crates/oxc_language_server/src/backend.rs +++ b/crates/oxc_language_server/src/backend.rs @@ -29,7 +29,23 @@ use crate::{ worker::WorkspaceWorker, }; +/// The Backend implements the LanguageServer trait to handle LSP requests and notifications. +/// +/// It manages multiple WorkspaceWorkers, each corresponding to a workspace folder. +/// Depending on the client's capabilities, it can dynamically register features and start up other services. +/// The Client will send requests and notifications to the Backend, which will delegate them to the appropriate WorkspaceWorker. +/// The Backend also manages the in-memory file system for open files. +/// +/// A basic flow of an Editor and Server interaction is as follows: +/// - Editor sends `initialize` request with workspace folders and client capabilities. +/// - Server responds with its capabilities. +/// - Editor sends `initialized` notification. +/// - Server registers dynamic capabilities like file watchers. +/// - Editor sends `textDocument/didOpen`, `textDocument/didChange`, `textDocument/didSave`, and `textDocument/didClose` notifications. +/// - Editor sends `shutdown` request when the user closes the editor. +/// - Editor sends `exit` notification and the server exits. pub struct Backend { + // The LSP client to communicate with the editor or IDE. client: Client, // 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 @@ -39,11 +55,21 @@ pub struct Backend { // 1. `initialize` request with workspace folders // 2. `workspace/didChangeWorkspaceFolders` request workspace_workers: Arc>>, + // Capabilities of the language server, set once during `initialize` request. + // Depending on the client capabilities, the server supports different capabilities. capabilities: OnceCell, + // A simple in-memory file system to store the content of open files. + // The client will send the content of in-memory files on `textDocument/didOpen` and `textDocument/didChange`. + // This is only needed when the client supports `textDocument/formatting` request. file_system: Arc>, } impl LanguageServer for Backend { + /// Initialize the language server with the given parameters. + /// This method sets up workspace workers, capabilities, and starts the + /// [WorkspaceWorker]s if the client sent the configuration with initialization options. + /// + /// See: #[expect(deprecated)] // `params.root_uri` is deprecated, we are only falling back to it if no workspace folder is provided async fn initialize(&self, params: InitializeParams) -> Result { let server_version = env!("CARGO_PKG_VERSION"); @@ -134,6 +160,12 @@ impl LanguageServer for Backend { }) } + /// It registers dynamic capabilities like file watchers and formatting if the client supports it. + /// It also starts the [WorkspaceWorker]s if they did not start during initialization. + /// If the client supports `workspace/configuration` request, it will request the configuration for each workspace folder + /// and start the [WorkspaceWorker]s with the received configuration. + /// + /// See: async fn initialized(&self, _params: InitializedParams) { debug!("oxc initialized."); let Some(capabilities) = self.capabilities.get() else { @@ -209,6 +241,9 @@ impl LanguageServer for Backend { } } + /// This method clears all diagnostics and the in-memory file system if dynamic formatting is enabled. + /// + /// See: async fn shutdown(&self) -> Result<()> { self.clear_all_diagnostics().await; if self.capabilities.get().is_some_and(|option| option.dynamic_formatting) { @@ -217,6 +252,12 @@ impl LanguageServer for Backend { Ok(()) } + /// This method updates the configuration of each [WorkspaceWorker] and restarts them if necessary. + /// It also manages dynamic registrations for file watchers and formatting based on the new configuration. + /// It will remove/add dynamic registrations if the client supports it. + /// As an example, if a workspace changes the configuration file path, the file watcher will be updated. + /// + /// See: async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { let workers = self.workspace_workers.read().await; let new_diagnostics: papaya::HashMap, FxBuildHasher> = @@ -354,6 +395,10 @@ impl LanguageServer for Backend { } } + /// This notification is sent when a configuration file of a tool changes (example: `.oxlintrc.json`). + /// The server will re-lint the affected files and send updated diagnostics. + /// + /// See: async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { let workers = self.workspace_workers.read().await; // ToDo: what if an empty changes flag is passed? @@ -393,6 +438,12 @@ impl LanguageServer for Backend { self.publish_all_diagnostics(x).await; } + /// The server will start new [WorkspaceWorker]s for added workspace folders + /// and stop and remove [WorkspaceWorker]s for removed workspace folders including: + /// - clearing diagnostics + /// - unregistering file watchers + /// + /// See: async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { let mut workers = self.workspace_workers.write().await; let mut cleared_diagnostics = vec![]; @@ -471,6 +522,10 @@ impl LanguageServer for Backend { } } + /// It will remove the in-memory file content, because the file is saved to disk. + /// It will re-lint the file and send updated diagnostics, if necessary. + /// + /// See: async fn did_save(&self, params: DidSaveTextDocumentParams) { debug!("oxc server did save"); let uri = ¶ms.text_document.uri; @@ -494,9 +549,10 @@ impl LanguageServer for Backend { .await; } } - - /// When the document changed, it may not be written to disk, so we should - /// get the file context from the language client + /// It will update the in-memory file content if the client supports dynamic formatting. + /// It will re-lint the file and send updated diagnostics, if necessary. + /// + /// See: async fn did_change(&self, params: DidChangeTextDocumentParams) { let uri = ¶ms.text_document.uri; let workers = self.workspace_workers.read().await; @@ -522,6 +578,10 @@ impl LanguageServer for Backend { } } + /// It will add the in-memory file content if the client supports dynamic formatting. + /// It will lint the file and send diagnostics, if necessary. + /// + /// See: async fn did_open(&self, params: DidOpenTextDocumentParams) { let uri = ¶ms.text_document.uri; let workers = self.workspace_workers.read().await; @@ -548,6 +608,10 @@ impl LanguageServer for Backend { } } + /// It will remove the in-memory file content if the client supports dynamic formatting. + /// It will clear the diagnostics (internally) for the closed file. + /// + /// See: async fn did_close(&self, params: DidCloseTextDocumentParams) { let uri = ¶ms.text_document.uri; let workers = self.workspace_workers.read().await; @@ -560,6 +624,10 @@ impl LanguageServer for Backend { worker.remove_diagnostics(¶ms.text_document.uri).await; } + /// It will return code actions or commands for the given range. + /// The client can send `context.only` to `source.fixAll.oxc` to fix all diagnostics of the file. + /// + /// See: async fn code_action(&self, params: CodeActionParams) -> Result> { let uri = ¶ms.text_document.uri; let workers = self.workspace_workers.read().await; @@ -582,6 +650,10 @@ impl LanguageServer for Backend { Ok(Some(code_actions)) } + /// It will execute the given command with the provided arguments. + /// Currently, only the `fixAll` command is supported. + /// + /// See: async fn execute_command( &self, params: ExecuteCommandParams, @@ -618,6 +690,9 @@ impl LanguageServer for Backend { Err(Error::invalid_request()) } + /// It will return text edits to format the document if formatting is enabled for the workspace. + /// + /// See: async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { let uri = ¶ms.text_document.uri; let workers = self.workspace_workers.read().await; @@ -629,6 +704,10 @@ impl LanguageServer for Backend { } impl Backend { + /// Create a new Backend with the given client. + /// The Backend will manage multiple [WorkspaceWorker]s and their configurations. + /// 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 { Self { client, @@ -670,7 +749,7 @@ impl Backend { options } - // clears all diagnostics for workspace folders + /// Clears all diagnostics for workspace folders async fn clear_all_diagnostics(&self) { let mut cleared_diagnostics = vec![]; let workers = &*self.workspace_workers.read().await; @@ -680,6 +759,7 @@ impl Backend { self.publish_all_diagnostics(&cleared_diagnostics).await; } + /// Publish diagnostics for all files. async fn publish_all_diagnostics(&self, result: &[(String, Vec)]) { join_all(result.iter().map(|(path, diagnostics)| { self.client.publish_diagnostics(Uri::from_str(path).unwrap(), diagnostics.clone(), None)