From f2848a092b36e3c7a57ecbfe63e953216ba72cf8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Mar 2022 18:45:01 -0700 Subject: [PATCH 01/10] WIP: Start on a delegation... like events, but single consumer that takes ownership of event --- crates/gpui/src/app.rs | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 65482239335af77ece790c6ac1261dcae855fd69..182d56b3c28a623af5c52651d16b69c84ead3154 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -60,6 +60,10 @@ pub trait View: Entity + Sized { } } +pub trait Delegator: Entity { + type Delegation; +} + pub trait ReadModel { fn read_model(&self, handle: &ModelHandle) -> &T; } @@ -740,6 +744,7 @@ type ActionCallback = type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; +type DelegationCallback = Box, &mut MutableAppContext) -> bool>; type ObservationCallback = Box bool>; type ReleaseObservationCallback = Box; @@ -757,6 +762,7 @@ pub struct MutableAppContext { next_subscription_id: usize, frame_count: usize, subscriptions: Arc>>>, + delegations: Arc>>, observations: Arc>>>, release_observations: Arc>>>, presenters_and_platform_windows: @@ -804,6 +810,7 @@ impl MutableAppContext { next_subscription_id: 0, frame_count: 0, subscriptions: Default::default(), + delegations: Default::default(), observations: Default::default(), release_observations: Default::default(), presenters_and_platform_windows: HashMap::new(), @@ -1149,6 +1156,37 @@ impl MutableAppContext { } } + pub fn become_delegate_internal(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(H, &E::Event, &mut Self) -> bool, + { + let id = post_inc(&mut self.next_subscription_id); + let emitter = handle.downgrade(); + self.subscriptions + .lock() + .entry(handle.id()) + .or_default() + .insert( + id, + Box::new(move |payload, cx| { + if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(emitter, payload, cx) + } else { + false + } + }), + ); + Subscription::Subscription { + id, + entity_id: handle.id(), + subscriptions: Some(Arc::downgrade(&self.subscriptions)), + } + } + pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -2590,6 +2628,27 @@ impl<'a, T: View> ViewContext<'a, T> { }) } + pub fn become_delegate(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Delegator, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(&mut T, H, E::Event, &mut ViewContext), + { + // let subscriber = self.weak_handle(); + // self.app + // .subscribe_internal(handle, move |emitter, event, cx| { + // if let Some(subscriber) = subscriber.upgrade(cx) { + // subscriber.update(cx, |subscriber, cx| { + // callback(subscriber, emitter, event, cx); + // }); + // true + // } else { + // false + // } + // }) + } + pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, From b49951ac57564dba229223a895de9b3f97d899d4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 8 Mar 2022 09:30:32 +0100 Subject: [PATCH 02/10] Implement `{ModelHandle,ViewHandle}::become_delegate` --- crates/gpui/src/app.rs | 143 ++++++++++++++++++++++++++++------------- 1 file changed, 99 insertions(+), 44 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 182d56b3c28a623af5c52651d16b69c84ead3154..85cd865f2b827a73cc03f91a6fc1a59b72469f32 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -60,10 +60,6 @@ pub trait View: Entity + Sized { } } -pub trait Delegator: Entity { - type Delegation; -} - pub trait ReadModel { fn read_model(&self, handle: &ModelHandle) -> &T; } @@ -1156,34 +1152,32 @@ impl MutableAppContext { } } - pub fn become_delegate_internal(&mut self, handle: &H, mut callback: F) -> Subscription + fn become_delegate_internal(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, - E::Event: 'static, H: Handle, - F: 'static + FnMut(H, &E::Event, &mut Self) -> bool, + F: 'static + FnMut(H, E::Event, &mut Self) -> bool, { let id = post_inc(&mut self.next_subscription_id); let emitter = handle.downgrade(); - self.subscriptions - .lock() - .entry(handle.id()) - .or_default() - .insert( + self.delegations.lock().insert( + handle.id(), + ( id, Box::new(move |payload, cx| { if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { - let payload = payload.downcast_ref().expect("downcast is type safe"); + let payload = *payload.downcast().expect("downcast is type safe"); callback(emitter, payload, cx) } else { false } }), - ); - Subscription::Subscription { + ), + ); + Subscription::Delegation { id, entity_id: handle.id(), - subscriptions: Some(Arc::downgrade(&self.subscriptions)), + delegations: Some(Arc::downgrade(&self.delegations)), } } @@ -1726,6 +1720,17 @@ impl MutableAppContext { } } } + + let delegate = self.delegations.lock().remove(&entity_id); + if let Some((id, mut callback)) = delegate { + let alive = callback(payload, self); + if alive { + self.delegations + .lock() + .entry(entity_id) + .or_insert_with(|| (id, callback)); + } + } } fn notify_model_observers(&mut self, observed_id: usize) { @@ -2363,6 +2368,26 @@ impl<'a, T: Entity> ModelContext<'a, T> { }) } + pub fn become_delegate(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + H: Handle, + F: 'static + FnMut(&mut T, H, E::Event, &mut ModelContext), + { + let delegate = self.weak_handle(); + self.app + .become_delegate_internal(handle, move |emitter, event, cx| { + if let Some(delegate) = delegate.upgrade(cx) { + delegate.update(cx, |subscriber, cx| { + callback(subscriber, emitter, event, cx); + }); + true + } else { + false + } + }) + } + pub fn observe_release( &mut self, handle: &ModelHandle, @@ -2630,23 +2655,22 @@ impl<'a, T: View> ViewContext<'a, T> { pub fn become_delegate(&mut self, handle: &H, mut callback: F) -> Subscription where - E: Delegator, - E::Event: 'static, + E: Entity, H: Handle, F: 'static + FnMut(&mut T, H, E::Event, &mut ViewContext), { - // let subscriber = self.weak_handle(); - // self.app - // .subscribe_internal(handle, move |emitter, event, cx| { - // if let Some(subscriber) = subscriber.upgrade(cx) { - // subscriber.update(cx, |subscriber, cx| { - // callback(subscriber, emitter, event, cx); - // }); - // true - // } else { - // false - // } - // }) + let delegate = self.weak_handle(); + self.app + .become_delegate_internal(handle, move |emitter, event, cx| { + if let Some(delegate) = delegate.upgrade(cx) { + delegate.update(cx, |subscriber, cx| { + callback(subscriber, emitter, event, cx); + }); + true + } else { + false + } + }) } pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription @@ -3802,6 +3826,11 @@ pub enum Subscription { entity_id: usize, subscriptions: Option>>>>, }, + Delegation { + id: usize, + entity_id: usize, + delegations: Option>>>, + }, Observation { id: usize, entity_id: usize, @@ -3821,6 +3850,9 @@ impl Subscription { Subscription::Subscription { subscriptions, .. } => { subscriptions.take(); } + Subscription::Delegation { delegations, .. } => { + delegations.take(); + } Subscription::Observation { observations, .. } => { observations.take(); } @@ -3867,6 +3899,19 @@ impl Drop for Subscription { } } } + Subscription::Delegation { + id, + entity_id, + delegations, + } => { + if let Some(delegations) = delegations.as_ref().and_then(Weak::upgrade) { + if let Entry::Occupied(entry) = delegations.lock().entry(*entity_id) { + if *id == entry.get().0 { + let _ = entry.remove(); + } + } + } + } } } } @@ -4120,7 +4165,7 @@ mod tests { } #[crate::test(self)] - fn test_subscribe_and_emit_from_model(cx: &mut MutableAppContext) { + fn test_model_events(cx: &mut MutableAppContext) { #[derive(Default)] struct Model { events: Vec, @@ -4132,11 +4177,16 @@ mod tests { let handle_1 = cx.add_model(|_| Model::default()); let handle_2 = cx.add_model(|_| Model::default()); - handle_1.update(cx, |_, c| { - c.subscribe(&handle_2, move |model: &mut Model, emitter, event, c| { + handle_1.update(cx, |_, cx| { + cx.become_delegate(&handle_2, |model, _, event, _| { + model.events.push(event * 3); + }) + .detach(); + + cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| { model.events.push(*event); - c.subscribe(&emitter, |model, _, event, _| { + cx.subscribe(&emitter, |model, _, event, _| { model.events.push(*event * 2); }) .detach(); @@ -4145,10 +4195,10 @@ mod tests { }); handle_2.update(cx, |_, c| c.emit(7)); - assert_eq!(handle_1.read(cx).events, vec![7]); + assert_eq!(handle_1.read(cx).events, vec![7, 21]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); + assert_eq!(handle_1.read(cx).events, vec![7, 21, 5, 10, 15]); } #[crate::test(self)] @@ -4375,7 +4425,7 @@ mod tests { } #[crate::test(self)] - fn test_subscribe_and_emit_from_view(cx: &mut MutableAppContext) { + fn test_view_events(cx: &mut MutableAppContext) { #[derive(Default)] struct View { events: Vec, @@ -4405,31 +4455,36 @@ mod tests { let handle_2 = cx.add_view(window_id, |_| View::default()); let handle_3 = cx.add_model(|_| Model); - handle_1.update(cx, |_, c| { - c.subscribe(&handle_2, move |me, emitter, event, c| { + handle_1.update(cx, |_, cx| { + cx.become_delegate(&handle_2, |me, _, event, _| { + me.events.push(event * 3); + }) + .detach(); + + cx.subscribe(&handle_2, move |me, emitter, event, cx| { me.events.push(*event); - c.subscribe(&emitter, |me, _, event, _| { + cx.subscribe(&emitter, |me, _, event, _| { me.events.push(*event * 2); }) .detach(); }) .detach(); - c.subscribe(&handle_3, |me, _, event, _| { + cx.subscribe(&handle_3, |me, _, event, _| { me.events.push(*event); }) .detach(); }); handle_2.update(cx, |_, c| c.emit(7)); - assert_eq!(handle_1.read(cx).events, vec![7]); + assert_eq!(handle_1.read(cx).events, vec![7, 21]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); + assert_eq!(handle_1.read(cx).events, vec![7, 21, 5, 10, 15]); handle_3.update(cx, |_, c| c.emit(9)); - assert_eq!(handle_1.read(cx).events, vec![7, 5, 10, 9]); + assert_eq!(handle_1.read(cx).events, vec![7, 21, 5, 10, 15, 9]); } #[crate::test(self)] From d6c8fdb3c468d7be9d2e76bfd3eeb775b3c83c9b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 8 Mar 2022 11:11:25 +0100 Subject: [PATCH 03/10] Send buffer operations via the `Project` instead of `Worktree` --- crates/language/src/buffer.rs | 25 ++------- crates/language/src/tests.rs | 99 ++++++++++++++++++++-------------- crates/project/src/project.rs | 38 +++++++++++-- crates/project/src/worktree.rs | 48 +---------------- crates/server/src/rpc.rs | 3 ++ 5 files changed, 101 insertions(+), 112 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f1bc5819321cbb5a328aa8f31684ac805a7d505f..061d9564443b7c8acb0974f2adec55aa251b1050 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -73,8 +73,6 @@ pub struct Buffer { language_server: Option, completion_triggers: Vec, deferred_ops: OperationQueue, - #[cfg(test)] - pub(crate) operations: Vec, } pub struct BufferSnapshot { @@ -143,7 +141,7 @@ struct LanguageServerSnapshot { path: Arc, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Operation { Buffer(text::Operation), UpdateDiagnostics { @@ -160,8 +158,9 @@ pub enum Operation { }, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + Operation(Operation), Edited, Dirtied, Saved, @@ -202,8 +201,6 @@ pub trait File { cx: &mut MutableAppContext, ) -> Task>; - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext); - fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); fn as_any(&self) -> &dyn Any; @@ -276,8 +273,6 @@ impl File for FakeFile { cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) } - fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} - fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} fn as_any(&self) -> &dyn Any { @@ -526,8 +521,6 @@ impl Buffer { language_server: None, completion_triggers: Default::default(), deferred_ops: OperationQueue::new(), - #[cfg(test)] - operations: Default::default(), } } @@ -1745,16 +1738,8 @@ impl Buffer { } } - #[cfg(not(test))] - pub fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext) { - if let Some(file) = &self.file { - file.buffer_updated(self.remote_id(), operation, cx.as_mut()); - } - } - - #[cfg(test)] - pub fn send_operation(&mut self, operation: Operation, _: &mut ModelContext) { - self.operations.push(operation); + fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext) { + cx.emit(Event::Operation(operation)); } pub fn remove_peer(&mut self, replica_id: ReplicaId, cx: &mut ModelContext) { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 112073aed652b340d3148edb51a50c5d8da63a5a..5ccd400e0c49442fa6f47926ee6dfda233b34f52 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -76,43 +76,48 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); - let buffer_ops = buffer1.update(cx, |buffer, cx| { - let buffer_1_events = buffer_1_events.clone(); - cx.subscribe(&buffer1, move |_, _, event, _| { - buffer_1_events.borrow_mut().push(event.clone()) - }) - .detach(); - let buffer_2_events = buffer_2_events.clone(); - cx.subscribe(&buffer2, move |_, _, event, _| { - buffer_2_events.borrow_mut().push(event.clone()) - }) - .detach(); + let buffer1_ops = Rc::new(RefCell::new(Vec::new())); + buffer1.update(cx, { + let buffer1_ops = buffer1_ops.clone(); + |buffer, cx| { + let buffer_1_events = buffer_1_events.clone(); + cx.become_delegate(&buffer1, move |_, _, event, _| match event { + Event::Operation(op) => buffer1_ops.borrow_mut().push(op), + event @ _ => buffer_1_events.borrow_mut().push(event), + }) + .detach(); + let buffer_2_events = buffer_2_events.clone(); + cx.subscribe(&buffer2, move |_, _, event, _| { + buffer_2_events.borrow_mut().push(event.clone()) + }) + .detach(); - // An edit emits an edited event, followed by a dirtied event, - // since the buffer was previously in a clean state. - buffer.edit(Some(2..4), "XYZ", cx); + // An edit emits an edited event, followed by a dirtied event, + // since the buffer was previously in a clean state. + buffer.edit(Some(2..4), "XYZ", cx); - // An empty transaction does not emit any events. - buffer.start_transaction(); - buffer.end_transaction(cx); + // An empty transaction does not emit any events. + buffer.start_transaction(); + buffer.end_transaction(cx); - // A transaction containing two edits emits one edited event. - now += Duration::from_secs(1); - buffer.start_transaction_at(now); - buffer.edit(Some(5..5), "u", cx); - buffer.edit(Some(6..6), "w", cx); - buffer.end_transaction_at(now, cx); + // A transaction containing two edits emits one edited event. + now += Duration::from_secs(1); + buffer.start_transaction_at(now); + buffer.edit(Some(5..5), "u", cx); + buffer.edit(Some(6..6), "w", cx); + buffer.end_transaction_at(now, cx); - // Undoing a transaction emits one edited event. - buffer.undo(cx); - - buffer.operations.clone() + // Undoing a transaction emits one edited event. + buffer.undo(cx); + } }); // Incorporating a set of remote ops emits a single edited event, // followed by a dirtied event. buffer2.update(cx, |buffer, cx| { - buffer.apply_ops(buffer_ops, cx).unwrap(); + buffer + .apply_ops(buffer1_ops.borrow_mut().drain(..), cx) + .unwrap(); }); let buffer_1_events = buffer_1_events.borrow(); @@ -1177,17 +1182,26 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { .collect::(); let mut replica_ids = Vec::new(); let mut buffers = Vec::new(); - let mut network = Network::new(rng.clone()); + let network = Rc::new(RefCell::new(Network::new(rng.clone()))); for i in 0..rng.gen_range(min_peers..=max_peers) { let buffer = cx.add_model(|cx| { let mut buffer = Buffer::new(i as ReplicaId, base_text.as_str(), cx); buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + let network = network.clone(); + cx.become_delegate(&cx.handle(), move |buffer, _, event, _| { + if let Event::Operation(op) = event { + network + .borrow_mut() + .broadcast(buffer.replica_id(), vec![proto::serialize_operation(&op)]); + } + }) + .detach(); buffer }); buffers.push(buffer); replica_ids.push(i as ReplicaId); - network.add_peer(i as ReplicaId); + network.borrow_mut().add_peer(i as ReplicaId); log::info!("Adding initial peer with replica id {}", i); } @@ -1268,10 +1282,20 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { let mut new_buffer = Buffer::from_proto(new_replica_id, old_buffer, None, cx).unwrap(); new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + let network = network.clone(); + cx.become_delegate(&cx.handle(), move |buffer, _, event, _| { + if let Event::Operation(op) = event { + network.borrow_mut().broadcast( + buffer.replica_id(), + vec![proto::serialize_operation(&op)], + ); + } + }) + .detach(); new_buffer })); replica_ids.push(new_replica_id); - network.replicate(replica_id, new_replica_id); + network.borrow_mut().replicate(replica_id, new_replica_id); } 60..=69 if mutation_count != 0 => { buffer.update(cx, |buffer, cx| { @@ -1280,8 +1304,9 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { }); mutation_count -= 1; } - _ if network.has_unreceived(replica_id) => { + _ if network.borrow().has_unreceived(replica_id) => { let ops = network + .borrow_mut() .receive(replica_id) .into_iter() .map(|op| proto::deserialize_operation(op).unwrap()); @@ -1297,14 +1322,6 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { _ => {} } - buffer.update(cx, |buffer, _| { - let ops = buffer - .operations - .drain(..) - .map(|op| proto::serialize_operation(&op)) - .collect(); - network.broadcast(buffer.replica_id(), ops); - }); now += Duration::from_millis(rng.gen_range(0..=200)); buffers.extend(new_buffer); @@ -1312,7 +1329,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { buffer.read(cx).check_invariants(); } - if mutation_count == 0 && network.is_idle() { + if mutation_count == 0 && network.borrow().is_idle() { break; } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 694755274d710fd3c08269659f7930cb47c44c5f..f517560d956493a98ed8f0cf7d7b1ab9f5f46caa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -17,8 +17,8 @@ use gpui::{ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, - Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, - ToLspPosition, ToOffset, ToPointUtf16, Transaction, + Diagnostic, DiagnosticEntry, Event as BufferEvent, File as _, Language, LanguageRegistry, + Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -945,10 +945,37 @@ impl Project { remote_id ))?, } - self.assign_language_to_buffer(&buffer, worktree, cx); + cx.become_delegate(buffer, Self::on_buffer_event).detach(); + self.assign_language_to_buffer(buffer, worktree, cx); + Ok(()) } + fn on_buffer_event( + &mut self, + buffer: ModelHandle, + event: BufferEvent, + cx: &mut ModelContext, + ) { + match event { + BufferEvent::Operation(operation) => { + if let Some(project_id) = self.remote_id() { + let request = self.client.request(proto::UpdateBuffer { + project_id, + buffer_id: buffer.read(cx).remote_id(), + operations: vec![language::proto::serialize_operation(&operation)], + }); + cx.foreground() + .spawn(async move { + request.await.log_err(); + }) + .detach(); + } + } + _ => {} + } + } + fn assign_language_to_buffer( &mut self, buffer: &ModelHandle, @@ -4452,7 +4479,10 @@ mod tests { buffer1.update(cx, |buffer, cx| { cx.subscribe(&buffer1, { let events = events.clone(); - move |_, _, event, _| events.borrow_mut().push(event.clone()) + move |_, _, event, _| match event { + BufferEvent::Operation(_) => {} + _ => events.borrow_mut().push(event.clone()), + } }) .detach(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 78dac236813f3f1dac6b57bcdc4a45a57f6256f7..8761c7b80ec2e1965bc6c5a003b9288c97641e1b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -19,7 +19,7 @@ use gpui::{ }; use language::{ proto::{deserialize_version, serialize_version}, - Buffer, DiagnosticEntry, Operation, PointUtf16, Rope, + Buffer, DiagnosticEntry, PointUtf16, Rope, }; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -71,7 +71,6 @@ pub struct LocalWorktree { share: Option, diagnostics: HashMap, Vec>>, diagnostic_summaries: TreeMap, - queued_operations: Vec<(u64, Operation)>, client: Arc, fs: Arc, visible: bool, @@ -84,7 +83,6 @@ pub struct RemoteWorktree { client: Arc, updates_tx: UnboundedSender, replica_id: ReplicaId, - queued_operations: Vec<(u64, Operation)>, diagnostic_summaries: TreeMap, visible: bool, } @@ -226,7 +224,6 @@ impl Worktree { snapshot_rx: snapshot_rx.clone(), updates_tx, client: client.clone(), - queued_operations: Default::default(), diagnostic_summaries: TreeMap::from_ordered_entries( worktree.diagnostic_summaries.into_iter().map(|summary| { ( @@ -420,42 +417,6 @@ impl Worktree { cx.notify(); } - - fn send_buffer_update( - &mut self, - buffer_id: u64, - operation: Operation, - cx: &mut ModelContext, - ) { - if let Some((project_id, rpc)) = match self { - Worktree::Local(worktree) => worktree - .share - .as_ref() - .map(|share| (share.project_id, worktree.client.clone())), - Worktree::Remote(worktree) => Some((worktree.project_id, worktree.client.clone())), - } { - cx.spawn(|worktree, mut cx| async move { - if let Err(error) = rpc - .request(proto::UpdateBuffer { - project_id, - buffer_id, - operations: vec![language::proto::serialize_operation(&operation)], - }) - .await - { - worktree.update(&mut cx, |worktree, _| { - log::error!("error sending buffer operation: {}", error); - match worktree { - Worktree::Local(t) => &mut t.queued_operations, - Worktree::Remote(t) => &mut t.queued_operations, - } - .push((buffer_id, operation)); - }); - } - }) - .detach(); - } - } } impl LocalWorktree { @@ -526,7 +487,6 @@ impl LocalWorktree { poll_task: None, diagnostics: Default::default(), diagnostic_summaries: Default::default(), - queued_operations: Default::default(), client, fs, visible, @@ -1455,12 +1415,6 @@ impl language::File for File { }) } - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { - self.worktree.update(cx, |worktree, cx| { - worktree.send_buffer_update(buffer_id, operation, cx); - }); - } - fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext) { self.worktree.update(cx, |worktree, _| { if let Worktree::Remote(worktree) = worktree { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index d05c9e2401327405fba3e27dbb0f60707c473649..7f1808fb08030b735214b8cfbd46821d0c40f4c5 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1034,6 +1034,7 @@ mod tests { use project::{ fs::{FakeFs, Fs as _}, search::SearchQuery, + worktree::WorktreeHandle, DiagnosticSummary, Project, ProjectPath, }; use rand::prelude::*; @@ -1411,6 +1412,8 @@ mod tests { buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty())); buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await; + worktree_a.flush_fs_events(cx_a).await; + // Make changes on host's file system, see those changes on guest worktrees. fs.rename( "/a/file1".as_ref(), From 51d5ed48f0168b60a826b25665c5611dc7a5f513 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 8 Mar 2022 11:17:20 +0100 Subject: [PATCH 04/10] Remove unused `CloseBuffer` message --- crates/language/src/buffer.rs | 5 ----- crates/project/src/project.rs | 11 ----------- crates/project/src/worktree.rs | 14 -------------- crates/rpc/proto/zed.proto | 1 - crates/rpc/src/proto.rs | 2 -- crates/server/src/rpc.rs | 14 -------------- 6 files changed, 47 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 061d9564443b7c8acb0974f2adec55aa251b1050..909861eeda5f399a2fd32d1ee7da2c5d70f3b2fb 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -201,8 +201,6 @@ pub trait File { cx: &mut MutableAppContext, ) -> Task>; - fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); - fn as_any(&self) -> &dyn Any; fn to_proto(&self) -> rpc::proto::File; @@ -273,8 +271,6 @@ impl File for FakeFile { cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) } - fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} - fn as_any(&self) -> &dyn Any { self } @@ -1870,7 +1866,6 @@ impl Entity for Buffer { fn release(&mut self, cx: &mut gpui::MutableAppContext) { if let Some(file) = self.file.as_ref() { - file.buffer_removed(self.remote_id(), cx); if let Some((lang_server, file)) = self.language_server.as_ref().zip(file.as_local()) { let request = lang_server .server diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f517560d956493a98ed8f0cf7d7b1ab9f5f46caa..5b1908c84d9ac2f0d821bd379214c1f371a23115 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -201,7 +201,6 @@ impl Project { client.add_entity_message_handler(Self::handle_add_collaborator); client.add_entity_message_handler(Self::handle_buffer_reloaded); client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_close_buffer); client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updated); client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updating); client.add_entity_message_handler(Self::handle_remove_collaborator); @@ -3456,16 +3455,6 @@ impl Project { }) } - async fn handle_close_buffer( - _: ModelHandle, - _: TypedEnvelope, - _: Arc, - _: AsyncAppContext, - ) -> Result<()> { - // TODO: use this for following - Ok(()) - } - async fn handle_buffer_saved( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8761c7b80ec2e1965bc6c5a003b9288c97641e1b..1130063c98ddce13646944e745a7059ac8a424d7 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1415,20 +1415,6 @@ impl language::File for File { }) } - fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext) { - self.worktree.update(cx, |worktree, _| { - if let Worktree::Remote(worktree) = worktree { - worktree - .client - .send(proto::CloseBuffer { - project_id: worktree.project_id, - buffer_id, - }) - .log_err(); - } - }); - } - fn as_any(&self) -> &dyn Any { self } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 377aef694674d025f443befc74ffbd9b73dcdcc5..7f43aaff1a9e361d63d1ca58c0e43f76c591de26 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -42,7 +42,6 @@ message Envelope { OpenBuffer open_buffer = 35; OpenBufferResponse open_buffer_response = 36; - CloseBuffer close_buffer = 37; UpdateBuffer update_buffer = 38; UpdateBufferFile update_buffer_file = 39; SaveBuffer save_buffer = 40; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index ffb74f493933aa53595925e69bab3c2e5481f28c..d252decb3acdfc116d077d2f3967a909cb5d2b20 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -146,7 +146,6 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (ChannelMessageSent, Foreground), - (CloseBuffer, Foreground), (DiskBasedDiagnosticsUpdated, Background), (DiskBasedDiagnosticsUpdating, Background), (Error, Foreground), @@ -247,7 +246,6 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, - CloseBuffer, DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdating, FormatBuffers, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7f1808fb08030b735214b8cfbd46821d0c40f4c5..0b5f5cf880da33dfe021c70a2ec0e780680c5351 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -102,7 +102,6 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) - .add_message_handler(Server::close_buffer) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -581,19 +580,6 @@ impl Server { .await?) } - async fn close_buffer( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result<()> { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - self.peer - .forward_send(request.sender_id, host_connection_id, request.payload)?; - Ok(()) - } - async fn save_buffer( self: Arc, request: TypedEnvelope, From 6662ba62a316fd036100e2ac511f968d3667c55a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 8 Mar 2022 11:34:16 +0100 Subject: [PATCH 05/10] Move `DidSaveTextDocument` notification from `Buffer` to `Project` --- crates/language/src/buffer.rs | 20 ------------------- crates/project/src/project.rs | 36 ++++++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 909861eeda5f399a2fd32d1ee7da2c5d70f3b2fb..3a7838cd1946feb91321ee2e5f294c766068ea0c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -691,26 +691,6 @@ impl Buffer { self.file = Some(new_file); self.file_update_count += 1; } - if let Some((state, local_file)) = &self - .language_server - .as_ref() - .zip(self.file.as_ref().and_then(|f| f.as_local())) - { - cx.background() - .spawn( - state - .server - .notify::( - lsp::DidSaveTextDocumentParams { - text_document: lsp::TextDocumentIdentifier { - uri: lsp::Url::from_file_path(local_file.abs_path(cx)).unwrap(), - }, - text: None, - }, - ), - ) - .detach() - } cx.emit(Event::Saved); cx.notify(); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5b1908c84d9ac2f0d821bd379214c1f371a23115..4c44258781a0460a5f3f3e4a8c7e2a12a736bdae 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -964,11 +964,37 @@ impl Project { buffer_id: buffer.read(cx).remote_id(), operations: vec![language::proto::serialize_operation(&operation)], }); - cx.foreground() - .spawn(async move { - request.await.log_err(); - }) - .detach(); + cx.background().spawn(request).detach_and_log_err(cx); + } + } + BufferEvent::Saved => { + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + let worktree_id = file.worktree_id(cx); + if let Some(abs_path) = file.as_local().map(|file| file.abs_path(cx)) { + let text_document = lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(abs_path).unwrap(), + }; + + let mut notifications = Vec::new(); + for ((lang_server_worktree_id, _), lang_server) in &self.language_servers { + if *lang_server_worktree_id != worktree_id { + continue; + } + + notifications.push( + lang_server.notify::( + lsp::DidSaveTextDocumentParams { + text_document: text_document.clone(), + text: None, + }, + ), + ); + } + + cx.background() + .spawn(futures::future::try_join_all(notifications)) + .detach_and_log_err(cx); + } } } _ => {} From 317a1bb07bd00ec0e7c0c257010454dbf2522187 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 8 Mar 2022 19:00:54 +0100 Subject: [PATCH 06/10] Remove language servers from buffers Co-Authored-By: Nathan Sobo Co-Authored-By: Antonio Scandurra Co-Authored-By: Keith Simmons --- Cargo.lock | 1 + crates/editor/src/editor.rs | 7 +- crates/editor/src/multi_buffer.rs | 6 +- crates/gpui/src/app.rs | 65 +- crates/language/src/buffer.rs | 425 +------ crates/language/src/diagnostic_set.rs | 4 +- crates/language/src/tests.rs | 609 +-------- crates/project/Cargo.toml | 1 + crates/project/src/lsp_command.rs | 52 +- crates/project/src/project.rs | 1648 ++++++++++++++++++++++--- crates/project/src/worktree.rs | 1 + crates/search/src/buffer_search.rs | 2 +- crates/server/src/rpc.rs | 4 +- crates/text/src/anchor.rs | 42 +- 14 files changed, 1608 insertions(+), 1259 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e6f8192a835964e7b046da1a34023f47dc9b4f3..7aafce3bbb00774249f6ae29e8bbeed1fda559bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3591,6 +3591,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.2", + "similar", "smol", "sum_tree", "tempdir", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 857b33f625f88214e06436d37003f293c45e7a57..649797be302500cac62ad61e6efd8e67f96e1ade 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -32,8 +32,8 @@ use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; pub use language::{char_kind, CharKind}; use language::{ - AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, + BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, + Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ @@ -8235,9 +8235,6 @@ mod tests { .update(cx, |project, cx| project.open_buffer(project_path, cx)) .await .unwrap(); - buffer.update(cx, |buffer, cx| { - buffer.set_language_server(Some(language_server), cx); - }); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); buffer.next_notification(&cx).await; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e05ddc56933bd83d67735a9e877252043f852f23..64683faa962229a77ba6384881eed7ca4ea87d1d 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -8,8 +8,8 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, - Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, - TransactionId, + Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, + ToPointUtf16 as _, TransactionId, }; use std::{ cell::{Ref, RefCell}, @@ -25,7 +25,7 @@ use text::{ locator::Locator, rope::TextDimension, subscription::{Subscription, Topic}, - AnchorRangeExt as _, Edit, Point, PointUtf16, TextSummary, + Edit, Point, PointUtf16, TextSummary, }; use theme::SyntaxTheme; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 85cd865f2b827a73cc03f91a6fc1a59b72469f32..6727fc5f08f8caeba74e1e344c587494f6fd8053 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -742,7 +742,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type DelegationCallback = Box, &mut MutableAppContext) -> bool>; type ObservationCallback = Box bool>; -type ReleaseObservationCallback = Box; +type ReleaseObservationCallback = Box; pub struct MutableAppContext { weak_self: Option>>, @@ -1186,14 +1186,20 @@ impl MutableAppContext { E: Entity, E::Event: 'static, H: Handle, - F: 'static + FnMut(&mut Self), + F: 'static + FnMut(&E, &mut Self), { let id = post_inc(&mut self.next_subscription_id); self.release_observations .lock() .entry(handle.id()) .or_default() - .insert(id, Box::new(move |cx| callback(cx))); + .insert( + id, + Box::new(move |released, cx| { + let released = released.downcast_ref().unwrap(); + callback(released, cx) + }), + ); Subscription::ReleaseObservation { id, entity_id: handle.id(), @@ -1552,9 +1558,8 @@ impl MutableAppContext { self.observations.lock().remove(&model_id); let mut model = self.cx.models.remove(&model_id).unwrap(); model.release(self); - self.pending_effects.push_back(Effect::Release { - entity_id: model_id, - }); + self.pending_effects + .push_back(Effect::ModelRelease { model_id, model }); } for (window_id, view_id) in dropped_views { @@ -1580,7 +1585,7 @@ impl MutableAppContext { } self.pending_effects - .push_back(Effect::Release { entity_id: view_id }); + .push_back(Effect::ViewRelease { view_id, view }); } for key in dropped_element_states { @@ -1607,7 +1612,12 @@ impl MutableAppContext { self.notify_view_observers(window_id, view_id) } Effect::Deferred(callback) => callback(self), - Effect::Release { entity_id } => self.notify_release_observers(entity_id), + Effect::ModelRelease { model_id, model } => { + self.notify_release_observers(model_id, model.as_any()) + } + Effect::ViewRelease { view_id, view } => { + self.notify_release_observers(view_id, view.as_any()) + } Effect::Focus { window_id, view_id } => { self.focus(window_id, view_id); } @@ -1781,11 +1791,11 @@ impl MutableAppContext { } } - fn notify_release_observers(&mut self, entity_id: usize) { + fn notify_release_observers(&mut self, entity_id: usize, entity: &dyn Any) { let callbacks = self.release_observations.lock().remove(&entity_id); if let Some(callbacks) = callbacks { for (_, mut callback) in callbacks { - callback(self); + callback(entity, self); } } } @@ -2112,8 +2122,13 @@ pub enum Effect { view_id: usize, }, Deferred(Box), - Release { - entity_id: usize, + ModelRelease { + model_id: usize, + model: Box, + }, + ViewRelease { + view_id: usize, + view: Box, }, Focus { window_id: usize, @@ -2142,9 +2157,13 @@ impl Debug for Effect { .field("view_id", view_id) .finish(), Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(), - Effect::Release { entity_id } => f - .debug_struct("Effect::Release") - .field("entity_id", entity_id) + Effect::ModelRelease { model_id, .. } => f + .debug_struct("Effect::ModelRelease") + .field("model_id", model_id) + .finish(), + Effect::ViewRelease { view_id, .. } => f + .debug_struct("Effect::ViewRelease") + .field("view_id", view_id) .finish(), Effect::Focus { window_id, view_id } => f .debug_struct("Effect::Focus") @@ -2395,13 +2414,13 @@ impl<'a, T: Entity> ModelContext<'a, T> { ) -> Subscription where S: Entity, - F: 'static + FnMut(&mut T, &mut ModelContext), + F: 'static + FnMut(&mut T, &S, &mut ModelContext), { let observer = self.weak_handle(); - self.app.observe_release(handle, move |cx| { + self.app.observe_release(handle, move |released, cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { - callback(observer, cx); + callback(observer, released, cx); }); } }) @@ -2677,13 +2696,13 @@ impl<'a, T: View> ViewContext<'a, T> { where E: Entity, H: Handle, - F: 'static + FnMut(&mut T, &mut ViewContext), + F: 'static + FnMut(&mut T, &E, &mut ViewContext), { let observer = self.weak_handle(); - self.app.observe_release(handle, move |cx| { + self.app.observe_release(handle, move |released, cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { - callback(observer, cx); + callback(observer, released, cx); }); } }) @@ -4403,12 +4422,12 @@ mod tests { cx.observe_release(&model, { let model_release_observed = model_release_observed.clone(); - move |_| model_release_observed.set(true) + move |_, _| model_release_observed.set(true) }) .detach(); cx.observe_release(&view, { let view_release_observed = view_release_observed.clone(); - move |_| view_release_observed.set(true) + move |_, _| view_release_observed.set(true) }) .detach(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3a7838cd1946feb91321ee2e5f294c766068ea0c..dfe2d5795d27151e66e2cd8c325406faf321467b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -7,16 +7,14 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, outline::OutlineItem, - range_from_lsp, CodeLabel, Outline, ToLspPosition, + CodeLabel, Outline, }; use anyhow::{anyhow, Result}; use clock::ReplicaId; use futures::FutureExt as _; use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; -use lsp::LanguageServer; use parking_lot::Mutex; -use postage::{prelude::Stream, sink::Sink, watch}; use similar::{ChangeTag, TextDiff}; use smol::future::yield_now; use std::{ @@ -26,7 +24,7 @@ use std::{ ffi::OsString, future::Future, iter::{Iterator, Peekable}, - ops::{Deref, DerefMut, Range, Sub}, + ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, str, sync::Arc, @@ -34,11 +32,11 @@ use std::{ vec, }; use sum_tree::TreeMap; -use text::{operation_queue::OperationQueue, rope::TextDimension}; -pub use text::{Buffer as TextBuffer, Operation as _, *}; +use text::operation_queue::OperationQueue; +pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Operation as _, *}; use theme::SyntaxTheme; use tree_sitter::{InputEdit, QueryCursor, Tree}; -use util::{post_inc, TryFutureExt as _}; +use util::TryFutureExt as _; #[cfg(any(test, feature = "test-support"))] pub use tree_sitter_rust; @@ -70,7 +68,6 @@ pub struct Buffer { diagnostics_update_count: usize, diagnostics_timestamp: clock::Lamport, file_update_count: usize, - language_server: Option, completion_triggers: Vec, deferred_ops: OperationQueue, } @@ -126,21 +123,6 @@ pub struct CodeAction { pub lsp_action: lsp::CodeAction, } -struct LanguageServerState { - server: Arc, - latest_snapshot: watch::Sender, - pending_snapshots: BTreeMap, - next_version: usize, - _maintain_server: Task>, -} - -#[derive(Clone)] -struct LanguageServerSnapshot { - buffer_snapshot: text::BufferSnapshot, - version: usize, - path: Arc, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub enum Operation { Buffer(text::Operation), @@ -479,15 +461,6 @@ impl Buffer { self } - pub fn with_language_server( - mut self, - server: Arc, - cx: &mut ModelContext, - ) -> Self { - self.set_language_server(Some(server), cx); - self - } - fn build(buffer: TextBuffer, file: Option>) -> Self { let saved_mtime; if let Some(file) = file.as_ref() { @@ -514,7 +487,6 @@ impl Buffer { diagnostics_update_count: 0, diagnostics_timestamp: Default::default(), file_update_count: 0, - language_server: None, completion_triggers: Default::default(), deferred_ops: OperationQueue::new(), } @@ -536,6 +508,14 @@ impl Buffer { } } + pub fn as_text_snapshot(&self) -> &text::BufferSnapshot { + &self.text + } + + pub fn text_snapshot(&self) -> text::BufferSnapshot { + self.text.snapshot() + } + pub fn file(&self) -> Option<&dyn File> { self.file.as_deref() } @@ -561,123 +541,15 @@ impl Buffer { }) } + pub fn saved_version(&self) -> &clock::Global { + &self.saved_version + } + pub fn set_language(&mut self, language: Option>, cx: &mut ModelContext) { self.language = language; self.reparse(cx); } - pub fn set_language_server( - &mut self, - language_server: Option>, - cx: &mut ModelContext, - ) { - self.language_server = if let Some((server, file)) = - language_server.zip(self.file.as_ref().and_then(|f| f.as_local())) - { - let initial_snapshot = LanguageServerSnapshot { - buffer_snapshot: self.text.snapshot(), - version: 0, - path: file.abs_path(cx).into(), - }; - let (latest_snapshot_tx, mut latest_snapshot_rx) = - watch::channel_with::(initial_snapshot.clone()); - - Some(LanguageServerState { - latest_snapshot: latest_snapshot_tx, - pending_snapshots: BTreeMap::from_iter([(0, initial_snapshot)]), - next_version: 1, - server: server.clone(), - _maintain_server: cx.spawn_weak(|this, mut cx| async move { - let capabilities = server.capabilities().await.or_else(|| { - log::info!("language server exited"); - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| this.language_server = None); - } - None - })?; - - let triggers = capabilities - .completion_provider - .and_then(|c| c.trigger_characters) - .unwrap_or_default(); - this.upgrade(&cx)?.update(&mut cx, |this, cx| { - let lamport_timestamp = this.text.lamport_clock.tick(); - this.completion_triggers = triggers.clone(); - this.send_operation( - Operation::UpdateCompletionTriggers { - triggers, - lamport_timestamp, - }, - cx, - ); - cx.notify(); - }); - - let maintain_changes = cx.background().spawn(async move { - let initial_snapshot = - latest_snapshot_rx.recv().await.ok_or_else(|| { - anyhow!("buffer dropped before sending DidOpenTextDocument") - })?; - server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - lsp::Url::from_file_path(initial_snapshot.path).unwrap(), - Default::default(), - initial_snapshot.version as i32, - initial_snapshot.buffer_snapshot.text(), - ), - }, - ) - .await?; - - let mut prev_version = initial_snapshot.buffer_snapshot.version().clone(); - while let Some(snapshot) = latest_snapshot_rx.recv().await { - let uri = lsp::Url::from_file_path(&snapshot.path).unwrap(); - let buffer_snapshot = snapshot.buffer_snapshot.clone(); - let content_changes = buffer_snapshot - .edits_since::<(PointUtf16, usize)>(&prev_version) - .map(|edit| { - let edit_start = edit.new.start.0; - let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); - let new_text = buffer_snapshot - .text_for_range(edit.new.start.1..edit.new.end.1) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - edit_start.to_lsp_position(), - edit_end.to_lsp_position(), - )), - range_length: None, - text: new_text, - } - }) - .collect(); - let changes = lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - uri, - snapshot.version as i32, - ), - content_changes, - }; - server - .notify::(changes) - .await?; - - prev_version = snapshot.buffer_snapshot.version().clone(); - } - - Ok::<_, anyhow::Error>(()) - }); - - maintain_changes.log_err().await - }), - }) - } else { - None - }; - } - pub fn did_save( &mut self, version: clock::Global, @@ -784,10 +656,6 @@ impl Buffer { self.language.as_ref() } - pub fn language_server(&self) -> Option<&Arc> { - self.language_server.as_ref().map(|state| &state.server) - } - pub fn parse_count(&self) -> usize { self.parse_count } @@ -899,100 +767,14 @@ impl Buffer { cx.notify(); } - pub fn update_diagnostics( - &mut self, - mut diagnostics: Vec>, - version: Option, - cx: &mut ModelContext, - ) -> Result<()> - where - T: Copy + Ord + TextDimension + Sub + Clip + ToPoint, - { - fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering { - Ordering::Equal - .then_with(|| b.is_primary.cmp(&a.is_primary)) - .then_with(|| a.is_disk_based.cmp(&b.is_disk_based)) - .then_with(|| a.severity.cmp(&b.severity)) - .then_with(|| a.message.cmp(&b.message)) - } - - let version = version.map(|version| version as usize); - let content = - if let Some((version, language_server)) = version.zip(self.language_server.as_mut()) { - language_server.snapshot_for_version(version)? - } else { - self.deref() - }; - - diagnostics.sort_unstable_by(|a, b| { - Ordering::Equal - .then_with(|| a.range.start.cmp(&b.range.start)) - .then_with(|| b.range.end.cmp(&a.range.end)) - .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic)) - }); - - let mut sanitized_diagnostics = Vec::new(); - let mut edits_since_save = content.edits_since::(&self.saved_version).peekable(); - let mut last_edit_old_end = T::default(); - let mut last_edit_new_end = T::default(); - 'outer: for entry in diagnostics { - let mut start = entry.range.start; - let mut end = entry.range.end; - - // Some diagnostics are based on files on disk instead of buffers' - // current contents. Adjust these diagnostics' ranges to reflect - // any unsaved edits. - if entry.diagnostic.is_disk_based { - while let Some(edit) = edits_since_save.peek() { - if edit.old.end <= start { - last_edit_old_end = edit.old.end; - last_edit_new_end = edit.new.end; - edits_since_save.next(); - } else if edit.old.start <= end && edit.old.end >= start { - continue 'outer; - } else { - break; - } - } - - let start_overshoot = start - last_edit_old_end; - start = last_edit_new_end; - start.add_assign(&start_overshoot); - - let end_overshoot = end - last_edit_old_end; - end = last_edit_new_end; - end.add_assign(&end_overshoot); - } - - let range = start.clip(Bias::Left, content)..end.clip(Bias::Right, content); - let mut range = range.start.to_point(content)..range.end.to_point(content); - // Expand empty ranges by one character - if range.start == range.end { - range.end.column += 1; - range.end = content.clip_point(range.end, Bias::Right); - if range.start == range.end && range.end.column > 0 { - range.start.column -= 1; - range.start = content.clip_point(range.start, Bias::Left); - } - } - - sanitized_diagnostics.push(DiagnosticEntry { - range, - diagnostic: entry.diagnostic, - }); - } - drop(edits_since_save); - - let set = DiagnosticSet::new(sanitized_diagnostics, content); + pub fn update_diagnostics(&mut self, diagnostics: DiagnosticSet, cx: &mut ModelContext) { let lamport_timestamp = self.text.lamport_clock.tick(); - self.apply_diagnostic_update(set.clone(), lamport_timestamp, cx); - let op = Operation::UpdateDiagnostics { - diagnostics: set.iter().cloned().collect(), + diagnostics: diagnostics.iter().cloned().collect(), lamport_timestamp, }; + self.apply_diagnostic_update(diagnostics, lamport_timestamp, cx); self.send_operation(op, cx); - Ok(()) } fn request_autoindent(&mut self, cx: &mut ModelContext) { @@ -1305,30 +1087,6 @@ impl Buffer { self.set_active_selections(Arc::from([]), cx); } - fn update_language_server(&mut self, cx: &AppContext) { - let language_server = if let Some(language_server) = self.language_server.as_mut() { - language_server - } else { - return; - }; - let file = if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) { - file - } else { - return; - }; - - let version = post_inc(&mut language_server.next_version); - let snapshot = LanguageServerSnapshot { - buffer_snapshot: self.text.snapshot(), - version, - path: Arc::from(file.abs_path(cx)), - }; - language_server - .pending_snapshots - .insert(version, snapshot.clone()); - let _ = language_server.latest_snapshot.blocking_send(snapshot); - } - pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option where T: Into, @@ -1455,115 +1213,6 @@ impl Buffer { Some(edit_id) } - pub fn edits_from_lsp( - &mut self, - lsp_edits: impl 'static + Send + IntoIterator, - version: Option, - cx: &mut ModelContext, - ) -> Task, String)>>> { - let snapshot = if let Some((version, state)) = version.zip(self.language_server.as_mut()) { - state - .snapshot_for_version(version as usize) - .map(Clone::clone) - } else { - Ok(TextBuffer::deref(self).clone()) - }; - - cx.background().spawn(async move { - let snapshot = snapshot?; - let mut lsp_edits = lsp_edits - .into_iter() - .map(|edit| (range_from_lsp(edit.range), edit.new_text)) - .peekable(); - - let mut edits = Vec::new(); - while let Some((mut range, mut new_text)) = lsp_edits.next() { - // Combine any LSP edits that are adjacent. - // - // Also, combine LSP edits that are separated from each other by only - // a newline. This is important because for some code actions, - // Rust-analyzer rewrites the entire buffer via a series of edits that - // are separated by unchanged newline characters. - // - // In order for the diffing logic below to work properly, any edits that - // cancel each other out must be combined into one. - while let Some((next_range, next_text)) = lsp_edits.peek() { - if next_range.start > range.end { - if next_range.start.row > range.end.row + 1 - || next_range.start.column > 0 - || snapshot.clip_point_utf16( - PointUtf16::new(range.end.row, u32::MAX), - Bias::Left, - ) > range.end - { - break; - } - new_text.push('\n'); - } - range.end = next_range.end; - new_text.push_str(&next_text); - lsp_edits.next(); - } - - if snapshot.clip_point_utf16(range.start, Bias::Left) != range.start - || snapshot.clip_point_utf16(range.end, Bias::Left) != range.end - { - return Err(anyhow!("invalid edits received from language server")); - } - - // For multiline edits, perform a diff of the old and new text so that - // we can identify the changes more precisely, preserving the locations - // of any anchors positioned in the unchanged regions. - if range.end.row > range.start.row { - let mut offset = range.start.to_offset(&snapshot); - let old_text = snapshot.text_for_range(range).collect::(); - - let diff = TextDiff::from_lines(old_text.as_str(), &new_text); - let mut moved_since_edit = true; - for change in diff.iter_all_changes() { - let tag = change.tag(); - let value = change.value(); - match tag { - ChangeTag::Equal => { - offset += value.len(); - moved_since_edit = true; - } - ChangeTag::Delete => { - let start = snapshot.anchor_after(offset); - let end = snapshot.anchor_before(offset + value.len()); - if moved_since_edit { - edits.push((start..end, String::new())); - } else { - edits.last_mut().unwrap().0.end = end; - } - offset += value.len(); - moved_since_edit = false; - } - ChangeTag::Insert => { - if moved_since_edit { - let anchor = snapshot.anchor_after(offset); - edits.push((anchor.clone()..anchor, value.to_string())); - } else { - edits.last_mut().unwrap().1.push_str(value); - } - moved_since_edit = false; - } - } - } - } else if range.end == range.start { - let anchor = snapshot.anchor_after(range.start); - edits.push((anchor.clone()..anchor, new_text)); - } else { - let edit_start = snapshot.anchor_after(range.start); - let edit_end = snapshot.anchor_before(range.end); - edits.push((edit_start..edit_end, new_text)); - } - } - - Ok(edits) - }) - } - fn did_edit( &mut self, old_version: &clock::Global, @@ -1575,7 +1224,6 @@ impl Buffer { } self.reparse(cx); - self.update_language_server(cx); cx.emit(Event::Edited); if !was_dirty { @@ -1788,7 +1436,7 @@ impl Buffer { } pub fn completion_triggers(&self) -> &[String] { - &self.completion_triggers + todo!() } } @@ -1843,23 +1491,6 @@ impl Buffer { impl Entity for Buffer { type Event = Event; - - fn release(&mut self, cx: &mut gpui::MutableAppContext) { - if let Some(file) = self.file.as_ref() { - if let Some((lang_server, file)) = self.language_server.as_ref().zip(file.as_local()) { - let request = lang_server - .server - .notify::( - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(file.abs_path(cx)).unwrap(), - ), - }, - ); - cx.foreground().spawn(request).detach_and_log_err(cx); - } - } - } } impl Deref for Buffer { @@ -2592,20 +2223,6 @@ impl operation_queue::Operation for Operation { } } -impl LanguageServerState { - fn snapshot_for_version(&mut self, version: usize) -> Result<&text::BufferSnapshot> { - const OLD_VERSIONS_TO_RETAIN: usize = 10; - - self.pending_snapshots - .retain(|&v, _| v + OLD_VERSIONS_TO_RETAIN >= version); - let snapshot = self - .pending_snapshots - .get(&version) - .ok_or_else(|| anyhow!("missing snapshot"))?; - Ok(&snapshot.buffer_snapshot) - } -} - impl Default for Diagnostic { fn default() -> Self { Self { diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 9c2091739f15acb8b2ddb6046104a5f18a5baf84..7dbc99d2d1761cdc201122a42df88059996d3aeb 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -6,7 +6,7 @@ use std::{ ops::Range, }; use sum_tree::{self, Bias, SumTree}; -use text::{Anchor, FromAnchor, Point, ToOffset}; +use text::{Anchor, FromAnchor, PointUtf16, ToOffset}; #[derive(Clone, Debug)] pub struct DiagnosticSet { @@ -46,7 +46,7 @@ impl DiagnosticSet { pub fn new(iter: I, buffer: &text::BufferSnapshot) -> Self where - I: IntoIterator>, + I: IntoIterator>, { let mut entries = iter.into_iter().collect::>(); entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end))); diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 5ccd400e0c49442fa6f47926ee6dfda233b34f52..972821f77b867cc3b26263ee8380ca7ab34fe0d1 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -6,7 +6,6 @@ use rand::prelude::*; use std::{ cell::RefCell, env, - iter::FromIterator, ops::Range, rc::Rc, time::{Duration, Instant}, @@ -558,584 +557,6 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte }); } -#[gpui::test] -async fn test_diagnostics(cx: &mut gpui::TestAppContext) { - let (language_server, mut fake) = cx.update(lsp::LanguageServer::fake); - let mut rust_lang = rust_lang(); - rust_lang.config.language_server = Some(LanguageServerConfig { - disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]), - ..Default::default() - }); - - let text = " - fn a() { A } - fn b() { BB } - fn c() { CCC } - " - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx) - .with_language(Arc::new(rust_lang), cx) - .with_language_server(language_server, cx) - }); - - let open_notification = fake - .receive_notification::() - .await; - - // Edit the buffer, moving the content down - buffer.update(cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx)); - let change_notification_1 = fake - .receive_notification::() - .await; - assert!(change_notification_1.text_document.version > open_notification.text_document.version); - - buffer.update(cx, |buffer, cx| { - // Receive diagnostics for an earlier version of the buffer. - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - is_disk_based: true, - message: "undefined variable 'CCC'".to_string(), - group_id: 2, - is_primary: true, - ..Default::default() - }, - }, - ], - Some(open_notification.text_document.version), - cx, - ) - .unwrap(); - - // The diagnostics have moved down since they were created. - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0)) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Point::new(4, 9)..Point::new(4, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'CCC'".to_string(), - is_disk_based: true, - group_id: 2, - is_primary: true, - ..Default::default() - } - } - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, 0..buffer.len()), - [ - ("\n\nfn a() { ".to_string(), None), - ("A".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn b() { ".to_string(), None), - ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn c() { ".to_string(), None), - ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\n".to_string(), None), - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), - [ - ("B".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn c() { ".to_string(), None), - ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), - ] - ); - - // Ensure overlapping diagnostics are highlighted correctly. - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "unreachable statement".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - ], - Some(open_notification.text_document.version), - cx, - ) - .unwrap(); - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0)) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "unreachable statement".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - } - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), - [ - ("fn a() { ".to_string(), None), - ("A".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }".to_string(), Some(DiagnosticSeverity::WARNING)), - ("\n".to_string(), None), - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), - [ - (" }".to_string(), Some(DiagnosticSeverity::WARNING)), - ("\n".to_string(), None), - ] - ); - }); - - // Keep editing the buffer and ensure disk-based diagnostics get translated according to the - // changes since the last save. - buffer.update(cx, |buffer, cx| { - buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx); - buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx); - }); - let change_notification_2 = fake - .receive_notification::() - .await; - assert!( - change_notification_2.text_document.version > change_notification_1.text_document.version - ); - - buffer.update(cx, |buffer, cx| { - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - }, - ], - Some(change_notification_2.text_document.version), - cx, - ) - .unwrap(); - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(0..buffer.len()) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(2, 21)..Point::new(2, 22), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - } - ] - ); - }); -} - -#[gpui::test] -async fn test_language_server_has_exited(cx: &mut gpui::TestAppContext) { - let (language_server, fake) = cx.update(lsp::LanguageServer::fake); - - // Simulate the language server failing to start up. - drop(fake); - - let buffer = cx.add_model(|cx| { - Buffer::from_file(0, "", Box::new(FakeFile::new("/some/path")), cx) - .with_language(Arc::new(rust_lang()), cx) - .with_language_server(language_server, cx) - }); - - // Run the buffer's task that retrieves the server's capabilities. - cx.foreground().advance_clock(Duration::from_millis(1)); - - buffer.read_with(cx, |buffer, _| { - assert!(buffer.language_server().is_none()); - }); -} - -#[gpui::test] -async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { - let (language_server, mut fake) = cx.update(lsp::LanguageServer::fake); - - let text = " - fn a() { - f1(); - } - fn b() { - f2(); - } - fn c() { - f3(); - } - " - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx) - .with_language(Arc::new(rust_lang()), cx) - .with_language_server(language_server, cx) - }); - - let lsp_document_version = fake - .receive_notification::() - .await - .text_document - .version; - - // Simulate editing the buffer after the language server computes some edits. - buffer.update(cx, |buffer, cx| { - buffer.edit( - [Point::new(0, 0)..Point::new(0, 0)], - "// above first function\n", - cx, - ); - buffer.edit( - [Point::new(2, 0)..Point::new(2, 0)], - " // inside first function\n", - cx, - ); - buffer.edit( - [Point::new(6, 4)..Point::new(6, 4)], - "// inside second function ", - cx, - ); - - assert_eq!( - buffer.text(), - " - // above first function - fn a() { - // inside first function - f1(); - } - fn b() { - // inside second function f2(); - } - fn c() { - f3(); - } - " - .unindent() - ); - }); - - let edits = buffer - .update(cx, |buffer, cx| { - buffer.edits_from_lsp( - vec![ - // replace body of first function - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)), - new_text: " - fn a() { - f10(); - } - " - .unindent(), - }, - // edit inside second function - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)), - new_text: "00".into(), - }, - // edit inside third function via two distinct edits - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)), - new_text: "4000".into(), - }, - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)), - new_text: "".into(), - }, - ], - Some(lsp_document_version), - cx, - ) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - for (range, new_text) in edits { - buffer.edit([range], new_text, cx); - } - assert_eq!( - buffer.text(), - " - // above first function - fn a() { - // inside first function - f10(); - } - fn b() { - // inside second function f200(); - } - fn c() { - f4000(); - } - " - .unindent() - ); - }); -} - -#[gpui::test] -async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { - let text = " - use a::b; - use a::c; - - fn f() { - b(); - c(); - } - " - .unindent(); - - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); - - // Simulate the language server sending us a small edit in the form of a very large diff. - // Rust-analyzer does this when performing a merge-imports code action. - let edits = buffer - .update(cx, |buffer, cx| { - buffer.edits_from_lsp( - [ - // Replace the first use statement without editing the semicolon. - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)), - new_text: "a::{b, c}".into(), - }, - // Reinsert the remainder of the file between the semicolon and the final - // newline of the file. - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), - new_text: "\n\n".into(), - }, - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), - new_text: " - fn f() { - b(); - c(); - }" - .unindent(), - }, - // Delete everything after the first newline of the file. - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)), - new_text: "".into(), - }, - ], - None, - cx, - ) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - let edits = edits - .into_iter() - .map(|(range, text)| { - ( - range.start.to_point(&buffer)..range.end.to_point(&buffer), - text, - ) - }) - .collect::>(); - - assert_eq!( - edits, - [ - (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), - (Point::new(1, 0)..Point::new(2, 0), "".into()) - ] - ); - - for (range, new_text) in edits { - buffer.edit([range], new_text, cx); - } - assert_eq!( - buffer.text(), - " - use a::{b, c}; - - fn f() { - b(); - c(); - } - " - .unindent() - ); - }); -} - -#[gpui::test] -async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { - cx.add_model(|cx| { - let text = concat!( - "let one = ;\n", // - "let two = \n", - "let three = 3;\n", - ); - - let mut buffer = Buffer::new(0, text, cx); - buffer.set_language(Some(Arc::new(rust_lang())), cx); - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "syntax error 1".to_string(), - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "syntax error 2".to_string(), - ..Default::default() - }, - }, - ], - None, - cx, - ) - .unwrap(); - - // An empty range is extended forward to include the following character. - // At the end of a line, an empty range is extended backward to include - // the preceding character. - let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len()); - assert_eq!( - chunks - .iter() - .map(|(s, d)| (s.as_str(), *d)) - .collect::>(), - &[ - ("let one = ", None), - (";", Some(DiagnosticSeverity::ERROR)), - ("\nlet two =", None), - (" ", Some(DiagnosticSeverity::ERROR)), - ("\nlet three = 3;\n", None) - ] - ); - buffer - }); -} - #[gpui::test] fn test_serialization(cx: &mut gpui::MutableAppContext) { let mut now = Instant::now(); @@ -1253,9 +674,10 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { 40..=49 if mutation_count != 0 && replica_id == 0 => { let entry_count = rng.gen_range(1..=5); buffer.update(cx, |buffer, cx| { - let diagnostics = (0..entry_count) - .map(|_| { + let diagnostics = DiagnosticSet::new( + (0..entry_count).map(|_| { let range = buffer.random_byte_range(0, &mut rng); + let range = range.to_point_utf16(buffer); DiagnosticEntry { range, diagnostic: Diagnostic { @@ -1263,10 +685,11 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { ..Default::default() }, } - }) - .collect(); + }), + buffer, + ); log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics); - buffer.update_diagnostics(diagnostics, None, cx).unwrap(); + buffer.update_diagnostics(diagnostics, cx); }); mutation_count -= 1; } @@ -1370,24 +793,6 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { } } -fn chunks_with_diagnostics( - buffer: &Buffer, - range: Range, -) -> Vec<(String, Option)> { - let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in buffer.snapshot().chunks(range, true) { - if chunks - .last() - .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic) - { - chunks.last_mut().unwrap().0.push_str(chunk.text); - } else { - chunks.push((chunk.text.to_string(), chunk.diagnostic)); - } - } - chunks -} - #[test] fn test_contiguous_ranges() { assert_eq!( diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index e4ff8376ec756ccf9079b68fe523ca2338e50b82..89f6999efa17c0d3bbae9b50d7115d1d4fd7ce99 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -42,6 +42,7 @@ regex = "1.5" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } sha2 = "0.10" +similar = "1.3" smol = "1.2.5" toml = "0.5" diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4b2a7d89c1f5a53221f14d73c446ba02e4df3733..abd0edd3632666de74cf7110847a51524a7ae903 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -223,21 +223,19 @@ impl LspCommand for PerformRename { mut cx: AsyncAppContext, ) -> Result { if let Some(edit) = message { - let (language_name, language_server) = buffer.read_with(&cx, |buffer, _| { - let language = buffer - .language() - .ok_or_else(|| anyhow!("buffer's language was removed"))?; - let language_server = buffer - .language_server() - .cloned() - .ok_or_else(|| anyhow!("buffer's language server was removed"))?; - Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) - })?; + let language_server = project + .read_with(&cx, |project, cx| { + project.language_server_for_buffer(&buffer, cx).cloned() + }) + .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let language = buffer + .read_with(&cx, |buffer, _| buffer.language().cloned()) + .ok_or_else(|| anyhow!("no language for buffer"))?; Project::deserialize_workspace_edit( project, edit, self.push_to_history, - language_name, + language.name(), language_server, &mut cx, ) @@ -343,14 +341,14 @@ impl LspCommand for GetDefinition { mut cx: AsyncAppContext, ) -> Result> { let mut definitions = Vec::new(); - let (language, language_server) = buffer - .read_with(&cx, |buffer, _| { - buffer - .language() - .cloned() - .zip(buffer.language_server().cloned()) + let language_server = project + .read_with(&cx, |project, cx| { + project.language_server_for_buffer(&buffer, cx).cloned() }) - .ok_or_else(|| anyhow!("buffer no longer has language server"))?; + .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let language = buffer + .read_with(&cx, |buffer, _| buffer.language().cloned()) + .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(message) = message { let mut unresolved_locations = Vec::new(); @@ -375,7 +373,7 @@ impl LspCommand for GetDefinition { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( target_uri, - language.name().to_string(), + language.name(), language_server.clone(), cx, ) @@ -519,14 +517,14 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let (language, language_server) = buffer - .read_with(&cx, |buffer, _| { - buffer - .language() - .cloned() - .zip(buffer.language_server().cloned()) + let language_server = project + .read_with(&cx, |project, cx| { + project.language_server_for_buffer(&buffer, cx).cloned() }) - .ok_or_else(|| anyhow!("buffer no longer has language server"))?; + .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let language = buffer + .read_with(&cx, |buffer, _| buffer.language().cloned()) + .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(locations) = locations { for lsp_location in locations { @@ -534,7 +532,7 @@ impl LspCommand for GetReferences { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( lsp_location.uri, - language.name().to_string(), + language.name(), language_server.clone(), cx, ) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4c44258781a0460a5f3f3e4a8c7e2a12a736bdae..855a45b5dcc461e2ec1114b792eae72bb0b65424 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -16,9 +16,10 @@ use gpui::{ }; use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, - Diagnostic, DiagnosticEntry, Event as BufferEvent, File as _, Language, LanguageRegistry, - Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, + range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, + DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, + LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset, + ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -26,10 +27,11 @@ use postage::watch; use rand::prelude::*; use search::SearchQuery; use sha2::{Digest, Sha256}; +use similar::{ChangeTag, TextDiff}; use smol::block_on; use std::{ cell::RefCell, - cmp, + cmp::{self, Ordering}, convert::TryInto, hash::Hash, mem, @@ -48,9 +50,8 @@ pub struct Project { worktrees: Vec, active_entry: Option, languages: Arc, - language_servers: HashMap<(WorktreeId, String), Arc>, - started_language_servers: - HashMap<(WorktreeId, String), Shared>>>>, + language_servers: HashMap<(WorktreeId, Arc), Arc>, + started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, client: Arc, user_store: ModelHandle, fs: Arc, @@ -67,6 +68,7 @@ pub struct Project { loading_local_worktrees: HashMap, Shared, Arc>>>>, opened_buffers: HashMap, + buffer_snapshots: HashMap>, nonce: u128, } @@ -285,6 +287,7 @@ impl Project { shared_buffers: Default::default(), loading_buffers: Default::default(), loading_local_worktrees: Default::default(), + buffer_snapshots: Default::default(), client_state: ProjectClientState::Local { is_shared: false, remote_id_tx, @@ -371,6 +374,7 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), opened_buffers: Default::default(), + buffer_snapshots: Default::default(), nonce: StdRng::from_entropy().gen(), }; for worktree in worktrees { @@ -722,7 +726,7 @@ impl Project { let buffer = cx.add_model(|cx| { Buffer::new(self.replica_id(), "", cx).with_language(language::PLAIN_TEXT.clone(), cx) }); - self.register_buffer(&buffer, None, cx)?; + self.register_buffer(&buffer, cx)?; Ok(buffer) } @@ -797,15 +801,9 @@ impl Project { let worktree = worktree.as_local_mut().unwrap(); worktree.load_buffer(path, cx) }); - let worktree = worktree.downgrade(); cx.spawn(|this, mut cx| async move { let buffer = load_buffer.await?; - let worktree = worktree - .upgrade(&cx) - .ok_or_else(|| anyhow!("worktree was removed"))?; - this.update(&mut cx, |this, cx| { - this.register_buffer(&buffer, Some(&worktree), cx) - })?; + this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))?; Ok(buffer) }) } @@ -838,7 +836,7 @@ impl Project { fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, - lang_name: String, + lang_name: Arc, lang_server: Arc, cx: &mut ModelContext, ) -> Task>> { @@ -890,7 +888,8 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.assign_language_to_buffer(&buffer, Some(&worktree), cx); + this.assign_language_to_buffer(&buffer, cx); + this.register_buffer_with_language_servers(&buffer, cx); }); Ok(()) }) @@ -916,7 +915,6 @@ impl Project { fn register_buffer( &mut self, buffer: &ModelHandle, - worktree: Option<&ModelHandle>, cx: &mut ModelContext, ) -> Result<()> { let remote_id = buffer.read(cx).remote_id(); @@ -944,109 +942,215 @@ impl Project { remote_id ))?, } - cx.become_delegate(buffer, Self::on_buffer_event).detach(); - self.assign_language_to_buffer(buffer, worktree, cx); + cx.become_delegate(buffer, |this, buffer, event, cx| { + this.on_buffer_event(buffer, event, cx); + }) + .detach(); + + self.assign_language_to_buffer(buffer, cx); + self.register_buffer_with_language_servers(buffer, cx); Ok(()) } + fn register_buffer_with_language_servers( + &mut self, + buffer_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + let buffer = buffer_handle.read(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 initial_snapshot = buffer.as_text_snapshot(); + self.buffer_snapshots + .insert(buffer.remote_id(), vec![(0, initial_snapshot.clone())]); + + let mut notifications = Vec::new(); + let did_open_text_document = lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + 0, + initial_snapshot.text(), + ), + }; + + 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) + .log_err(); + } + } + + 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::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + uri.clone(), + ), + }, + ), + ); + } + 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); + } + } + } + fn on_buffer_event( &mut self, buffer: ModelHandle, event: BufferEvent, cx: &mut ModelContext, - ) { + ) -> Option<()> { match event { BufferEvent::Operation(operation) => { - if let Some(project_id) = self.remote_id() { - let request = self.client.request(proto::UpdateBuffer { - project_id, - buffer_id: buffer.read(cx).remote_id(), - operations: vec![language::proto::serialize_operation(&operation)], - }); - cx.background().spawn(request).detach_and_log_err(cx); - } + let project_id = self.remote_id()?; + let request = self.client.request(proto::UpdateBuffer { + project_id, + buffer_id: buffer.read(cx).remote_id(), + operations: vec![language::proto::serialize_operation(&operation)], + }); + cx.background().spawn(request).detach_and_log_err(cx); } - BufferEvent::Saved => { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - let worktree_id = file.worktree_id(cx); - if let Some(abs_path) = file.as_local().map(|file| file.abs_path(cx)) { - let text_document = lsp::TextDocumentIdentifier { - uri: lsp::Url::from_file_path(abs_path).unwrap(), - }; + BufferEvent::Edited => { + let buffer = buffer.read(cx); + let file = File::from_dyn(buffer.file())?; + let worktree_id = file.worktree_id(cx); + let abs_path = file.as_local()?.abs_path(cx); + let uri = lsp::Url::from_file_path(abs_path).unwrap(); + let buffer_snapshots = self.buffer_snapshots.entry(buffer.remote_id()).or_default(); + let (version, prev_snapshot) = buffer_snapshots.last()?; + let next_snapshot = buffer.text_snapshot(); + let next_version = version + 1; + + let content_changes = buffer + .edits_since::<(PointUtf16, usize)>(prev_snapshot.version()) + .map(|edit| { + let edit_start = edit.new.start.0; + let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); + let new_text = next_snapshot + .text_for_range(edit.new.start.1..edit.new.end.1) + .collect(); + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + edit_start.to_lsp_position(), + edit_end.to_lsp_position(), + )), + range_length: None, + text: new_text, + } + }) + .collect(); - let mut notifications = Vec::new(); - for ((lang_server_worktree_id, _), lang_server) in &self.language_servers { - if *lang_server_worktree_id != worktree_id { - continue; - } + let changes = lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new(uri, next_version), + content_changes, + }; - notifications.push( - lang_server.notify::( - lsp::DidSaveTextDocumentParams { - text_document: text_document.clone(), - text: None, - }, - ), - ); - } + buffer_snapshots.push((next_version, next_snapshot)); - cx.background() - .spawn(futures::future::try_join_all(notifications)) - .detach_and_log_err(cx); - } + let mut notifications = Vec::new(); + for lang_server in self.language_servers_for_worktree(worktree_id) { + notifications.push( + lang_server + .notify::(changes.clone()), + ); + } + + 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())?; + let worktree_id = file.worktree_id(cx); + let abs_path = file.as_local()?.abs_path(cx); + let text_document = lsp::TextDocumentIdentifier { + 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::DidSaveTextDocumentParams { + text_document: text_document.clone(), + text: None, + }, + ), + ); } + + cx.background() + .spawn(futures::future::try_join_all(notifications)) + .detach_and_log_err(cx); } _ => {} } + + None + } + + fn language_servers_for_worktree( + &self, + worktree_id: WorktreeId, + ) -> 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) + } else { + None + } + }, + ) } fn assign_language_to_buffer( &mut self, buffer: &ModelHandle, - worktree: Option<&ModelHandle>, cx: &mut ModelContext, ) -> Option<()> { - let (path, full_path) = { - let file = buffer.read(cx).file()?; - (file.path().clone(), file.full_path(cx)) - }; - - // If the buffer has a language, set it and start/assign the language server - if let Some(language) = self.languages.select_language(&full_path) { - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(language.clone()), cx); - }); - - // For local worktrees, start a language server if needed. - // Also assign the language server and any previously stored diagnostics to the buffer. - if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) { - let worktree_id = local_worktree.id(); - let worktree_abs_path = local_worktree.abs_path().clone(); - let buffer = buffer.downgrade(); - let language_server = - self.start_language_server(worktree_id, worktree_abs_path, language, cx); - - cx.spawn_weak(|_, mut cx| async move { - if let Some(language_server) = language_server.await { - if let Some(buffer) = buffer.upgrade(&cx) { - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language_server(Some(language_server), cx); - }); - } - } - }) - .detach(); - } - } + // If the buffer has a language, set it and start the language server if we haven't already. + let full_path = buffer.read(cx).file()?.full_path(cx); + let language = self.languages.select_language(&full_path)?; + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(language.clone()), cx); + }); - if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) { - if let Some(diagnostics) = local_worktree.diagnostics_for_path(&path) { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(diagnostics, None, cx).log_err(); - }); - } - } + let file = File::from_dyn(buffer.read(cx).file())?; + let worktree = file.worktree.read(cx).as_local()?; + let worktree_id = worktree.id(); + let worktree_abs_path = worktree.abs_path().clone(); + self.start_language_server(worktree_id, worktree_abs_path, language, cx); None } @@ -1057,14 +1161,14 @@ impl Project { worktree_path: Arc, language: Arc, cx: &mut ModelContext, - ) -> Shared>>> { + ) { enum LspEvent { DiagnosticsStart, DiagnosticsUpdate(lsp::PublishDiagnosticsParams), DiagnosticsFinish, } - let key = (worktree_id, language.name().to_string()); + let key = (worktree_id, language.name()); self.started_language_servers .entry(key.clone()) .or_insert_with(|| { @@ -1077,11 +1181,44 @@ impl Project { let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { let language_server = language_server?.await.log_err()?; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - this.language_servers.insert(key, language_server.clone()); - }); - } + 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( + language_server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + *version, + initial_snapshot.text(), + ), + }, + ), + ); + } + } + } + } + }); + + futures::future::try_join_all(open_notifications) + .await + .log_err(); let disk_based_sources = language .disk_based_diagnostic_sources() @@ -1153,6 +1290,7 @@ impl Project { .detach(); // Process all the LSP events. + let this = this.downgrade(); cx.spawn(|mut cx| async move { while let Ok(message) = diagnostics_rx.recv().await { let this = this.upgrade(&cx)?; @@ -1194,9 +1332,7 @@ impl Project { Some(language_server) }) - .shared() - }) - .clone() + }); } pub fn update_diagnostics( @@ -1326,9 +1462,7 @@ impl Project { .file() .map_or(false, |file| *file.path() == project_path.path) { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(diagnostics.clone(), version, cx) - })?; + self.update_buffer_diagnostics(&buffer, diagnostics.clone(), version, cx)?; break; } } @@ -1343,6 +1477,90 @@ impl Project { Ok(()) } + fn update_buffer_diagnostics( + &mut self, + buffer: &ModelHandle, + mut diagnostics: Vec>, + version: Option, + cx: &mut ModelContext, + ) -> Result<()> { + fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering { + Ordering::Equal + .then_with(|| b.is_primary.cmp(&a.is_primary)) + .then_with(|| a.is_disk_based.cmp(&b.is_disk_based)) + .then_with(|| a.severity.cmp(&b.severity)) + .then_with(|| a.message.cmp(&b.message)) + } + + let snapshot = self.buffer_snapshot_for_lsp_version(buffer, version, cx)?; + + diagnostics.sort_unstable_by(|a, b| { + Ordering::Equal + .then_with(|| a.range.start.cmp(&b.range.start)) + .then_with(|| b.range.end.cmp(&a.range.end)) + .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic)) + }); + + let mut sanitized_diagnostics = Vec::new(); + let mut edits_since_save = snapshot + .edits_since::(buffer.read(cx).saved_version()) + .peekable(); + let mut last_edit_old_end = PointUtf16::zero(); + let mut last_edit_new_end = PointUtf16::zero(); + 'outer: for entry in diagnostics { + let mut start = entry.range.start; + let mut end = entry.range.end; + + // Some diagnostics are based on files on disk instead of buffers' + // current contents. Adjust these diagnostics' ranges to reflect + // any unsaved edits. + if entry.diagnostic.is_disk_based { + while let Some(edit) = edits_since_save.peek() { + if edit.old.end <= start { + last_edit_old_end = edit.old.end; + last_edit_new_end = edit.new.end; + edits_since_save.next(); + } else if edit.old.start <= end && edit.old.end >= start { + continue 'outer; + } else { + break; + } + } + + let start_overshoot = start - last_edit_old_end; + start = last_edit_new_end; + start += start_overshoot; + + let end_overshoot = end - last_edit_old_end; + end = last_edit_new_end; + end += end_overshoot; + } + + let mut range = snapshot.clip_point_utf16(start, Bias::Left) + ..snapshot.clip_point_utf16(end, Bias::Right); + + // Expand empty ranges by one character + if range.start == range.end { + range.end.column += 1; + range.end = snapshot.clip_point_utf16(range.end, Bias::Right); + if range.start == range.end && range.end.column > 0 { + range.start.column -= 1; + range.start = snapshot.clip_point_utf16(range.start, Bias::Left); + } + } + + sanitized_diagnostics.push(DiagnosticEntry { + range, + diagnostic: entry.diagnostic, + }); + } + drop(edits_since_save); + + let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot); + buffer.update(cx, |buffer, cx| buffer.update_diagnostics(set, cx)); + Ok(()) + } + pub fn format( &self, buffers: HashSet>, @@ -1361,7 +1579,7 @@ impl Project { if let Some(lang) = buffer.language() { if let Some(server) = self .language_servers - .get(&(worktree.read(cx).id(), lang.name().to_string())) + .get(&(worktree.read(cx).id(), lang.name())) { lang_server = server.clone(); } else { @@ -1449,9 +1667,9 @@ impl Project { }; if let Some(lsp_edits) = lsp_edits { - let edits = buffer - .update(&mut cx, |buffer, cx| { - buffer.edits_from_lsp(lsp_edits, None, cx) + let edits = this + .update(&mut cx, |this, cx| { + this.edits_from_lsp(&buffer, lsp_edits, None, cx) }) .await?; buffer.update(&mut cx, |buffer, cx| { @@ -1616,10 +1834,10 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let language_server = if let Some(server) = self - .language_servers - .get(&(symbol.source_worktree_id, symbol.language_name.clone())) - { + let language_server = if let Some(server) = self.language_servers.get(&( + symbol.source_worktree_id, + Arc::from(symbol.language_name.as_str()), + )) { server.clone() } else { return Task::ready(Err(anyhow!( @@ -1645,7 +1863,7 @@ impl Project { self.open_local_buffer_via_lsp( symbol_uri, - symbol.language_name.clone(), + Arc::from(symbol.language_name.as_str()), language_server, cx, ) @@ -1689,11 +1907,12 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = if let Some(server) = source_buffer.language_server().cloned() { - server - } else { - return Task::ready(Ok(Default::default())); - }; + let lang_server = + if let Some(server) = self.language_server_for_buffer(&source_buffer_handle, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; cx.spawn(|_, cx| async move { let completions = lang_server @@ -1800,19 +2019,22 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = if let Some(language_server) = buffer.language_server() { - language_server.clone() - } else { - return Task::ready(Err(anyhow!("buffer does not have a language server"))); - }; + let lang_server = + if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; - cx.spawn(|_, mut cx| async move { + cx.spawn(|this, mut cx| async move { let resolved_completion = lang_server .request::(completion.lsp_completion) .await?; if let Some(edits) = resolved_completion.additional_text_edits { - let edits = buffer_handle - .update(&mut cx, |buffer, cx| buffer.edits_from_lsp(edits, None, cx)) + let edits = this + .update(&mut cx, |this, cx| { + this.edits_from_lsp(&buffer_handle, edits, None, cx) + }) .await?; buffer_handle.update(&mut cx, |buffer, cx| { buffer.finalize_last_transaction(); @@ -1892,7 +2114,7 @@ impl Project { let lang_name; let lang_server; if let Some(lang) = buffer.language() { - lang_name = lang.name().to_string(); + lang_name = lang.name(); if let Some(server) = self .language_servers .get(&(worktree.read(cx).id(), lang_name.clone())) @@ -1993,15 +2215,16 @@ impl Project { if self.is_local() { let buffer = buffer_handle.read(cx); let lang_name = if let Some(lang) = buffer.language() { - lang.name().to_string() + lang.name() } else { return Task::ready(Ok(Default::default())); }; - let lang_server = if let Some(language_server) = buffer.language_server() { - language_server.clone() - } else { - return Task::ready(Err(anyhow!("buffer does not have a language server"))); - }; + let lang_server = + if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; let range = action.range.to_point_utf16(buffer); cx.spawn(|this, mut cx| async move { @@ -2074,7 +2297,7 @@ impl Project { this: ModelHandle, edit: lsp::WorkspaceEdit, push_to_history: bool, - language_name: String, + language_name: Arc, language_server: Arc, cx: &mut AsyncAppContext, ) -> Result { @@ -2158,13 +2381,18 @@ impl Project { }) .await?; - let edits = buffer_to_edit - .update(cx, |buffer, cx| { + let edits = this + .update(cx, |this, cx| { let edits = op.edits.into_iter().map(|edit| match edit { lsp::OneOf::Left(edit) => edit, lsp::OneOf::Right(edit) => edit.text_edit, }); - buffer.edits_from_lsp(edits, op.text_document.version, cx) + this.edits_from_lsp( + &buffer_to_edit, + edits, + op.text_document.version, + cx, + ) }) .await?; @@ -2441,7 +2669,9 @@ impl Project { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, language_server)) = file.zip(buffer.language_server().cloned()) { + if let Some((file, language_server)) = + file.zip(self.language_server_for_buffer(&buffer_handle, cx).cloned()) + { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); return cx.spawn(|this, cx| async move { if !language_server @@ -2602,7 +2832,7 @@ impl Project { self.worktrees .push(WorktreeHandle::Strong(worktree.clone())); } else { - cx.observe_release(&worktree, |this, cx| { + cx.observe_release(&worktree, |this, _, cx| { this.worktrees .retain(|worktree| worktree.upgrade(cx).is_some()); cx.notify(); @@ -3441,9 +3671,7 @@ impl Project { Buffer::from_proto(replica_id, buffer, buffer_file, cx).unwrap() }); - this.update(&mut cx, |this, cx| { - this.register_buffer(&buffer, buffer_worktree.as_ref(), cx) - })?; + this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))?; *opened_buffer_tx.borrow_mut().borrow_mut() = (); Ok(buffer) @@ -3570,6 +3798,161 @@ impl Project { .await } } + + fn edits_from_lsp( + &mut self, + buffer: &ModelHandle, + lsp_edits: impl 'static + Send + IntoIterator, + version: Option, + cx: &mut ModelContext, + ) -> Task, String)>>> { + let snapshot = self.buffer_snapshot_for_lsp_version(buffer, version, cx); + cx.background().spawn(async move { + let snapshot = snapshot?; + let mut lsp_edits = lsp_edits + .into_iter() + .map(|edit| (range_from_lsp(edit.range), edit.new_text)) + .peekable(); + + let mut edits = Vec::new(); + while let Some((mut range, mut new_text)) = lsp_edits.next() { + // Combine any LSP edits that are adjacent. + // + // Also, combine LSP edits that are separated from each other by only + // a newline. This is important because for some code actions, + // Rust-analyzer rewrites the entire buffer via a series of edits that + // are separated by unchanged newline characters. + // + // In order for the diffing logic below to work properly, any edits that + // cancel each other out must be combined into one. + while let Some((next_range, next_text)) = lsp_edits.peek() { + if next_range.start > range.end { + if next_range.start.row > range.end.row + 1 + || next_range.start.column > 0 + || snapshot.clip_point_utf16( + PointUtf16::new(range.end.row, u32::MAX), + Bias::Left, + ) > range.end + { + break; + } + new_text.push('\n'); + } + range.end = next_range.end; + new_text.push_str(&next_text); + lsp_edits.next(); + } + + if snapshot.clip_point_utf16(range.start, Bias::Left) != range.start + || snapshot.clip_point_utf16(range.end, Bias::Left) != range.end + { + return Err(anyhow!("invalid edits received from language server")); + } + + // For multiline edits, perform a diff of the old and new text so that + // we can identify the changes more precisely, preserving the locations + // of any anchors positioned in the unchanged regions. + if range.end.row > range.start.row { + let mut offset = range.start.to_offset(&snapshot); + let old_text = snapshot.text_for_range(range).collect::(); + + let diff = TextDiff::from_lines(old_text.as_str(), &new_text); + let mut moved_since_edit = true; + for change in diff.iter_all_changes() { + let tag = change.tag(); + let value = change.value(); + match tag { + ChangeTag::Equal => { + offset += value.len(); + moved_since_edit = true; + } + ChangeTag::Delete => { + let start = snapshot.anchor_after(offset); + let end = snapshot.anchor_before(offset + value.len()); + if moved_since_edit { + edits.push((start..end, String::new())); + } else { + edits.last_mut().unwrap().0.end = end; + } + offset += value.len(); + moved_since_edit = false; + } + ChangeTag::Insert => { + if moved_since_edit { + let anchor = snapshot.anchor_after(offset); + edits.push((anchor.clone()..anchor, value.to_string())); + } else { + edits.last_mut().unwrap().1.push_str(value); + } + moved_since_edit = false; + } + } + } + } else if range.end == range.start { + let anchor = snapshot.anchor_after(range.start); + edits.push((anchor.clone()..anchor, new_text)); + } else { + let edit_start = snapshot.anchor_after(range.start); + let edit_end = snapshot.anchor_before(range.end); + edits.push((edit_start..edit_end, new_text)); + } + } + + Ok(edits) + }) + } + + fn buffer_snapshot_for_lsp_version( + &mut self, + buffer: &ModelHandle, + version: Option, + cx: &AppContext, + ) -> Result { + const OLD_VERSIONS_TO_RETAIN: i32 = 10; + + if let Some(version) = version { + let buffer_id = buffer.read(cx).remote_id(); + let snapshots = self + .buffer_snapshots + .get_mut(&buffer_id) + .ok_or_else(|| anyhow!("no snapshot found for buffer {}", buffer_id))?; + let mut found_snapshot = None; + snapshots.retain(|(snapshot_version, snapshot)| { + if snapshot_version + OLD_VERSIONS_TO_RETAIN < version { + false + } else { + if *snapshot_version == version { + found_snapshot = Some(snapshot.clone()); + } + true + } + }); + + found_snapshot.ok_or_else(|| { + anyhow!( + "snapshot not found for buffer {} at version {}", + buffer_id, + version + ) + }) + } else { + Ok((**buffer.read(cx)).clone()) + } + } + + fn language_server_for_buffer( + &self, + buffer: &ModelHandle, + cx: &AppContext, + ) -> Option<&Arc> { + let buffer = buffer.read(cx); + if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { + let worktree_id = file.worktree_id(cx); + self.language_servers.get(&(worktree_id, language.name())) + } else { + None + } + } } impl WorktreeHandle { @@ -3802,7 +4185,8 @@ mod tests { use futures::StreamExt; use gpui::test::subscribe; use language::{ - tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageServerConfig, Point, + tree_sitter_rust, Diagnostic, LanguageConfig, LanguageServerConfig, OffsetRangeExt, Point, + ToPoint, }; use lsp::Url; use serde_json::json; @@ -3875,66 +4259,289 @@ mod tests { } #[gpui::test] - async fn test_language_server_diagnostics(cx: &mut gpui::TestAppContext) { - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let progress_token = language_server_config - .disk_based_diagnostics_progress_token - .clone() - .unwrap(); + async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); - let language = Arc::new(Language::new( + let (lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake(); + let rust_language = Arc::new(Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: Some(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(), + path_suffixes: vec!["json".to_string()], + language_server: Some(json_lsp_config), + ..Default::default() + }, + None, + )); + let fs = FakeFs::new(cx.background()); fs.insert_tree( - "/dir", + "/the-root", json!({ - "a.rs": "fn a() { A }", - "b.rs": "const y: i32 = 1", + "test.rs": "const A: i32 = 1;", + "Cargo.toml": "a = 1", + "package.json": "{\"a\": 1}", }), ) .await; let project = Project::test(fs, cx); project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); + project.languages.add(rust_language); + project.languages.add(json_language); }); - let (tree, _) = project + let worktree_id = project .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) + project.find_or_create_local_worktree("/the-root", true, cx) }) .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); - // Cause worktree to start the fake language server - let _buffer = project + // Open a buffer without an associated language server. + let toml_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, Path::new("b.rs")), cx) + project.open_buffer((worktree_id, "Cargo.toml"), cx) }) .await .unwrap(); - let mut events = subscribe(&project, cx); + // Open a buffer with an associated language server. + let rust_buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "test.rs"), cx) + }) + .await + .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); - fake_server.start_progress(&progress_token).await; + // A server is started up, and it is notified about both open buffers. + let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsStarted + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + version: 0, + text: "a = 1".to_string(), + language_id: Default::default() + } ); - + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + version: 0, + text: "const A: i32 = 1;".to_string(), + language_id: Default::default() + } + ); + + // 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!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + 1 + ) + ); + + // Open a third buffer with a different associated language server. + let json_buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "package.json"), cx) + }) + .await + .unwrap(); + + // Another language server is started up, and it is notified about + // all three open buffers. + let mut fake_json_server = fake_json_servers.next().await.unwrap(); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + version: 0, + text: "a = 1".to_string(), + language_id: Default::default() + } + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + version: 0, + text: "{\"a\": 1}".to_string(), + language_id: Default::default() + } + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + version: 1, + text: "const A: i32 = 12;".to_string(), + language_id: Default::default() + } + ); + + // The first language server is also notified about the new open buffer. + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + version: 0, + text: "{\"a\": 1}".to_string(), + language_id: Default::default() + } + ); + + // 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!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + 1 + ) + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await, + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + 1 + ), + content_changes: vec![lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + lsp::Position::new(0, 5), + lsp::Position::new(0, 5) + )), + range_length: None, + text: "23".to_string(), + }], + }, + ); + + // Close a buffer. Both language servers are notified. + cx.update(|_| drop(json_buffer)); + let close_message = lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/package.json").unwrap(), + ), + }; + assert_eq!( + fake_json_server + .receive_notification::() + .await, + close_message, + ); + assert_eq!( + fake_rust_server + .receive_notification::() + .await, + close_message, + ); + } + + #[gpui::test] + async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); + let progress_token = language_server_config + .disk_based_diagnostics_progress_token + .clone() + .unwrap(); + + 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 fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "fn a() { A }", + "b.rs": "const y: i32 = 1", + }), + ) + .await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(language)); + + let (tree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(cx, |tree, _| tree.id()); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + // Cause worktree to start the fake language server + let _buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, Path::new("b.rs")), cx) + }) + .await + .unwrap(); + + let mut events = subscribe(&project, cx); + + let mut fake_server = fake_servers.next().await.unwrap(); + fake_server.start_progress(&progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsStarted + ); + fake_server.start_progress(&progress_token).await; fake_server.end_progress(&progress_token).await; fake_server.start_progress(&progress_token).await; @@ -3993,6 +4600,699 @@ mod tests { }); } + #[gpui::test] + async fn test_transforming_disk_based_diagnostics(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); + lsp_config + .disk_based_diagnostic_sources + .insert("disk".to_string()); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(lsp_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = " + fn a() { A } + fn b() { BB } + fn c() { CCC } + " + .unindent(); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "a.rs": text })).await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(language)); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + let open_notification = fake_server + .receive_notification::() + .await; + + // Edit the buffer, moving the content down + buffer.update(cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx)); + let change_notification_1 = fake_server + .receive_notification::() + .await; + assert!( + change_notification_1.text_document.version > open_notification.text_document.version + ); + + // Report some diagnostics for the initial version of the buffer + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'BB'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)), + severity: Some(DiagnosticSeverity::ERROR), + source: Some("disk".to_string()), + message: "undefined variable 'CCC'".to_string(), + ..Default::default() + }, + ], + }) + .await; + + // The diagnostics have moved down since they were created. + buffer.next_notification(cx).await; + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0)) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Point::new(4, 9)..Point::new(4, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'CCC'".to_string(), + is_disk_based: true, + group_id: 2, + is_primary: true, + ..Default::default() + } + } + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, 0..buffer.len()), + [ + ("\n\nfn a() { ".to_string(), None), + ("A".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn b() { ".to_string(), None), + ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn c() { ".to_string(), None), + ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\n".to_string(), None), + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), + [ + ("B".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn c() { ".to_string(), None), + ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), + ] + ); + }); + + // Ensure overlapping diagnostics are highlighted correctly. + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)), + severity: Some(DiagnosticSeverity::WARNING), + message: "unreachable statement".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + ], + }) + .await; + + buffer.next_notification(cx).await; + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0)) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "unreachable statement".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 0, + is_primary: true, + ..Default::default() + }, + } + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), + [ + ("fn a() { ".to_string(), None), + ("A".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }".to_string(), Some(DiagnosticSeverity::WARNING)), + ("\n".to_string(), None), + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), + [ + (" }".to_string(), Some(DiagnosticSeverity::WARNING)), + ("\n".to_string(), None), + ] + ); + }); + + // Keep editing the buffer and ensure disk-based diagnostics get translated according to the + // changes since the last save. + buffer.update(cx, |buffer, cx| { + buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx); + buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx); + }); + let change_notification_2 = + fake_server.receive_notification::(); + assert!( + change_notification_2.await.text_document.version + > change_notification_1.text_document.version + ); + + // Handle out-of-order diagnostics + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'BB'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::WARNING), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + ], + }) + .await; + + buffer.next_notification(cx).await; + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(0..buffer.len()) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(2, 21)..Point::new(2, 22), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 0, + is_primary: true, + ..Default::default() + }, + } + ] + ); + }); + } + + #[gpui::test] + async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let text = concat!( + "let one = ;\n", // + "let two = \n", + "let three = 3;\n", + ); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "a.rs": text })).await; + + let project = Project::test(fs, cx); + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + project.update(cx, |project, cx| { + project + .update_buffer_diagnostics( + &buffer, + vec![ + DiagnosticEntry { + range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 1".to_string(), + ..Default::default() + }, + }, + DiagnosticEntry { + range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 2".to_string(), + ..Default::default() + }, + }, + ], + None, + cx, + ) + .unwrap(); + }); + + // An empty range is extended forward to include the following character. + // At the end of a line, an empty range is extended backward to include + // the preceding character. + buffer.read_with(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let one = ", None), + (";", Some(DiagnosticSeverity::ERROR)), + ("\nlet two =", None), + (" ", Some(DiagnosticSeverity::ERROR)), + ("\nlet three = 3;\n", None) + ] + ); + }); + } + + #[gpui::test] + async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let (lsp_config, mut fake_servers) = LanguageServerConfig::fake(); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(lsp_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = " + fn a() { + f1(); + } + fn b() { + f2(); + } + fn c() { + f3(); + } + " + .unindent(); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(language)); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + let lsp_document_version = fake_server + .receive_notification::() + .await + .text_document + .version; + + // Simulate editing the buffer after the language server computes some edits. + buffer.update(cx, |buffer, cx| { + buffer.edit( + [Point::new(0, 0)..Point::new(0, 0)], + "// above first function\n", + cx, + ); + buffer.edit( + [Point::new(2, 0)..Point::new(2, 0)], + " // inside first function\n", + cx, + ); + buffer.edit( + [Point::new(6, 4)..Point::new(6, 4)], + "// inside second function ", + cx, + ); + + assert_eq!( + buffer.text(), + " + // above first function + fn a() { + // inside first function + f1(); + } + fn b() { + // inside second function f2(); + } + fn c() { + f3(); + } + " + .unindent() + ); + }); + + let edits = project + .update(cx, |project, cx| { + project.edits_from_lsp( + &buffer, + vec![ + // replace body of first function + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(3, 0), + ), + new_text: " + fn a() { + f10(); + } + " + .unindent(), + }, + // edit inside second function + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(4, 6), + lsp::Position::new(4, 6), + ), + new_text: "00".into(), + }, + // edit inside third function via two distinct edits + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(7, 5), + lsp::Position::new(7, 5), + ), + new_text: "4000".into(), + }, + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(7, 5), + lsp::Position::new(7, 6), + ), + new_text: "".into(), + }, + ], + Some(lsp_document_version), + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + for (range, new_text) in edits { + buffer.edit([range], new_text, cx); + } + assert_eq!( + buffer.text(), + " + // above first function + fn a() { + // inside first function + f10(); + } + fn b() { + // inside second function f200(); + } + fn c() { + f4000(); + } + " + .unindent() + ); + }); + } + + #[gpui::test] + async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let text = " + use a::b; + use a::c; + + fn f() { + b(); + c(); + } + " + .unindent(); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, cx); + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + // Simulate the language server sending us a small edit in the form of a very large diff. + // Rust-analyzer does this when performing a merge-imports code action. + let edits = project + .update(cx, |project, cx| { + project.edits_from_lsp( + &buffer, + [ + // Replace the first use statement without editing the semicolon. + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 4), + lsp::Position::new(0, 8), + ), + new_text: "a::{b, c}".into(), + }, + // Reinsert the remainder of the file between the semicolon and the final + // newline of the file. + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 9), + lsp::Position::new(0, 9), + ), + new_text: "\n\n".into(), + }, + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 9), + lsp::Position::new(0, 9), + ), + new_text: " + fn f() { + b(); + c(); + }" + .unindent(), + }, + // Delete everything after the first newline of the file. + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(1, 0), + lsp::Position::new(7, 0), + ), + new_text: "".into(), + }, + ], + None, + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + let edits = edits + .into_iter() + .map(|(range, text)| { + ( + range.start.to_point(&buffer)..range.end.to_point(&buffer), + text, + ) + }) + .collect::>(); + + assert_eq!( + edits, + [ + (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), + (Point::new(1, 0)..Point::new(2, 0), "".into()) + ] + ); + + for (range, new_text) in edits { + buffer.edit([range], new_text, cx); + } + assert_eq!( + buffer.text(), + " + use a::{b, c}; + + fn f() { + b(); + c(); + } + " + .unindent() + ); + }); + } + + fn chunks_with_diagnostics( + buffer: &Buffer, + range: Range, + ) -> Vec<(String, Option)> { + let mut chunks: Vec<(String, Option)> = Vec::new(); + for chunk in buffer.snapshot().chunks(range, true) { + if chunks + .last() + .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic) + { + chunks.last_mut().unwrap().0.push_str(chunk.text); + } else { + chunks.push((chunk.text.to_string(), chunk.diagnostic)); + } + } + chunks + } + #[gpui::test] async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { let dir = temp_tree(json!({ diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1130063c98ddce13646944e745a7059ac8a424d7..290f44c5cfde298b6a3e9758b61f6d099d0c9b1d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -556,6 +556,7 @@ impl LocalWorktree { } pub fn diagnostics_for_path(&self, path: &Path) -> Option>> { + dbg!(&self.diagnostics); self.diagnostics.get(path).cloned() } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 9a8f4a77d7030609335df130ac499e4e33989193..a5184ea5f30d4221ab5a2524e5ee439f31da590f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -5,7 +5,7 @@ use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::AnchorRangeExt; +use language::OffsetRangeExt; use postage::watch; use project::search::SearchQuery; use std::ops::Range; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0b5f5cf880da33dfe021c70a2ec0e780680c5351..7fa4dc7db9cf223538e4537e10936da65f9118f4 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1011,8 +1011,8 @@ mod tests { }; use gpui::{executor, ModelHandle, TestAppContext}; use language::{ - tree_sitter_rust, AnchorRangeExt, Diagnostic, DiagnosticEntry, Language, LanguageConfig, - LanguageRegistry, LanguageServerConfig, Point, ToLspPosition, + tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, + LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, }; use lsp; use parking_lot::Mutex; diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index a14a16cbc4f55f991b7ce9a1271fffe5cb5de6ec..28da998d6770f6881ac65fcb439726ae972d139c 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -1,5 +1,5 @@ use super::{Point, ToOffset}; -use crate::{rope::TextDimension, BufferSnapshot, PointUtf16, ToPointUtf16}; +use crate::{rope::TextDimension, BufferSnapshot, PointUtf16, ToPoint, ToPointUtf16}; use anyhow::Result; use std::{cmp::Ordering, fmt::Debug, ops::Range}; use sum_tree::Bias; @@ -74,11 +74,33 @@ impl Anchor { } } +pub trait OffsetRangeExt { + fn to_offset(&self, snapshot: &BufferSnapshot) -> Range; + fn to_point(&self, snapshot: &BufferSnapshot) -> Range; + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range; +} + +impl OffsetRangeExt for Range +where + T: ToOffset, +{ + fn to_offset(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot)..self.end.to_offset(&snapshot) + } + + fn to_point(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot).to_point(snapshot) + ..self.end.to_offset(snapshot).to_point(snapshot) + } + + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot).to_point_utf16(snapshot) + ..self.end.to_offset(snapshot).to_point_utf16(snapshot) + } +} + pub trait AnchorRangeExt { fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Result; - fn to_offset(&self, content: &BufferSnapshot) -> Range; - fn to_point(&self, content: &BufferSnapshot) -> Range; - fn to_point_utf16(&self, content: &BufferSnapshot) -> Range; } impl AnchorRangeExt for Range { @@ -88,16 +110,4 @@ impl AnchorRangeExt for Range { ord @ _ => ord, }) } - - fn to_offset(&self, content: &BufferSnapshot) -> Range { - self.start.to_offset(&content)..self.end.to_offset(&content) - } - - fn to_point(&self, content: &BufferSnapshot) -> Range { - self.start.summary::(&content)..self.end.summary::(&content) - } - - fn to_point_utf16(&self, content: &BufferSnapshot) -> Range { - self.start.to_point_utf16(content)..self.end.to_point_utf16(content) - } } From 4cb4b99c56683992d3f77d0fc8e874a61808d69e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Mar 2022 17:41:52 -0800 Subject: [PATCH 07/10] 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 5e7d29bfa68665b8ad205384c708b3b1d970778b..bd56ed3e1b3c13e7de5a08b84133f62b3e3f5903 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 649797be302500cac62ad61e6efd8e67f96e1ade..1ffe18114b5465014abde46fd3e7729e1b40875c 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 dfe2d5795d27151e66e2cd8c325406faf321467b..3d79ecadd649d587200facd035b7e33c084629cf 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 a056d84c40c09cc12ea6cd1f28b6cf5750109eab..e8ae505a9c40fe41e1f5d50538e1b7f69f6b2bfa 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 5f3eb2caaffa9c055c8f038197beb41b0d105b73..bba5960bb421342dd2ca4e670d2ab53b83261b83 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()], + 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::( - &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 855a45b5dcc461e2ec1114b792eae72bb0b65424..15d9b3f6b9fea7c2635b341b51bd4c385675aa6a 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 290f44c5cfde298b6a3e9758b61f6d099d0c9b1d..1130063c98ddce13646944e745a7059ac8a424d7 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 7fa4dc7db9cf223538e4537e10936da65f9118f4..444f2858b617e149369e1381ee5f759a3740eff1 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, From 0a9595b5fa7ab6726f9914f4b22f27cf5ffaea35 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 9 Mar 2022 10:34:42 +0100 Subject: [PATCH 08/10] Notify all language servers only when a buffer is saved Other notifications such as opening, closing or changing a document are still tied to the buffer's language. --- crates/project/src/lsp_command.rs | 12 +- crates/project/src/project.rs | 315 ++++++++++++------------------ 2 files changed, 129 insertions(+), 198 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index abd0edd3632666de74cf7110847a51524a7ae903..4867ada7cb0f86b124cc7c11f5a568df83236eb7 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -225,7 +225,9 @@ impl LspCommand for PerformRename { if let Some(edit) = message { let language_server = project .read_with(&cx, |project, cx| { - project.language_server_for_buffer(&buffer, cx).cloned() + project + .language_server_for_buffer(buffer.read(cx), cx) + .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; let language = buffer @@ -343,7 +345,9 @@ impl LspCommand for GetDefinition { let mut definitions = Vec::new(); let language_server = project .read_with(&cx, |project, cx| { - project.language_server_for_buffer(&buffer, cx).cloned() + project + .language_server_for_buffer(buffer.read(cx), cx) + .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; let language = buffer @@ -519,7 +523,9 @@ impl LspCommand for GetReferences { let mut references = Vec::new(); let language_server = project .read_with(&cx, |project, cx| { - project.language_server_for_buffer(&buffer, cx).cloned() + project + .language_server_for_buffer(buffer.read(cx), cx) + .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; let language = buffer diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 15d9b3f6b9fea7c2635b341b51bd4c385675aa6a..fab9212b16f6ef4f54840349a2bb988cd013969a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -889,7 +889,7 @@ impl Project { .await?; this.update(&mut cx, |this, cx| { this.assign_language_to_buffer(&buffer, cx); - this.register_buffer_with_language_servers(&buffer, cx); + this.register_buffer_with_language_server(&buffer, cx); }); Ok(()) }) @@ -948,35 +948,23 @@ impl Project { .detach(); self.assign_language_to_buffer(buffer, cx); - self.register_buffer_with_language_servers(buffer, cx); + self.register_buffer_with_language_server(buffer, cx); Ok(()) } - fn register_buffer_with_language_servers( + fn register_buffer_with_language_server( &mut self, buffer_handle: &ModelHandle, cx: &mut ModelContext, ) { let buffer = buffer_handle.read(cx); - let buffer_language_name = buffer.language().map(|l| l.name().clone()); + let buffer_id = buffer.remote_id(); 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 initial_snapshot = buffer.as_text_snapshot(); - self.buffer_snapshots - .insert(buffer.remote_id(), vec![(0, initial_snapshot.clone())]); - - let mut notifications = Vec::new(); - let did_open_text_document = lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri, - Default::default(), - 0, - initial_snapshot.text(), - ), - }; + let initial_snapshot = buffer.text_snapshot(); + let language_server = self.language_server_for_buffer(buffer, cx).cloned(); if let Some(local_worktree) = file.worktree.read(cx).as_local() { if let Some(diagnostics) = local_worktree.diagnostics_for_path(file.path()) { @@ -985,32 +973,40 @@ 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, - ) - }); - } + if let Some(server) = language_server { + server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + 0, + initial_snapshot.text(), + ), + } + .clone(), + ) + .log_err(); + 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, + ) + }); + self.buffer_snapshots + .insert(buffer_id, vec![(0, initial_snapshot)]); } 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(); - for (_, server) in this.language_servers_for_worktree(worktree_id) { + if let Some(server) = this.language_server_for_buffer(buffer, cx) { server .notify::( lsp::DidCloseTextDocumentParams { @@ -1046,9 +1042,11 @@ impl Project { cx.background().spawn(request).detach_and_log_err(cx); } BufferEvent::Edited => { + let language_server = self + .language_server_for_buffer(buffer.read(cx), cx)? + .clone(); let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; - let worktree_id = file.worktree_id(cx); let abs_path = file.as_local()?.abs_path(cx); let uri = lsp::Url::from_file_path(abs_path).unwrap(); let buffer_snapshots = self.buffer_snapshots.entry(buffer.remote_id()).or_default(); @@ -1075,18 +1073,19 @@ impl Project { }) .collect(); - let changes = lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new(uri, next_version), - content_changes, - }; - buffer_snapshots.push((next_version, next_snapshot)); - for (_, server) in self.language_servers_for_worktree(worktree_id) { - server - .notify::(changes.clone()) - .log_err(); - } + language_server + .notify::( + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + uri, + next_version, + ), + content_changes, + }, + ) + .log_err(); } BufferEvent::Saved => { let file = File::from_dyn(buffer.read(cx).file())?; @@ -1177,17 +1176,27 @@ impl Project { let language_server = language_server?.await.log_err()?; let this = this.upgrade(&cx)?; this.update(&mut cx, |this, cx| { - this.language_servers.insert(key, language_server.clone()); + this.language_servers + .insert(key.clone(), language_server.clone()); + // Tell the language server about every open buffer in the worktree that matches the language. for buffer in this.opened_buffers.values() { 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 { + let file = if let Some(file) = File::from_dyn(buffer.file()) { + file + } else { + continue; + }; + let language = if let Some(language) = buffer.language() { + language + } else { + continue; + }; + if (file.worktree.read(cx).id(), language.name()) != key { continue; } - // Tell the language server about every open buffer in the worktree. let file = file.as_local()?; let versions = this .buffer_snapshots @@ -1207,26 +1216,19 @@ impl Project { }, ) .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 - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider.trigger_characters.clone() - }) - .unwrap_or(Vec::new()), - cx, - ) - }); - } + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + language_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider.trigger_characters.clone() + }) + .unwrap_or(Vec::new()), + cx, + ) + }); } } @@ -1584,25 +1586,11 @@ impl Project { let mut remote_buffers = None; for buffer_handle in buffers { let buffer = buffer_handle.read(cx); - let worktree; if let Some(file) = File::from_dyn(buffer.file()) { - worktree = file.worktree.clone(); if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { - let lang_server; - if let Some(lang) = buffer.language() { - if let Some(server) = self - .language_servers - .get(&(worktree.read(cx).id(), lang.name())) - { - lang_server = server.clone(); - } else { - return Task::ready(Ok(Default::default())); - }; - } else { - return Task::ready(Ok(Default::default())); + if let Some(server) = self.language_server_for_buffer(buffer, cx) { + local_buffers.push((buffer_handle, buffer_abs_path, server.clone())); } - - local_buffers.push((buffer_handle, buffer_abs_path, lang_server)); } else { remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); } @@ -1918,7 +1906,7 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); let lang_server = - if let Some(server) = self.language_server_for_buffer(&source_buffer_handle, cx) { + if let Some(server) = self.language_server_for_buffer(source_buffer, cx) { server.clone() } else { return Task::ready(Ok(Default::default())); @@ -2029,12 +2017,11 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = - if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { - server.clone() - } else { - return Task::ready(Ok(Default::default())); - }; + let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; cx.spawn(|this, mut cx| async move { let resolved_completion = lang_server @@ -2121,21 +2108,11 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_name; - let lang_server; - if let Some(lang) = buffer.language() { - lang_name = lang.name(); - if let Some(server) = self - .language_servers - .get(&(worktree.read(cx).id(), lang_name.clone())) - { - lang_server = server.clone(); - } else { - return Task::ready(Ok(Default::default())); - }; + let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() } else { return Task::ready(Ok(Default::default())); - } + }; let lsp_range = lsp::Range::new( range.start.to_point_utf16(buffer).to_lsp_position(), @@ -2223,12 +2200,11 @@ impl Project { } else { return Task::ready(Ok(Default::default())); }; - let lang_server = - if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { - server.clone() - } else { - return Task::ready(Ok(Default::default())); - }; + let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; let range = action.range.to_point_utf16(buffer); cx.spawn(|this, mut cx| async move { @@ -2674,7 +2650,7 @@ impl Project { if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); if let Some((file, language_server)) = - file.zip(self.language_server_for_buffer(&buffer_handle, cx).cloned()) + file.zip(self.language_server_for_buffer(buffer, cx).cloned()) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); return cx.spawn(|this, cx| async move { @@ -3934,16 +3910,15 @@ impl Project { ) }) } else { - Ok((**buffer.read(cx)).clone()) + Ok((buffer.read(cx)).text_snapshot()) } } fn language_server_for_buffer( &self, - buffer: &ModelHandle, + buffer: &Buffer, cx: &AppContext, ) -> Option<&Arc> { - let buffer = buffer.read(cx); if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); self.language_servers.get(&(worktree_id, language.name())) @@ -4339,20 +4314,8 @@ mod tests { .await .unwrap(); - // A server is started up, and it is notified about both open buffers. + // A server is started up, and it is notified about Rust files. let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), - version: 0, - text: "a = 1".to_string(), - language_id: Default::default() - } - ); assert_eq!( fake_rust_server .receive_notification::() @@ -4401,18 +4364,6 @@ mod tests { // Another language server is started up, and it is notified about // all three open buffers. let mut fake_json_server = fake_json_servers.next().await.unwrap(); - assert_eq!( - fake_json_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), - version: 0, - text: "a = 1".to_string(), - language_id: Default::default() - } - ); assert_eq!( fake_json_server .receive_notification::() @@ -4425,18 +4376,6 @@ mod tests { language_id: Default::default() } ); - assert_eq!( - fake_json_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), - version: 1, - text: "const A: i32 = 12;".to_string(), - language_id: Default::default() - } - ); // This buffer is configured based on the second language server's // capabilities. @@ -4444,20 +4383,6 @@ mod tests { assert_eq!(buffer.completion_triggers(), &[":".to_string()]); }); - // The first language server is also notified about the new open buffer. - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), - version: 0, - text: "{\"a\": 1}".to_string(), - language_id: Default::default() - } - ); - // 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 @@ -4473,39 +4398,45 @@ mod tests { ); }); - // Edit a buffer. The changes are reported to both the language servers. + // Changes are reported only to servers matching the buffer's language. toml_buffer.update(cx, |buffer, cx| buffer.edit([5..5], "23", cx)); + rust_buffer2.update(cx, |buffer, cx| buffer.edit([0..0], "let x = 1;", cx)); assert_eq!( fake_rust_server .receive_notification::() .await .text_document, lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + lsp::Url::from_file_path("/the-root/test2.rs").unwrap(), 1 ) ); + + // Save notifications are reported to all servers. + toml_buffer + .update(cx, |buffer, cx| buffer.save(cx)) + .await + .unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap() + ) + ); assert_eq!( fake_json_server - .receive_notification::() - .await, - lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), - 1 - ), - content_changes: vec![lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - lsp::Position::new(0, 5), - lsp::Position::new(0, 5) - )), - range_length: None, - text: "23".to_string(), - }], - }, + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap() + ) ); - // Close a buffer. Both language servers are notified. + // Close notifications are reported only to servers matching the buffer's language. cx.update(|_| drop(json_buffer)); let close_message = lsp::DidCloseTextDocumentParams { text_document: lsp::TextDocumentIdentifier::new( @@ -4518,12 +4449,6 @@ mod tests { .await, close_message, ); - assert_eq!( - fake_rust_server - .receive_notification::() - .await, - close_message, - ); } #[gpui::test] From ef1ec88523329810ec867110dd60803421133f97 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 9 Mar 2022 10:48:52 +0100 Subject: [PATCH 09/10] Remove delegate support from GPUI We added this because we thought it would save some allocations when sending operations given that we could move them to the delegate upon notifying it, but the reality is that we serialize operations and that only requires a reference. --- crates/gpui/src/app.rs | 124 ++-------------------------------- crates/language/src/tests.rs | 6 +- crates/project/src/project.rs | 4 +- 3 files changed, 10 insertions(+), 124 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 6727fc5f08f8caeba74e1e344c587494f6fd8053..e91963bfa6341fac51089a53de34e1121346cec9 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -740,7 +740,6 @@ type ActionCallback = type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; -type DelegationCallback = Box, &mut MutableAppContext) -> bool>; type ObservationCallback = Box bool>; type ReleaseObservationCallback = Box; @@ -758,7 +757,6 @@ pub struct MutableAppContext { next_subscription_id: usize, frame_count: usize, subscriptions: Arc>>>, - delegations: Arc>>, observations: Arc>>>, release_observations: Arc>>>, presenters_and_platform_windows: @@ -806,7 +804,6 @@ impl MutableAppContext { next_subscription_id: 0, frame_count: 0, subscriptions: Default::default(), - delegations: Default::default(), observations: Default::default(), release_observations: Default::default(), presenters_and_platform_windows: HashMap::new(), @@ -1152,35 +1149,6 @@ impl MutableAppContext { } } - fn become_delegate_internal(&mut self, handle: &H, mut callback: F) -> Subscription - where - E: Entity, - H: Handle, - F: 'static + FnMut(H, E::Event, &mut Self) -> bool, - { - let id = post_inc(&mut self.next_subscription_id); - let emitter = handle.downgrade(); - self.delegations.lock().insert( - handle.id(), - ( - id, - Box::new(move |payload, cx| { - if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { - let payload = *payload.downcast().expect("downcast is type safe"); - callback(emitter, payload, cx) - } else { - false - } - }), - ), - ); - Subscription::Delegation { - id, - entity_id: handle.id(), - delegations: Some(Arc::downgrade(&self.delegations)), - } - } - pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -1730,17 +1698,6 @@ impl MutableAppContext { } } } - - let delegate = self.delegations.lock().remove(&entity_id); - if let Some((id, mut callback)) = delegate { - let alive = callback(payload, self); - if alive { - self.delegations - .lock() - .entry(entity_id) - .or_insert_with(|| (id, callback)); - } - } } fn notify_model_observers(&mut self, observed_id: usize) { @@ -2387,26 +2344,6 @@ impl<'a, T: Entity> ModelContext<'a, T> { }) } - pub fn become_delegate(&mut self, handle: &H, mut callback: F) -> Subscription - where - E: Entity, - H: Handle, - F: 'static + FnMut(&mut T, H, E::Event, &mut ModelContext), - { - let delegate = self.weak_handle(); - self.app - .become_delegate_internal(handle, move |emitter, event, cx| { - if let Some(delegate) = delegate.upgrade(cx) { - delegate.update(cx, |subscriber, cx| { - callback(subscriber, emitter, event, cx); - }); - true - } else { - false - } - }) - } - pub fn observe_release( &mut self, handle: &ModelHandle, @@ -2672,26 +2609,6 @@ impl<'a, T: View> ViewContext<'a, T> { }) } - pub fn become_delegate(&mut self, handle: &H, mut callback: F) -> Subscription - where - E: Entity, - H: Handle, - F: 'static + FnMut(&mut T, H, E::Event, &mut ViewContext), - { - let delegate = self.weak_handle(); - self.app - .become_delegate_internal(handle, move |emitter, event, cx| { - if let Some(delegate) = delegate.upgrade(cx) { - delegate.update(cx, |subscriber, cx| { - callback(subscriber, emitter, event, cx); - }); - true - } else { - false - } - }) - } - pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -3845,11 +3762,6 @@ pub enum Subscription { entity_id: usize, subscriptions: Option>>>>, }, - Delegation { - id: usize, - entity_id: usize, - delegations: Option>>>, - }, Observation { id: usize, entity_id: usize, @@ -3869,9 +3781,6 @@ impl Subscription { Subscription::Subscription { subscriptions, .. } => { subscriptions.take(); } - Subscription::Delegation { delegations, .. } => { - delegations.take(); - } Subscription::Observation { observations, .. } => { observations.take(); } @@ -3918,19 +3827,6 @@ impl Drop for Subscription { } } } - Subscription::Delegation { - id, - entity_id, - delegations, - } => { - if let Some(delegations) = delegations.as_ref().and_then(Weak::upgrade) { - if let Entry::Occupied(entry) = delegations.lock().entry(*entity_id) { - if *id == entry.get().0 { - let _ = entry.remove(); - } - } - } - } } } } @@ -4197,11 +4093,6 @@ mod tests { let handle_1 = cx.add_model(|_| Model::default()); let handle_2 = cx.add_model(|_| Model::default()); handle_1.update(cx, |_, cx| { - cx.become_delegate(&handle_2, |model, _, event, _| { - model.events.push(event * 3); - }) - .detach(); - cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| { model.events.push(*event); @@ -4214,10 +4105,10 @@ mod tests { }); handle_2.update(cx, |_, c| c.emit(7)); - assert_eq!(handle_1.read(cx).events, vec![7, 21]); + assert_eq!(handle_1.read(cx).events, vec![7]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 21, 5, 10, 15]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); } #[crate::test(self)] @@ -4475,11 +4366,6 @@ mod tests { let handle_3 = cx.add_model(|_| Model); handle_1.update(cx, |_, cx| { - cx.become_delegate(&handle_2, |me, _, event, _| { - me.events.push(event * 3); - }) - .detach(); - cx.subscribe(&handle_2, move |me, emitter, event, cx| { me.events.push(*event); @@ -4497,13 +4383,13 @@ mod tests { }); handle_2.update(cx, |_, c| c.emit(7)); - assert_eq!(handle_1.read(cx).events, vec![7, 21]); + assert_eq!(handle_1.read(cx).events, vec![7]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 21, 5, 10, 15]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); handle_3.update(cx, |_, c| c.emit(9)); - assert_eq!(handle_1.read(cx).events, vec![7, 21, 5, 10, 15, 9]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10, 9]); } #[crate::test(self)] diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 972821f77b867cc3b26263ee8380ca7ab34fe0d1..582410a9be9968f79f50fb514bea35fb5694be5b 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -80,7 +80,7 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { let buffer1_ops = buffer1_ops.clone(); |buffer, cx| { let buffer_1_events = buffer_1_events.clone(); - cx.become_delegate(&buffer1, move |_, _, event, _| match event { + cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() { Event::Operation(op) => buffer1_ops.borrow_mut().push(op), event @ _ => buffer_1_events.borrow_mut().push(event), }) @@ -610,7 +610,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { let mut buffer = Buffer::new(i as ReplicaId, base_text.as_str(), cx); buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); let network = network.clone(); - cx.become_delegate(&cx.handle(), move |buffer, _, event, _| { + cx.subscribe(&cx.handle(), move |buffer, _, event, _| { if let Event::Operation(op) = event { network .borrow_mut() @@ -706,7 +706,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { Buffer::from_proto(new_replica_id, old_buffer, None, cx).unwrap(); new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); let network = network.clone(); - cx.become_delegate(&cx.handle(), move |buffer, _, event, _| { + cx.subscribe(&cx.handle(), move |buffer, _, event, _| { if let Event::Operation(op) = event { network.borrow_mut().broadcast( buffer.replica_id(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fab9212b16f6ef4f54840349a2bb988cd013969a..e8ed9fbbb8196e9dbfabfc9aa5bc709b1917c40c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -942,7 +942,7 @@ impl Project { remote_id ))?, } - cx.become_delegate(buffer, |this, buffer, event, cx| { + cx.subscribe(buffer, |this, buffer, event, cx| { this.on_buffer_event(buffer, event, cx); }) .detach(); @@ -1028,7 +1028,7 @@ impl Project { fn on_buffer_event( &mut self, buffer: ModelHandle, - event: BufferEvent, + event: &BufferEvent, cx: &mut ModelContext, ) -> Option<()> { match event { From 7546ede28802eefb54f6a6f1e4cba5483c19a1a7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 9 Mar 2022 12:29:28 +0100 Subject: [PATCH 10/10] Split language server initialization from construction This gives clients a chance to register to notifications. --- crates/language/src/language.rs | 43 +++--- crates/lsp/src/lsp.rs | 217 ++++++++++++++---------------- crates/project/src/project.rs | 226 +++++++++++++++++--------------- crates/server/src/rpc.rs | 78 ++++++----- 4 files changed, 277 insertions(+), 287 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e8ae505a9c40fe41e1f5d50538e1b7f69f6b2bfa..5af18241b6d942f2abba949ced7a25128132ac5c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -245,7 +245,7 @@ impl LanguageRegistry { root_path: Arc, http_client: Arc, cx: &mut MutableAppContext, - ) -> Option>>> { + ) -> Option>> { #[cfg(any(test, feature = "test-support"))] if language .config @@ -264,23 +264,26 @@ impl LanguageRegistry { .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 (server, mut fake_server) = cx.update(|cx| { + lsp::LanguageServer::fake_with_capabilities( + fake_config.capabilities.clone(), + cx, + ) + }); + if let Some(initializer) = &fake_config.initializer { + initializer(&mut fake_server); } - fake_config - .servers_tx - .clone() - .unbounded_send(fake_server) - .ok(); - Ok(server.clone()) + + let servers_tx = fake_config.servers_tx.clone(); + cx.background() + .spawn(async move { + fake_server + .receive_notification::() + .await; + servers_tx.unbounded_send(fake_server).ok(); + }) + .detach(); + Ok(server) })); } @@ -316,15 +319,13 @@ impl LanguageRegistry { let server_binary_path = server_binary_path.await?; let server_args = adapter.server_args(); - let server = lsp::LanguageServer::new( + lsp::LanguageServer::new( &server_binary_path, server_args, - adapter.initialization_options(), &root_path, + adapter.initialization_options(), background, ) - .await?; - Ok(server) })) } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index bba5960bb421342dd2ca4e670d2ab53b83261b83..d9024975e4fe635051c05656f3050a8547ac9a07 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -14,6 +14,7 @@ use smol::{ use std::{ future::Future, io::Write, + path::PathBuf, str::FromStr, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -40,6 +41,8 @@ pub struct LanguageServer { executor: Arc, io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, + root_path: PathBuf, + options: Option, } pub struct Subscription { @@ -99,13 +102,13 @@ struct Error { } impl LanguageServer { - pub async fn new( + pub fn new( binary_path: &Path, args: &[&str], - options: Option, root_path: &Path, + options: Option, background: Arc, - ) -> Result> { + ) -> Result { let mut server = Command::new(binary_path) .current_dir(root_path) .args(args) @@ -115,16 +118,18 @@ impl LanguageServer { .spawn()?; let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); - Self::new_internal(stdin, stdout, root_path, options, background).await + Ok(Self::new_internal( + stdin, stdout, root_path, options, background, + )) } - async fn new_internal( + fn new_internal( stdin: Stdin, stdout: Stdout, root_path: &Path, options: Option, executor: Arc, - ) -> Result> + ) -> Self where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, @@ -214,7 +219,7 @@ impl LanguageServer { .log_err() }); - let mut this = Arc::new(Self { + Self { notification_handlers, response_handlers, capabilities: Default::default(), @@ -223,80 +228,73 @@ impl LanguageServer { executor: executor.clone(), io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), - }); + root_path: root_path.to_path_buf(), + options, + } + } - let root_uri = Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?; - - 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() + pub async fn initialize(mut self) -> Result> { + let options = self.options.take(); + let mut this = Arc::new(self); + let root_uri = Url::from_file_path(&this.root_path).unwrap(); + #[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()], }), - experimental: Some(json!({ - "serverStatusNotification": true, - })), - window: Some(WindowClientCapabilities { - work_done_progress: Some(true), + ..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() - }, - trace: Default::default(), - workspace_folders: Default::default(), - client_info: Default::default(), - locale: Default::default(), - }; - - 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 + }), + ..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(), + }; + + let response = this.request::(params).await?; + Arc::get_mut(&mut this).unwrap().capabilities = response.capabilities; + this.notify::(InitializedParams {})?; + Ok(this) } pub fn shutdown(&self) -> Option>> { @@ -331,7 +329,7 @@ impl LanguageServer { } } - pub fn on_notification(&self, mut f: F) -> Subscription + pub fn on_notification(&mut self, mut f: F) -> Subscription where T: notification::Notification, F: 'static + Send + Sync + FnMut(T::Params), @@ -357,7 +355,7 @@ impl LanguageServer { } } - pub fn capabilities(&self) -> &ServerCapabilities { + pub fn capabilities<'a>(self: &'a Arc) -> &'a ServerCapabilities { &self.capabilities } @@ -368,16 +366,12 @@ impl LanguageServer { where T::Result: 'static + Send, { - let this = self.clone(); - async move { - Self::request_internal::( - &this.next_id, - &this.response_handlers, - &this.outbound_tx, - params, - ) - .await - } + Self::request_internal::( + &self.next_id, + &self.response_handlers, + &self.outbound_tx, + params, + ) } fn request_internal( @@ -492,16 +486,14 @@ impl LanguageServer { } } - pub fn fake( - cx: &mut gpui::MutableAppContext, - ) -> impl Future, FakeLanguageServer)> { + pub fn fake(cx: &mut gpui::MutableAppContext) -> (Self, FakeLanguageServer) { Self::fake_with_capabilities(Self::full_capabilities(), cx) } pub fn fake_with_capabilities( capabilities: ServerCapabilities, cx: &mut gpui::MutableAppContext, - ) -> impl Future, FakeLanguageServer)> { + ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); @@ -515,14 +507,9 @@ impl LanguageServer { }); let executor = cx.background().clone(); - async move { - let server = - Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), None, executor) - .await - .unwrap(); - - (server, fake) - } + let server = + Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), None, executor); + (server, fake) } } @@ -547,6 +534,7 @@ impl FakeLanguageServer { let mut stdin = smol::io::BufReader::new(stdin); while Self::receive(&mut stdin, &mut buffer).await.is_ok() { cx.background().simulate_random_delay().await; + if let Ok(request) = serde_json::from_slice::(&buffer) { assert_eq!(request.jsonrpc, JSON_RPC_VERSION); @@ -602,7 +590,7 @@ impl FakeLanguageServer { } } - pub async fn notify(&mut self, params: T::Params) { + pub fn notify(&mut self, params: T::Params) { let message = serde_json::to_vec(&Notification { jsonrpc: JSON_RPC_VERSION, method: T::METHOD, @@ -667,16 +655,14 @@ impl FakeLanguageServer { self.notify::(ProgressParams { token: NumberOrString::String(token.into()), value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())), - }) - .await; + }); } pub async fn end_progress(&mut self, token: impl Into) { self.notify::(ProgressParams { token: NumberOrString::String(token.into()), value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())), - }) - .await; + }); } async fn receive( @@ -721,7 +707,7 @@ mod tests { #[gpui::test] async fn test_fake(cx: &mut TestAppContext) { - let (server, mut fake) = cx.update(LanguageServer::fake).await; + let (mut server, mut fake) = cx.update(LanguageServer::fake); let (message_tx, message_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); @@ -736,6 +722,7 @@ mod tests { }) .detach(); + let server = server.initialize().await.unwrap(); server .notify::(DidOpenTextDocumentParams { text_document: TextDocumentItem::new( @@ -758,14 +745,12 @@ mod tests { fake.notify::(ShowMessageParams { typ: MessageType::ERROR, message: "ok".to_string(), - }) - .await; + }); fake.notify::(PublishDiagnosticsParams { uri: Url::from_str("file://b/c").unwrap(), version: Some(5), diagnostics: vec![], - }) - .await; + }); assert_eq!(message_rx.recv().await.unwrap().message, "ok"); assert_eq!( diagnostics_rx.recv().await.unwrap().uri.as_str(), @@ -777,16 +762,4 @@ mod tests { drop(server); fake.receive_notification::().await; } - - pub enum ServerStatusNotification {} - - impl notification::Notification for ServerStatusNotification { - type Params = ServerStatusParams; - const METHOD: &'static str = "experimental/serverStatus"; - } - - #[derive(Deserialize, Serialize, PartialEq, Eq, Clone)] - pub struct ServerStatusParams { - pub quiescent: bool, - } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e8ed9fbbb8196e9dbfabfc9aa5bc709b1917c40c..9a45e0816410b23c595279294fd8a8f3bbf971a1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1173,67 +1173,8 @@ impl Project { ); let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { - let language_server = language_server?.await.log_err()?; + let mut language_server = language_server?.await.log_err()?; let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { - this.language_servers - .insert(key.clone(), language_server.clone()); - - // Tell the language server about every open buffer in the worktree that matches the language. - for buffer in this.opened_buffers.values() { - if let Some(buffer_handle) = buffer.upgrade(cx) { - let buffer = buffer_handle.read(cx); - let file = if let Some(file) = File::from_dyn(buffer.file()) { - file - } else { - continue; - }; - let language = if let Some(language) = buffer.language() { - language - } else { - continue; - }; - if (file.worktree.read(cx).id(), language.name()) != key { - continue; - } - - 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()?; - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - language_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider.trigger_characters.clone() - }) - .unwrap_or(Vec::new()), - cx, - ) - }); - } - } - - Some(()) - }); let disk_based_sources = language .disk_based_diagnostic_sources() @@ -1305,45 +1246,112 @@ impl Project { .detach(); // Process all the LSP events. - let this = this.downgrade(); - cx.spawn(|mut cx| async move { - while let Ok(message) = diagnostics_rx.recv().await { - let this = this.upgrade(&cx)?; - match message { - LspEvent::DiagnosticsStart => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_started(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdating { - project_id, - }) + cx.spawn(|mut cx| { + let this = this.downgrade(); + async move { + while let Ok(message) = diagnostics_rx.recv().await { + let this = this.upgrade(&cx)?; + match message { + LspEvent::DiagnosticsStart => { + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_started(cx); + if let Some(project_id) = this.remote_id() { + rpc.send(proto::DiskBasedDiagnosticsUpdating { + project_id, + }) + .log_err(); + } + }); + } + LspEvent::DiagnosticsUpdate(mut params) => { + language.process_diagnostics(&mut params); + this.update(&mut cx, |this, cx| { + this.update_diagnostics( + params, + &disk_based_sources, + cx, + ) .log_err(); - } - }); + }); + } + LspEvent::DiagnosticsFinish => { + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_finished(cx); + if let Some(project_id) = this.remote_id() { + rpc.send(proto::DiskBasedDiagnosticsUpdated { + project_id, + }) + .log_err(); + } + }); + } } - LspEvent::DiagnosticsUpdate(mut params) => { - language.process_diagnostics(&mut params); - this.update(&mut cx, |this, cx| { - this.update_diagnostics(params, &disk_based_sources, cx) - .log_err(); - }); + } + Some(()) + } + }) + .detach(); + + let language_server = language_server.initialize().await.log_err()?; + this.update(&mut cx, |this, cx| { + this.language_servers + .insert(key.clone(), language_server.clone()); + + // Tell the language server about every open buffer in the worktree that matches the language. + for buffer in this.opened_buffers.values() { + if let Some(buffer_handle) = buffer.upgrade(cx) { + let buffer = buffer_handle.read(cx); + let file = if let Some(file) = File::from_dyn(buffer.file()) { + file + } else { + continue; + }; + let language = if let Some(language) = buffer.language() { + language + } else { + continue; + }; + if (file.worktree.read(cx).id(), language.name()) != key { + continue; } - LspEvent::DiagnosticsFinish => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_finished(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdated { - project_id, + + 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()?; + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + language_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider.trigger_characters.clone() }) - .log_err(); - } - }); - } + .unwrap_or(Vec::new()), + cx, + ) + }); } } + Some(()) - }) - .detach(); + }); Some(language_server) }) @@ -2654,7 +2662,7 @@ impl Project { { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); return cx.spawn(|this, cx| async move { - if !request.check_capabilities(language_server.capabilities()) { + if !request.check_capabilities(&language_server.capabilities()) { return Ok(Default::default()); } @@ -4516,8 +4524,8 @@ mod tests { fake_server.end_progress(&progress_token).await; fake_server.start_progress(&progress_token).await; - fake_server - .notify::(lsp::PublishDiagnosticsParams { + fake_server.notify::( + lsp::PublishDiagnosticsParams { uri: Url::from_file_path("/dir/a.rs").unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { @@ -4526,8 +4534,8 @@ mod tests { message: "undefined variable 'A'".to_string(), ..Default::default() }], - }) - .await; + }, + ); assert_eq!( events.next().await.unwrap(), Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) @@ -4632,8 +4640,8 @@ mod tests { ); // Report some diagnostics for the initial version of the buffer - fake_server - .notify::(lsp::PublishDiagnosticsParams { + fake_server.notify::( + lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), version: Some(open_notification.text_document.version), diagnostics: vec![ @@ -4659,8 +4667,8 @@ mod tests { ..Default::default() }, ], - }) - .await; + }, + ); // The diagnostics have moved down since they were created. buffer.next_notification(cx).await; @@ -4718,8 +4726,8 @@ mod tests { }); // Ensure overlapping diagnostics are highlighted correctly. - fake_server - .notify::(lsp::PublishDiagnosticsParams { + fake_server.notify::( + lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), version: Some(open_notification.text_document.version), diagnostics: vec![ @@ -4738,8 +4746,8 @@ mod tests { ..Default::default() }, ], - }) - .await; + }, + ); buffer.next_notification(cx).await; buffer.read_with(cx, |buffer, _| { @@ -4805,8 +4813,8 @@ mod tests { ); // Handle out-of-order diagnostics - fake_server - .notify::(lsp::PublishDiagnosticsParams { + fake_server.notify::( + lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), version: Some(open_notification.text_document.version), diagnostics: vec![ @@ -4825,8 +4833,8 @@ mod tests { ..Default::default() }, ], - }) - .await; + }, + ); buffer.next_notification(cx).await; buffer.read_with(cx, |buffer, _| { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 444f2858b617e149369e1381ee5f759a3740eff1..2b954b57741d008aff46423561af5fdc261c1aa9 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1948,8 +1948,8 @@ mod tests { fake_language_server .receive_notification::() .await; - fake_language_server - .notify::(lsp::PublishDiagnosticsParams { + fake_language_server.notify::( + lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { @@ -1958,8 +1958,8 @@ mod tests { message: "message 1".to_string(), ..Default::default() }], - }) - .await; + }, + ); // Wait for server to see the diagnostics update. server @@ -2008,8 +2008,8 @@ mod tests { }); // Simulate a language server reporting more errors for a file. - fake_language_server - .notify::(lsp::PublishDiagnosticsParams { + fake_language_server.notify::( + lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), version: None, diagnostics: vec![ @@ -2029,8 +2029,8 @@ mod tests { ..Default::default() }, ], - }) - .await; + }, + ); // Client b gets the updated summaries project_b @@ -2374,10 +2374,6 @@ mod tests { .await .unwrap(); - let format = project_b.update(cx_b, |project, cx| { - project.format(HashSet::from_iter([buffer_b.clone()]), true, cx) - }); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| { Some(vec![ @@ -2392,7 +2388,12 @@ mod tests { ]) }); - format.await.unwrap(); + project_b + .update(cx_b, |project, cx| { + project.format(HashSet::from_iter([buffer_b.clone()]), true, cx) + }) + .await + .unwrap(); assert_eq!( buffer_b.read_with(cx_b, |buffer, _| buffer.text()), "let honey = two" @@ -2482,8 +2483,6 @@ mod tests { .unwrap(); // Request the definition of a symbol as the guest. - let definitions_1 = project_b.update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx)); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| { Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( @@ -2492,7 +2491,10 @@ mod tests { ))) }); - let definitions_1 = definitions_1.await.unwrap(); + let definitions_1 = project_b + .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx)) + .await + .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_1.len(), 1); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); @@ -2509,7 +2511,6 @@ mod tests { // Try getting more definitions for the same buffer, ensuring the buffer gets reused from // the previous call to `definition`. - let definitions_2 = project_b.update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx)); fake_language_server.handle_request::(|_, _| { Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( lsp::Url::from_file_path("/root-2/b.rs").unwrap(), @@ -2517,7 +2518,10 @@ mod tests { ))) }); - let definitions_2 = definitions_2.await.unwrap(); + let definitions_2 = project_b + .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx)) + .await + .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_2.len(), 1); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); @@ -2618,8 +2622,6 @@ mod tests { .unwrap(); // Request references to a symbol as the guest. - let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|params, _| { assert_eq!( @@ -2642,7 +2644,10 @@ mod tests { ]) }); - let references = references.await.unwrap(); + let references = project_b + .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)) + .await + .unwrap(); cx_b.read(|cx| { assert_eq!(references.len(), 3); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); @@ -2846,8 +2851,6 @@ mod tests { .unwrap(); // Request document highlights as the guest. - let highlights = project_b.update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| { @@ -2889,7 +2892,10 @@ mod tests { }, ); - let highlights = highlights.await.unwrap(); + let highlights = project_b + .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)) + .await + .unwrap(); buffer_b.read_with(cx_b, |buffer, _| { let snapshot = buffer.snapshot(); @@ -2991,8 +2997,6 @@ mod tests { .await .unwrap(); - // Request the definition of a symbol as the guest. - let symbols = project_b.update(cx_b, |p, cx| p.symbols("two", cx)); let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| { #[allow(deprecated)] @@ -3009,7 +3013,11 @@ mod tests { }]) }); - let symbols = symbols.await.unwrap(); + // Request the definition of a symbol as the guest. + let symbols = project_b + .update(cx_b, |p, cx| p.symbols("two", cx)) + .await + .unwrap(); assert_eq!(symbols.len(), 1); assert_eq!(symbols[0].name, "TWO"); @@ -3120,6 +3128,14 @@ mod tests { .await .unwrap(); + let mut fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.handle_request::(|_, _| { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ))) + }); + let definitions; let buffer_b2; if rng.gen() { @@ -3130,14 +3146,6 @@ mod tests { definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); } - let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ))) - }); - let buffer_b2 = buffer_b2.await.unwrap(); let definitions = definitions.await.unwrap(); assert_eq!(definitions.len(), 1);