Merge pull request #1921 from zed-industries/multibuffer-following

Allow following collaborators into editors with multi-excerpt buffers (refactors + find-all-refs)
This commit is contained in:
Max Brunsfeld 2022-12-14 15:33:11 -08:00 committed by GitHub
commit e08d6cd6de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1245 additions and 466 deletions

View file

@ -12,8 +12,8 @@ use client::{
}; };
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use editor::{ use editor::{
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset, self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer,
ToggleCodeActions, Undo, Redo, Rename, ToOffset, ToggleCodeActions, Undo,
}; };
use fs::{FakeFs, Fs as _, HomeDir, LineEnding}; use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
use futures::{channel::oneshot, StreamExt as _}; use futures::{channel::oneshot, StreamExt as _};
@ -22,7 +22,7 @@ use gpui::{
TestAppContext, ViewHandle, TestAppContext, ViewHandle,
}; };
use language::{ 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, LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, PointUtf16, Rope,
}; };
use live_kit_client::MacOSDisplay; 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)); let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
// TODO // Client A sees client B's selection
// // Create a selection set as client B and see that selection set as client A. deterministic.run_until_parked();
// buffer_a buffer_a.read_with(cx_a, |buffer, _| {
// .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) buffer
// .await; .snapshot()
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
.count()
== 1
});
// Edit the buffer as client B and see that edit as client A. // 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)); editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
buffer_a deterministic.run_until_parked();
.condition(cx_a, |buffer, _| buffer.text() == "ok, b-contents") buffer_a.read_with(cx_a, |buffer, _| {
.await; assert_eq!(buffer.text(), "ok, b-contents")
});
// Client B can invite client C on a project shared by client A. // Client B can invite client C on a project shared by client A.
active_call_b active_call_b
@ -1091,12 +1096,16 @@ async fn test_share_project(
.build_remote_project(initial_project.id, cx_c) .build_remote_project(initial_project.id, cx_c)
.await; .await;
// TODO // Client B closes the editor, and client A sees client B's selections removed.
// // Remove the selection set as client B, see those selections disappear as client A.
cx_b.update(move |_| drop(editor_b)); cx_b.update(move |_| drop(editor_b));
// buffer_a deterministic.run_until_parked();
// .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) buffer_a.read_with(cx_a, |buffer, _| {
// .await; buffer
.snapshot()
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
.count()
== 0
});
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
@ -1250,13 +1259,9 @@ async fn test_host_disconnect(
server.forbid_connections(); server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap()); server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
project_a project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
.condition(cx_a, |project, _| project.collaborators().is_empty())
.await;
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project_b project_b.read_with(cx_b, |project, _| project.is_read_only());
.condition(cx_b, |project, _| project.is_read_only())
.await;
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); 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. // 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 .await
.unwrap(); .unwrap();
buffer_a deterministic.run_until_parked();
.condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
.await;
buffer_a.update(cx_a, |buf, cx| { buffer_a.update(cx_a, |buf, cx| {
buf.edit([(buf.len()..buf.len(), "i-am-a")], None, 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.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
buffer_b cx_a.foreground().forbid_parking();
.condition(cx_b, |buffer_b, _| !buffer_b.is_dirty()) buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
.await;
buffer_b.read_with(cx_b, |buf, _| { buffer_b.read_with(cx_b, |buf, _| {
assert!(!buf.has_conflict()); assert!(!buf.has_conflict());
}); });
@ -2359,12 +2362,9 @@ async fn test_buffer_reloading(
.save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows) .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
.await .await
.unwrap(); .unwrap();
buffer_b cx_a.foreground().run_until_parked();
.condition(cx_b, |buf, _| {
buf.text() == new_contents.to_string() && !buf.is_dirty()
})
.await;
buffer_b.read_with(cx_b, |buf, _| { buffer_b.read_with(cx_b, |buf, _| {
assert_eq!(buf.text(), new_contents.to_string());
assert!(!buf.is_dirty()); assert!(!buf.is_dirty());
assert!(!buf.has_conflict()); assert!(!buf.has_conflict());
assert_eq!(buf.line_ending(), LineEnding::Windows); 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 text = buffer_a.read_with(cx_a, |buf, _| buf.text());
let buffer_b = buffer_b.await.unwrap(); 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)] #[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; let project_b = client_b.build_remote_project(project_id, cx_b).await;
// See that a guest has joined as client A. // See that a guest has joined as client A.
project_a cx_a.foreground().run_until_parked();
.condition(cx_a, |p, _| p.collaborators().len() == 1) project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
.await;
// Begin opening a buffer as client B, but leave the project before the open completes. // Begin opening a buffer as client B, but leave the project before the open completes.
let buffer_b = cx_b let buffer_b = cx_b
@ -2458,9 +2458,8 @@ async fn test_leaving_worktree_while_opening_buffer(
drop(buffer_b); drop(buffer_b);
// See that the guest has left. // See that the guest has left.
project_a cx_a.foreground().run_until_parked();
.condition(cx_a, |p, _| p.collaborators().is_empty()) project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
.await;
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
@ -2979,9 +2978,10 @@ async fn test_collaborating_with_completion(
}); });
let fake_language_server = fake_language_servers.next().await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap();
buffer_b cx_a.foreground().run_until_parked();
.condition(cx_b, |buffer, _| !buffer.completion_triggers().is_empty()) buffer_b.read_with(cx_b, |buffer, _| {
.await; assert!(!buffer.completion_triggers().is_empty())
});
// Type a completion trigger character as the guest. // Type a completion trigger character as the guest.
editor_b.update(cx_b, |editor, cx| { 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)) .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
buffer_a cx_a.foreground().run_until_parked();
.condition(cx_a, |buffer, _| buffer.text() == "fn main() { a. }") buffer_a.read_with(cx_a, |buffer, _| {
.await; assert_eq!(buffer.text(), "fn main() { a. }")
});
// Confirm a completion on the guest. // Confirm a completion on the guest.
editor_b editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
.condition(cx_b, |editor, _| editor.context_menu_visible())
.await;
editor_b.update(cx_b, |editor, cx| { editor_b.update(cx_b, |editor, cx| {
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); 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. // The additional edit is applied.
buffer_a cx_a.foreground().run_until_parked();
.condition(cx_a, |buffer, _| { buffer_a.read_with(cx_a, |buffer, _| {
buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }" assert_eq!(
}) buffer.text(),
.await; "use d::SomeTrait;\nfn main() { a.first_method() }"
buffer_b );
.condition(cx_b, |buffer, _| { });
buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }" buffer_b.read_with(cx_b, |buffer, _| {
}) assert_eq!(
.await; buffer.text(),
"use d::SomeTrait;\nfn main() { a.first_method() }"
);
});
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
@ -3134,9 +3136,8 @@ async fn test_reloading_buffer_manually(
assert!(buffer.is_dirty()); assert!(buffer.is_dirty());
assert!(!buffer.has_conflict()); assert!(!buffer.has_conflict());
}); });
buffer_a cx_a.foreground().run_until_parked();
.condition(cx_a, |buffer, _| buffer.text() == "let six = 6;") buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
.await;
client_a client_a
.fs .fs
@ -3147,12 +3148,9 @@ async fn test_reloading_buffer_manually(
) )
.await .await
.unwrap(); .unwrap();
buffer_a cx_a.foreground().run_until_parked();
.condition(cx_a, |buffer, _| buffer.has_conflict()) buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
.await; buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
buffer_b
.condition(cx_b, |buffer, _| buffer.has_conflict())
.await;
project_b project_b
.update(cx_b, |project, cx| { .update(cx_b, |project, cx| {
@ -4178,9 +4176,8 @@ async fn test_collaborating_with_code_actions(
cx, cx,
); );
}); });
editor_b cx_a.foreground().run_until_parked();
.condition(cx_b, |editor, _| editor.context_menu_visible()) editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
.await;
fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>(); fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
@ -5162,9 +5159,9 @@ async fn test_following(
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
"1.txt": "one", "1.txt": "one\none\none",
"2.txt": "two", "2.txt": "two\ntwo\ntwo",
"3.txt": "three", "3.txt": "three\nthree\nthree",
}), }),
) )
.await; .await;
@ -5263,11 +5260,60 @@ async fn test_following(
workspace_a.update(cx_a, |workspace, cx| { workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx) workspace.activate_item(&editor_a1, cx)
}); });
workspace_b deterministic.run_until_parked();
.condition(cx_b, |workspace, cx| { workspace_b.read_with(cx_b, |workspace, cx| {
workspace.active_item(cx).unwrap().id() == editor_b1.id() assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
}) });
.await;
// 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::<Editor>()
.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. // When client A navigates back and forth, client B does so as well.
workspace_a workspace_a
@ -5275,47 +5321,52 @@ async fn test_following(
workspace::Pane::go_back(workspace, None, cx) workspace::Pane::go_back(workspace, None, cx)
}) })
.await; .await;
workspace_b deterministic.run_until_parked();
.condition(cx_b, |workspace, cx| { workspace_b.read_with(cx_b, |workspace, cx| {
workspace.active_item(cx).unwrap().id() == editor_b2.id() 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; .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 workspace_a
.update(cx_a, |workspace, cx| { .update(cx_a, |workspace, cx| {
workspace::Pane::go_forward(workspace, None, cx) workspace::Pane::go_forward(workspace, None, cx)
}) })
.await; .await;
workspace_b deterministic.run_until_parked();
.condition(cx_b, |workspace, cx| { workspace_b.read_with(cx_b, |workspace, cx| {
workspace.active_item(cx).unwrap().id() == editor_b1.id() assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
}) });
.await;
// Changes to client A's editor are reflected on client B. // Changes to client A's editor are reflected on client B.
editor_a1.update(cx_a, |editor, cx| { editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
}); });
editor_b1 deterministic.run_until_parked();
.condition(cx_b, |editor, cx| { editor_b1.read_with(cx_b, |editor, cx| {
editor.selections.ranges(cx) == vec![1..1, 2..2] assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
}) });
.await;
editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
editor_b1 deterministic.run_until_parked();
.condition(cx_b, |editor, cx| editor.text(cx) == "TWO") editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
.await;
editor_a1.update(cx_a, |editor, cx| { editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([3..3])); editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
editor.set_scroll_position(vec2f(0., 100.), cx); editor.set_scroll_position(vec2f(0., 100.), cx);
}); });
editor_b1 deterministic.run_until_parked();
.condition(cx_b, |editor, cx| { editor_b1.read_with(cx_b, |editor, cx| {
editor.selections.ranges(cx) == vec![3..3] assert_eq!(editor.selections.ranges(cx), &[3..3]);
}) });
.await;
// After unfollowing, client B stops receiving updates from client A. // After unfollowing, client B stops receiving updates from client A.
workspace_b.update(cx_b, |workspace, cx| { workspace_b.update(cx_b, |workspace, cx| {
@ -5384,13 +5435,21 @@ async fn test_following(
.await .await
.unwrap(); .unwrap();
deterministic.run_until_parked(); deterministic.run_until_parked();
assert_eq!( workspace_a.read_with(cx_a, |workspace, cx| {
workspace_a.read_with(cx_a, |workspace, cx| workspace assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
.active_item(cx) });
.unwrap()
.id()), // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
editor_a1.id() 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 // Client B activates an external window again, and the previously-opened screen-sharing item
// gets activated. // gets activated.

View file

@ -164,7 +164,7 @@ impl ProjectDiagnosticsEditor {
editor.set_vertical_scroll_margin(5, cx); editor.set_vertical_scroll_margin(5, cx);
editor editor
}); });
cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event)) cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
.detach(); .detach();
let project = project_handle.read(cx); let project = project_handle.read(cx);

View file

@ -84,7 +84,7 @@ use std::{
pub use sum_tree::Bias; pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme}; use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt}; use util::{post_inc, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, Workspace, WorkspaceId}; use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display; use crate::git::diff_hunk_to_display;
@ -467,6 +467,7 @@ pub struct Editor {
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>, keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
input_enabled: bool, input_enabled: bool,
leader_replica_id: Option<u16>, leader_replica_id: Option<u16>,
remote_id: Option<ViewId>,
hover_state: HoverState, hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState, link_go_to_definition_state: LinkGoToDefinitionState,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
@ -1108,6 +1109,7 @@ impl Editor {
keymap_context_layers: Default::default(), keymap_context_layers: Default::default(),
input_enabled: true, input_enabled: true,
leader_replica_id: None, leader_replica_id: None,
remote_id: None,
hover_state: Default::default(), hover_state: Default::default(),
link_go_to_definition_state: Default::default(), link_go_to_definition_state: Default::default(),
_subscriptions: vec![ _subscriptions: vec![
@ -5883,25 +5885,36 @@ impl Editor {
fn on_buffer_event( fn on_buffer_event(
&mut self, &mut self,
_: ModelHandle<MultiBuffer>, _: ModelHandle<MultiBuffer>,
event: &language::Event, event: &multi_buffer::Event,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
language::Event::Edited => { multi_buffer::Event::Edited => {
self.refresh_active_diagnostics(cx); self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx); self.refresh_code_actions(cx);
cx.emit(Event::BufferEdited); cx.emit(Event::BufferEdited);
} }
language::Event::Reparsed => cx.emit(Event::Reparsed), multi_buffer::Event::ExcerptsAdded {
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged), buffer,
language::Event::Saved => cx.emit(Event::Saved), predecessor,
language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), excerpts,
language::Event::Reloaded => cx.emit(Event::TitleChanged), } => cx.emit(Event::ExcerptsAdded {
language::Event::Closed => cx.emit(Event::Closed), buffer: buffer.clone(),
language::Event::DiagnosticsUpdated => { 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); 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 { pub enum Event {
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
BufferEdited, BufferEdited,
Edited, Edited,
Reparsed, Reparsed,
@ -6093,8 +6114,12 @@ pub enum Event {
DirtyChanged, DirtyChanged,
Saved, Saved,
TitleChanged, TitleChanged,
SelectionsChanged { local: bool }, SelectionsChanged {
ScrollPositionChanged { local: bool }, local: bool,
},
ScrollPositionChanged {
local: bool,
},
Closed, Closed,
} }

View file

@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc, time::Instant};
use drag_and_drop::DragAndDrop; use drag_and_drop::DragAndDrop;
use futures::StreamExt; use futures::StreamExt;
use indoc::indoc; use indoc::indoc;
use rpc::PeerId;
use unindent::Unindent; use unindent::Unindent;
use super::*; use super::*;
@ -24,7 +25,7 @@ use util::{
}; };
use workspace::{ use workspace::{
item::{FollowableItem, ItemHandle}, item::{FollowableItem, ItemHandle},
NavigationEntry, Pane, NavigationEntry, Pane, ViewId,
}; };
#[gpui::test] #[gpui::test]
@ -41,7 +42,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event, event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged Event::Edited | Event::BufferEdited | Event::DirtyChanged
) { ) {
events.borrow_mut().push(("editor1", *event)); events.borrow_mut().push(("editor1", event.clone()));
} }
}) })
.detach(); .detach();
@ -56,7 +57,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event, event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged Event::Edited | Event::BufferEdited | Event::DirtyChanged
) { ) {
events.borrow_mut().push(("editor2", *event)); events.borrow_mut().push(("editor2", event.clone()));
} }
}) })
.detach(); .detach();
@ -4969,19 +4970,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
} }
#[gpui::test] #[gpui::test]
fn test_following(cx: &mut gpui::MutableAppContext) { async fn test_following(cx: &mut gpui::TestAppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); 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 buffer = project.update(cx, |project, cx| {
let buffer = project
let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); .create_buffer(&sample_text(16, 8, 'a'), None, cx)
let (_, follower) = cx.add_window( .unwrap();
WindowOptions { cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), });
..Default::default() let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
}, let (_, follower) = cx.update(|cx| {
|cx| build_editor(buffer.clone(), 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 is_still_following = Rc::new(RefCell::new(true));
let pending_update = Rc::new(RefCell::new(None)); 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.update(cx, |leader, cx| {
leader.change_selections(None, cx, |s| s.select_ranges([1..1])); leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
}); });
follower.update(cx, |follower, cx| { follower
follower .update(cx, |follower, cx| {
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); })
.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); assert_eq!(*is_still_following.borrow(), true);
// Update the scroll position only // Update the scroll position only
leader.update(cx, |leader, cx| { leader.update(cx, |leader, cx| {
leader.set_scroll_position(vec2f(1.5, 3.5), cx); leader.set_scroll_position(vec2f(1.5, 3.5), cx);
}); });
follower.update(cx, |follower, cx| { follower
follower .update(cx, |follower, cx| {
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); })
}); .await
.unwrap();
assert_eq!( assert_eq!(
follower.update(cx, |follower, cx| follower.scroll_position(cx)), follower.update(cx, |follower, cx| follower.scroll_position(cx)),
vec2f(1.5, 3.5) vec2f(1.5, 3.5)
); );
assert_eq!(*is_still_following.borrow(), true); 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.update(cx, |leader, cx| {
leader.change_selections(None, cx, |s| s.select_ranges([0..0])); leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
leader.request_autoscroll(Autoscroll::newest(), cx); leader.request_autoscroll(Autoscroll::newest(), cx);
leader.set_scroll_position(vec2f(1.5, 3.5), 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| { follower.update(cx, |follower, cx| {
let initial_scroll_position = follower.scroll_position(cx); assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0));
follower assert_eq!(follower.selections.ranges(cx), vec![0..0]);
.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.read(cx).selections.ranges(cx), vec![0..0]);
assert_eq!(*is_still_following.borrow(), true); assert_eq!(*is_still_following.borrow(), true);
// Creating a pending selection that precedes another selection // 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.change_selections(None, cx, |s| s.select_ranges([1..1]));
leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
}); });
follower.update(cx, |follower, cx| { follower
follower .update(cx, |follower, cx| {
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); })
.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); assert_eq!(*is_still_following.borrow(), true);
// Extend the pending selection so that it surrounds another selection // Extend the pending selection so that it surrounds another selection
leader.update(cx, |leader, cx| { leader.update(cx, |leader, cx| {
leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
}); });
follower.update(cx, |follower, cx| { follower
follower .update(cx, |follower, cx| {
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); })
.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 // Scrolling locally breaks the follow
follower.update(cx, |follower, cx| { follower.update(cx, |follower, cx| {
@ -5087,6 +5108,165 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
assert_eq!(*is_still_following.borrow(), false); 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] #[test]
fn test_combine_syntax_and_fuzzy_match_highlights() { fn test_combine_syntax_and_fuzzy_match_highlights() {
let string = "abcdefghijklmnop"; let string = "abcdefghijklmnop";

View file

@ -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 anyhow::{anyhow, Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, 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 language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath}; use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view}; use rpc::proto::{self, update_view};
@ -13,97 +22,136 @@ use std::{
borrow::Cow, borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},
fmt::Write, fmt::Write,
iter,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use text::Selection; use text::Selection;
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::item::FollowableItemHandle;
use workspace::{ use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId, ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, 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,
}; };
pub const MAX_TAB_TITLE_LEN: usize = 24; pub const MAX_TAB_TITLE_LEN: usize = 24;
impl FollowableItem for Editor { impl FollowableItem for Editor {
fn remote_id(&self) -> Option<ViewId> {
self.remote_id
}
fn from_state_proto( fn from_state_proto(
pane: ViewHandle<workspace::Pane>, pane: ViewHandle<workspace::Pane>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>, state: &mut Option<proto::view::Variant>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Option<Task<Result<ViewHandle<Self>>>> { ) -> Option<Task<Result<ViewHandle<Self>>>> {
let state = if matches!(state, Some(proto::view::Variant::Editor(_))) { let Some(proto::view::Variant::Editor(_)) = state else { return None };
if let Some(proto::view::Variant::Editor(state)) = state.take() { let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
state
} else {
unreachable!()
}
} else {
return None;
};
let buffer = project.update(cx, |project, cx| { let client = project.read(cx).client();
project.open_buffer_by_id(state.buffer_id, cx) let replica_id = project.read(cx).replica_id();
let buffer_ids = state
.excerpts
.iter()
.map(|excerpt| excerpt.buffer_id)
.collect::<HashSet<_>>();
let buffers = project.update(cx, |project, cx| {
buffer_ids
.iter()
.map(|id| project.open_buffer_by_id(*id, cx))
.collect::<Vec<_>>()
}); });
Some(cx.spawn(|mut cx| async move { Some(cx.spawn(|mut cx| async move {
let buffer = buffer.await?; let mut buffers = futures::future::try_join_all(buffers).await?;
let editor = pane let editor = pane.read_with(&cx, |pane, cx| {
.read_with(&cx, |pane, cx| { let mut editors = pane.items_of_type::<Self>();
pane.items_of_type::<Self>().find(|editor| { editors.find(|editor| {
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) 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| { editor.update(&mut cx, |editor, cx| {
let excerpt_id; editor.remote_id = Some(remote_id);
let buffer_id; let buffer = editor.buffer.read(cx).read(cx);
{
let buffer = editor.buffer.read(cx).read(cx);
let singleton = buffer.as_singleton().unwrap();
excerpt_id = singleton.0.clone();
buffer_id = singleton.1;
}
let selections = state let selections = state
.selections .selections
.into_iter() .into_iter()
.map(|selection| { .map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection) deserialize_selection(&buffer, selection)
.ok_or_else(|| anyhow!("invalid selection")) .ok_or_else(|| anyhow!("invalid selection"))
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
let scroll_top_anchor = state
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
drop(buffer);
if !selections.is_empty() { if !selections.is_empty() {
editor.set_selections_from_remote(selections, cx); 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( editor.set_scroll_anchor_remote(
ScrollAnchor { ScrollAnchor {
top_anchor: Anchor { top_anchor: scroll_top_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"))?,
},
offset: vec2f(state.scroll_x, state.scroll_y), offset: vec2f(state.scroll_x, state.scroll_y),
}, },
cx, cx,
); );
} }
Ok::<_, anyhow::Error>(()) anyhow::Ok(())
})?; })?;
Ok(editor) Ok(editor)
})) }))
} }
@ -134,13 +182,32 @@ impl FollowableItem for Editor {
} }
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> { fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
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 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 { Some(proto::view::Variant::Editor(proto::view::Editor {
buffer_id, singleton: buffer.is_singleton(),
scroll_top_anchor: Some(language::proto::serialize_anchor( title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
&scroll_anchor.top_anchor.text_anchor, excerpts,
)), scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)),
scroll_x: scroll_anchor.offset.x(), scroll_x: scroll_anchor.offset.x(),
scroll_y: scroll_anchor.offset.y(), scroll_y: scroll_anchor.offset.y(),
selections: self selections: self
@ -156,18 +223,43 @@ impl FollowableItem for Editor {
&self, &self,
event: &Self::Event, event: &Self::Event,
update: &mut Option<proto::update_view::Variant>, update: &mut Option<proto::update_view::Variant>,
_: &AppContext, cx: &AppContext,
) -> bool { ) -> bool {
let update = let update =
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default())); update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
match update { match update {
proto::update_view::Variant::Editor(update) => match event { 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 { .. } => { Event::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor(); let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(language::proto::serialize_anchor( update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor));
&scroll_anchor.top_anchor.text_anchor,
));
update.scroll_x = scroll_anchor.offset.x(); update.scroll_x = scroll_anchor.offset.x();
update.scroll_y = scroll_anchor.offset.y(); update.scroll_y = scroll_anchor.offset.y();
true true
@ -189,45 +281,98 @@ impl FollowableItem for Editor {
fn apply_update_proto( fn apply_update_proto(
&mut self, &mut self,
project: &ModelHandle<Project>,
message: update_view::Variant, message: update_view::Variant,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Result<()> { ) -> Task<Result<()>> {
match message { let update_view::Variant::Editor(message) = message;
update_view::Variant::Editor(message) => { let multibuffer = self.buffer.read(cx);
let buffer = self.buffer.read(cx); let multibuffer = multibuffer.read(cx);
let buffer = buffer.read(cx);
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
let excerpt_id = excerpt_id.clone();
drop(buffer);
let selections = message let buffer_ids = message
.selections .inserted_excerpts
.into_iter() .iter()
.filter_map(|selection| { .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
deserialize_selection(&excerpt_id, buffer_id, selection) .collect::<HashSet<_>>();
})
.collect::<Vec<_>>(); let mut removals = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
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::<Vec<_>>()
});
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() { if !selections.is_empty() {
self.set_selections_from_remote(selections, cx); this.set_selections_from_remote(selections, cx);
self.request_autoscroll_remotely(Autoscroll::newest(), cx); this.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = message.scroll_top_anchor { } else if let Some(anchor) = scroll_top_anchor {
self.set_scroll_anchor_remote( this.set_scroll_anchor_remote(ScrollAnchor {
ScrollAnchor { top_anchor: anchor,
top_anchor: Anchor { offset: vec2f(message.scroll_x, message.scroll_y)
buffer_id: Some(buffer_id), }, cx);
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,
);
} }
} });
} Ok(())
Ok(()) })
} }
fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { 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<language::Anchor>,
) -> Option<proto::Excerpt> {
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<Anchor>) -> proto::Selection { fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection { proto::Selection {
id: selection.id as u64, id: selection.id as u64,
start: Some(language::proto::serialize_anchor( start: Some(serialize_anchor(&selection.start)),
&selection.start.text_anchor, end: Some(serialize_anchor(&selection.end)),
)),
end: Some(language::proto::serialize_anchor(
&selection.end.text_anchor,
)),
reversed: selection.reversed, 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<ExcerptRange<language::Anchor>> {
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( fn deserialize_selection(
excerpt_id: &ExcerptId, buffer: &MultiBufferSnapshot,
buffer_id: usize,
selection: proto::Selection, selection: proto::Selection,
) -> Option<Selection<Anchor>> { ) -> Option<Selection<Anchor>> {
Some(Selection { Some(Selection {
id: selection.id as usize, id: selection.id as usize,
start: Anchor { start: deserialize_anchor(buffer, selection.start?)?,
buffer_id: Some(buffer_id), end: deserialize_anchor(buffer, selection.end?)?,
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?)?,
},
reversed: selection.reversed, reversed: selection.reversed,
goal: SelectionGoal::None, goal: SelectionGoal::None,
}) })
} }
fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
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 { impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool { fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Ok(data) = data.downcast::<NavigationData>() { if let Ok(data) = data.downcast::<NavigationData>() {

View file

@ -9,9 +9,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion; pub use language::Completion;
use language::{ use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, DiagnosticEntry, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _,
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, ToPointUtf16 as _, TransactionId, Unclipped,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
@ -50,6 +50,26 @@ pub struct MultiBuffer {
title: Option<String>, title: Option<String>,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
Edited,
Reloaded,
Reparsed,
Saved,
FileHandleChanged,
Closed,
DirtyChanged,
DiagnosticsUpdated,
}
#[derive(Clone)] #[derive(Clone)]
struct History { struct History {
next_transaction_id: TransactionId, next_transaction_id: TransactionId,
@ -833,6 +853,30 @@ impl MultiBuffer {
) -> Vec<ExcerptId> ) -> Vec<ExcerptId>
where where
O: text::ToOffset, 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<O>(
&mut self,
prev_excerpt_id: ExcerptId,
buffer: ModelHandle<Buffer>,
ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
cx: &mut ModelContext<Self>,
) where
O: text::ToOffset,
{ {
assert_eq!(self.history.transaction_depth, 0); assert_eq!(self.history.transaction_depth, 0);
let mut ranges = ranges.into_iter().peekable(); let mut ranges = ranges.into_iter().peekable();
@ -858,7 +902,7 @@ impl MultiBuffer {
cx.observe(&buffer, |_, _, cx| cx.notify()), cx.observe(&buffer, |_, _, cx| cx.notify()),
cx.subscribe(&buffer, Self::on_buffer_event), cx.subscribe(&buffer, Self::on_buffer_event),
], ],
buffer, buffer: buffer.clone(),
}); });
let mut snapshot = self.snapshot.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut();
@ -883,8 +927,8 @@ impl MultiBuffer {
Locator::max() Locator::max()
}; };
let mut ids = Vec::new(); let mut excerpts = Vec::new();
while let Some(range) = ranges.next() { while let Some((id, range)) = ranges.next() {
let locator = Locator::between(&prev_locator, &next_locator); let locator = Locator::between(&prev_locator, &next_locator);
if let Err(ix) = buffer_state.excerpts.binary_search(&locator) { if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
buffer_state.excerpts.insert(ix, locator.clone()); buffer_state.excerpts.insert(ix, locator.clone());
@ -897,7 +941,10 @@ impl MultiBuffer {
..buffer_snapshot.anchor_after(&primary.end) ..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( let excerpt = Excerpt::new(
id, id,
locator.clone(), locator.clone(),
@ -909,7 +956,6 @@ impl MultiBuffer {
new_excerpts.push(excerpt, &()); new_excerpts.push(excerpt, &());
prev_locator = locator.clone(); prev_locator = locator.clone();
new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &()); new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
ids.push(id);
} }
let edit_end = new_excerpts.summary().text.len; let edit_end = new_excerpts.summary().text.len;
@ -929,12 +975,17 @@ impl MultiBuffer {
new: edit_start..edit_end, new: edit_start..edit_end,
}]); }]);
cx.emit(Event::Edited); cx.emit(Event::Edited);
cx.emit(Event::ExcerptsAdded {
buffer,
predecessor: prev_excerpt_id,
excerpts,
});
cx.notify(); cx.notify();
ids
} }
pub fn clear(&mut self, cx: &mut ModelContext<Self>) { pub fn clear(&mut self, cx: &mut ModelContext<Self>) {
self.sync(cx); self.sync(cx);
let ids = self.excerpt_ids();
self.buffers.borrow_mut().clear(); self.buffers.borrow_mut().clear();
let mut snapshot = self.snapshot.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut();
let prev_len = snapshot.len(); let prev_len = snapshot.len();
@ -948,6 +999,7 @@ impl MultiBuffer {
new: 0..0, new: 0..0,
}]); }]);
cx.emit(Event::Edited); cx.emit(Event::Edited);
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify(); cx.notify();
} }
@ -1071,12 +1123,14 @@ impl MultiBuffer {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.sync(cx); self.sync(cx);
let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
let mut buffers = self.buffers.borrow_mut(); let mut buffers = self.buffers.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut();
let mut new_excerpts = SumTree::new(); let mut new_excerpts = SumTree::new();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
let mut edits = Vec::new(); 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() { while let Some(excerpt_id) = excerpt_ids.next() {
// Seek to the next excerpt to remove, preserving any preceding excerpts. // Seek to the next excerpt to remove, preserving any preceding excerpts.
@ -1143,6 +1197,7 @@ impl MultiBuffer {
self.subscriptions.publish_mut(edits); self.subscriptions.publish_mut(edits);
cx.emit(Event::Edited); cx.emit(Event::Edited);
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify(); cx.notify();
} }
@ -1165,10 +1220,22 @@ impl MultiBuffer {
fn on_buffer_event( fn on_buffer_event(
&mut self, &mut self,
_: ModelHandle<Buffer>, _: ModelHandle<Buffer>,
event: &Event, event: &language::Event,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
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<ModelHandle<Buffer>> { pub fn all_buffers(&self) -> HashSet<ModelHandle<Buffer>> {
@ -1604,7 +1671,7 @@ impl MultiBuffer {
} }
impl Entity for MultiBuffer { impl Entity for MultiBuffer {
type Event = language::Event; type Event = Event;
} }
impl MultiBufferSnapshot { impl MultiBufferSnapshot {
@ -2450,6 +2517,14 @@ impl MultiBufferSnapshot {
} }
} }
pub fn excerpts(
&self,
) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<text::Anchor>)> {
self.excerpts
.iter()
.map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
}
pub fn excerpt_boundaries_in_range<R, T>( pub fn excerpt_boundaries_in_range<R, T>(
&self, &self,
range: R, range: R,
@ -2746,6 +2821,10 @@ impl MultiBufferSnapshot {
} }
} }
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
Some(self.excerpt(excerpt_id)?.buffer_id)
}
fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> { fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(); let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
let locator = self.excerpt_locator_for_id(excerpt_id); let locator = self.excerpt_locator_for_id(excerpt_id);
@ -3080,6 +3159,14 @@ impl ExcerptId {
Self(usize::MAX) 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 { pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
let a = snapshot.excerpt_locator_for_id(*self); let a = snapshot.excerpt_locator_for_id(*self);
let b = snapshot.excerpt_locator_for_id(*other); let b = snapshot.excerpt_locator_for_id(*other);
@ -3468,7 +3555,7 @@ mod tests {
use util::test::sample_text; use util::test::sample_text;
#[gpui::test] #[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 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)); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
@ -3495,7 +3582,7 @@ mod tests {
} }
#[gpui::test] #[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 host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx));
let guest_buffer = cx.add_model(|cx| { let guest_buffer = cx.add_model(|cx| {
let state = host_buffer.read(cx).to_proto(); let state = host_buffer.read(cx).to_proto();
@ -3526,7 +3613,7 @@ mod tests {
} }
#[gpui::test] #[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_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 buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@ -3535,7 +3622,9 @@ mod tests {
multibuffer.update(cx, |_, cx| { multibuffer.update(cx, |_, cx| {
let events = events.clone(); let events = events.clone();
cx.subscribe(&multibuffer, move |_, _, event, _| { cx.subscribe(&multibuffer, move |_, _, event, _| {
events.borrow_mut().push(event.clone()) if let Event::Edited = event {
events.borrow_mut().push(event.clone())
}
}) })
.detach(); .detach();
}); });
@ -3748,7 +3837,84 @@ mod tests {
} }
#[gpui::test] #[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 buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
@ -3784,7 +3950,7 @@ mod tests {
} }
#[gpui::test] #[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 multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let snapshot = multibuffer.read(cx).snapshot(cx); let snapshot = multibuffer.read(cx).snapshot(cx);
@ -3872,9 +4038,7 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts( fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut MutableAppContext) {
cx: &mut MutableAppContext,
) {
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); 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 buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

View file

@ -9,7 +9,7 @@ use rpc::proto;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use text::*; use text::*;
pub use proto::{BufferState, Operation, SelectionSet}; pub use proto::{BufferState, Operation};
pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding { pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding {
match message { match message {
@ -122,8 +122,14 @@ pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto:
pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection { pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection { proto::Selection {
id: selection.id as u64, id: selection.id as u64,
start: Some(serialize_anchor(&selection.start)), start: Some(proto::EditorAnchor {
end: Some(serialize_anchor(&selection.end)), 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, reversed: selection.reversed,
} }
} }
@ -229,8 +235,8 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
.filter_map(|selection| { .filter_map(|selection| {
Some(Selection { Some(Selection {
id: selection.id as usize, id: selection.id as usize,
start: deserialize_anchor(selection.start?)?, start: deserialize_anchor(selection.start?.anchor?)?,
end: deserialize_anchor(selection.end?)?, end: deserialize_anchor(selection.end?.anchor?)?,
reversed: selection.reversed, reversed: selection.reversed,
goal: SelectionGoal::None, goal: SelectionGoal::None,
}) })
@ -321,8 +327,8 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> { pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
Some(Selection { Some(Selection {
id: selection.id as usize, id: selection.id as usize,
start: deserialize_anchor(selection.start?)?, start: deserialize_anchor(selection.start?.anchor?)?,
end: deserialize_anchor(selection.end?)?, end: deserialize_anchor(selection.end?.anchor?)?,
reversed: selection.reversed, reversed: selection.reversed,
goal: SelectionGoal::None, goal: SelectionGoal::None,
}) })

View file

@ -798,7 +798,7 @@ message Follow {
} }
message FollowResponse { message FollowResponse {
optional uint64 active_view_id = 1; optional ViewId active_view_id = 1;
repeated View views = 2; repeated View views = 2;
} }
@ -826,13 +826,18 @@ message GetPrivateUserInfoResponse {
// Entities // Entities
message ViewId {
uint32 creator = 1;
uint64 id = 2;
}
message UpdateActiveView { message UpdateActiveView {
optional uint64 id = 1; optional ViewId id = 1;
optional uint32 leader_id = 2; optional uint32 leader_id = 2;
} }
message UpdateView { message UpdateView {
uint64 id = 1; ViewId id = 1;
optional uint32 leader_id = 2; optional uint32 leader_id = 2;
oneof variant { oneof variant {
@ -840,15 +845,17 @@ message UpdateView {
} }
message Editor { message Editor {
repeated Selection selections = 1; repeated ExcerptInsertion inserted_excerpts = 1;
Anchor scroll_top_anchor = 2; repeated uint64 deleted_excerpts = 2;
float scroll_x = 3; repeated Selection selections = 3;
float scroll_y = 4; EditorAnchor scroll_top_anchor = 4;
float scroll_x = 5;
float scroll_y = 6;
} }
} }
message View { message View {
uint64 id = 1; ViewId id = 1;
optional uint32 leader_id = 2; optional uint32 leader_id = 2;
oneof variant { oneof variant {
@ -856,11 +863,13 @@ message View {
} }
message Editor { message Editor {
uint64 buffer_id = 1; bool singleton = 1;
repeated Selection selections = 2; optional string title = 2;
Anchor scroll_top_anchor = 3; repeated Excerpt excerpts = 3;
float scroll_x = 4; repeated Selection selections = 4;
float scroll_y = 5; EditorAnchor scroll_top_anchor = 5;
float scroll_x = 6;
float scroll_y = 7;
} }
} }
@ -913,21 +922,18 @@ enum LineEnding {
Windows = 1; 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 { message Selection {
uint64 id = 1; uint64 id = 1;
Anchor start = 2; EditorAnchor start = 2;
Anchor end = 3; EditorAnchor end = 3;
bool reversed = 4; bool reversed = 4;
} }
message EditorAnchor {
uint64 excerpt_id = 1;
Anchor anchor = 2;
}
enum CursorShape { enum CursorShape {
CursorBar = 0; CursorBar = 0;
CursorBlock = 1; CursorBlock = 1;
@ -935,6 +941,20 @@ enum CursorShape {
CursorHollow = 3; 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 { message Anchor {
uint32 replica_id = 1; uint32 replica_id = 1;
uint32 local_timestamp = 2; uint32 local_timestamp = 2;

View file

@ -402,7 +402,7 @@ impl ProjectSearchView {
}); });
// Subcribe to query_editor in order to reraise editor events for workspace item activation purposes // Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
cx.subscribe(&query_editor, |_, _, event, cx| { cx.subscribe(&query_editor, |_, _, event, cx| {
cx.emit(ViewEvent::EditorEvent(*event)) cx.emit(ViewEvent::EditorEvent(event.clone()))
}) })
.detach(); .detach();
@ -419,7 +419,7 @@ impl ProjectSearchView {
this.update_match_index(cx); this.update_match_index(cx);
} }
// Reraise editor events for workspace item activation purposes // Reraise editor events for workspace item activation purposes
cx.emit(ViewEvent::EditorEvent(*event)); cx.emit(ViewEvent::EditorEvent(event.clone()));
}) })
.detach(); .detach();

View file

@ -1496,6 +1496,10 @@ impl BufferSnapshot {
&self.visible_text &self.visible_text
} }
pub fn remote_id(&self) -> u64 {
self.remote_id
}
pub fn replica_id(&self) -> ReplicaId { pub fn replica_id(&self) -> ReplicaId {
self.replica_id self.replica_id
} }

View file

@ -5,12 +5,15 @@ use std::{
fmt, fmt,
path::PathBuf, path::PathBuf,
rc::Rc, rc::Rc,
sync::atomic::{AtomicBool, Ordering}, sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration, time::Duration,
}; };
use anyhow::Result; use anyhow::Result;
use client::proto; use client::{proto, Client};
use gpui::{ use gpui::{
AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext, AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle, ViewHandle, WeakViewHandle,
@ -23,7 +26,8 @@ use util::ResultExt;
use crate::{ use crate::{
pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction, pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId, FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
WorkspaceId,
}; };
#[derive(Eq, PartialEq, Hash)] #[derive(Eq, PartialEq, Hash)]
@ -278,7 +282,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
if let Some(message) = followed_item.to_state_proto(cx) { if let Some(message) = followed_item.to_state_proto(cx) {
workspace.update_followers( workspace.update_followers(
proto::update_followers::Variant::CreateView(proto::View { 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), variant: Some(message),
leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
}), }),
@ -332,7 +338,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
this.update_followers( this.update_followers(
proto::update_followers::Variant::UpdateView( proto::update_followers::Variant::UpdateView(
proto::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(), variant: pending_update.borrow_mut().take(),
leader_id: leader_id.map(|id| id.0), leader_id: leader_id.map(|id| id.0),
}, },
@ -584,10 +592,12 @@ pub trait ProjectItem: Item {
} }
pub trait FollowableItem: Item { pub trait FollowableItem: Item {
fn remote_id(&self) -> Option<ViewId>;
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>; fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
fn from_state_proto( fn from_state_proto(
pane: ViewHandle<Pane>, pane: ViewHandle<Pane>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
id: ViewId,
state: &mut Option<proto::view::Variant>, state: &mut Option<proto::view::Variant>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Option<Task<Result<ViewHandle<Self>>>>; ) -> Option<Task<Result<ViewHandle<Self>>>>;
@ -599,15 +609,17 @@ pub trait FollowableItem: Item {
) -> bool; ) -> bool;
fn apply_update_proto( fn apply_update_proto(
&mut self, &mut self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant, message: proto::update_view::Variant,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Result<()>; ) -> Task<Result<()>>;
fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>); fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
} }
pub trait FollowableItemHandle: ItemHandle { pub trait FollowableItemHandle: ItemHandle {
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext); fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>; fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto( fn add_event_to_update_proto(
@ -618,13 +630,23 @@ pub trait FollowableItemHandle: ItemHandle {
) -> bool; ) -> bool;
fn apply_update_proto( fn apply_update_proto(
&self, &self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant, message: proto::update_view::Variant,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Result<()>; ) -> Task<Result<()>>;
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
} }
impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> { impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
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<u16>, cx: &mut MutableAppContext) { fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
self.update(cx, |this, cx| { self.update(cx, |this, cx| {
this.set_leader_replica_id(leader_replica_id, cx) this.set_leader_replica_id(leader_replica_id, cx)
@ -650,10 +672,11 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
fn apply_update_proto( fn apply_update_proto(
&self, &self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant, message: proto::update_view::Variant,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Result<()> { ) -> Task<Result<()>> {
self.update(cx, |this, cx| this.apply_update_proto(message, cx)) self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
} }
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {

View file

@ -14,23 +14,18 @@ pub mod sidebar;
mod status_bar; mod status_bar;
mod toolbar; mod toolbar;
use std::{ use anyhow::{anyhow, Result};
any::TypeId,
borrow::Cow,
future::Future,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use anyhow::{anyhow, Context, Result};
use call::ActiveCall; use call::ActiveCall;
use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockDefaultItemFactory, ToggleDockButton}; use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
use drag_and_drop::DragAndDrop; use drag_and_drop::DragAndDrop;
use fs::{self, Fs}; use fs::{self, Fs};
use futures::{channel::oneshot, FutureExt, StreamExt}; use futures::{
channel::{mpsc, oneshot},
future::try_join_all,
FutureExt, StreamExt,
};
use gpui::{ use gpui::{
actions, actions,
elements::*, elements::*,
@ -42,7 +37,19 @@ use gpui::{
}; };
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use language::LanguageRegistry; 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 log::{error, warn};
use notifications::NotificationHandle; use notifications::NotificationHandle;
pub use pane::*; pub use pane::*;
@ -64,11 +71,6 @@ use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::ResultExt; use util::ResultExt;
use crate::{
notifications::simple_message_notification::{MessageNotification, OsOpen},
persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId); pub struct RemoveWorktreeFromProject(pub WorktreeId);
@ -316,6 +318,7 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
type FollowableItemBuilder = fn( type FollowableItemBuilder = fn(
ViewHandle<Pane>, ViewHandle<Pane>,
ModelHandle<Project>, ModelHandle<Project>,
ViewId,
&mut Option<proto::view::Variant>, &mut Option<proto::view::Variant>,
&mut MutableAppContext, &mut MutableAppContext,
) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>; ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
@ -331,8 +334,8 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
builders.insert( builders.insert(
TypeId::of::<I>(), TypeId::of::<I>(),
( (
|pane, project, state, cx| { |pane, project, id, state, cx| {
I::from_state_proto(pane, project, state, cx).map(|task| { I::from_state_proto(pane, project, id, state, cx).map(|task| {
cx.foreground() cx.foreground()
.spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
}) })
@ -458,25 +461,6 @@ impl DelayedDebouncedEditAction {
} }
} }
#[derive(Default)]
struct LeaderState {
followers: HashSet<PeerId>,
}
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)]
struct FollowerState {
active_view_id: Option<u64>,
items_by_leader_view_id: HashMap<u64, FollowerItem>,
}
#[derive(Debug)]
enum FollowerItem {
Loading(Vec<proto::update_view::Variant>),
Loaded(Box<dyn FollowableItemHandle>),
}
pub enum Event { pub enum Event {
DockAnchorChanged, DockAnchorChanged,
PaneAdded(ViewHandle<Pane>), PaneAdded(ViewHandle<Pane>),
@ -507,10 +491,31 @@ pub struct Workspace {
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>, last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool, window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>, active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId, database_id: WorkspaceId,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: 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<PeerId>,
}
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)]
struct FollowerState {
active_view_id: Option<ViewId>,
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
}
impl Workspace { impl Workspace {
pub fn new( pub fn new(
serialized_workspace: Option<SerializedWorkspace>, serialized_workspace: Option<SerializedWorkspace>,
@ -576,10 +581,24 @@ impl Workspace {
}) })
} }
}); });
let handle = cx.handle(); let handle = cx.handle();
let weak_handle = cx.weak_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())); cx.emit_global(WorkspaceCreated(weak_handle.clone()));
let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left)); let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
@ -637,6 +656,8 @@ impl Workspace {
active_call, active_call,
database_id: workspace_id, database_id: workspace_id,
_observe_current_user, _observe_current_user,
_apply_leader_updates,
leader_updates_tx,
}; };
this.project_remote_id_changed(project.read(cx).remote_id(), cx); this.project_remote_id_changed(project.read(cx).remote_id(), cx);
cx.defer(|this, cx| this.update_window_title(cx)); cx.defer(|this, cx| this.update_window_title(cx));
@ -1440,7 +1461,11 @@ impl Workspace {
self.update_followers( self.update_followers(
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { 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), leader_id: self.leader_for_pane(&pane).map(|id| id.0),
}), }),
cx, cx,
@ -1586,9 +1611,7 @@ impl Workspace {
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
for state in states_by_pane.into_values() { for state in states_by_pane.into_values() {
for item in state.items_by_leader_view_id.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) .get_mut(&leader_id)
.and_then(|states_by_pane| states_by_pane.get_mut(&pane)) .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
.ok_or_else(|| anyhow!("following interrupted"))?; .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>(()) Ok::<_, anyhow::Error>(())
})?; })?;
Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx) Self::add_views_from_leader(
.await?; this.clone(),
leader_id,
vec![pane],
response.views,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
} }
Ok(()) Ok(())
})) }))
@ -1681,9 +1711,7 @@ impl Workspace {
let leader_id = *leader_id; let leader_id = *leader_id;
if let Some(state) = states_by_pane.remove(pane) { if let Some(state) = states_by_pane.remove(pane) {
for (_, item) in state.items_by_leader_view_id { 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() { if states_by_pane.is_empty() {
@ -1874,14 +1902,18 @@ impl Workspace {
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<proto::FollowResponse> { ) -> Result<proto::FollowResponse> {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let client = &this.client;
this.leader_state this.leader_state
.followers .followers
.insert(envelope.original_sender_id()?); .insert(envelope.original_sender_id()?);
let active_view_id = this let active_view_id = this.active_item(cx).and_then(|i| {
.active_item(cx) Some(
.and_then(|i| i.to_followable_item_handle(cx)) i.to_followable_item_handle(cx)?
.map(|i| i.id() as u64); .remote_id(client, cx)?
.to_proto(),
)
});
Ok(proto::FollowResponse { Ok(proto::FollowResponse {
active_view_id, active_view_id,
views: this views: this
@ -1892,11 +1924,11 @@ impl Workspace {
pane.read(cx).items().filter_map({ pane.read(cx).items().filter_map({
let cx = &cx; let cx = &cx;
move |item| { move |item| {
let id = item.id() as u64;
let item = item.to_followable_item_handle(cx)?; let item = item.to_followable_item_handle(cx)?;
let id = item.remote_id(client, cx)?.to_proto();
let variant = item.to_state_proto(cx)?; let variant = item.to_state_proto(cx)?;
Some(proto::View { Some(proto::View {
id, id: Some(id),
leader_id, leader_id,
variant: Some(variant), variant: Some(variant),
}) })
@ -1926,45 +1958,58 @@ impl Workspace {
this: ViewHandle<Self>, this: ViewHandle<Self>,
envelope: TypedEnvelope<proto::UpdateFollowers>, envelope: TypedEnvelope<proto::UpdateFollowers>,
_: Arc<Client>, _: Arc<Client>,
mut cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<()> { ) -> Result<()> {
let leader_id = envelope.original_sender_id()?; let leader_id = envelope.original_sender_id()?;
match envelope this.read_with(&cx, |this, _| {
.payload this.leader_updates_tx
.variant .unbounded_send((leader_id, envelope.payload))
.ok_or_else(|| anyhow!("invalid update"))? })?;
{ Ok(())
}
async fn process_leader_update(
this: ViewHandle<Self>,
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) => { proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
this.update(&mut cx, |this, cx| { this.update(cx, |this, _| {
this.update_leader_state(leader_id, cx, |state, _| { if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
state.active_view_id = update_active_view.id; for state in state.values_mut() {
}); state.active_view_id =
Ok::<_, anyhow::Error>(()) update_active_view.id.clone().map(ViewId::from_proto);
}) }
}
});
} }
proto::update_followers::Variant::UpdateView(update_view) => { proto::update_followers::Variant::UpdateView(update_view) => {
this.update(&mut cx, |this, cx| { let variant = update_view
let variant = update_view .variant
.variant .ok_or_else(|| anyhow!("missing update view variant"))?;
.ok_or_else(|| anyhow!("missing update view variant"))?; let id = update_view
this.update_leader_state(leader_id, cx, |state, cx| { .id
let variant = variant.clone(); .ok_or_else(|| anyhow!("missing update view id"))?;
match state let mut tasks = Vec::new();
.items_by_leader_view_id this.update(cx, |this, cx| {
.entry(update_view.id) let project = this.project.clone();
.or_insert(FollowerItem::Loading(Vec::new())) if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
{ for state in state.values_mut() {
FollowerItem::Loaded(item) => { if let Some(item) = state
item.apply_update_proto(variant, cx).log_err(); .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) => { 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 this.follower_states_by_leader
.get(&leader_id) .get(&leader_id)
.into_iter() .into_iter()
@ -1972,13 +2017,10 @@ impl Workspace {
.cloned() .cloned()
.collect() .collect()
}); });
Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx) Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
.await?;
Ok(())
} }
} }
.log_err(); this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
Ok(()) Ok(())
} }
@ -2011,16 +2053,19 @@ impl Workspace {
let mut item_tasks = Vec::new(); let mut item_tasks = Vec::new();
let mut leader_view_ids = Vec::new(); let mut leader_view_ids = Vec::new();
for view in &views { for view in &views {
let Some(id) = &view.id else { continue };
let id = ViewId::from_proto(id.clone());
let mut variant = view.variant.clone(); let mut variant = view.variant.clone();
if variant.is_none() { if variant.is_none() {
Err(anyhow!("missing variant"))?; Err(anyhow!("missing variant"))?;
} }
for build_item in &item_builders { for build_item in &item_builders {
let task = let task = cx.update(|cx| {
cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); build_item(pane.clone(), project.clone(), id, &mut variant, cx)
});
if let Some(task) = task { if let Some(task) = task {
item_tasks.push(task); item_tasks.push(task);
leader_view_ids.push(view.id); leader_view_ids.push(id);
break; break;
} else { } else {
assert!(variant.is_some()); assert!(variant.is_some());
@ -2041,29 +2086,12 @@ impl Workspace {
for (id, item) in leader_view_ids.into_iter().zip(items) { for (id, item) in leader_view_ids.into_iter().zip(items) {
item.set_leader_replica_id(Some(replica_id), cx); item.set_leader_replica_id(Some(replica_id), cx);
match state.items_by_leader_view_id.entry(id) { state.items_by_leader_view_id.insert(id, item);
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));
}
}
} }
Some(()) Some(())
}); });
} }
this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
Ok(()) Ok(())
} }
@ -2097,23 +2125,6 @@ impl Workspace {
}) })
} }
fn update_leader_state(
&mut self,
leader_id: PeerId,
cx: &mut ViewContext<Self>,
mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
) {
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<Self>) -> Option<()> { fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
cx.notify(); cx.notify();
@ -2126,7 +2137,7 @@ impl Workspace {
call::ParticipantLocation::SharedProject { project_id } => { call::ParticipantLocation::SharedProject { project_id } => {
if Some(project_id) == self.project.read(cx).remote_id() { if Some(project_id) == self.project.read(cx).remote_id() {
for (pane, state) in self.follower_states_by_leader.get(&leader_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 .active_view_id
.and_then(|id| state.items_by_leader_view_id.get(&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 { pub trait WorkspaceHandle {
fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>; fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
} }

View file

@ -15,12 +15,16 @@ use editor::{Editor, MultiBuffer};
use gpui::{ use gpui::{
actions, actions,
geometry::vector::vec2f, geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
impl_actions, impl_actions,
platform::{WindowBounds, WindowOptions}, platform::{WindowBounds, WindowOptions},
AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind, AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
}; };
use language::Rope; use language::Rope;
use lazy_static::lazy_static;
pub use lsp; pub use lsp;
pub use project; pub use project;
use project_panel::ProjectPanel; use project_panel::ProjectPanel;
@ -68,6 +72,17 @@ actions!(
const MIN_FONT_SIZE: f32 = 6.0; const MIN_FONT_SIZE: f32 = 6.0;
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var);
static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var);
}
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_action(about); cx.add_action(about);
cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| { cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
@ -336,8 +351,13 @@ pub fn initialize_workspace(
} }
pub fn build_window_options() -> WindowOptions<'static> { 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 { WindowOptions {
bounds: WindowBounds::Maximized, bounds,
titlebar: Some(TitlebarOptions { titlebar: Some(TitlebarOptions {
title: None, title: None,
appears_transparent: true, appears_transparent: true,
@ -612,6 +632,13 @@ fn schema_file_match(path: &Path) -> &Path {
.unwrap() .unwrap()
} }
fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -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

View file

@ -12,8 +12,16 @@ function isStyleSet(key: any): key is StyleSets {
"negative", "negative",
].includes(key); ].includes(key);
} }
function isStyle(key: any): key is Styles { 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( function getStyle(
layer: Layer, layer: Layer,