Assign buffer's completion triggers from LSP capabilities

Max Brunsfeld created

Also, make LanguageServer::new() async. The future resolves
once the server is initialized.

Change summary

crates/client/src/user.rs       |   2 
crates/editor/src/editor.rs     |  68 ++++----
crates/language/src/buffer.rs   |  88 +----------
crates/language/src/language.rs |  55 ++++--
crates/lsp/src/lsp.rs           | 216 ++++++++++++-----------------
crates/project/src/project.rs   | 255 ++++++++++++++++++++--------------
crates/project/src/worktree.rs  |   1 
crates/server/src/rpc.rs        |  28 ++-
8 files changed, 345 insertions(+), 368 deletions(-)

Detailed changes

crates/client/src/user.rs 🔗

@@ -186,7 +186,7 @@ impl UserStore {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Arc<User>>> {
         if let Some(user) = self.users.get(&user_id).cloned() {
-            return cx.spawn_weak(|_, _| async move { Ok(user) });
+            return cx.foreground().spawn(async move { Ok(user) });
         }
 
         let load_users = self.load_users(vec![user_id], cx);

crates/editor/src/editor.rs 🔗

@@ -5912,9 +5912,9 @@ pub fn styled_runs_for_code_label<'a>(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use language::LanguageConfig;
+    use language::{LanguageConfig, LanguageServerConfig};
     use lsp::FakeLanguageServer;
-    use project::{FakeFs, ProjectPath};
+    use project::FakeFs;
     use smol::stream::StreamExt;
     use std::{cell::RefCell, rc::Rc, time::Instant};
     use text::Point;
@@ -8196,18 +8196,24 @@ mod tests {
     #[gpui::test]
     async fn test_completion(cx: &mut gpui::TestAppContext) {
         let settings = cx.read(Settings::test);
-        let (language_server, mut fake) = cx.update(|cx| {
-            lsp::LanguageServer::fake_with_capabilities(
-                lsp::ServerCapabilities {
-                    completion_provider: Some(lsp::CompletionOptions {
-                        trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-                        ..Default::default()
-                    }),
-                    ..Default::default()
-                },
-                cx,
-            )
+
+        let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake();
+        language_server_config.set_fake_capabilities(lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
         });
+        let language = Arc::new(Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                language_server: Some(language_server_config),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        ));
 
         let text = "
             one
@@ -8217,28 +8223,26 @@ mod tests {
         .unindent();
 
         let fs = FakeFs::new(cx.background().clone());
-        fs.insert_file("/file", text).await;
+        fs.insert_file("/file.rs", text).await;
 
         let project = Project::test(fs, cx);
+        project.update(cx, |project, _| project.languages().add(language));
 
-        let (worktree, relative_path) = project
+        let worktree_id = project
             .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/file", true, cx)
+                project.find_or_create_local_worktree("/file.rs", true, cx)
             })
             .await
-            .unwrap();
-        let project_path = ProjectPath {
-            worktree_id: worktree.read_with(cx, |worktree, _| worktree.id()),
-            path: relative_path.into(),
-        };
+            .unwrap()
+            .0
+            .read_with(cx, |tree, _| tree.id());
         let buffer = project
-            .update(cx, |project, cx| project.open_buffer(project_path, cx))
+            .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
             .await
             .unwrap();
+        let mut fake_server = fake_servers.next().await.unwrap();
 
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        buffer.next_notification(&cx).await;
-
         let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx));
 
         editor.update(cx, |editor, cx| {
@@ -8248,8 +8252,8 @@ mod tests {
         });
 
         handle_completion_request(
-            &mut fake,
-            "/file",
+            &mut fake_server,
+            "/file.rs",
             Point::new(0, 4),
             vec![
                 (Point::new(0, 4)..Point::new(0, 4), "first_completion"),
@@ -8279,7 +8283,7 @@ mod tests {
         });
 
         handle_resolve_completion_request(
-            &mut fake,
+            &mut fake_server,
             Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")),
         )
         .await;
@@ -8312,8 +8316,8 @@ mod tests {
         });
 
         handle_completion_request(
-            &mut fake,
-            "/file",
+            &mut fake_server,
+            "/file.rs",
             Point::new(2, 7),
             vec![
                 (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"),
@@ -8331,8 +8335,8 @@ mod tests {
         });
 
         handle_completion_request(
-            &mut fake,
-            "/file",
+            &mut fake_server,
+            "/file.rs",
             Point::new(2, 8),
             vec![
                 (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"),
@@ -8361,7 +8365,7 @@ mod tests {
             );
             apply_additional_edits
         });
-        handle_resolve_completion_request(&mut fake, None).await;
+        handle_resolve_completion_request(&mut fake_server, None).await;
         apply_additional_edits.await.unwrap();
 
         async fn handle_completion_request(

crates/language/src/buffer.rs 🔗

@@ -203,79 +203,6 @@ pub trait LocalFile: File {
     );
 }
 
-#[cfg(any(test, feature = "test-support"))]
-pub struct FakeFile {
-    pub path: Arc<Path>,
-}
-
-#[cfg(any(test, feature = "test-support"))]
-impl FakeFile {
-    pub fn new(path: impl AsRef<Path>) -> Self {
-        Self {
-            path: path.as_ref().into(),
-        }
-    }
-}
-
-#[cfg(any(test, feature = "test-support"))]
-impl File for FakeFile {
-    fn as_local(&self) -> Option<&dyn LocalFile> {
-        Some(self)
-    }
-
-    fn mtime(&self) -> SystemTime {
-        SystemTime::UNIX_EPOCH
-    }
-
-    fn path(&self) -> &Arc<Path> {
-        &self.path
-    }
-
-    fn full_path(&self, _: &AppContext) -> PathBuf {
-        self.path.to_path_buf()
-    }
-
-    fn file_name(&self, _: &AppContext) -> OsString {
-        self.path.file_name().unwrap().to_os_string()
-    }
-
-    fn is_deleted(&self) -> bool {
-        false
-    }
-
-    fn save(
-        &self,
-        _: u64,
-        _: Rope,
-        _: clock::Global,
-        cx: &mut MutableAppContext,
-    ) -> Task<Result<(clock::Global, SystemTime)>> {
-        cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) })
-    }
-
-    fn as_any(&self) -> &dyn Any {
-        self
-    }
-
-    fn to_proto(&self) -> rpc::proto::File {
-        unimplemented!()
-    }
-}
-
-#[cfg(any(test, feature = "test-support"))]
-impl LocalFile for FakeFile {
-    fn abs_path(&self, _: &AppContext) -> PathBuf {
-        self.path.to_path_buf()
-    }
-
-    fn load(&self, cx: &AppContext) -> Task<Result<String>> {
-        cx.background().spawn(async move { Ok(Default::default()) })
-    }
-
-    fn buffer_reloaded(&self, _: u64, _: &clock::Global, _: SystemTime, _: &mut MutableAppContext) {
-    }
-}
-
 pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
 
 #[derive(Clone)]
@@ -1435,8 +1362,21 @@ impl Buffer {
         redone
     }
 
+    pub fn set_completion_triggers(&mut self, triggers: Vec<String>, cx: &mut ModelContext<Self>) {
+        self.completion_triggers = triggers.clone();
+        let lamport_timestamp = self.text.lamport_clock.tick();
+        self.send_operation(
+            Operation::UpdateCompletionTriggers {
+                triggers,
+                lamport_timestamp,
+            },
+            cx,
+        );
+        cx.notify();
+    }
+
     pub fn completion_triggers(&self) -> &[String] {
-        todo!()
+        &self.completion_triggers
     }
 }
 

crates/language/src/language.rs 🔗

@@ -247,29 +247,41 @@ impl LanguageRegistry {
         cx: &mut MutableAppContext,
     ) -> Option<Task<Result<Arc<lsp::LanguageServer>>>> {
         #[cfg(any(test, feature = "test-support"))]
-        if let Some(config) = &language.config.language_server {
-            if let Some(fake_config) = &config.fake_config {
-                let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities(
-                    fake_config.capabilities.clone(),
-                    cx,
-                );
-
+        if language
+            .config
+            .language_server
+            .as_ref()
+            .and_then(|config| config.fake_config.as_ref())
+            .is_some()
+        {
+            let language = language.clone();
+            return Some(cx.spawn(|mut cx| async move {
+                let fake_config = language
+                    .config
+                    .language_server
+                    .as_ref()
+                    .unwrap()
+                    .fake_config
+                    .as_ref()
+                    .unwrap();
+                let (server, mut fake_server) = cx
+                    .update(|cx| {
+                        lsp::LanguageServer::fake_with_capabilities(
+                            fake_config.capabilities.clone(),
+                            cx,
+                        )
+                    })
+                    .await;
                 if let Some(initalizer) = &fake_config.initializer {
                     initalizer(&mut fake_server);
                 }
-
-                let servers_tx = fake_config.servers_tx.clone();
-                let initialized = server.capabilities();
-                cx.background()
-                    .spawn(async move {
-                        if initialized.await.is_some() {
-                            servers_tx.unbounded_send(fake_server).ok();
-                        }
-                    })
-                    .detach();
-
-                return Some(Task::ready(Ok(server.clone())));
-            }
+                fake_config
+                    .servers_tx
+                    .clone()
+                    .unbounded_send(fake_server)
+                    .ok();
+                Ok(server.clone())
+            }));
         }
 
         let download_dir = self
@@ -310,7 +322,8 @@ impl LanguageRegistry {
                 adapter.initialization_options(),
                 &root_path,
                 background,
-            )?;
+            )
+            .await?;
             Ok(server)
         }))
     }

crates/lsp/src/lsp.rs 🔗

@@ -3,7 +3,7 @@ use collections::HashMap;
 use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite};
 use gpui::{executor, Task};
 use parking_lot::{Mutex, RwLock};
-use postage::{barrier, prelude::Stream, watch};
+use postage::{barrier, prelude::Stream};
 use serde::{Deserialize, Serialize};
 use serde_json::{json, value::RawValue, Value};
 use smol::{
@@ -34,12 +34,11 @@ type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
 pub struct LanguageServer {
     next_id: AtomicUsize,
     outbound_tx: channel::Sender<Vec<u8>>,
-    capabilities: watch::Receiver<Option<ServerCapabilities>>,
+    capabilities: ServerCapabilities,
     notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
     response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
     executor: Arc<executor::Background>,
     io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
-    initialized: barrier::Receiver,
     output_done_rx: Mutex<Option<barrier::Receiver>>,
 }
 
@@ -100,7 +99,7 @@ struct Error {
 }
 
 impl LanguageServer {
-    pub fn new(
+    pub async fn new(
         binary_path: &Path,
         args: &[&str],
         options: Option<Value>,
@@ -116,10 +115,10 @@ impl LanguageServer {
             .spawn()?;
         let stdin = server.stdin.take().unwrap();
         let stdout = server.stdout.take().unwrap();
-        Self::new_internal(stdin, stdout, root_path, options, background)
+        Self::new_internal(stdin, stdout, root_path, options, background).await
     }
 
-    fn new_internal<Stdin, Stdout>(
+    async fn new_internal<Stdin, Stdout>(
         stdin: Stdin,
         stdout: Stdout,
         root_path: &Path,
@@ -215,109 +214,89 @@ impl LanguageServer {
             .log_err()
         });
 
-        let (initialized_tx, initialized_rx) = barrier::channel();
-        let (mut capabilities_tx, capabilities_rx) = watch::channel();
-        let this = Arc::new(Self {
+        let mut this = Arc::new(Self {
             notification_handlers,
             response_handlers,
-            capabilities: capabilities_rx,
+            capabilities: Default::default(),
             next_id: Default::default(),
             outbound_tx,
             executor: executor.clone(),
             io_tasks: Mutex::new(Some((input_task, output_task))),
-            initialized: initialized_rx,
             output_done_rx: Mutex::new(Some(output_done_rx)),
         });
 
         let root_uri = Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?;
-        executor
-            .spawn({
-                let this = this.clone();
-                async move {
-                    if let Some(capabilities) = this.init(root_uri, options).log_err().await {
-                        *capabilities_tx.borrow_mut() = Some(capabilities);
-                    }
-
-                    drop(initialized_tx);
-                }
-            })
-            .detach();
-
-        Ok(this)
-    }
 
-    async fn init(
-        self: Arc<Self>,
-        root_uri: Url,
-        options: Option<Value>,
-    ) -> Result<ServerCapabilities> {
-        #[allow(deprecated)]
-        let params = InitializeParams {
-            process_id: Default::default(),
-            root_path: Default::default(),
-            root_uri: Some(root_uri),
-            initialization_options: options,
-            capabilities: ClientCapabilities {
-                text_document: Some(TextDocumentClientCapabilities {
-                    definition: Some(GotoCapability {
-                        link_support: Some(true),
-                        ..Default::default()
-                    }),
-                    code_action: Some(CodeActionClientCapabilities {
-                        code_action_literal_support: Some(CodeActionLiteralSupport {
-                            code_action_kind: CodeActionKindLiteralSupport {
-                                value_set: vec![
-                                    CodeActionKind::REFACTOR.as_str().into(),
-                                    CodeActionKind::QUICKFIX.as_str().into(),
-                                ],
-                            },
-                        }),
-                        data_support: Some(true),
-                        resolve_support: Some(CodeActionCapabilityResolveSupport {
-                            properties: vec!["edit".to_string()],
-                        }),
-                        ..Default::default()
-                    }),
-                    completion: Some(CompletionClientCapabilities {
-                        completion_item: Some(CompletionItemCapability {
-                            snippet_support: Some(true),
-                            resolve_support: Some(CompletionItemCapabilityResolveSupport {
-                                properties: vec!["additionalTextEdits".to_string()],
+        executor
+            .spawn(async move {
+                #[allow(deprecated)]
+                let params = InitializeParams {
+                    process_id: Default::default(),
+                    root_path: Default::default(),
+                    root_uri: Some(root_uri),
+                    initialization_options: options,
+                    capabilities: ClientCapabilities {
+                        text_document: Some(TextDocumentClientCapabilities {
+                            definition: Some(GotoCapability {
+                                link_support: Some(true),
+                                ..Default::default()
+                            }),
+                            code_action: Some(CodeActionClientCapabilities {
+                                code_action_literal_support: Some(CodeActionLiteralSupport {
+                                    code_action_kind: CodeActionKindLiteralSupport {
+                                        value_set: vec![
+                                            CodeActionKind::REFACTOR.as_str().into(),
+                                            CodeActionKind::QUICKFIX.as_str().into(),
+                                        ],
+                                    },
+                                }),
+                                data_support: Some(true),
+                                resolve_support: Some(CodeActionCapabilityResolveSupport {
+                                    properties: vec!["edit".to_string()],
+                                }),
+                                ..Default::default()
                             }),
+                            completion: Some(CompletionClientCapabilities {
+                                completion_item: Some(CompletionItemCapability {
+                                    snippet_support: Some(true),
+                                    resolve_support: Some(CompletionItemCapabilityResolveSupport {
+                                        properties: vec!["additionalTextEdits".to_string()],
+                                    }),
+                                    ..Default::default()
+                                }),
+                                ..Default::default()
+                            }),
+                            ..Default::default()
+                        }),
+                        experimental: Some(json!({
+                            "serverStatusNotification": true,
+                        })),
+                        window: Some(WindowClientCapabilities {
+                            work_done_progress: Some(true),
                             ..Default::default()
                         }),
                         ..Default::default()
-                    }),
-                    ..Default::default()
-                }),
-                experimental: Some(json!({
-                    "serverStatusNotification": true,
-                })),
-                window: Some(WindowClientCapabilities {
-                    work_done_progress: Some(true),
-                    ..Default::default()
-                }),
-                ..Default::default()
-            },
-            trace: Default::default(),
-            workspace_folders: Default::default(),
-            client_info: Default::default(),
-            locale: Default::default(),
-        };
+                    },
+                    trace: Default::default(),
+                    workspace_folders: Default::default(),
+                    client_info: Default::default(),
+                    locale: Default::default(),
+                };
 
-        let this = self.clone();
-        let request = Self::request_internal::<request::Initialize>(
-            &this.next_id,
-            &this.response_handlers,
-            &this.outbound_tx,
-            params,
-        );
-        let response = request.await?;
-        Self::notify_internal::<notification::Initialized>(
-            &this.outbound_tx,
-            InitializedParams {},
-        )?;
-        Ok(response.capabilities)
+                let request = Self::request_internal::<request::Initialize>(
+                    &this.next_id,
+                    &this.response_handlers,
+                    &this.outbound_tx,
+                    params,
+                );
+                Arc::get_mut(&mut this).unwrap().capabilities = request.await?.capabilities;
+                Self::notify_internal::<notification::Initialized>(
+                    &this.outbound_tx,
+                    InitializedParams {},
+                )?;
+                Ok(this)
+            })
+            .await
     }
 
     pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>>> {
@@ -378,16 +357,8 @@ impl LanguageServer {
         }
     }
 
-    pub fn capabilities(&self) -> impl 'static + Future<Output = Option<ServerCapabilities>> {
-        let mut rx = self.capabilities.clone();
-        async move {
-            loop {
-                let value = rx.recv().await?;
-                if value.is_some() {
-                    return value;
-                }
-            }
-        }
+    pub fn capabilities(&self) -> &ServerCapabilities {
+        &self.capabilities
     }
 
     pub fn request<T: request::Request>(
@@ -399,7 +370,6 @@ impl LanguageServer {
     {
         let this = self.clone();
         async move {
-            this.initialized.clone().recv().await;
             Self::request_internal::<T>(
                 &this.next_id,
                 &this.response_handlers,
@@ -452,16 +422,8 @@ impl LanguageServer {
         }
     }
 
-    pub fn notify<T: notification::Notification>(
-        self: &Arc<Self>,
-        params: T::Params,
-    ) -> impl Future<Output = Result<()>> {
-        let this = self.clone();
-        async move {
-            this.initialized.clone().recv().await;
-            Self::notify_internal::<T>(&this.outbound_tx, params)?;
-            Ok(())
-        }
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) -> Result<()> {
+        Self::notify_internal::<T>(&self.outbound_tx, params)
     }
 
     fn notify_internal<T: notification::Notification>(
@@ -530,14 +492,16 @@ impl LanguageServer {
         }
     }
 
-    pub fn fake(cx: &mut gpui::MutableAppContext) -> (Arc<Self>, FakeLanguageServer) {
+    pub fn fake(
+        cx: &mut gpui::MutableAppContext,
+    ) -> impl Future<Output = (Arc<Self>, FakeLanguageServer)> {
         Self::fake_with_capabilities(Self::full_capabilities(), cx)
     }
 
     pub fn fake_with_capabilities(
         capabilities: ServerCapabilities,
         cx: &mut gpui::MutableAppContext,
-    ) -> (Arc<Self>, FakeLanguageServer) {
+    ) -> impl Future<Output = (Arc<Self>, FakeLanguageServer)> {
         let (stdin_writer, stdin_reader) = async_pipe::pipe();
         let (stdout_writer, stdout_reader) = async_pipe::pipe();
 
@@ -550,16 +514,15 @@ impl LanguageServer {
             }
         });
 
-        let server = Self::new_internal(
-            stdin_writer,
-            stdout_reader,
-            Path::new("/"),
-            None,
-            cx.background().clone(),
-        )
-        .unwrap();
+        let executor = cx.background().clone();
+        async move {
+            let server =
+                Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), None, executor)
+                    .await
+                    .unwrap();
 
-        (server, fake)
+            (server, fake)
+        }
     }
 }
 
@@ -758,7 +721,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_fake(cx: &mut TestAppContext) {
-        let (server, mut fake) = cx.update(LanguageServer::fake);
+        let (server, mut fake) = cx.update(LanguageServer::fake).await;
 
         let (message_tx, message_rx) = channel::unbounded();
         let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
@@ -782,7 +745,6 @@ mod tests {
                     "".to_string(),
                 ),
             })
-            .await
             .unwrap();
         assert_eq!(
             fake.receive_notification::<notification::DidOpenTextDocument>()

crates/project/src/project.rs 🔗

@@ -959,6 +959,7 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) {
         let buffer = buffer_handle.read(cx);
+        let buffer_language_name = buffer.language().map(|l| l.name().clone());
         if let Some(file) = File::from_dyn(buffer.file()) {
             let worktree_id = file.worktree_id(cx);
             if file.is_local() {
@@ -977,14 +978,6 @@ impl Project {
                     ),
                 };
 
-                for lang_server in self.language_servers_for_worktree(worktree_id) {
-                    notifications.push(
-                        lang_server.notify::<lsp::notification::DidOpenTextDocument>(
-                            did_open_text_document.clone(),
-                        ),
-                    );
-                }
-
                 if let Some(local_worktree) = file.worktree.read(cx).as_local() {
                     if let Some(diagnostics) = local_worktree.diagnostics_for_path(file.path()) {
                         self.update_buffer_diagnostics(&buffer_handle, diagnostics, None, cx)
@@ -992,34 +985,46 @@ impl Project {
                     }
                 }
 
+                for (language_name, server) in self.language_servers_for_worktree(worktree_id) {
+                    notifications.push(server.notify::<lsp::notification::DidOpenTextDocument>(
+                        did_open_text_document.clone(),
+                    ));
+
+                    if Some(language_name) == buffer_language_name.as_deref() {
+                        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,
+                            )
+                        });
+                    }
+                }
+
                 cx.observe_release(buffer_handle, |this, buffer, cx| {
                     if let Some(file) = File::from_dyn(buffer.file()) {
                         let worktree_id = file.worktree_id(cx);
                         if file.is_local() {
                             let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                            let mut notifications = Vec::new();
-                            for lang_server in this.language_servers_for_worktree(worktree_id) {
-                                notifications.push(
-                                    lang_server.notify::<lsp::notification::DidCloseTextDocument>(
+                            for (_, server) in this.language_servers_for_worktree(worktree_id) {
+                                server
+                                    .notify::<lsp::notification::DidCloseTextDocument>(
                                         lsp::DidCloseTextDocumentParams {
                                             text_document: lsp::TextDocumentIdentifier::new(
                                                 uri.clone(),
                                             ),
                                         },
-                                    ),
-                                );
+                                    )
+                                    .log_err();
                             }
-                            cx.background()
-                                .spawn(futures::future::try_join_all(notifications))
-                                .detach_and_log_err(cx);
                         }
                     }
                 })
                 .detach();
-
-                cx.background()
-                    .spawn(futures::future::try_join_all(notifications))
-                    .detach_and_log_err(cx);
             }
         }
     }
@@ -1077,17 +1082,11 @@ impl Project {
 
                 buffer_snapshots.push((next_version, next_snapshot));
 
-                let mut notifications = Vec::new();
-                for lang_server in self.language_servers_for_worktree(worktree_id) {
-                    notifications.push(
-                        lang_server
-                            .notify::<lsp::notification::DidChangeTextDocument>(changes.clone()),
-                    );
+                for (_, server) in self.language_servers_for_worktree(worktree_id) {
+                    server
+                        .notify::<lsp::notification::DidChangeTextDocument>(changes.clone())
+                        .log_err();
                 }
-
-                cx.background()
-                    .spawn(futures::future::try_join_all(notifications))
-                    .detach_and_log_err(cx);
             }
             BufferEvent::Saved => {
                 let file = File::from_dyn(buffer.read(cx).file())?;
@@ -1097,21 +1096,16 @@ impl Project {
                     uri: lsp::Url::from_file_path(abs_path).unwrap(),
                 };
 
-                let mut notifications = Vec::new();
-                for lang_server in self.language_servers_for_worktree(worktree_id) {
-                    notifications.push(
-                        lang_server.notify::<lsp::notification::DidSaveTextDocument>(
+                for (_, server) in self.language_servers_for_worktree(worktree_id) {
+                    server
+                        .notify::<lsp::notification::DidSaveTextDocument>(
                             lsp::DidSaveTextDocumentParams {
                                 text_document: text_document.clone(),
                                 text: None,
                             },
-                        ),
-                    );
+                        )
+                        .log_err();
                 }
-
-                cx.background()
-                    .spawn(futures::future::try_join_all(notifications))
-                    .detach_and_log_err(cx);
             }
             _ => {}
         }
@@ -1122,11 +1116,11 @@ impl Project {
     fn language_servers_for_worktree(
         &self,
         worktree_id: WorktreeId,
-    ) -> impl Iterator<Item = &Arc<LanguageServer>> {
+    ) -> impl Iterator<Item = (&str, &Arc<LanguageServer>)> {
         self.language_servers.iter().filter_map(
-            move |((lang_server_worktree_id, _), lang_server)| {
-                if *lang_server_worktree_id == worktree_id {
-                    Some(lang_server)
+            move |((language_server_worktree_id, language_name), server)| {
+                if *language_server_worktree_id == worktree_id {
+                    Some((language_name.as_ref(), server))
                 } else {
                     None
                 }
@@ -1182,43 +1176,62 @@ impl Project {
                 cx.spawn_weak(|this, mut cx| async move {
                     let language_server = language_server?.await.log_err()?;
                     let this = this.upgrade(&cx)?;
-                    let mut open_notifications = Vec::new();
                     this.update(&mut cx, |this, cx| {
                         this.language_servers.insert(key, language_server.clone());
+
                         for buffer in this.opened_buffers.values() {
-                            if let Some(buffer) = buffer.upgrade(cx) {
-                                let buffer = buffer.read(cx);
-                                if let Some(file) = File::from_dyn(buffer.file()) {
-                                    if let Some(file) = file.as_local() {
-                                        let versions = this
-                                            .buffer_snapshots
-                                            .entry(buffer.remote_id())
-                                            .or_insert_with(|| vec![(0, buffer.text_snapshot())]);
-                                        let (version, initial_snapshot) = versions.last().unwrap();
-                                        let uri =
-                                            lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                                        open_notifications.push(
+                            if let Some(buffer_handle) = buffer.upgrade(cx) {
+                                let buffer = buffer_handle.read(cx);
+                                let file = File::from_dyn(buffer.file())?;
+                                if file.worktree.read(cx).id() != worktree_id {
+                                    continue;
+                                }
+
+                                // Tell the language server about every open buffer in the worktree.
+                                let file = file.as_local()?;
+                                let versions = this
+                                    .buffer_snapshots
+                                    .entry(buffer.remote_id())
+                                    .or_insert_with(|| vec![(0, buffer.text_snapshot())]);
+                                let (version, initial_snapshot) = versions.last().unwrap();
+                                let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+                                language_server
+                                    .notify::<lsp::notification::DidOpenTextDocument>(
+                                        lsp::DidOpenTextDocumentParams {
+                                            text_document: lsp::TextDocumentItem::new(
+                                                uri,
+                                                Default::default(),
+                                                *version,
+                                                initial_snapshot.text(),
+                                            ),
+                                        },
+                                    )
+                                    .log_err()?;
+
+                                // Update the language buffers
+                                if buffer
+                                    .language()
+                                    .map_or(false, |l| l.name() == language.name())
+                                {
+                                    buffer_handle.update(cx, |buffer, cx| {
+                                        buffer.set_completion_triggers(
                                             language_server
-                                                .notify::<lsp::notification::DidOpenTextDocument>(
-                                                    lsp::DidOpenTextDocumentParams {
-                                                        text_document: lsp::TextDocumentItem::new(
-                                                            uri,
-                                                            Default::default(),
-                                                            *version,
-                                                            initial_snapshot.text(),
-                                                        ),
-                                                    },
-                                                ),
-                                        );
-                                    }
+                                                .capabilities()
+                                                .completion_provider
+                                                .as_ref()
+                                                .and_then(|provider| {
+                                                    provider.trigger_characters.clone()
+                                                })
+                                                .unwrap_or(Vec::new()),
+                                            cx,
+                                        )
+                                    });
                                 }
                             }
                         }
-                    });
 
-                    futures::future::try_join_all(open_notifications)
-                        .await
-                        .log_err();
+                        Some(())
+                    });
 
                     let disk_based_sources = language
                         .disk_based_diagnostic_sources()
@@ -1623,21 +1636,17 @@ impl Project {
                     .await?;
             }
 
-            for (buffer, buffer_abs_path, lang_server) in local_buffers {
-                let capabilities = if let Some(capabilities) = lang_server.capabilities().await {
-                    capabilities
-                } else {
-                    continue;
-                };
-
+            for (buffer, buffer_abs_path, language_server) in local_buffers {
                 let text_document = lsp::TextDocumentIdentifier::new(
                     lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
                 );
+                let capabilities = &language_server.capabilities();
                 let lsp_edits = if capabilities
                     .document_formatting_provider
-                    .map_or(false, |provider| provider != lsp::OneOf::Left(false))
+                    .as_ref()
+                    .map_or(false, |provider| *provider != lsp::OneOf::Left(false))
                 {
-                    lang_server
+                    language_server
                         .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
                             text_document,
                             options: Default::default(),
@@ -1646,13 +1655,14 @@ impl Project {
                         .await?
                 } else if capabilities
                     .document_range_formatting_provider
-                    .map_or(false, |provider| provider != lsp::OneOf::Left(false))
+                    .as_ref()
+                    .map_or(false, |provider| *provider != lsp::OneOf::Left(false))
                 {
                     let buffer_start = lsp::Position::new(0, 0);
                     let buffer_end = buffer
                         .read_with(&cx, |buffer, _| buffer.max_point_utf16())
                         .to_lsp_position();
-                    lang_server
+                    language_server
                         .request::<lsp::request::RangeFormatting>(
                             lsp::DocumentRangeFormattingParams {
                                 text_document,
@@ -2132,13 +2142,7 @@ impl Project {
                 range.end.to_point_utf16(buffer).to_lsp_position(),
             );
             cx.foreground().spawn(async move {
-                if !lang_server
-                    .capabilities()
-                    .await
-                    .map_or(false, |capabilities| {
-                        capabilities.code_action_provider.is_some()
-                    })
-                {
+                if !lang_server.capabilities().code_action_provider.is_some() {
                     return Ok(Default::default());
                 }
 
@@ -2674,13 +2678,7 @@ impl Project {
             {
                 let lsp_params = request.to_lsp(&file.abs_path(cx), cx);
                 return cx.spawn(|this, cx| async move {
-                    if !language_server
-                        .capabilities()
-                        .await
-                        .map_or(false, |capabilities| {
-                            request.check_capabilities(&capabilities)
-                        })
-                    {
+                    if !request.check_capabilities(language_server.capabilities()) {
                         return Ok(Default::default());
                     }
 
@@ -4262,18 +4260,32 @@ mod tests {
     async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();
 
-        let (lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake();
+        let (mut rust_lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake();
+        let (mut json_lsp_config, mut fake_json_servers) = LanguageServerConfig::fake();
+        rust_lsp_config.set_fake_capabilities(lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        });
+        json_lsp_config.set_fake_capabilities(lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        });
+
         let rust_language = Arc::new(Language::new(
             LanguageConfig {
                 name: "Rust".into(),
                 path_suffixes: vec!["rs".to_string()],
-                language_server: Some(lsp_config),
+                language_server: Some(rust_lsp_config),
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
         ));
-
-        let (json_lsp_config, mut fake_json_servers) = LanguageServerConfig::fake();
         let json_language = Arc::new(Language::new(
             LanguageConfig {
                 name: "JSON".into(),
@@ -4289,6 +4301,7 @@ mod tests {
             "/the-root",
             json!({
                 "test.rs": "const A: i32 = 1;",
+                "test2.rs": "",
                 "Cargo.toml": "a = 1",
                 "package.json": "{\"a\": 1}",
             }),
@@ -4353,6 +4366,17 @@ mod tests {
             }
         );
 
+        // The buffer is configured based on the language server's capabilities.
+        rust_buffer.read_with(cx, |buffer, _| {
+            assert_eq!(
+                buffer.completion_triggers(),
+                &[".".to_string(), "::".to_string()]
+            );
+        });
+        toml_buffer.read_with(cx, |buffer, _| {
+            assert!(buffer.completion_triggers().is_empty());
+        });
+
         // Edit a buffer. The changes are reported to the language server.
         rust_buffer.update(cx, |buffer, cx| buffer.edit([16..16], "2", cx));
         assert_eq!(
@@ -4414,6 +4438,12 @@ mod tests {
             }
         );
 
+        // This buffer is configured based on the second language server's
+        // capabilities.
+        json_buffer.read_with(cx, |buffer, _| {
+            assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
+        });
+
         // The first language server is also notified about the new open buffer.
         assert_eq!(
             fake_rust_server
@@ -4428,6 +4458,21 @@ mod tests {
             }
         );
 
+        // When opening another buffer whose language server is already running,
+        // it is also configured based on the existing language server's capabilities.
+        let rust_buffer2 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "test2.rs"), cx)
+            })
+            .await
+            .unwrap();
+        rust_buffer2.read_with(cx, |buffer, _| {
+            assert_eq!(
+                buffer.completion_triggers(),
+                &[".".to_string(), "::".to_string()]
+            );
+        });
+
         // Edit a buffer. The changes are reported to both the language servers.
         toml_buffer.update(cx, |buffer, cx| buffer.edit([5..5], "23", cx));
         assert_eq!(
@@ -6000,6 +6045,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/the-dir",
@@ -6259,6 +6306,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_rename(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+
         let (language_server_config, mut fake_servers) = LanguageServerConfig::fake();
         let language = Arc::new(Language::new(
             LanguageConfig {

crates/project/src/worktree.rs 🔗

@@ -556,7 +556,6 @@ impl LocalWorktree {
     }
 
     pub fn diagnostics_for_path(&self, path: &Path) -> Option<Vec<DiagnosticEntry<PointUtf16>>> {
-        dbg!(&self.diagnostics);
         self.diagnostics.get(path).cloned()
     }
 

crates/server/src/rpc.rs 🔗

@@ -1833,13 +1833,13 @@ mod tests {
 
         // Client A sees that a guest has joined.
         project_a
-            .condition(&cx_a, |p, _| p.collaborators().len() == 1)
+            .condition(cx_a, |p, _| p.collaborators().len() == 1)
             .await;
 
         // Drop client B's connection and ensure client A observes client B leaving the project.
         client_b.disconnect(&cx_b.to_async()).unwrap();
         project_a
-            .condition(&cx_a, |p, _| p.collaborators().len() == 0)
+            .condition(cx_a, |p, _| p.collaborators().len() == 0)
             .await;
 
         // Rejoin the project as client B
@@ -1856,14 +1856,15 @@ mod tests {
 
         // Client A sees that a guest has re-joined.
         project_a
-            .condition(&cx_a, |p, _| p.collaborators().len() == 1)
+            .condition(cx_a, |p, _| p.collaborators().len() == 1)
             .await;
 
         // Simulate connection loss for client B and ensure client A observes client B leaving the project.
+        client_b.wait_for_current_user(cx_b).await;
         server.disconnect_client(client_b.current_user_id(cx_b));
         cx_a.foreground().advance_clock(Duration::from_secs(3));
         project_a
-            .condition(&cx_a, |p, _| p.collaborators().len() == 0)
+            .condition(cx_a, |p, _| p.collaborators().len() == 0)
             .await;
     }
 
@@ -1944,6 +1945,9 @@ mod tests {
 
         // Simulate a language server reporting errors for a file.
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        fake_language_server
+            .receive_notification::<lsp::notification::DidOpenTextDocument>()
+            .await;
         fake_language_server
             .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
                 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
@@ -4467,17 +4471,16 @@ mod tests {
 
             let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
             let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-            let mut authed_user =
-                user_store.read_with(cx, |user_store, _| user_store.watch_current_user());
-            while authed_user.next().await.unwrap().is_none() {}
 
-            TestClient {
+            let client = TestClient {
                 client,
                 peer_id,
                 user_store,
                 project: Default::default(),
                 buffers: Default::default(),
-            }
+            };
+            client.wait_for_current_user(cx).await;
+            client
         }
 
         fn disconnect_client(&self, user_id: UserId) {
@@ -4557,6 +4560,13 @@ mod tests {
             )
         }
 
+        async fn wait_for_current_user(&self, cx: &TestAppContext) {
+            let mut authed_user = self
+                .user_store
+                .read_with(cx, |user_store, _| user_store.watch_current_user());
+            while authed_user.next().await.unwrap().is_none() {}
+        }
+
         fn simulate_host(
             mut self,
             project: ModelHandle<Project>,