Skip to content
Merged
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
88 changes: 84 additions & 4 deletions crates/oxc_language_server/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,11 +55,21 @@ pub struct Backend {
// 1. `initialize` request with workspace folders
// 2. `workspace/didChangeWorkspaceFolders` request
workspace_workers: Arc<RwLock<Vec<WorkspaceWorker>>>,
// Capabilities of the language server, set once during `initialize` request.
// Depending on the client capabilities, the server supports different capabilities.
capabilities: OnceCell<Capabilities>,
// 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<RwLock<LSPFileSystem>>,
}

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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize>
#[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<InitializeResult> {
let server_version = env!("CARGO_PKG_VERSION");
Expand Down Expand Up @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialized>
async fn initialized(&self, _params: InitializedParams) {
debug!("oxc initialized.");
let Some(capabilities) = self.capabilities.get() else {
Expand Down Expand Up @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#shutdown>
async fn shutdown(&self) -> Result<()> {
self.clear_all_diagnostics().await;
if self.capabilities.get().is_some_and(|option| option.dynamic_formatting) {
Expand All @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeConfiguration>
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
let workers = self.workspace_workers.read().await;
let new_diagnostics: papaya::HashMap<String, Vec<Diagnostic>, FxBuildHasher> =
Expand Down Expand Up @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeWatchedFiles>
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?
Expand Down Expand Up @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeWorkspaceFolders>
async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
let mut workers = self.workspace_workers.write().await;
let mut cleared_diagnostics = vec![];
Expand Down Expand Up @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didSave>
async fn did_save(&self, params: DidSaveTextDocumentParams) {
debug!("oxc server did save");
let uri = &params.text_document.uri;
Expand All @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didChange>
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = &params.text_document.uri;
let workers = self.workspace_workers.read().await;
Expand All @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didOpen>
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = &params.text_document.uri;
let workers = self.workspace_workers.read().await;
Expand All @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didClose>
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = &params.text_document.uri;
let workers = self.workspace_workers.read().await;
Expand All @@ -560,6 +624,10 @@ impl LanguageServer for Backend {
worker.remove_diagnostics(&params.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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction>
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
let uri = &params.text_document.uri;
let workers = self.workspace_workers.read().await;
Expand All @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand>
async fn execute_command(
&self,
params: ExecuteCommandParams,
Expand Down Expand Up @@ -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: <https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting>
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
let uri = &params.text_document.uri;
let workers = self.workspace_workers.read().await;
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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<Diagnostic>)]) {
join_all(result.iter().map(|(path, diagnostics)| {
self.client.publish_diagnostics(Uri::from_str(path).unwrap(), diagnostics.clone(), None)
Expand Down
Loading