Skip to content

Commit dd1429c

Browse files
authored
fix(lsp): debounce diagnostics after rapid changes (#10770)
1 parent 376a572 commit dd1429c

6 files changed

Lines changed: 275 additions & 11 deletions

File tree

.changeset/six-wasps-do.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved the Biome Language Server DX by orchestrating certain operations, so that they won't block the editor during typing. This improvement is more visible in large documents.

crates/biome_lsp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ parking_lot = { workspace = true }
3737
rustc-hash = { workspace = true }
3838
serde = { workspace = true, features = ["derive"] }
3939
serde_json = { workspace = true }
40-
tokio = { workspace = true, features = ["io-std", "rt"] }
40+
tokio = { workspace = true, features = ["io-std", "rt", "time"] }
4141
tower-lsp-server = { workspace = true }
4242
tracing = { workspace = true, features = ["attributes"] }
4343
url = { workspace = true }

crates/biome_lsp/src/handlers/text_document.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ pub(crate) async fn did_open(
7171

7272
session.insert_document(url.clone(), doc);
7373

74-
if let Err(err) = session.update_diagnostics(url).await {
75-
error!("Failed to update diagnostics: {}", err);
76-
}
74+
session.schedule_diagnostics(url, version);
7775

7876
Ok(())
7977
}
@@ -186,7 +184,7 @@ fn resolve_workspace_base_path(session: &Session, project_path: &Utf8Path) -> Ut
186184
/// Handler for `textDocument/didChange` LSP notification
187185
#[tracing::instrument(level = "debug", skip_all, fields(url = field::display(&params.text_document.uri.as_str()), version = params.text_document.version), err)]
188186
pub(crate) async fn did_change(
189-
session: &Session,
187+
session: &Arc<Session>,
190188
params: lsp::DidChangeTextDocumentParams,
191189
) -> Result<(), LspError> {
192190
let url = params.text_document.uri;
@@ -234,9 +232,7 @@ pub(crate) async fn did_change(
234232
editor_features: None,
235233
})?;
236234

237-
if let Err(err) = session.update_diagnostics(url).await {
238-
error!("Failed to update diagnostics: {}", err);
239-
}
235+
session.schedule_diagnostics(url, version);
240236

241237
Ok(())
242238
}
@@ -287,6 +283,7 @@ pub(crate) async fn did_close(
287283
params: lsp::DidCloseTextDocumentParams,
288284
) -> Result<(), LspError> {
289285
let uri = params.text_document.uri;
286+
session.close_diagnostics(&uri);
290287
let path = session.file_path(&uri)?;
291288
let Some(project_key) = session.remove_document(&uri) else {
292289
debug!("Document wasn't open: {}", uri.as_str());

crates/biome_lsp/src/server.tests.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ fn create_document_content_change_event() -> Vec<TextDocumentContentChangeEvent>
122122
]
123123
}
124124

125+
fn full_document_change(text: impl Into<String>) -> Vec<TextDocumentContentChangeEvent> {
126+
vec![TextDocumentContentChangeEvent {
127+
range: None,
128+
range_length: None,
129+
text: text.into(),
130+
}]
131+
}
132+
125133
const EXPECTED_CST: &str = "0: JS_MODULE@0..57
126134
0: (empty)
127135
1: (empty)
@@ -566,6 +574,88 @@ async fn pull_diagnostics() -> Result<()> {
566574
Ok(())
567575
}
568576

577+
#[tokio::test]
578+
async fn debounces_diagnostics_after_rapid_changes() -> Result<()> {
579+
let factory = ServerFactory::default();
580+
let (service, client) = factory.create().into_inner();
581+
let (stream, sink) = client.split();
582+
let mut server = Server::new(service);
583+
584+
let (sender, mut receiver) = channel(CHANNEL_BUFFER_SIZE);
585+
let reader = tokio::spawn(client_handler(stream, sink, sender));
586+
587+
server.initialize().await?;
588+
server.initialized().await?;
589+
590+
server.open_document("const a = 1; a = 2;").await?;
591+
let _ = wait_for_notification(&mut receiver, |n| n.is_publish_diagnostics()).await;
592+
593+
server
594+
.change_document(1, full_document_change("const b = 1; b = 2;"))
595+
.await?;
596+
server
597+
.change_document(2, full_document_change("const c = 1; c = 2;"))
598+
.await?;
599+
600+
let notification = wait_for_notification(&mut receiver, |n| n.is_publish_diagnostics()).await;
601+
let Some(ServerNotification::PublishDiagnostics(params)) = notification else {
602+
panic!("expected publishDiagnostics notification");
603+
};
604+
605+
assert_eq!(params.version, Some(2));
606+
607+
server.close_document().await?;
608+
609+
server.shutdown().await?;
610+
reader.abort();
611+
612+
Ok(())
613+
}
614+
615+
#[tokio::test]
616+
async fn does_not_publish_debounced_diagnostics_after_close() -> Result<()> {
617+
let factory = ServerFactory::default();
618+
let (service, client) = factory.create().into_inner();
619+
let (stream, sink) = client.split();
620+
let mut server = Server::new(service);
621+
622+
let (sender, mut receiver) = channel(CHANNEL_BUFFER_SIZE);
623+
let reader = tokio::spawn(client_handler(stream, sink, sender));
624+
625+
server.initialize().await?;
626+
server.initialized().await?;
627+
628+
server.open_document("const a = 1; a = 2;").await?;
629+
let _ = wait_for_notification(&mut receiver, |n| n.is_publish_diagnostics()).await;
630+
631+
server
632+
.change_document(1, full_document_change("const b = 1; b = 2;"))
633+
.await?;
634+
server.close_document().await?;
635+
636+
let notification = wait_for_notification(&mut receiver, |n| n.is_publish_diagnostics()).await;
637+
assert_eq!(
638+
notification,
639+
Some(ServerNotification::PublishDiagnostics(
640+
PublishDiagnosticsParams {
641+
uri: uri!("document.js"),
642+
version: None,
643+
diagnostics: vec![],
644+
}
645+
))
646+
);
647+
648+
wait_for_no_notification(&mut receiver, Duration::from_millis(750), |n| {
649+
n.is_publish_diagnostics()
650+
})
651+
.await;
652+
653+
server.shutdown().await?;
654+
reader.abort();
655+
656+
Ok(())
657+
}
658+
569659
#[tokio::test]
570660
async fn pull_diagnostics_of_syntax_rules() -> Result<()> {
571661
let factory = ServerFactory::default();

crates/biome_lsp/src/server_test_utils.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,22 @@ pub(crate) async fn wait_for_notification(
432432
}
433433
}
434434

435+
pub(crate) async fn wait_for_no_notification(
436+
receiver: &mut (impl Stream<Item = ServerNotification> + Unpin),
437+
duration: Duration,
438+
check: impl Fn(&ServerNotification) -> bool,
439+
) {
440+
loop {
441+
match tokio::time::timeout(duration, receiver.next()).await {
442+
Ok(Some(notification)) if check(&notification) => {
443+
panic!("unexpected server notification: {notification:?}");
444+
}
445+
Ok(Some(_)) => {}
446+
Ok(None) | Err(_) => return,
447+
}
448+
}
449+
}
450+
435451
/// Basic handler for requests and notifications coming from the server for tests
436452
pub(crate) async fn client_handler<I, O>(
437453
stream: I,

0 commit comments

Comments
 (0)