From 4cb4b99c56683992d3f77d0fc8e874a61808d69e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Mar 2022 17:41:52 -0800 Subject: [PATCH] Assign buffer's completion triggers from LSP capabilities Also, make LanguageServer::new() async. The future resolves once the server is initialized. --- 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(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 5e7d29bfa6..bd56ed3e1b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -186,7 +186,7 @@ impl UserStore { cx: &mut ModelContext, ) -> Task>> { 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); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 649797be30..1ffe18114b 100644 --- a/crates/editor/src/editor.rs +++ b/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( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index dfe2d5795d..3d79ecadd6 100644 --- a/crates/language/src/buffer.rs +++ b/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, -} - -#[cfg(any(test, feature = "test-support"))] -impl FakeFile { - pub fn new(path: impl AsRef) -> 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 { - &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> { - 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> { - cx.background().spawn(async move { Ok(Default::default()) }) - } - - fn buffer_reloaded(&self, _: u64, _: &clock::Global, _: SystemTime, _: &mut MutableAppContext) { - } -} - pub(crate) struct QueryCursorHandle(Option); #[derive(Clone)] @@ -1435,8 +1362,21 @@ impl Buffer { redone } + pub fn set_completion_triggers(&mut self, triggers: Vec, cx: &mut ModelContext) { + 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 } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index a056d84c40..e8ae505a9c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -247,29 +247,41 @@ impl LanguageRegistry { cx: &mut MutableAppContext, ) -> Option>>> { #[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) })) } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 5f3eb2caaf..bba5960bb4 100644 --- a/crates/lsp/src/lsp.rs +++ b/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)>; pub struct LanguageServer { next_id: AtomicUsize, outbound_tx: channel::Sender>, - capabilities: watch::Receiver>, + capabilities: ServerCapabilities, notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, io_tasks: Mutex>, Task>)>>, - initialized: barrier::Receiver, output_done_rx: Mutex>, } @@ -100,7 +99,7 @@ struct Error { } impl LanguageServer { - pub fn new( + pub async fn new( binary_path: &Path, args: &[&str], options: Option, @@ -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( + async fn new_internal( 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, - root_uri: Url, - options: Option, - ) -> Result { - #[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()], + .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::( - &this.next_id, - &this.response_handlers, - &this.outbound_tx, - params, - ); - let response = request.await?; - Self::notify_internal::( - &this.outbound_tx, - InitializedParams {}, - )?; - Ok(response.capabilities) + let request = Self::request_internal::( + &this.next_id, + &this.response_handlers, + &this.outbound_tx, + params, + ); + Arc::get_mut(&mut this).unwrap().capabilities = request.await?.capabilities; + Self::notify_internal::( + &this.outbound_tx, + InitializedParams {}, + )?; + Ok(this) + }) + .await } pub fn shutdown(&self) -> Option>> { @@ -378,16 +357,8 @@ impl LanguageServer { } } - pub fn capabilities(&self) -> impl 'static + Future> { - 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( @@ -399,7 +370,6 @@ impl LanguageServer { { let this = self.clone(); async move { - this.initialized.clone().recv().await; Self::request_internal::( &this.next_id, &this.response_handlers, @@ -452,16 +422,8 @@ impl LanguageServer { } } - pub fn notify( - self: &Arc, - params: T::Params, - ) -> impl Future> { - let this = self.clone(); - async move { - this.initialized.clone().recv().await; - Self::notify_internal::(&this.outbound_tx, params)?; - Ok(()) - } + pub fn notify(&self, params: T::Params) -> Result<()> { + Self::notify_internal::(&self.outbound_tx, params) } fn notify_internal( @@ -530,14 +492,16 @@ impl LanguageServer { } } - pub fn fake(cx: &mut gpui::MutableAppContext) -> (Arc, FakeLanguageServer) { + pub fn fake( + cx: &mut gpui::MutableAppContext, + ) -> impl Future, FakeLanguageServer)> { Self::fake_with_capabilities(Self::full_capabilities(), cx) } pub fn fake_with_capabilities( capabilities: ServerCapabilities, cx: &mut gpui::MutableAppContext, - ) -> (Arc, FakeLanguageServer) { + ) -> impl Future, 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::() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 855a45b5dc..15d9b3f6b9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -959,6 +959,7 @@ impl Project { cx: &mut ModelContext, ) { 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::( - 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::( + 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::( + for (_, server) in this.language_servers_for_worktree(worktree_id) { + server + .notify::( 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::(changes.clone()), - ); + for (_, server) in self.language_servers_for_worktree(worktree_id) { + server + .notify::(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::( + for (_, server) in self.language_servers_for_worktree(worktree_id) { + server + .notify::( 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> { + ) -> impl Iterator)> { 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::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::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::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::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 { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 290f44c5cf..1130063c98 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -556,7 +556,6 @@ impl LocalWorktree { } pub fn diagnostics_for_path(&self, path: &Path) -> Option>> { - dbg!(&self.diagnostics); self.diagnostics.get(path).cloned() } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7fa4dc7db9..444f2858b6 100644 --- a/crates/server/src/rpc.rs +++ b/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::() + .await; fake_language_server .notify::(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,