lsp: Track completion triggers for each language separately (#20471)

This PR improves how we handle completions in buffers with multiple
LSPs.

Context: while working on
https://github.com/zed-industries/zed/issues/19777 with @mgsloan we
noticed that completion triggers coming from language servers are not
tracked properly. Namely, each buffer has `completion_triggers` field
which is read from the configuration of a language server. The problem
is, there can be multiple language servers for a single buffer, in which
case we'd just stick to the one that was registered last.

This PR makes the tracking a bit more fine-grained. We now track not
only what the completion triggers are, but also their origin server id.
Whenever completion triggers are updated, we recreate the completion
triggers set.
Release Notes:

- Fixed completions not triggering when multiple language servers are
used for a single file.
This commit is contained in:
Piotr Osiewicz 2024-11-10 10:29:10 +01:00 committed by GitHub
parent 2b7ee1e872
commit f3320998a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 91 additions and 24 deletions

View file

@ -13700,10 +13700,7 @@ impl CompletionProvider for Model<Project> {
return true; return true;
} }
buffer buffer.completion_triggers().contains(text)
.completion_triggers()
.iter()
.any(|string| string == text)
} }
} }

View file

@ -40,7 +40,7 @@ use std::{
borrow::Cow, borrow::Cow,
cell::Cell, cell::Cell,
cmp::{self, Ordering, Reverse}, cmp::{self, Ordering, Reverse},
collections::BTreeMap, collections::{BTreeMap, BTreeSet},
ffi::OsStr, ffi::OsStr,
fmt, fmt,
future::Future, future::Future,
@ -126,7 +126,8 @@ pub struct Buffer {
diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>, diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
remote_selections: TreeMap<ReplicaId, SelectionSet>, remote_selections: TreeMap<ReplicaId, SelectionSet>,
diagnostics_timestamp: clock::Lamport, diagnostics_timestamp: clock::Lamport,
completion_triggers: Vec<String>, completion_triggers: BTreeSet<String>,
completion_triggers_per_language_server: HashMap<LanguageServerId, BTreeSet<String>>,
completion_triggers_timestamp: clock::Lamport, completion_triggers_timestamp: clock::Lamport,
deferred_ops: OperationQueue<Operation>, deferred_ops: OperationQueue<Operation>,
capability: Capability, capability: Capability,
@ -315,6 +316,8 @@ pub enum Operation {
triggers: Vec<String>, triggers: Vec<String>,
/// The buffer's lamport timestamp. /// The buffer's lamport timestamp.
lamport_timestamp: clock::Lamport, lamport_timestamp: clock::Lamport,
/// The language server ID.
server_id: LanguageServerId,
}, },
} }
@ -697,12 +700,15 @@ impl Buffer {
})); }));
} }
operations.push(proto::serialize_operation( for (server_id, completions) in &self.completion_triggers_per_language_server {
&Operation::UpdateCompletionTriggers { operations.push(proto::serialize_operation(
triggers: self.completion_triggers.clone(), &Operation::UpdateCompletionTriggers {
lamport_timestamp: self.completion_triggers_timestamp, triggers: completions.iter().cloned().collect(),
}, lamport_timestamp: self.completion_triggers_timestamp,
)); server_id: *server_id,
},
));
}
let text_operations = self.text.operations().clone(); let text_operations = self.text.operations().clone();
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {
@ -774,6 +780,7 @@ impl Buffer {
diagnostics: Default::default(), diagnostics: Default::default(),
diagnostics_timestamp: Default::default(), diagnostics_timestamp: Default::default(),
completion_triggers: Default::default(), completion_triggers: Default::default(),
completion_triggers_per_language_server: Default::default(),
completion_triggers_timestamp: Default::default(), completion_triggers_timestamp: Default::default(),
deferred_ops: OperationQueue::new(), deferred_ops: OperationQueue::new(),
has_conflict: false, has_conflict: false,
@ -2229,8 +2236,21 @@ impl Buffer {
Operation::UpdateCompletionTriggers { Operation::UpdateCompletionTriggers {
triggers, triggers,
lamport_timestamp, lamport_timestamp,
server_id,
} => { } => {
self.completion_triggers = triggers; if triggers.is_empty() {
self.completion_triggers_per_language_server
.remove(&server_id);
self.completion_triggers = self
.completion_triggers_per_language_server
.values()
.flat_map(|triggers| triggers.into_iter().cloned())
.collect();
} else {
self.completion_triggers_per_language_server
.insert(server_id, triggers.iter().cloned().collect());
self.completion_triggers.extend(triggers);
}
self.text.lamport_clock.observe(lamport_timestamp); self.text.lamport_clock.observe(lamport_timestamp);
} }
} }
@ -2374,13 +2394,31 @@ impl Buffer {
} }
/// Override current completion triggers with the user-provided completion triggers. /// Override current completion triggers with the user-provided completion triggers.
pub fn set_completion_triggers(&mut self, triggers: Vec<String>, cx: &mut ModelContext<Self>) { pub fn set_completion_triggers(
self.completion_triggers.clone_from(&triggers); &mut self,
server_id: LanguageServerId,
triggers: BTreeSet<String>,
cx: &mut ModelContext<Self>,
) {
self.completion_triggers_timestamp = self.text.lamport_clock.tick(); self.completion_triggers_timestamp = self.text.lamport_clock.tick();
if triggers.is_empty() {
self.completion_triggers_per_language_server
.remove(&server_id);
self.completion_triggers = self
.completion_triggers_per_language_server
.values()
.flat_map(|triggers| triggers.into_iter().cloned())
.collect();
} else {
self.completion_triggers_per_language_server
.insert(server_id, triggers.clone());
self.completion_triggers.extend(triggers.iter().cloned());
}
self.send_operation( self.send_operation(
Operation::UpdateCompletionTriggers { Operation::UpdateCompletionTriggers {
triggers, triggers: triggers.iter().cloned().collect(),
lamport_timestamp: self.completion_triggers_timestamp, lamport_timestamp: self.completion_triggers_timestamp,
server_id,
}, },
true, true,
cx, cx,
@ -2390,7 +2428,7 @@ impl Buffer {
/// Returns a list of strings which trigger a completion menu for this language. /// Returns a list of strings which trigger a completion menu for this language.
/// Usually this is driven by LSP server which returns a list of trigger characters for completions. /// Usually this is driven by LSP server which returns a list of trigger characters for completions.
pub fn completion_triggers(&self) -> &[String] { pub fn completion_triggers(&self) -> &BTreeSet<String> {
&self.completion_triggers &self.completion_triggers
} }

View file

@ -79,11 +79,13 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
crate::Operation::UpdateCompletionTriggers { crate::Operation::UpdateCompletionTriggers {
triggers, triggers,
lamport_timestamp, lamport_timestamp,
server_id,
} => proto::operation::Variant::UpdateCompletionTriggers( } => proto::operation::Variant::UpdateCompletionTriggers(
proto::operation::UpdateCompletionTriggers { proto::operation::UpdateCompletionTriggers {
replica_id: lamport_timestamp.replica_id as u32, replica_id: lamport_timestamp.replica_id as u32,
lamport_timestamp: lamport_timestamp.value, lamport_timestamp: lamport_timestamp.value,
triggers: triggers.clone(), triggers: triggers.iter().cloned().collect(),
language_server_id: server_id.to_proto(),
}, },
), ),
}), }),
@ -326,6 +328,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
replica_id: message.replica_id as ReplicaId, replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp, value: message.lamport_timestamp,
}, },
server_id: LanguageServerId::from_proto(message.language_server_id),
} }
} }
}, },

View file

@ -352,7 +352,6 @@ impl LspAdapter for NodeVersionAdapter {
} }
remove_matching(&container_dir, |entry| entry != destination_path).await; remove_matching(&container_dir, |entry| entry != destination_path).await;
} }
Ok(LanguageServerBinary { Ok(LanguageServerBinary {
path: destination_path, path: destination_path,
env: None, env: None,

View file

@ -3351,11 +3351,17 @@ impl LspStore {
buffer_handle.update(cx, |buffer, cx| { buffer_handle.update(cx, |buffer, cx| {
buffer.set_completion_triggers( buffer.set_completion_triggers(
server.server_id(),
server server
.capabilities() .capabilities()
.completion_provider .completion_provider
.as_ref() .as_ref()
.and_then(|provider| provider.trigger_characters.clone()) .and_then(|provider| {
provider
.trigger_characters
.as_ref()
.map(|characters| characters.iter().cloned().collect())
})
.unwrap_or_default(), .unwrap_or_default(),
cx, cx,
); );
@ -3394,6 +3400,7 @@ impl LspStore {
for adapter in self.languages.lsp_adapters(&language.name()) { for adapter in self.languages.lsp_adapters(&language.name()) {
if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) { if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) {
buffer.update_diagnostics(*server_id, DiagnosticSet::new([], buffer), cx); buffer.update_diagnostics(*server_id, DiagnosticSet::new([], buffer), cx);
buffer.set_completion_triggers(*server_id, Default::default(), cx);
} }
} }
} }
@ -5797,6 +5804,7 @@ impl LspStore {
DiagnosticSet::new([], buffer), DiagnosticSet::new([], buffer),
cx, cx,
); );
buffer.set_completion_triggers(server_id, Default::default(), cx);
}); });
} }
}); });
@ -6685,11 +6693,17 @@ impl LspStore {
buffer_handle.update(cx, |buffer, cx| { buffer_handle.update(cx, |buffer, cx| {
buffer.set_completion_triggers( buffer.set_completion_triggers(
server_id,
language_server language_server
.capabilities() .capabilities()
.completion_provider .completion_provider
.as_ref() .as_ref()
.and_then(|provider| provider.trigger_characters.clone()) .and_then(|provider| {
provider
.trigger_characters
.as_ref()
.map(|characters| characters.iter().cloned().collect())
})
.unwrap_or_default(), .unwrap_or_default(),
cx, cx,
) )

View file

@ -481,7 +481,11 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
// The buffer is configured based on the language server's capabilities. // The buffer is configured based on the language server's capabilities.
rust_buffer.update(cx, |buffer, _| { rust_buffer.update(cx, |buffer, _| {
assert_eq!( assert_eq!(
buffer.completion_triggers(), buffer
.completion_triggers()
.into_iter()
.cloned()
.collect::<Vec<_>>(),
&[".".to_string(), "::".to_string()] &[".".to_string(), "::".to_string()]
); );
}); });
@ -528,7 +532,14 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
// This buffer is configured based on the second language server's // This buffer is configured based on the second language server's
// capabilities. // capabilities.
json_buffer.update(cx, |buffer, _| { json_buffer.update(cx, |buffer, _| {
assert_eq!(buffer.completion_triggers(), &[":".to_string()]); assert_eq!(
buffer
.completion_triggers()
.into_iter()
.cloned()
.collect::<Vec<_>>(),
&[":".to_string()]
);
}); });
// When opening another buffer whose language server is already running, // When opening another buffer whose language server is already running,
@ -541,7 +552,11 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
.unwrap(); .unwrap();
rust_buffer2.update(cx, |buffer, _| { rust_buffer2.update(cx, |buffer, _| {
assert_eq!( assert_eq!(
buffer.completion_triggers(), buffer
.completion_triggers()
.into_iter()
.cloned()
.collect::<Vec<_>>(),
&[".".to_string(), "::".to_string()] &[".".to_string(), "::".to_string()]
); );
}); });

View file

@ -1906,6 +1906,7 @@ message Operation {
uint32 replica_id = 1; uint32 replica_id = 1;
uint32 lamport_timestamp = 2; uint32 lamport_timestamp = 2;
repeated string triggers = 3; repeated string triggers = 3;
uint64 language_server_id = 4;
} }
} }