diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index bee8f9a34f..61e914ad87 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -12,8 +12,8 @@ use client::{ }; use collections::{BTreeMap, HashMap, HashSet}; use editor::{ - self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset, - ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, + Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; use fs::{FakeFs, Fs as _, HomeDir, LineEnding}; use futures::{channel::oneshot, StreamExt as _}; @@ -22,7 +22,7 @@ use gpui::{ TestAppContext, ViewHandle, }; use language::{ - range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, + range_to_lsp, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, PointUtf16, Rope, }; use live_kit_client::MacOSDisplay; @@ -1058,17 +1058,22 @@ async fn test_share_project( let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx)); - // TODO - // // Create a selection set as client B and see that selection set as client A. - // buffer_a - // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) - // .await; + // Client A sees client B's selection + deterministic.run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(Anchor::MIN..Anchor::MAX) + .count() + == 1 + }); // Edit the buffer as client B and see that edit as client A. editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); - buffer_a - .condition(cx_a, |buffer, _| buffer.text() == "ok, b-contents") - .await; + deterministic.run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "ok, b-contents") + }); // Client B can invite client C on a project shared by client A. active_call_b @@ -1091,12 +1096,16 @@ async fn test_share_project( .build_remote_project(initial_project.id, cx_c) .await; - // TODO - // // Remove the selection set as client B, see those selections disappear as client A. + // Client B closes the editor, and client A sees client B's selections removed. cx_b.update(move |_| drop(editor_b)); - // buffer_a - // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) - // .await; + deterministic.run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(Anchor::MIN..Anchor::MAX) + .count() + == 0 + }); } #[gpui::test(iterations = 10)] @@ -1250,13 +1259,9 @@ async fn test_host_disconnect( server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - project_a - .condition(cx_a, |project, _| project.collaborators().is_empty()) - .await; + project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - project_b - .condition(cx_b, |project, _| project.is_read_only()) - .await; + project_b.read_with(cx_b, |project, _| project.is_read_only()); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); // Ensure client B's edited state is reset and that the whole window is blurred. @@ -1641,9 +1646,8 @@ async fn test_propagate_saves_and_fs_changes( .await .unwrap(); - buffer_a - .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") - .await; + deterministic.run_until_parked(); + buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, ")); buffer_a.update(cx_a, |buf, cx| { buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx) }); @@ -2297,9 +2301,8 @@ async fn test_buffer_conflict_after_save( }); buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap(); - buffer_b - .condition(cx_b, |buffer_b, _| !buffer_b.is_dirty()) - .await; + cx_a.foreground().forbid_parking(); + buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty())); buffer_b.read_with(cx_b, |buf, _| { assert!(!buf.has_conflict()); }); @@ -2359,12 +2362,9 @@ async fn test_buffer_reloading( .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows) .await .unwrap(); - buffer_b - .condition(cx_b, |buf, _| { - buf.text() == new_contents.to_string() && !buf.is_dirty() - }) - .await; + cx_a.foreground().run_until_parked(); buffer_b.read_with(cx_b, |buf, _| { + assert_eq!(buf.text(), new_contents.to_string()); assert!(!buf.is_dirty()); assert!(!buf.has_conflict()); assert_eq!(buf.line_ending(), LineEnding::Windows); @@ -2416,7 +2416,8 @@ async fn test_editing_while_guest_opens_buffer( let text = buffer_a.read_with(cx_a, |buf, _| buf.text()); let buffer_b = buffer_b.await.unwrap(); - buffer_b.condition(cx_b, |buf, _| buf.text() == text).await; + cx_a.foreground().run_until_parked(); + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text)); } #[gpui::test(iterations = 10)] @@ -2446,9 +2447,8 @@ async fn test_leaving_worktree_while_opening_buffer( let project_b = client_b.build_remote_project(project_id, cx_b).await; // See that a guest has joined as client A. - project_a - .condition(cx_a, |p, _| p.collaborators().len() == 1) - .await; + cx_a.foreground().run_until_parked(); + project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1)); // Begin opening a buffer as client B, but leave the project before the open completes. let buffer_b = cx_b @@ -2458,9 +2458,8 @@ async fn test_leaving_worktree_while_opening_buffer( drop(buffer_b); // See that the guest has left. - project_a - .condition(cx_a, |p, _| p.collaborators().is_empty()) - .await; + cx_a.foreground().run_until_parked(); + project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty())); } #[gpui::test(iterations = 10)] @@ -2979,9 +2978,10 @@ async fn test_collaborating_with_completion( }); let fake_language_server = fake_language_servers.next().await.unwrap(); - buffer_b - .condition(cx_b, |buffer, _| !buffer.completion_triggers().is_empty()) - .await; + cx_a.foreground().run_until_parked(); + buffer_b.read_with(cx_b, |buffer, _| { + assert!(!buffer.completion_triggers().is_empty()) + }); // Type a completion trigger character as the guest. editor_b.update(cx_b, |editor, cx| { @@ -3043,14 +3043,13 @@ async fn test_collaborating_with_completion( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - buffer_a - .condition(cx_a, |buffer, _| buffer.text() == "fn main() { a. }") - .await; + cx_a.foreground().run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a. }") + }); // Confirm a completion on the guest. - editor_b - .condition(cx_b, |editor, _| editor.context_menu_visible()) - .await; + editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); editor_b.update(cx_b, |editor, cx| { editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); @@ -3079,16 +3078,19 @@ async fn test_collaborating_with_completion( ); // The additional edit is applied. - buffer_a - .condition(cx_a, |buffer, _| { - buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }" - }) - .await; - buffer_b - .condition(cx_b, |buffer, _| { - buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }" - }) - .await; + cx_a.foreground().run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); } #[gpui::test(iterations = 10)] @@ -3134,9 +3136,8 @@ async fn test_reloading_buffer_manually( assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); - buffer_a - .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;") - .await; + cx_a.foreground().run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;")); client_a .fs @@ -3147,12 +3148,9 @@ async fn test_reloading_buffer_manually( ) .await .unwrap(); - buffer_a - .condition(cx_a, |buffer, _| buffer.has_conflict()) - .await; - buffer_b - .condition(cx_b, |buffer, _| buffer.has_conflict()) - .await; + cx_a.foreground().run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict())); + buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict())); project_b .update(cx_b, |project, cx| { @@ -4178,9 +4176,8 @@ async fn test_collaborating_with_code_actions( cx, ); }); - editor_b - .condition(cx_b, |editor, _| editor.context_menu_visible()) - .await; + cx_a.foreground().run_until_parked(); + editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); fake_language_server.remove_request_handler::(); @@ -5162,9 +5159,9 @@ async fn test_following( .insert_tree( "/a", json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", }), ) .await; @@ -5263,11 +5260,60 @@ async fn test_following( workspace_a.update(cx_a, |workspace, cx| { workspace.activate_item(&editor_a1, cx) }); - workspace_b - .condition(cx_b, |workspace, cx| { - workspace.active_item(cx).unwrap().id() == editor_b1.id() - }) - .await; + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + // When client A opens a multibuffer, client B does so as well. + let multibuffer_a = cx_a.add_model(|cx| { + let buffer_a1 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "1.txt").into(), cx) + .unwrap() + }); + let buffer_a2 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "2.txt").into(), cx) + .unwrap() + }); + let mut result = MultiBuffer::new(0); + result.push_excerpts( + buffer_a1, + [ExcerptRange { + context: 0..3, + primary: None, + }], + cx, + ); + result.push_excerpts( + buffer_a2, + [ExcerptRange { + context: 4..7, + primary: None, + }], + cx, + ); + result + }); + let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { + let editor = + cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor + }); + deterministic.run_until_parked(); + let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), + multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), + ); // When client A navigates back and forth, client B does so as well. workspace_a @@ -5275,47 +5321,52 @@ async fn test_following( workspace::Pane::go_back(workspace, None, cx) }) .await; - workspace_b - .condition(cx_b, |workspace, cx| { - workspace.active_item(cx).unwrap().id() == editor_b2.id() + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace::Pane::go_back(workspace, None, cx) }) .await; + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); + }); workspace_a .update(cx_a, |workspace, cx| { workspace::Pane::go_forward(workspace, None, cx) }) .await; - workspace_b - .condition(cx_b, |workspace, cx| { - workspace.active_item(cx).unwrap().id() == editor_b1.id() - }) - .await; + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); // Changes to client A's editor are reflected on client B. editor_a1.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); }); - editor_b1 - .condition(cx_b, |editor, cx| { - editor.selections.ranges(cx) == vec![1..1, 2..2] - }) - .await; + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); + }); editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); - editor_b1 - .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") - .await; + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); editor_a1.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([3..3])); editor.set_scroll_position(vec2f(0., 100.), cx); }); - editor_b1 - .condition(cx_b, |editor, cx| { - editor.selections.ranges(cx) == vec![3..3] - }) - .await; + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[3..3]); + }); // After unfollowing, client B stops receiving updates from client A. workspace_b.update(cx_b, |workspace, cx| { @@ -5384,13 +5435,21 @@ async fn test_following( .await .unwrap(); deterministic.run_until_parked(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - editor_a1.id() - ); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) + }); + + // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_item(&multibuffer_editor_b, cx) + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); // Client B activates an external window again, and the previously-opened screen-sharing item // gets activated. diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 9122706ad3..89f5bb54a9 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -164,7 +164,7 @@ impl ProjectDiagnosticsEditor { editor.set_vertical_scroll_margin(5, cx); editor }); - cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event)) + cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())) .detach(); let project = project_handle.read(cx); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ab2bdf1889..8a3c7452ef 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -84,7 +84,7 @@ use std::{ pub use sum_tree::Bias; use theme::{DiagnosticStyle, Theme}; use util::{post_inc, ResultExt, TryFutureExt}; -use workspace::{ItemNavHistory, Workspace, WorkspaceId}; +use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId}; use crate::git::diff_hunk_to_display; @@ -467,6 +467,7 @@ pub struct Editor { keymap_context_layers: BTreeMap, input_enabled: bool, leader_replica_id: Option, + remote_id: Option, hover_state: HoverState, link_go_to_definition_state: LinkGoToDefinitionState, _subscriptions: Vec, @@ -1108,6 +1109,7 @@ impl Editor { keymap_context_layers: Default::default(), input_enabled: true, leader_replica_id: None, + remote_id: None, hover_state: Default::default(), link_go_to_definition_state: Default::default(), _subscriptions: vec![ @@ -5883,25 +5885,36 @@ impl Editor { fn on_buffer_event( &mut self, _: ModelHandle, - event: &language::Event, + event: &multi_buffer::Event, cx: &mut ViewContext, ) { match event { - language::Event::Edited => { + multi_buffer::Event::Edited => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); cx.emit(Event::BufferEdited); } - language::Event::Reparsed => cx.emit(Event::Reparsed), - language::Event::DirtyChanged => cx.emit(Event::DirtyChanged), - language::Event::Saved => cx.emit(Event::Saved), - language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), - language::Event::Reloaded => cx.emit(Event::TitleChanged), - language::Event::Closed => cx.emit(Event::Closed), - language::Event::DiagnosticsUpdated => { + multi_buffer::Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => cx.emit(Event::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }), + multi_buffer::Event::ExcerptsRemoved { ids } => { + cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) + } + multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed), + multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(Event::Saved), + multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged), + multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged), + multi_buffer::Event::Closed => cx.emit(Event::Closed), + multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } - _ => {} } } @@ -6084,8 +6097,16 @@ impl Deref for EditorSnapshot { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + ExcerptsAdded { + buffer: ModelHandle, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + }, BufferEdited, Edited, Reparsed, @@ -6093,8 +6114,12 @@ pub enum Event { DirtyChanged, Saved, TitleChanged, - SelectionsChanged { local: bool }, - ScrollPositionChanged { local: bool }, + SelectionsChanged { + local: bool, + }, + ScrollPositionChanged { + local: bool, + }, Closed, } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 582c75904d..c3c15bb5b4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc, time::Instant}; use drag_and_drop::DragAndDrop; use futures::StreamExt; use indoc::indoc; +use rpc::PeerId; use unindent::Unindent; use super::*; @@ -24,7 +25,7 @@ use util::{ }; use workspace::{ item::{FollowableItem, ItemHandle}, - NavigationEntry, Pane, + NavigationEntry, Pane, ViewId, }; #[gpui::test] @@ -41,7 +42,7 @@ fn test_edit_events(cx: &mut MutableAppContext) { event, Event::Edited | Event::BufferEdited | Event::DirtyChanged ) { - events.borrow_mut().push(("editor1", *event)); + events.borrow_mut().push(("editor1", event.clone())); } }) .detach(); @@ -56,7 +57,7 @@ fn test_edit_events(cx: &mut MutableAppContext) { event, Event::Edited | Event::BufferEdited | Event::DirtyChanged ) { - events.borrow_mut().push(("editor2", *event)); + events.borrow_mut().push(("editor2", event.clone())); } }) .detach(); @@ -4969,19 +4970,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { } #[gpui::test] -fn test_following(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); +async fn test_following(cx: &mut gpui::TestAppContext) { + Settings::test_async(cx); + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - cx.set_global(Settings::test(cx)); - - let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); - let (_, follower) = cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), - ..Default::default() - }, - |cx| build_editor(buffer.clone(), cx), - ); + let buffer = project.update(cx, |project, cx| { + let buffer = project + .create_buffer(&sample_text(16, 8, 'a'), None, cx) + .unwrap(); + cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) + }); + let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let (_, follower) = cx.update(|cx| { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ) + }); let is_still_following = Rc::new(RefCell::new(true)); let pending_update = Rc::new(RefCell::new(None)); @@ -5009,44 +5018,50 @@ fn test_following(cx: &mut gpui::MutableAppContext) { leader.update(cx, |leader, cx| { leader.change_selections(None, cx, |s| s.select_ranges([1..1])); }); - follower.update(cx, |follower, cx| { - follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) - .unwrap(); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![1..1]); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]); assert_eq!(*is_still_following.borrow(), true); // Update the scroll position only leader.update(cx, |leader, cx| { leader.set_scroll_position(vec2f(1.5, 3.5), cx); }); - follower.update(cx, |follower, cx| { - follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) - .unwrap(); - }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); assert_eq!( follower.update(cx, |follower, cx| follower.scroll_position(cx)), vec2f(1.5, 3.5) ); assert_eq!(*is_still_following.borrow(), true); - // Update the selections and scroll position + // Update the selections and scroll position. The follower's scroll position is updated + // via autoscroll, not via the leader's exact scroll position. leader.update(cx, |leader, cx| { leader.change_selections(None, cx, |s| s.select_ranges([0..0])); leader.request_autoscroll(Autoscroll::newest(), cx); leader.set_scroll_position(vec2f(1.5, 3.5), cx); }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); follower.update(cx, |follower, cx| { - let initial_scroll_position = follower.scroll_position(cx); - follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) - .unwrap(); - assert_eq!(follower.scroll_position(cx), initial_scroll_position); - assert!(follower.scroll_manager.has_autoscroll_request()); + assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); + assert_eq!(follower.selections.ranges(cx), vec![0..0]); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]); assert_eq!(*is_still_following.borrow(), true); // Creating a pending selection that precedes another selection @@ -5054,24 +5069,30 @@ fn test_following(cx: &mut gpui::MutableAppContext) { leader.change_selections(None, cx, |s| s.select_ranges([1..1])); leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); }); - follower.update(cx, |follower, cx| { - follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) - .unwrap(); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]); assert_eq!(*is_still_following.borrow(), true); // Extend the pending selection so that it surrounds another selection leader.update(cx, |leader, cx| { leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); }); - follower.update(cx, |follower, cx| { - follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) - .unwrap(); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..2]); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]); // Scrolling locally breaks the follow follower.update(cx, |follower, cx| { @@ -5087,6 +5108,165 @@ fn test_following(cx: &mut gpui::MutableAppContext) { assert_eq!(*is_still_following.borrow(), false); } +#[gpui::test] +async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { + Settings::test_async(cx); + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let (_, pane) = cx.add_window(|cx| Pane::new(None, cx)); + + let leader = pane.update(cx, |_, cx| { + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) + }); + + // Start following the editor when it has no excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_1 = cx + .update(|cx| { + Editor::from_state_proto( + pane.clone(), + project.clone(), + ViewId { + creator: PeerId(0), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + let update_message = Rc::new(RefCell::new(None)); + follower_1.update(cx, { + let update = update_message.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + } + }); + + let (buffer_1, buffer_2) = project.update(cx, |project, cx| { + ( + project + .create_buffer("abc\ndef\nghi\njkl\n", None, cx) + .unwrap(), + project + .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) + .unwrap(), + ) + }); + + // Insert some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: 1..6, + primary: None, + }, + ExcerptRange { + context: 12..15, + primary: None, + }, + ExcerptRange { + context: 0..3, + primary: None, + }, + ], + cx, + ); + multibuffer.insert_excerpts_after( + excerpt_ids[0], + buffer_2.clone(), + [ + ExcerptRange { + context: 8..12, + primary: None, + }, + ExcerptRange { + context: 0..6, + primary: None, + }, + ], + cx, + ); + }); + }); + + // Apply the update of adding the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + assert_eq!( + follower_1.read_with(cx, Editor::text), + leader.read_with(cx, Editor::text) + ); + update_message.borrow_mut().take(); + + // Start following separately after it already has excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_2 = cx + .update(|cx| { + Editor::from_state_proto( + pane.clone(), + project.clone(), + ViewId { + creator: PeerId(0), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + follower_2.read_with(cx, Editor::text), + leader.read_with(cx, Editor::text) + ); + + // Remove some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.excerpt_ids(); + multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx); + multibuffer.remove_excerpts([excerpt_ids[0]], cx); + }); + }); + + // Apply the update of removing the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + follower_2 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + update_message.borrow_mut().take(); + assert_eq!( + follower_1.read_with(cx, Editor::text), + leader.read_with(cx, Editor::text) + ); +} + #[test] fn test_combine_syntax_and_fuzzy_match_highlights() { let string = "abcdefghijklmnop"; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5883091d47..0057df778b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,9 +1,18 @@ +use crate::{ + display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, + movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, + Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, + FORMAT_TIMEOUT, +}; use anyhow::{anyhow, Context, Result}; +use collections::HashSet; +use futures::future::try_join_all; use futures::FutureExt; use gpui::{ elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::proto::serialize_anchor as serialize_text_anchor; use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal}; use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath}; use rpc::proto::{self, update_view}; @@ -13,97 +22,136 @@ use std::{ borrow::Cow, cmp::{self, Ordering}, fmt::Write, + iter, ops::Range, path::{Path, PathBuf}, }; use text::Selection; use util::{ResultExt, TryFutureExt}; +use workspace::item::FollowableItemHandle; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, - ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId, -}; - -use crate::{ - display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, - movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, - Event, ExcerptId, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, - FORMAT_TIMEOUT, + ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace, + WorkspaceId, }; pub const MAX_TAB_TITLE_LEN: usize = 24; impl FollowableItem for Editor { + fn remote_id(&self) -> Option { + self.remote_id + } + fn from_state_proto( pane: ViewHandle, project: ModelHandle, + remote_id: ViewId, state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>> { - let state = if matches!(state, Some(proto::view::Variant::Editor(_))) { - if let Some(proto::view::Variant::Editor(state)) = state.take() { - state - } else { - unreachable!() - } - } else { - return None; - }; + let Some(proto::view::Variant::Editor(_)) = state else { return None }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; - let buffer = project.update(cx, |project, cx| { - project.open_buffer_by_id(state.buffer_id, cx) + let client = project.read(cx).client(); + let replica_id = project.read(cx).replica_id(); + let buffer_ids = state + .excerpts + .iter() + .map(|excerpt| excerpt.buffer_id) + .collect::>(); + let buffers = project.update(cx, |project, cx| { + buffer_ids + .iter() + .map(|id| project.open_buffer_by_id(*id, cx)) + .collect::>() }); + Some(cx.spawn(|mut cx| async move { - let buffer = buffer.await?; - let editor = pane - .read_with(&cx, |pane, cx| { - pane.items_of_type::().find(|editor| { - editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) - }) + let mut buffers = futures::future::try_join_all(buffers).await?; + let editor = pane.read_with(&cx, |pane, cx| { + let mut editors = pane.items_of_type::(); + editors.find(|editor| { + editor.remote_id(&client, cx) == Some(remote_id) + || state.singleton + && buffers.len() == 1 + && editor.read(cx).buffer.read(cx).as_singleton().as_ref() + == Some(&buffers[0]) }) - .unwrap_or_else(|| { - pane.update(&mut cx, |_, cx| { - cx.add_view(|cx| Editor::for_buffer(buffer, Some(project), cx)) - }) - }); + }); + + let editor = editor.unwrap_or_else(|| { + pane.update(&mut cx, |_, cx| { + let multibuffer = cx.add_model(|cx| { + let mut multibuffer; + if state.singleton && buffers.len() == 1 { + multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx) + } else { + multibuffer = MultiBuffer::new(replica_id); + let mut excerpts = state.excerpts.into_iter().peekable(); + while let Some(excerpt) = excerpts.peek() { + let buffer_id = excerpt.buffer_id; + let buffer_excerpts = iter::from_fn(|| { + let excerpt = excerpts.peek()?; + (excerpt.buffer_id == buffer_id) + .then(|| excerpts.next().unwrap()) + }); + let buffer = + buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id); + if let Some(buffer) = buffer { + multibuffer.push_excerpts( + buffer.clone(), + buffer_excerpts.filter_map(deserialize_excerpt_range), + cx, + ); + } + } + }; + + if let Some(title) = &state.title { + multibuffer = multibuffer.with_title(title.clone()) + } + + multibuffer + }); + + cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx)) + }) + }); + editor.update(&mut cx, |editor, cx| { - let excerpt_id; - let buffer_id; - { - let buffer = editor.buffer.read(cx).read(cx); - let singleton = buffer.as_singleton().unwrap(); - excerpt_id = singleton.0.clone(); - buffer_id = singleton.1; - } + editor.remote_id = Some(remote_id); + let buffer = editor.buffer.read(cx).read(cx); let selections = state .selections .into_iter() .map(|selection| { - deserialize_selection(&excerpt_id, buffer_id, selection) + deserialize_selection(&buffer, selection) .ok_or_else(|| anyhow!("invalid selection")) }) .collect::>>()?; + let scroll_top_anchor = state + .scroll_top_anchor + .and_then(|anchor| deserialize_anchor(&buffer, anchor)); + drop(buffer); + if !selections.is_empty() { editor.set_selections_from_remote(selections, cx); } - if let Some(anchor) = state.scroll_top_anchor { + if let Some(scroll_top_anchor) = scroll_top_anchor { editor.set_scroll_anchor_remote( ScrollAnchor { - top_anchor: Anchor { - buffer_id: Some(state.buffer_id as usize), - excerpt_id, - text_anchor: language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?, - }, + top_anchor: scroll_top_anchor, offset: vec2f(state.scroll_x, state.scroll_y), }, cx, ); } - Ok::<_, anyhow::Error>(()) + anyhow::Ok(()) })?; + Ok(editor) })) } @@ -134,13 +182,32 @@ impl FollowableItem for Editor { } fn to_state_proto(&self, cx: &AppContext) -> Option { - let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); + let buffer = self.buffer.read(cx); let scroll_anchor = self.scroll_manager.anchor(); + let excerpts = buffer + .read(cx) + .excerpts() + .map(|(id, buffer, range)| proto::Excerpt { + id: id.to_proto(), + buffer_id: buffer.remote_id(), + context_start: Some(serialize_text_anchor(&range.context.start)), + context_end: Some(serialize_text_anchor(&range.context.end)), + primary_start: range + .primary + .as_ref() + .map(|range| serialize_text_anchor(&range.start)), + primary_end: range + .primary + .as_ref() + .map(|range| serialize_text_anchor(&range.end)), + }) + .collect(); + Some(proto::view::Variant::Editor(proto::view::Editor { - buffer_id, - scroll_top_anchor: Some(language::proto::serialize_anchor( - &scroll_anchor.top_anchor.text_anchor, - )), + singleton: buffer.is_singleton(), + title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), + excerpts, + scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)), scroll_x: scroll_anchor.offset.x(), scroll_y: scroll_anchor.offset.y(), selections: self @@ -156,18 +223,43 @@ impl FollowableItem for Editor { &self, event: &Self::Event, update: &mut Option, - _: &AppContext, + cx: &AppContext, ) -> bool { let update = update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default())); match update { proto::update_view::Variant::Editor(update) => match event { + Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => { + let buffer_id = buffer.read(cx).remote_id(); + let mut excerpts = excerpts.iter(); + if let Some((id, range)) = excerpts.next() { + update.inserted_excerpts.push(proto::ExcerptInsertion { + previous_excerpt_id: Some(predecessor.to_proto()), + excerpt: serialize_excerpt(buffer_id, id, range), + }); + update.inserted_excerpts.extend(excerpts.map(|(id, range)| { + proto::ExcerptInsertion { + previous_excerpt_id: None, + excerpt: serialize_excerpt(buffer_id, id, range), + } + })) + } + true + } + Event::ExcerptsRemoved { ids } => { + update + .deleted_excerpts + .extend(ids.iter().map(ExcerptId::to_proto)); + true + } Event::ScrollPositionChanged { .. } => { let scroll_anchor = self.scroll_manager.anchor(); - update.scroll_top_anchor = Some(language::proto::serialize_anchor( - &scroll_anchor.top_anchor.text_anchor, - )); + update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor)); update.scroll_x = scroll_anchor.offset.x(); update.scroll_y = scroll_anchor.offset.y(); true @@ -189,45 +281,98 @@ impl FollowableItem for Editor { fn apply_update_proto( &mut self, + project: &ModelHandle, message: update_view::Variant, cx: &mut ViewContext, - ) -> Result<()> { - match message { - update_view::Variant::Editor(message) => { - let buffer = self.buffer.read(cx); - let buffer = buffer.read(cx); - let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); - let excerpt_id = excerpt_id.clone(); - drop(buffer); + ) -> Task> { + let update_view::Variant::Editor(message) = message; + let multibuffer = self.buffer.read(cx); + let multibuffer = multibuffer.read(cx); - let selections = message - .selections - .into_iter() - .filter_map(|selection| { - deserialize_selection(&excerpt_id, buffer_id, selection) - }) - .collect::>(); + let buffer_ids = message + .inserted_excerpts + .iter() + .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) + .collect::>(); + + let mut removals = message + .deleted_excerpts + .into_iter() + .map(ExcerptId::from_proto) + .collect::>(); + removals.sort_by(|a, b| a.cmp(&b, &multibuffer)); + + let selections = message + .selections + .into_iter() + .filter_map(|selection| deserialize_selection(&multibuffer, selection)) + .collect::>(); + let scroll_top_anchor = message + .scroll_top_anchor + .and_then(|anchor| deserialize_anchor(&multibuffer, anchor)); + drop(multibuffer); + + let buffers = project.update(cx, |project, cx| { + buffer_ids + .into_iter() + .map(|id| project.open_buffer_by_id(id, cx)) + .collect::>() + }); + + let project = project.clone(); + cx.spawn(|this, mut cx| async move { + let _buffers = try_join_all(buffers).await?; + this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |multibuffer, cx| { + let mut insertions = message.inserted_excerpts.into_iter().peekable(); + while let Some(insertion) = insertions.next() { + let Some(excerpt) = insertion.excerpt else { continue }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue }; + let buffer_id = excerpt.buffer_id; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue }; + + let adjacent_excerpts = iter::from_fn(|| { + let insertion = insertions.peek()?; + if insertion.previous_excerpt_id.is_none() + && insertion.excerpt.as_ref()?.buffer_id == buffer_id + { + insertions.next()?.excerpt + } else { + None + } + }); + + multibuffer.insert_excerpts_with_ids_after( + ExcerptId::from_proto(previous_excerpt_id), + buffer, + [excerpt] + .into_iter() + .chain(adjacent_excerpts) + .filter_map(|excerpt| { + Some(( + ExcerptId::from_proto(excerpt.id), + deserialize_excerpt_range(excerpt)?, + )) + }), + cx, + ); + } + + multibuffer.remove_excerpts(removals, cx); + }); if !selections.is_empty() { - self.set_selections_from_remote(selections, cx); - self.request_autoscroll_remotely(Autoscroll::newest(), cx); - } else if let Some(anchor) = message.scroll_top_anchor { - self.set_scroll_anchor_remote( - ScrollAnchor { - top_anchor: Anchor { - buffer_id: Some(buffer_id), - excerpt_id, - text_anchor: language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?, - }, - offset: vec2f(message.scroll_x, message.scroll_y), - }, - cx, - ); + this.set_selections_from_remote(selections, cx); + this.request_autoscroll_remotely(Autoscroll::newest(), cx); + } else if let Some(anchor) = scroll_top_anchor { + this.set_scroll_anchor_remote(ScrollAnchor { + top_anchor: anchor, + offset: vec2f(message.scroll_x, message.scroll_y) + }, cx); } - } - } - Ok(()) + }); + Ok(()) + }) } fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { @@ -240,41 +385,82 @@ impl FollowableItem for Editor { } } +fn serialize_excerpt( + buffer_id: u64, + id: &ExcerptId, + range: &ExcerptRange, +) -> Option { + Some(proto::Excerpt { + id: id.to_proto(), + buffer_id, + context_start: Some(serialize_text_anchor(&range.context.start)), + context_end: Some(serialize_text_anchor(&range.context.end)), + primary_start: range + .primary + .as_ref() + .map(|r| serialize_text_anchor(&r.start)), + primary_end: range + .primary + .as_ref() + .map(|r| serialize_text_anchor(&r.end)), + }) +} + fn serialize_selection(selection: &Selection) -> proto::Selection { proto::Selection { id: selection.id as u64, - start: Some(language::proto::serialize_anchor( - &selection.start.text_anchor, - )), - end: Some(language::proto::serialize_anchor( - &selection.end.text_anchor, - )), + start: Some(serialize_anchor(&selection.start)), + end: Some(serialize_anchor(&selection.end)), reversed: selection.reversed, } } +fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor { + proto::EditorAnchor { + excerpt_id: anchor.excerpt_id.to_proto(), + anchor: Some(serialize_text_anchor(&anchor.text_anchor)), + } +} + +fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option> { + let context = { + let start = language::proto::deserialize_anchor(excerpt.context_start?)?; + let end = language::proto::deserialize_anchor(excerpt.context_end?)?; + start..end + }; + let primary = excerpt + .primary_start + .zip(excerpt.primary_end) + .and_then(|(start, end)| { + let start = language::proto::deserialize_anchor(start)?; + let end = language::proto::deserialize_anchor(end)?; + Some(start..end) + }); + Some(ExcerptRange { context, primary }) +} + fn deserialize_selection( - excerpt_id: &ExcerptId, - buffer_id: usize, + buffer: &MultiBufferSnapshot, selection: proto::Selection, ) -> Option> { Some(Selection { id: selection.id as usize, - start: Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: language::proto::deserialize_anchor(selection.start?)?, - }, - end: Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: language::proto::deserialize_anchor(selection.end?)?, - }, + start: deserialize_anchor(buffer, selection.start?)?, + end: deserialize_anchor(buffer, selection.end?)?, reversed: selection.reversed, goal: SelectionGoal::None, }) } +fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option { + let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id); + Some(Anchor { + excerpt_id, + text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?, + buffer_id: buffer.buffer_id_for_excerpt(excerpt_id), + }) +} + impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Ok(data) = data.downcast::() { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d758792e6c..d0dd34a931 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -9,9 +9,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, - DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, - OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, - ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, + DiagnosticEntry, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, + Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, + ToPointUtf16 as _, TransactionId, Unclipped, }; use smallvec::SmallVec; use std::{ @@ -50,6 +50,26 @@ pub struct MultiBuffer { title: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + ExcerptsAdded { + buffer: ModelHandle, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + }, + Edited, + Reloaded, + Reparsed, + Saved, + FileHandleChanged, + Closed, + DirtyChanged, + DiagnosticsUpdated, +} + #[derive(Clone)] struct History { next_transaction_id: TransactionId, @@ -833,6 +853,30 @@ impl MultiBuffer { ) -> Vec where O: text::ToOffset, + { + let mut ids = Vec::new(); + let mut next_excerpt_id = self.next_excerpt_id; + self.insert_excerpts_with_ids_after( + prev_excerpt_id, + buffer, + ranges.into_iter().map(|range| { + let id = ExcerptId(post_inc(&mut next_excerpt_id)); + ids.push(id); + (id, range) + }), + cx, + ); + ids + } + + pub fn insert_excerpts_with_ids_after( + &mut self, + prev_excerpt_id: ExcerptId, + buffer: ModelHandle, + ranges: impl IntoIterator)>, + cx: &mut ModelContext, + ) where + O: text::ToOffset, { assert_eq!(self.history.transaction_depth, 0); let mut ranges = ranges.into_iter().peekable(); @@ -858,7 +902,7 @@ impl MultiBuffer { cx.observe(&buffer, |_, _, cx| cx.notify()), cx.subscribe(&buffer, Self::on_buffer_event), ], - buffer, + buffer: buffer.clone(), }); let mut snapshot = self.snapshot.borrow_mut(); @@ -883,8 +927,8 @@ impl MultiBuffer { Locator::max() }; - let mut ids = Vec::new(); - while let Some(range) = ranges.next() { + let mut excerpts = Vec::new(); + while let Some((id, range)) = ranges.next() { let locator = Locator::between(&prev_locator, &next_locator); if let Err(ix) = buffer_state.excerpts.binary_search(&locator) { buffer_state.excerpts.insert(ix, locator.clone()); @@ -897,7 +941,10 @@ impl MultiBuffer { ..buffer_snapshot.anchor_after(&primary.end) }), }; - let id = ExcerptId(post_inc(&mut self.next_excerpt_id)); + if id.0 >= self.next_excerpt_id { + self.next_excerpt_id = id.0 + 1; + } + excerpts.push((id, range.clone())); let excerpt = Excerpt::new( id, locator.clone(), @@ -909,7 +956,6 @@ impl MultiBuffer { new_excerpts.push(excerpt, &()); prev_locator = locator.clone(); new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &()); - ids.push(id); } let edit_end = new_excerpts.summary().text.len; @@ -929,12 +975,17 @@ impl MultiBuffer { new: edit_start..edit_end, }]); cx.emit(Event::Edited); + cx.emit(Event::ExcerptsAdded { + buffer, + predecessor: prev_excerpt_id, + excerpts, + }); cx.notify(); - ids } pub fn clear(&mut self, cx: &mut ModelContext) { self.sync(cx); + let ids = self.excerpt_ids(); self.buffers.borrow_mut().clear(); let mut snapshot = self.snapshot.borrow_mut(); let prev_len = snapshot.len(); @@ -948,6 +999,7 @@ impl MultiBuffer { new: 0..0, }]); cx.emit(Event::Edited); + cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1071,12 +1123,14 @@ impl MultiBuffer { cx: &mut ModelContext, ) { self.sync(cx); + let ids = excerpt_ids.into_iter().collect::>(); + let mut buffers = self.buffers.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut(); let mut new_excerpts = SumTree::new(); let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); let mut edits = Vec::new(); - let mut excerpt_ids = excerpt_ids.into_iter().peekable(); + let mut excerpt_ids = ids.iter().copied().peekable(); while let Some(excerpt_id) = excerpt_ids.next() { // Seek to the next excerpt to remove, preserving any preceding excerpts. @@ -1143,6 +1197,7 @@ impl MultiBuffer { self.subscriptions.publish_mut(edits); cx.emit(Event::Edited); + cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1165,10 +1220,22 @@ impl MultiBuffer { fn on_buffer_event( &mut self, _: ModelHandle, - event: &Event, + event: &language::Event, cx: &mut ModelContext, ) { - cx.emit(event.clone()); + cx.emit(match event { + language::Event::Edited => Event::Edited, + language::Event::DirtyChanged => Event::DirtyChanged, + language::Event::Saved => Event::Saved, + language::Event::FileHandleChanged => Event::FileHandleChanged, + language::Event::Reloaded => Event::Reloaded, + language::Event::Reparsed => Event::Reparsed, + language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, + language::Event::Closed => Event::Closed, + + // + language::Event::Operation(_) => return, + }); } pub fn all_buffers(&self) -> HashSet> { @@ -1604,7 +1671,7 @@ impl MultiBuffer { } impl Entity for MultiBuffer { - type Event = language::Event; + type Event = Event; } impl MultiBufferSnapshot { @@ -2450,6 +2517,14 @@ impl MultiBufferSnapshot { } } + pub fn excerpts( + &self, + ) -> impl Iterator)> { + self.excerpts + .iter() + .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) + } + pub fn excerpt_boundaries_in_range( &self, range: R, @@ -2746,6 +2821,10 @@ impl MultiBufferSnapshot { } } + pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option { + Some(self.excerpt(excerpt_id)?.buffer_id) + } + fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> { let mut cursor = self.excerpts.cursor::>(); let locator = self.excerpt_locator_for_id(excerpt_id); @@ -3080,6 +3159,14 @@ impl ExcerptId { Self(usize::MAX) } + pub fn to_proto(&self) -> u64 { + self.0 as _ + } + + pub fn from_proto(proto: u64) -> Self { + Self(proto as _) + } + pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { let a = snapshot.excerpt_locator_for_id(*self); let b = snapshot.excerpt_locator_for_id(*other); @@ -3468,7 +3555,7 @@ mod tests { use util::test::sample_text; #[gpui::test] - fn test_singleton_multibuffer(cx: &mut MutableAppContext) { + fn test_singleton(cx: &mut MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); @@ -3495,7 +3582,7 @@ mod tests { } #[gpui::test] - fn test_remote_multibuffer(cx: &mut MutableAppContext) { + fn test_remote(cx: &mut MutableAppContext) { let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx)); let guest_buffer = cx.add_model(|cx| { let state = host_buffer.read(cx).to_proto(); @@ -3526,7 +3613,7 @@ mod tests { } #[gpui::test] - fn test_excerpt_buffer(cx: &mut MutableAppContext) { + fn test_excerpt_boundaries_and_clipping(cx: &mut MutableAppContext) { let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); @@ -3535,7 +3622,9 @@ mod tests { multibuffer.update(cx, |_, cx| { let events = events.clone(); cx.subscribe(&multibuffer, move |_, _, event, _| { - events.borrow_mut().push(event.clone()) + if let Event::Edited = event { + events.borrow_mut().push(event.clone()) + } }) .detach(); }); @@ -3748,7 +3837,84 @@ mod tests { } #[gpui::test] - fn test_excerpts_with_context_lines(cx: &mut MutableAppContext) { + fn test_excerpt_events(cx: &mut MutableAppContext) { + let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx)); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx)); + + let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + + follower_multibuffer.update(cx, |_, cx| { + cx.subscribe(&leader_multibuffer, |follower, _, event, cx| { + match event.clone() { + Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), + Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), + _ => {} + } + }) + .detach(); + }); + + leader_multibuffer.update(cx, |leader, cx| { + leader.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: 0..8, + primary: None, + }, + ExcerptRange { + context: 12..16, + primary: None, + }, + ], + cx, + ); + leader.insert_excerpts_after( + leader.excerpt_ids()[0], + buffer_2.clone(), + [ + ExcerptRange { + context: 0..5, + primary: None, + }, + ExcerptRange { + context: 10..15, + primary: None, + }, + ], + cx, + ) + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + + leader_multibuffer.update(cx, |leader, cx| { + let excerpt_ids = leader.excerpt_ids(); + leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + + leader_multibuffer.update(cx, |leader, cx| { + leader.clear(cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + } + + #[gpui::test] + fn test_push_excerpts_with_context_lines(cx: &mut MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { @@ -3784,7 +3950,7 @@ mod tests { } #[gpui::test] - fn test_empty_excerpt_buffer(cx: &mut MutableAppContext) { + fn test_empty_multibuffer(cx: &mut MutableAppContext) { let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -3872,9 +4038,7 @@ mod tests { } #[gpui::test] - fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts( - cx: &mut MutableAppContext, - ) { + fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut MutableAppContext) { let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 674ce4f50e..9612deb5bd 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -9,7 +9,7 @@ use rpc::proto; use std::{ops::Range, sync::Arc}; use text::*; -pub use proto::{BufferState, Operation, SelectionSet}; +pub use proto::{BufferState, Operation}; pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding { match message { @@ -122,8 +122,14 @@ pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec) -> proto::Selection { proto::Selection { id: selection.id as u64, - start: Some(serialize_anchor(&selection.start)), - end: Some(serialize_anchor(&selection.end)), + start: Some(proto::EditorAnchor { + anchor: Some(serialize_anchor(&selection.start)), + excerpt_id: 0, + }), + end: Some(proto::EditorAnchor { + anchor: Some(serialize_anchor(&selection.end)), + excerpt_id: 0, + }), reversed: selection.reversed, } } @@ -229,8 +235,8 @@ pub fn deserialize_operation(message: proto::Operation) -> Result) -> Arc<[Selecti pub fn deserialize_selection(selection: proto::Selection) -> Option> { Some(Selection { id: selection.id as usize, - start: deserialize_anchor(selection.start?)?, - end: deserialize_anchor(selection.end?)?, + start: deserialize_anchor(selection.start?.anchor?)?, + end: deserialize_anchor(selection.end?.anchor?)?, reversed: selection.reversed, goal: SelectionGoal::None, }) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index cf58adfe0b..84db17a494 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -798,7 +798,7 @@ message Follow { } message FollowResponse { - optional uint64 active_view_id = 1; + optional ViewId active_view_id = 1; repeated View views = 2; } @@ -826,13 +826,18 @@ message GetPrivateUserInfoResponse { // Entities +message ViewId { + uint32 creator = 1; + uint64 id = 2; +} + message UpdateActiveView { - optional uint64 id = 1; + optional ViewId id = 1; optional uint32 leader_id = 2; } message UpdateView { - uint64 id = 1; + ViewId id = 1; optional uint32 leader_id = 2; oneof variant { @@ -840,15 +845,17 @@ message UpdateView { } message Editor { - repeated Selection selections = 1; - Anchor scroll_top_anchor = 2; - float scroll_x = 3; - float scroll_y = 4; + repeated ExcerptInsertion inserted_excerpts = 1; + repeated uint64 deleted_excerpts = 2; + repeated Selection selections = 3; + EditorAnchor scroll_top_anchor = 4; + float scroll_x = 5; + float scroll_y = 6; } } message View { - uint64 id = 1; + ViewId id = 1; optional uint32 leader_id = 2; oneof variant { @@ -856,11 +863,13 @@ message View { } message Editor { - uint64 buffer_id = 1; - repeated Selection selections = 2; - Anchor scroll_top_anchor = 3; - float scroll_x = 4; - float scroll_y = 5; + bool singleton = 1; + optional string title = 2; + repeated Excerpt excerpts = 3; + repeated Selection selections = 4; + EditorAnchor scroll_top_anchor = 5; + float scroll_x = 6; + float scroll_y = 7; } } @@ -913,21 +922,18 @@ enum LineEnding { Windows = 1; } -message SelectionSet { - uint32 replica_id = 1; - repeated Selection selections = 2; - uint32 lamport_timestamp = 3; - bool line_mode = 4; - CursorShape cursor_shape = 5; -} - message Selection { uint64 id = 1; - Anchor start = 2; - Anchor end = 3; + EditorAnchor start = 2; + EditorAnchor end = 3; bool reversed = 4; } +message EditorAnchor { + uint64 excerpt_id = 1; + Anchor anchor = 2; +} + enum CursorShape { CursorBar = 0; CursorBlock = 1; @@ -935,6 +941,20 @@ enum CursorShape { CursorHollow = 3; } +message ExcerptInsertion { + Excerpt excerpt = 1; + optional uint64 previous_excerpt_id = 2; +} + +message Excerpt { + uint64 id = 1; + uint64 buffer_id = 2; + Anchor context_start = 3; + Anchor context_end = 4; + Anchor primary_start = 5; + Anchor primary_end = 6; +} + message Anchor { uint32 replica_id = 1; uint32 local_timestamp = 2; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 13b754a417..1659ddd451 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -402,7 +402,7 @@ impl ProjectSearchView { }); // Subcribe to query_editor in order to reraise editor events for workspace item activation purposes cx.subscribe(&query_editor, |_, _, event, cx| { - cx.emit(ViewEvent::EditorEvent(*event)) + cx.emit(ViewEvent::EditorEvent(event.clone())) }) .detach(); @@ -419,7 +419,7 @@ impl ProjectSearchView { this.update_match_index(cx); } // Reraise editor events for workspace item activation purposes - cx.emit(ViewEvent::EditorEvent(*event)); + cx.emit(ViewEvent::EditorEvent(event.clone())); }) .detach(); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 5c2f7b7a51..5aa91ede8a 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1496,6 +1496,10 @@ impl BufferSnapshot { &self.visible_text } + pub fn remote_id(&self) -> u64 { + self.remote_id + } + pub fn replica_id(&self) -> ReplicaId { self.replica_id } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 14f847fd54..1d6b4a9eb5 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -5,12 +5,15 @@ use std::{ fmt, path::PathBuf, rc::Rc, - sync::atomic::{AtomicBool, Ordering}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, time::Duration, }; use anyhow::Result; -use client::proto; +use client::{proto, Client}; use gpui::{ AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -23,7 +26,8 @@ use util::ResultExt; use crate::{ pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction, - FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId, + FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, + WorkspaceId, }; #[derive(Eq, PartialEq, Hash)] @@ -278,7 +282,9 @@ impl ItemHandle for ViewHandle { if let Some(message) = followed_item.to_state_proto(cx) { workspace.update_followers( proto::update_followers::Variant::CreateView(proto::View { - id: followed_item.id() as u64, + id: followed_item + .remote_id(&workspace.client, cx) + .map(|id| id.to_proto()), variant: Some(message), leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), }), @@ -332,7 +338,9 @@ impl ItemHandle for ViewHandle { this.update_followers( proto::update_followers::Variant::UpdateView( proto::UpdateView { - id: item.id() as u64, + id: item + .remote_id(&this.client, cx) + .map(|id| id.to_proto()), variant: pending_update.borrow_mut().take(), leader_id: leader_id.map(|id| id.0), }, @@ -584,10 +592,12 @@ pub trait ProjectItem: Item { } pub trait FollowableItem: Item { + fn remote_id(&self) -> Option; fn to_state_proto(&self, cx: &AppContext) -> Option; fn from_state_proto( pane: ViewHandle, project: ModelHandle, + id: ViewId, state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; @@ -599,15 +609,17 @@ pub trait FollowableItem: Item { ) -> bool; fn apply_update_proto( &mut self, + project: &ModelHandle, message: proto::update_view::Variant, cx: &mut ViewContext, - ) -> Result<()>; + ) -> Task>; fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option; fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); fn to_state_proto(&self, cx: &AppContext) -> Option; fn add_event_to_update_proto( @@ -618,13 +630,23 @@ pub trait FollowableItemHandle: ItemHandle { ) -> bool; fn apply_update_proto( &self, + project: &ModelHandle, message: proto::update_view::Variant, cx: &mut MutableAppContext, - ) -> Result<()>; + ) -> Task>; fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; } impl FollowableItemHandle for ViewHandle { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { + self.read(cx).remote_id().or_else(|| { + client.peer_id().map(|creator| ViewId { + creator, + id: self.id() as u64, + }) + }) + } + fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext) { self.update(cx, |this, cx| { this.set_leader_replica_id(leader_replica_id, cx) @@ -650,10 +672,11 @@ impl FollowableItemHandle for ViewHandle { fn apply_update_proto( &self, + project: &ModelHandle, message: proto::update_view::Variant, cx: &mut MutableAppContext, - ) -> Result<()> { - self.update(cx, |this, cx| this.apply_update_proto(message, cx)) + ) -> Task> { + self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) } fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 387a18006a..5cf65568f8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -14,23 +14,18 @@ pub mod sidebar; mod status_bar; mod toolbar; -use std::{ - any::TypeId, - borrow::Cow, - future::Future, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; - -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use call::ActiveCall; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{Dock, DockDefaultItemFactory, ToggleDockButton}; use drag_and_drop::DragAndDrop; use fs::{self, Fs}; -use futures::{channel::oneshot, FutureExt, StreamExt}; +use futures::{ + channel::{mpsc, oneshot}, + future::try_join_all, + FutureExt, StreamExt, +}; use gpui::{ actions, elements::*, @@ -42,7 +37,19 @@ use gpui::{ }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use language::LanguageRegistry; +use std::{ + any::TypeId, + borrow::Cow, + future::Future, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; +use crate::{ + notifications::simple_message_notification::{MessageNotification, OsOpen}, + persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace}, +}; use log::{error, warn}; use notifications::NotificationHandle; pub use pane::*; @@ -64,11 +71,6 @@ use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; -use crate::{ - notifications::simple_message_notification::{MessageNotification, OsOpen}, - persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace}, -}; - #[derive(Clone, PartialEq)] pub struct RemoveWorktreeFromProject(pub WorktreeId); @@ -316,6 +318,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { type FollowableItemBuilder = fn( ViewHandle, ModelHandle, + ViewId, &mut Option, &mut MutableAppContext, ) -> Option>>>; @@ -331,8 +334,8 @@ pub fn register_followable_item(cx: &mut MutableAppContext) { builders.insert( TypeId::of::(), ( - |pane, project, state, cx| { - I::from_state_proto(pane, project, state, cx).map(|task| { + |pane, project, id, state, cx| { + I::from_state_proto(pane, project, id, state, cx).map(|task| { cx.foreground() .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) }) @@ -458,25 +461,6 @@ impl DelayedDebouncedEditAction { } } -#[derive(Default)] -struct LeaderState { - followers: HashSet, -} - -type FollowerStatesByLeader = HashMap, FollowerState>>; - -#[derive(Default)] -struct FollowerState { - active_view_id: Option, - items_by_leader_view_id: HashMap, -} - -#[derive(Debug)] -enum FollowerItem { - Loading(Vec), - Loaded(Box), -} - pub enum Event { DockAnchorChanged, PaneAdded(ViewHandle), @@ -507,10 +491,31 @@ pub struct Workspace { last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, active_call: Option<(ModelHandle, Vec)>, + leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, + _apply_leader_updates: Task>, _observe_current_user: Task<()>, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ViewId { + pub creator: PeerId, + pub id: u64, +} + +#[derive(Default)] +struct LeaderState { + followers: HashSet, +} + +type FollowerStatesByLeader = HashMap, FollowerState>>; + +#[derive(Default)] +struct FollowerState { + active_view_id: Option, + items_by_leader_view_id: HashMap>, +} + impl Workspace { pub fn new( serialized_workspace: Option, @@ -576,10 +581,24 @@ impl Workspace { }) } }); - let handle = cx.handle(); let weak_handle = cx.weak_handle(); + // All leader updates are enqueued and then processed in a single task, so + // that each asynchronous operation can be run in order. + let (leader_updates_tx, mut leader_updates_rx) = + mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); + let _apply_leader_updates = cx.spawn_weak(|this, mut cx| async move { + while let Some((leader_id, update)) = leader_updates_rx.next().await { + let Some(this) = this.upgrade(&cx) else { break }; + Self::process_leader_update(this, leader_id, update, &mut cx) + .await + .log_err(); + } + + Ok(()) + }); + cx.emit_global(WorkspaceCreated(weak_handle.clone())); let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left)); @@ -637,6 +656,8 @@ impl Workspace { active_call, database_id: workspace_id, _observe_current_user, + _apply_leader_updates, + leader_updates_tx, }; this.project_remote_id_changed(project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); @@ -1440,7 +1461,11 @@ impl Workspace { self.update_followers( proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { - id: self.active_item(cx).map(|item| item.id() as u64), + id: self.active_item(cx).and_then(|item| { + item.to_followable_item_handle(cx)? + .remote_id(&self.client, cx) + .map(|id| id.to_proto()) + }), leader_id: self.leader_for_pane(&pane).map(|id| id.0), }), cx, @@ -1586,9 +1611,7 @@ impl Workspace { if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { for state in states_by_pane.into_values() { for item in state.items_by_leader_view_id.into_values() { - if let FollowerItem::Loaded(item) = item { - item.set_leader_replica_id(None, cx); - } + item.set_leader_replica_id(None, cx); } } } @@ -1631,11 +1654,18 @@ impl Workspace { .get_mut(&leader_id) .and_then(|states_by_pane| states_by_pane.get_mut(&pane)) .ok_or_else(|| anyhow!("following interrupted"))?; - state.active_view_id = response.active_view_id; + state.active_view_id = response.active_view_id.map(ViewId::from_proto); Ok::<_, anyhow::Error>(()) })?; - Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx) - .await?; + Self::add_views_from_leader( + this.clone(), + leader_id, + vec![pane], + response.views, + &mut cx, + ) + .await?; + this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx)); } Ok(()) })) @@ -1681,9 +1711,7 @@ impl Workspace { let leader_id = *leader_id; if let Some(state) = states_by_pane.remove(pane) { for (_, item) in state.items_by_leader_view_id { - if let FollowerItem::Loaded(item) = item { - item.set_leader_replica_id(None, cx); - } + item.set_leader_replica_id(None, cx); } if states_by_pane.is_empty() { @@ -1874,14 +1902,18 @@ impl Workspace { mut cx: AsyncAppContext, ) -> Result { this.update(&mut cx, |this, cx| { + let client = &this.client; this.leader_state .followers .insert(envelope.original_sender_id()?); - let active_view_id = this - .active_item(cx) - .and_then(|i| i.to_followable_item_handle(cx)) - .map(|i| i.id() as u64); + let active_view_id = this.active_item(cx).and_then(|i| { + Some( + i.to_followable_item_handle(cx)? + .remote_id(client, cx)? + .to_proto(), + ) + }); Ok(proto::FollowResponse { active_view_id, views: this @@ -1892,11 +1924,11 @@ impl Workspace { pane.read(cx).items().filter_map({ let cx = &cx; move |item| { - let id = item.id() as u64; let item = item.to_followable_item_handle(cx)?; + let id = item.remote_id(client, cx)?.to_proto(); let variant = item.to_state_proto(cx)?; Some(proto::View { - id, + id: Some(id), leader_id, variant: Some(variant), }) @@ -1926,45 +1958,58 @@ impl Workspace { this: ViewHandle, envelope: TypedEnvelope, _: Arc, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result<()> { let leader_id = envelope.original_sender_id()?; - match envelope - .payload - .variant - .ok_or_else(|| anyhow!("invalid update"))? - { + this.read_with(&cx, |this, _| { + this.leader_updates_tx + .unbounded_send((leader_id, envelope.payload)) + })?; + Ok(()) + } + + async fn process_leader_update( + this: ViewHandle, + leader_id: PeerId, + update: proto::UpdateFollowers, + cx: &mut AsyncAppContext, + ) -> Result<()> { + match update.variant.ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - this.update(&mut cx, |this, cx| { - this.update_leader_state(leader_id, cx, |state, _| { - state.active_view_id = update_active_view.id; - }); - Ok::<_, anyhow::Error>(()) - }) + this.update(cx, |this, _| { + if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) { + for state in state.values_mut() { + state.active_view_id = + update_active_view.id.clone().map(ViewId::from_proto); + } + } + }); } proto::update_followers::Variant::UpdateView(update_view) => { - this.update(&mut cx, |this, cx| { - let variant = update_view - .variant - .ok_or_else(|| anyhow!("missing update view variant"))?; - this.update_leader_state(leader_id, cx, |state, cx| { - let variant = variant.clone(); - match state - .items_by_leader_view_id - .entry(update_view.id) - .or_insert(FollowerItem::Loading(Vec::new())) - { - FollowerItem::Loaded(item) => { - item.apply_update_proto(variant, cx).log_err(); + let variant = update_view + .variant + .ok_or_else(|| anyhow!("missing update view variant"))?; + let id = update_view + .id + .ok_or_else(|| anyhow!("missing update view id"))?; + let mut tasks = Vec::new(); + this.update(cx, |this, cx| { + let project = this.project.clone(); + if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) { + for state in state.values_mut() { + if let Some(item) = state + .items_by_leader_view_id + .get(&ViewId::from_proto(id.clone())) + { + tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); } - FollowerItem::Loading(updates) => updates.push(variant), } - }); - Ok(()) - }) + } + }); + try_join_all(tasks).await.log_err(); } proto::update_followers::Variant::CreateView(view) => { - let panes = this.read_with(&cx, |this, _| { + let panes = this.read_with(cx, |this, _| { this.follower_states_by_leader .get(&leader_id) .into_iter() @@ -1972,13 +2017,10 @@ impl Workspace { .cloned() .collect() }); - Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx) - .await?; - Ok(()) + Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?; } } - .log_err(); - + this.update(cx, |this, cx| this.leader_updated(leader_id, cx)); Ok(()) } @@ -2011,16 +2053,19 @@ impl Workspace { let mut item_tasks = Vec::new(); let mut leader_view_ids = Vec::new(); for view in &views { + let Some(id) = &view.id else { continue }; + let id = ViewId::from_proto(id.clone()); let mut variant = view.variant.clone(); if variant.is_none() { Err(anyhow!("missing variant"))?; } for build_item in &item_builders { - let task = - cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); + let task = cx.update(|cx| { + build_item(pane.clone(), project.clone(), id, &mut variant, cx) + }); if let Some(task) = task { item_tasks.push(task); - leader_view_ids.push(view.id); + leader_view_ids.push(id); break; } else { assert!(variant.is_some()); @@ -2041,29 +2086,12 @@ impl Workspace { for (id, item) in leader_view_ids.into_iter().zip(items) { item.set_leader_replica_id(Some(replica_id), cx); - match state.items_by_leader_view_id.entry(id) { - hash_map::Entry::Occupied(e) => { - let e = e.into_mut(); - if let FollowerItem::Loading(updates) = e { - for update in updates.drain(..) { - item.apply_update_proto(update, cx) - .context("failed to apply view update") - .log_err(); - } - } - *e = FollowerItem::Loaded(item); - } - hash_map::Entry::Vacant(e) => { - e.insert(FollowerItem::Loaded(item)); - } - } + state.items_by_leader_view_id.insert(id, item); } Some(()) }); } - this.update(cx, |this, cx| this.leader_updated(leader_id, cx)); - Ok(()) } @@ -2097,23 +2125,6 @@ impl Workspace { }) } - fn update_leader_state( - &mut self, - leader_id: PeerId, - cx: &mut ViewContext, - mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext), - ) { - for (_, state) in self - .follower_states_by_leader - .get_mut(&leader_id) - .into_iter() - .flatten() - { - update_fn(state, cx); - } - self.leader_updated(leader_id, cx); - } - fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { cx.notify(); @@ -2126,7 +2137,7 @@ impl Workspace { call::ParticipantLocation::SharedProject { project_id } => { if Some(project_id) == self.project.read(cx).remote_id() { for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { - if let Some(FollowerItem::Loaded(item)) = state + if let Some(item) = state .active_view_id .and_then(|id| state.items_by_leader_view_id.get(&id)) { @@ -2575,6 +2586,22 @@ impl View for Workspace { } } +impl ViewId { + pub(crate) fn from_proto(message: proto::ViewId) -> Self { + Self { + creator: PeerId(message.creator), + id: message.id, + } + } + + pub(crate) fn to_proto(&self) -> proto::ViewId { + proto::ViewId { + creator: self.creator.0, + id: self.id, + } + } +} + pub trait WorkspaceHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec; } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9a827da8b7..099fb4eea9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -15,12 +15,16 @@ use editor::{Editor, MultiBuffer}; use gpui::{ actions, - geometry::vector::vec2f, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, impl_actions, platform::{WindowBounds, WindowOptions}, AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind, }; use language::Rope; +use lazy_static::lazy_static; pub use lsp; pub use project; use project_panel::ProjectPanel; @@ -68,6 +72,17 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; +lazy_static! { + static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") + .ok() + .as_deref() + .and_then(parse_pixel_position_env_var); + static ref ZED_WINDOW_POSITION: Option = env::var("ZED_WINDOW_POSITION") + .ok() + .as_deref() + .and_then(parse_pixel_position_env_var); +} + pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action(about); cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| { @@ -336,8 +351,13 @@ pub fn initialize_workspace( } pub fn build_window_options() -> WindowOptions<'static> { + let bounds = if let Some((position, size)) = ZED_WINDOW_POSITION.zip(*ZED_WINDOW_SIZE) { + WindowBounds::Fixed(RectF::new(position, size)) + } else { + WindowBounds::Maximized + }; WindowOptions { - bounds: WindowBounds::Maximized, + bounds, titlebar: Some(TitlebarOptions { title: None, appears_transparent: true, @@ -612,6 +632,13 @@ fn schema_file_match(path: &Path) -> &Path { .unwrap() } +fn parse_pixel_position_env_var(value: &str) -> Option { + let mut parts = value.split(','); + let width: usize = parts.next()?.parse().ok()?; + let height: usize = parts.next()?.parse().ok()?; + Some(vec2f(width as f32, height as f32)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/script/start-local-collaboration b/script/start-local-collaboration new file mode 100755 index 0000000000..9c63b301e5 --- /dev/null +++ b/script/start-local-collaboration @@ -0,0 +1,50 @@ +#!/bin/bash + +set -e + +if [[ -z "$GITHUB_TOKEN" ]]; then + cat <<-MESSAGE +Missing \`GITHUB_TOKEN\` environment variable. This token is needed +for fetching your GitHub identity from the command-line. + +Create an access token here: https://github.com/settings/tokens +Then edit your \`~/.zshrc\` (or other shell initialization script), +adding a line like this: + + export GITHUB_TOKEN="(the token)" + +MESSAGE + exit 1 +fi + +# Start one Zed instance as the current user and a second instance with a different user. +username_1=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login) +username_2=nathansobo +if [[ $username_1 == $username_2 ]]; then + username_2=as-cii +fi + +# Make each Zed instance take up half of the screen. +resolution_line=$(system_profiler SPDisplaysDataType | grep Resolution | head -n1) +screen_size=($(echo $resolution_line | egrep -o '[0-9]+')) +scale_factor=1 +if [[ $resolution_line =~ Retina ]]; then scale_factor=2; fi +width=$(expr ${screen_size[0]} / 2 / $scale_factor) +height=${screen_size[1] / $scale_factor} + +position_1=0,0 +position_2=${width},0 + +# Authenticate using the collab server's admin secret. +export ZED_ADMIN_API_TOKEN=secret +export ZED_SERVER_URL=http://localhost:8080 +export ZED_WINDOW_SIZE=${width},${height} + +cargo build +sleep 0.5 + +# Start the two Zed child processes. Open the given paths with the first instance. +trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT +ZED_IMPERSONATE=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ & +ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & +wait diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index 3244e7e4ea..847b937416 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -12,8 +12,16 @@ function isStyleSet(key: any): key is StyleSets { "negative", ].includes(key); } + function isStyle(key: any): key is Styles { - return ["default", "active", "disabled", "hovered", "pressed", "inverted"].includes(key); + return [ + "default", + "active", + "disabled", + "hovered", + "pressed", + "inverted", + ].includes(key); } function getStyle( layer: Layer,