Assign buffer's completion triggers from LSP capabilities

Also, make LanguageServer::new() async. The future resolves
once the server is initialized.
This commit is contained in:
Max Brunsfeld 2022-03-08 17:41:52 -08:00
parent 317a1bb07b
commit 4cb4b99c56
8 changed files with 345 additions and 368 deletions

View file

@ -186,7 +186,7 @@ impl UserStore {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Arc<User>>> { ) -> Task<Result<Arc<User>>> {
if let Some(user) = self.users.get(&user_id).cloned() { 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); let load_users = self.load_users(vec![user_id], cx);

View file

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

View file

@ -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>); pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
#[derive(Clone)] #[derive(Clone)]
@ -1435,8 +1362,21 @@ impl Buffer {
redone 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] { pub fn completion_triggers(&self) -> &[String] {
todo!() &self.completion_triggers
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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