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