Skip to content

Commit dc700f5

Browse files
committed
refactor(language_server): introduce LSPFileSystem (#13731)
Formatting requests has no source text included: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentFormattingParams Because users can format in memory files, the server needs to keep track of the changes. Created in `Backend` struct, so when we later support `textDocument/diagnostic` (we support currently only `textDocument/publishDiagnostic)`, we can use the same File System for it too :) Putting the struct into a Workspace Worker is an option too, but there can be multiple of it. With this solution, there is only one instance of it :)
1 parent 42e2c1d commit dc700f5

File tree

3 files changed

+64
-0
lines changed

3 files changed

+64
-0
lines changed

crates/oxc_language_server/src/capabilities.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub struct Capabilities {
1414
pub workspace_execute_command: bool,
1515
pub workspace_configuration: bool,
1616
pub dynamic_watchers: bool,
17+
pub dynamic_formatting: bool,
1718
}
1819

1920
impl From<ClientCapabilities> for Capabilities {
@@ -39,13 +40,20 @@ impl From<ClientCapabilities> for Capabilities {
3940
watched_files.dynamic_registration.is_some_and(|dynamic| dynamic)
4041
})
4142
});
43+
// TODO: enable it when we support formatting
44+
// let formatting = value.text_document.as_ref().is_some_and(|text_document| {
45+
// text_document.formatting.is_some_and(|formatting| {
46+
// formatting.dynamic_registration.is_some_and(|dynamic| dynamic)
47+
// })
48+
// });
4249

4350
Self {
4451
code_action_provider,
4552
workspace_apply_edit,
4653
workspace_execute_command,
4754
workspace_configuration,
4855
dynamic_watchers,
56+
dynamic_formatting: false,
4957
}
5058
}
5159
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use tower_lsp_server::lsp_types::Uri;
2+
3+
use crate::ConcurrentHashMap;
4+
5+
#[derive(Debug, Default)]
6+
pub struct LSPFileSystem {
7+
files: ConcurrentHashMap<Uri, String>,
8+
}
9+
10+
impl LSPFileSystem {
11+
pub fn clear(&self) {
12+
self.files.pin().clear();
13+
}
14+
15+
pub fn set(&self, uri: &Uri, content: String) {
16+
self.files.pin().insert(uri.clone(), content);
17+
}
18+
19+
#[expect(dead_code)] // used for the oxc_formatter in the future
20+
pub fn get(&self, uri: &Uri) -> Option<String> {
21+
self.files.pin().get(uri).cloned()
22+
}
23+
24+
pub fn remove(&self, uri: &Uri) {
25+
self.files.pin().remove(uri);
26+
}
27+
}

crates/oxc_language_server/src/main.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use tower_lsp_server::{
2222
mod capabilities;
2323
mod code_actions;
2424
mod commands;
25+
mod file_system;
2526
mod formatter;
2627
mod linter;
2728
mod options;
@@ -36,6 +37,8 @@ use linter::server_linter::ServerLinterRun;
3637
use options::{Options, WorkspaceOption};
3738
use worker::WorkspaceWorker;
3839

40+
use crate::file_system::LSPFileSystem;
41+
3942
type ConcurrentHashMap<K, V> = papaya::HashMap<K, V, FxBuildHasher>;
4043

4144
const OXC_CONFIG_FILE: &str = ".oxlintrc.json";
@@ -51,6 +54,7 @@ struct Backend {
5154
// 2. `workspace/didChangeWorkspaceFolders` request
5255
workspace_workers: Arc<RwLock<Vec<WorkspaceWorker>>>,
5356
capabilities: OnceCell<Capabilities>,
57+
file_system: Arc<RwLock<LSPFileSystem>>,
5458
}
5559

5660
impl LanguageServer for Backend {
@@ -203,6 +207,9 @@ impl LanguageServer for Backend {
203207

204208
async fn shutdown(&self) -> Result<()> {
205209
self.clear_all_diagnostics().await;
210+
if self.capabilities.get().is_some_and(|option| option.dynamic_formatting) {
211+
self.file_system.write().await.clear();
212+
}
206213
Ok(())
207214
}
208215

@@ -444,6 +451,12 @@ impl LanguageServer for Backend {
444451
let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(uri)) else {
445452
return;
446453
};
454+
455+
if self.capabilities.get().is_some_and(|option| option.dynamic_formatting) {
456+
// saving the file means we can read again from the file system
457+
self.file_system.write().await.remove(uri);
458+
}
459+
447460
if let Some(diagnostics) = worker.lint_file(uri, None, ServerLinterRun::OnSave).await {
448461
self.client
449462
.publish_diagnostics(
@@ -464,6 +477,13 @@ impl LanguageServer for Backend {
464477
return;
465478
};
466479
let content = params.content_changes.first().map(|c| c.text.clone());
480+
481+
if self.capabilities.get().is_some_and(|option| option.dynamic_formatting)
482+
&& let Some(content) = &content
483+
{
484+
self.file_system.write().await.set(uri, content.to_string());
485+
}
486+
467487
if let Some(diagnostics) = worker.lint_file(uri, content, ServerLinterRun::OnType).await {
468488
self.client
469489
.publish_diagnostics(
@@ -483,6 +503,11 @@ impl LanguageServer for Backend {
483503
};
484504

485505
let content = params.text_document.text;
506+
507+
if self.capabilities.get().is_some_and(|option| option.dynamic_formatting) {
508+
self.file_system.write().await.set(uri, content.to_string());
509+
}
510+
486511
if let Some(diagnostics) =
487512
worker.lint_file(uri, Some(content), ServerLinterRun::Always).await
488513
{
@@ -502,6 +527,9 @@ impl LanguageServer for Backend {
502527
let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(uri)) else {
503528
return;
504529
};
530+
if self.capabilities.get().is_some_and(|option| option.dynamic_formatting) {
531+
self.file_system.write().await.remove(uri);
532+
}
505533
worker.remove_diagnostics(&params.text_document.uri).await;
506534
}
507535

@@ -626,6 +654,7 @@ async fn main() {
626654
client,
627655
workspace_workers: Arc::new(RwLock::new(vec![])),
628656
capabilities: OnceCell::new(),
657+
file_system: Arc::new(RwLock::new(LSPFileSystem::default())),
629658
})
630659
.finish();
631660

0 commit comments

Comments
 (0)