Notify all language servers only when a buffer is saved

Other notifications such as opening, closing or changing a document
are still tied to the buffer's language.
This commit is contained in:
Antonio Scandurra 2022-03-09 10:34:42 +01:00
parent 4cb4b99c56
commit 0a9595b5fa
2 changed files with 129 additions and 198 deletions

View file

@ -225,7 +225,9 @@ impl LspCommand for PerformRename {
if let Some(edit) = message { if let Some(edit) = message {
let language_server = project let language_server = project
.read_with(&cx, |project, cx| { .read_with(&cx, |project, cx| {
project.language_server_for_buffer(&buffer, cx).cloned() project
.language_server_for_buffer(buffer.read(cx), cx)
.cloned()
}) })
.ok_or_else(|| anyhow!("no language server found for buffer"))?; .ok_or_else(|| anyhow!("no language server found for buffer"))?;
let language = buffer let language = buffer
@ -343,7 +345,9 @@ impl LspCommand for GetDefinition {
let mut definitions = Vec::new(); let mut definitions = Vec::new();
let language_server = project let language_server = project
.read_with(&cx, |project, cx| { .read_with(&cx, |project, cx| {
project.language_server_for_buffer(&buffer, cx).cloned() project
.language_server_for_buffer(buffer.read(cx), cx)
.cloned()
}) })
.ok_or_else(|| anyhow!("no language server found for buffer"))?; .ok_or_else(|| anyhow!("no language server found for buffer"))?;
let language = buffer let language = buffer
@ -519,7 +523,9 @@ impl LspCommand for GetReferences {
let mut references = Vec::new(); let mut references = Vec::new();
let language_server = project let language_server = project
.read_with(&cx, |project, cx| { .read_with(&cx, |project, cx| {
project.language_server_for_buffer(&buffer, cx).cloned() project
.language_server_for_buffer(buffer.read(cx), cx)
.cloned()
}) })
.ok_or_else(|| anyhow!("no language server found for buffer"))?; .ok_or_else(|| anyhow!("no language server found for buffer"))?;
let language = buffer let language = buffer

View file

@ -889,7 +889,7 @@ impl Project {
.await?; .await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.assign_language_to_buffer(&buffer, cx); this.assign_language_to_buffer(&buffer, cx);
this.register_buffer_with_language_servers(&buffer, cx); this.register_buffer_with_language_server(&buffer, cx);
}); });
Ok(()) Ok(())
}) })
@ -948,35 +948,23 @@ impl Project {
.detach(); .detach();
self.assign_language_to_buffer(buffer, cx); self.assign_language_to_buffer(buffer, cx);
self.register_buffer_with_language_servers(buffer, cx); self.register_buffer_with_language_server(buffer, cx);
Ok(()) Ok(())
} }
fn register_buffer_with_language_servers( fn register_buffer_with_language_server(
&mut self, &mut self,
buffer_handle: &ModelHandle<Buffer>, buffer_handle: &ModelHandle<Buffer>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
let buffer_language_name = buffer.language().map(|l| l.name().clone()); let buffer_id = buffer.remote_id();
if let Some(file) = File::from_dyn(buffer.file()) { if let Some(file) = File::from_dyn(buffer.file()) {
let worktree_id = file.worktree_id(cx);
if file.is_local() { if file.is_local() {
let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
let initial_snapshot = buffer.as_text_snapshot(); let initial_snapshot = buffer.text_snapshot();
self.buffer_snapshots let language_server = self.language_server_for_buffer(buffer, cx).cloned();
.insert(buffer.remote_id(), vec![(0, initial_snapshot.clone())]);
let mut notifications = Vec::new();
let did_open_text_document = lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
uri,
Default::default(),
0,
initial_snapshot.text(),
),
};
if let Some(local_worktree) = file.worktree.read(cx).as_local() { if let Some(local_worktree) = file.worktree.read(cx).as_local() {
if let Some(diagnostics) = local_worktree.diagnostics_for_path(file.path()) { if let Some(diagnostics) = local_worktree.diagnostics_for_path(file.path()) {
@ -985,32 +973,40 @@ impl Project {
} }
} }
for (language_name, server) in self.language_servers_for_worktree(worktree_id) { if let Some(server) = language_server {
notifications.push(server.notify::<lsp::notification::DidOpenTextDocument>( server
did_open_text_document.clone(), .notify::<lsp::notification::DidOpenTextDocument>(
)); lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
if Some(language_name) == buffer_language_name.as_deref() { uri,
buffer_handle.update(cx, |buffer, cx| { Default::default(),
buffer.set_completion_triggers( 0,
server initial_snapshot.text(),
.capabilities() ),
.completion_provider }
.as_ref() .clone(),
.and_then(|provider| provider.trigger_characters.clone()) )
.unwrap_or(Vec::new()), .log_err();
cx, buffer_handle.update(cx, |buffer, cx| {
) buffer.set_completion_triggers(
}); server
} .capabilities()
.completion_provider
.as_ref()
.and_then(|provider| provider.trigger_characters.clone())
.unwrap_or(Vec::new()),
cx,
)
});
self.buffer_snapshots
.insert(buffer_id, vec![(0, initial_snapshot)]);
} }
cx.observe_release(buffer_handle, |this, buffer, cx| { cx.observe_release(buffer_handle, |this, buffer, cx| {
if let Some(file) = File::from_dyn(buffer.file()) { if let Some(file) = File::from_dyn(buffer.file()) {
let worktree_id = file.worktree_id(cx);
if file.is_local() { if file.is_local() {
let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
for (_, server) in this.language_servers_for_worktree(worktree_id) { if let Some(server) = this.language_server_for_buffer(buffer, cx) {
server server
.notify::<lsp::notification::DidCloseTextDocument>( .notify::<lsp::notification::DidCloseTextDocument>(
lsp::DidCloseTextDocumentParams { lsp::DidCloseTextDocumentParams {
@ -1046,9 +1042,11 @@ impl Project {
cx.background().spawn(request).detach_and_log_err(cx); cx.background().spawn(request).detach_and_log_err(cx);
} }
BufferEvent::Edited => { BufferEvent::Edited => {
let language_server = self
.language_server_for_buffer(buffer.read(cx), cx)?
.clone();
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let file = File::from_dyn(buffer.file())?; let file = File::from_dyn(buffer.file())?;
let worktree_id = file.worktree_id(cx);
let abs_path = file.as_local()?.abs_path(cx); let abs_path = file.as_local()?.abs_path(cx);
let uri = lsp::Url::from_file_path(abs_path).unwrap(); let uri = lsp::Url::from_file_path(abs_path).unwrap();
let buffer_snapshots = self.buffer_snapshots.entry(buffer.remote_id()).or_default(); let buffer_snapshots = self.buffer_snapshots.entry(buffer.remote_id()).or_default();
@ -1075,18 +1073,19 @@ impl Project {
}) })
.collect(); .collect();
let changes = lsp::DidChangeTextDocumentParams {
text_document: lsp::VersionedTextDocumentIdentifier::new(uri, next_version),
content_changes,
};
buffer_snapshots.push((next_version, next_snapshot)); buffer_snapshots.push((next_version, next_snapshot));
for (_, server) in self.language_servers_for_worktree(worktree_id) { language_server
server .notify::<lsp::notification::DidChangeTextDocument>(
.notify::<lsp::notification::DidChangeTextDocument>(changes.clone()) lsp::DidChangeTextDocumentParams {
.log_err(); text_document: lsp::VersionedTextDocumentIdentifier::new(
} uri,
next_version,
),
content_changes,
},
)
.log_err();
} }
BufferEvent::Saved => { BufferEvent::Saved => {
let file = File::from_dyn(buffer.read(cx).file())?; let file = File::from_dyn(buffer.read(cx).file())?;
@ -1177,17 +1176,27 @@ impl Project {
let language_server = language_server?.await.log_err()?; let language_server = language_server?.await.log_err()?;
let this = this.upgrade(&cx)?; let this = this.upgrade(&cx)?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.language_servers.insert(key, language_server.clone()); this.language_servers
.insert(key.clone(), language_server.clone());
// Tell the language server about every open buffer in the worktree that matches the language.
for buffer in this.opened_buffers.values() { for buffer in this.opened_buffers.values() {
if let Some(buffer_handle) = buffer.upgrade(cx) { if let Some(buffer_handle) = buffer.upgrade(cx) {
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
let file = File::from_dyn(buffer.file())?; let file = if let Some(file) = File::from_dyn(buffer.file()) {
if file.worktree.read(cx).id() != worktree_id { file
} else {
continue;
};
let language = if let Some(language) = buffer.language() {
language
} else {
continue;
};
if (file.worktree.read(cx).id(), language.name()) != key {
continue; continue;
} }
// Tell the language server about every open buffer in the worktree.
let file = file.as_local()?; let file = file.as_local()?;
let versions = this let versions = this
.buffer_snapshots .buffer_snapshots
@ -1207,26 +1216,19 @@ impl Project {
}, },
) )
.log_err()?; .log_err()?;
buffer_handle.update(cx, |buffer, cx| {
// Update the language buffers buffer.set_completion_triggers(
if buffer language_server
.language() .capabilities()
.map_or(false, |l| l.name() == language.name()) .completion_provider
{ .as_ref()
buffer_handle.update(cx, |buffer, cx| { .and_then(|provider| {
buffer.set_completion_triggers( provider.trigger_characters.clone()
language_server })
.capabilities() .unwrap_or(Vec::new()),
.completion_provider cx,
.as_ref() )
.and_then(|provider| { });
provider.trigger_characters.clone()
})
.unwrap_or(Vec::new()),
cx,
)
});
}
} }
} }
@ -1584,25 +1586,11 @@ impl Project {
let mut remote_buffers = None; let mut remote_buffers = None;
for buffer_handle in buffers { for buffer_handle in buffers {
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
let worktree;
if let Some(file) = File::from_dyn(buffer.file()) { if let Some(file) = File::from_dyn(buffer.file()) {
worktree = file.worktree.clone();
if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) {
let lang_server; if let Some(server) = self.language_server_for_buffer(buffer, cx) {
if let Some(lang) = buffer.language() { local_buffers.push((buffer_handle, buffer_abs_path, server.clone()));
if let Some(server) = self
.language_servers
.get(&(worktree.read(cx).id(), lang.name()))
{
lang_server = server.clone();
} else {
return Task::ready(Ok(Default::default()));
};
} else {
return Task::ready(Ok(Default::default()));
} }
local_buffers.push((buffer_handle, buffer_abs_path, lang_server));
} else { } else {
remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); remote_buffers.get_or_insert(Vec::new()).push(buffer_handle);
} }
@ -1918,7 +1906,7 @@ impl Project {
if worktree.read(cx).as_local().is_some() { if worktree.read(cx).as_local().is_some() {
let buffer_abs_path = buffer_abs_path.unwrap(); let buffer_abs_path = buffer_abs_path.unwrap();
let lang_server = let lang_server =
if let Some(server) = self.language_server_for_buffer(&source_buffer_handle, cx) { if let Some(server) = self.language_server_for_buffer(source_buffer, cx) {
server.clone() server.clone()
} else { } else {
return Task::ready(Ok(Default::default())); return Task::ready(Ok(Default::default()));
@ -2029,12 +2017,11 @@ impl Project {
let buffer_id = buffer.remote_id(); let buffer_id = buffer.remote_id();
if self.is_local() { if self.is_local() {
let lang_server = let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) {
if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { server.clone()
server.clone() } else {
} else { return Task::ready(Ok(Default::default()));
return Task::ready(Ok(Default::default())); };
};
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let resolved_completion = lang_server let resolved_completion = lang_server
@ -2121,21 +2108,11 @@ impl Project {
if worktree.read(cx).as_local().is_some() { if worktree.read(cx).as_local().is_some() {
let buffer_abs_path = buffer_abs_path.unwrap(); let buffer_abs_path = buffer_abs_path.unwrap();
let lang_name; let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) {
let lang_server; server.clone()
if let Some(lang) = buffer.language() {
lang_name = lang.name();
if let Some(server) = self
.language_servers
.get(&(worktree.read(cx).id(), lang_name.clone()))
{
lang_server = server.clone();
} else {
return Task::ready(Ok(Default::default()));
};
} else { } else {
return Task::ready(Ok(Default::default())); return Task::ready(Ok(Default::default()));
} };
let lsp_range = lsp::Range::new( let lsp_range = lsp::Range::new(
range.start.to_point_utf16(buffer).to_lsp_position(), range.start.to_point_utf16(buffer).to_lsp_position(),
@ -2223,12 +2200,11 @@ impl Project {
} else { } else {
return Task::ready(Ok(Default::default())); return Task::ready(Ok(Default::default()));
}; };
let lang_server = let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) {
if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { server.clone()
server.clone() } else {
} else { return Task::ready(Ok(Default::default()));
return Task::ready(Ok(Default::default())); };
};
let range = action.range.to_point_utf16(buffer); let range = action.range.to_point_utf16(buffer);
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
@ -2674,7 +2650,7 @@ impl Project {
if self.is_local() { if self.is_local() {
let file = File::from_dyn(buffer.file()).and_then(File::as_local); let file = File::from_dyn(buffer.file()).and_then(File::as_local);
if let Some((file, language_server)) = if let Some((file, language_server)) =
file.zip(self.language_server_for_buffer(&buffer_handle, cx).cloned()) file.zip(self.language_server_for_buffer(buffer, cx).cloned())
{ {
let lsp_params = request.to_lsp(&file.abs_path(cx), cx); let lsp_params = request.to_lsp(&file.abs_path(cx), cx);
return cx.spawn(|this, cx| async move { return cx.spawn(|this, cx| async move {
@ -3934,16 +3910,15 @@ impl Project {
) )
}) })
} else { } else {
Ok((**buffer.read(cx)).clone()) Ok((buffer.read(cx)).text_snapshot())
} }
} }
fn language_server_for_buffer( fn language_server_for_buffer(
&self, &self,
buffer: &ModelHandle<Buffer>, buffer: &Buffer,
cx: &AppContext, cx: &AppContext,
) -> Option<&Arc<LanguageServer>> { ) -> Option<&Arc<LanguageServer>> {
let buffer = buffer.read(cx);
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
let worktree_id = file.worktree_id(cx); let worktree_id = file.worktree_id(cx);
self.language_servers.get(&(worktree_id, language.name())) self.language_servers.get(&(worktree_id, language.name()))
@ -4339,20 +4314,8 @@ mod tests {
.await .await
.unwrap(); .unwrap();
// A server is started up, and it is notified about both open buffers. // A server is started up, and it is notified about Rust files.
let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
assert_eq!(
fake_rust_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(),
version: 0,
text: "a = 1".to_string(),
language_id: Default::default()
}
);
assert_eq!( assert_eq!(
fake_rust_server fake_rust_server
.receive_notification::<lsp::notification::DidOpenTextDocument>() .receive_notification::<lsp::notification::DidOpenTextDocument>()
@ -4401,18 +4364,6 @@ mod tests {
// Another language server is started up, and it is notified about // Another language server is started up, and it is notified about
// all three open buffers. // all three open buffers.
let mut fake_json_server = fake_json_servers.next().await.unwrap(); let mut fake_json_server = fake_json_servers.next().await.unwrap();
assert_eq!(
fake_json_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(),
version: 0,
text: "a = 1".to_string(),
language_id: Default::default()
}
);
assert_eq!( assert_eq!(
fake_json_server fake_json_server
.receive_notification::<lsp::notification::DidOpenTextDocument>() .receive_notification::<lsp::notification::DidOpenTextDocument>()
@ -4425,18 +4376,6 @@ mod tests {
language_id: Default::default() language_id: Default::default()
} }
); );
assert_eq!(
fake_json_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
version: 1,
text: "const A: i32 = 12;".to_string(),
language_id: Default::default()
}
);
// This buffer is configured based on the second language server's // This buffer is configured based on the second language server's
// capabilities. // capabilities.
@ -4444,20 +4383,6 @@ mod tests {
assert_eq!(buffer.completion_triggers(), &[":".to_string()]); assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
}); });
// The first language server is also notified about the new open buffer.
assert_eq!(
fake_rust_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
version: 0,
text: "{\"a\": 1}".to_string(),
language_id: Default::default()
}
);
// When opening another buffer whose language server is already running, // When opening another buffer whose language server is already running,
// it is also configured based on the existing language server's capabilities. // it is also configured based on the existing language server's capabilities.
let rust_buffer2 = project let rust_buffer2 = project
@ -4473,39 +4398,45 @@ mod tests {
); );
}); });
// Edit a buffer. The changes are reported to both the language servers. // Changes are reported only to servers matching the buffer's language.
toml_buffer.update(cx, |buffer, cx| buffer.edit([5..5], "23", cx)); toml_buffer.update(cx, |buffer, cx| buffer.edit([5..5], "23", cx));
rust_buffer2.update(cx, |buffer, cx| buffer.edit([0..0], "let x = 1;", cx));
assert_eq!( assert_eq!(
fake_rust_server fake_rust_server
.receive_notification::<lsp::notification::DidChangeTextDocument>() .receive_notification::<lsp::notification::DidChangeTextDocument>()
.await .await
.text_document, .text_document,
lsp::VersionedTextDocumentIdentifier::new( lsp::VersionedTextDocumentIdentifier::new(
lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), lsp::Url::from_file_path("/the-root/test2.rs").unwrap(),
1 1
) )
); );
// Save notifications are reported to all servers.
toml_buffer
.update(cx, |buffer, cx| buffer.save(cx))
.await
.unwrap();
assert_eq!(
fake_rust_server
.receive_notification::<lsp::notification::DidSaveTextDocument>()
.await
.text_document,
lsp::TextDocumentIdentifier::new(
lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap()
)
);
assert_eq!( assert_eq!(
fake_json_server fake_json_server
.receive_notification::<lsp::notification::DidChangeTextDocument>() .receive_notification::<lsp::notification::DidSaveTextDocument>()
.await, .await
lsp::DidChangeTextDocumentParams { .text_document,
text_document: lsp::VersionedTextDocumentIdentifier::new( lsp::TextDocumentIdentifier::new(
lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap()
1 )
),
content_changes: vec![lsp::TextDocumentContentChangeEvent {
range: Some(lsp::Range::new(
lsp::Position::new(0, 5),
lsp::Position::new(0, 5)
)),
range_length: None,
text: "23".to_string(),
}],
},
); );
// Close a buffer. Both language servers are notified. // Close notifications are reported only to servers matching the buffer's language.
cx.update(|_| drop(json_buffer)); cx.update(|_| drop(json_buffer));
let close_message = lsp::DidCloseTextDocumentParams { let close_message = lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new( text_document: lsp::TextDocumentIdentifier::new(
@ -4518,12 +4449,6 @@ mod tests {
.await, .await,
close_message, close_message,
); );
assert_eq!(
fake_rust_server
.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await,
close_message,
);
} }
#[gpui::test] #[gpui::test]