Detailed changes
@@ -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);
@@ -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(
@@ -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
}
}
@@ -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)
}))
}
@@ -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>()
@@ -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 {
@@ -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()
}
@@ -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>,