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 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();
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_a.read_with(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.id()),
editor_a1.id()
);
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.

View File

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

View File

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

View File

@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc, time::Instant};
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
use indoc::indoc;
use rpc::PeerId;
use unindent::Unindent;
use super::*;
@ -24,7 +25,7 @@ use util::{
};
use workspace::{
item::{FollowableItem, ItemHandle},
NavigationEntry, Pane,
NavigationEntry, Pane, ViewId,
};
#[gpui::test]
@ -41,7 +42,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged
) {
events.borrow_mut().push(("editor1", *event));
events.borrow_mut().push(("editor1", event.clone()));
}
})
.detach();
@ -56,7 +57,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged
) {
events.borrow_mut().push(("editor2", *event));
events.borrow_mut().push(("editor2", event.clone()));
}
})
.detach();
@ -4969,19 +4970,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
}
#[gpui::test]
fn test_following(cx: &mut gpui::MutableAppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
async fn test_following(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
cx.set_global(Settings::test(cx));
let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
let (_, follower) = cx.add_window(
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)
.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)
.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| {
let initial_scroll_position = follower.scroll_position(cx);
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
assert_eq!(follower.scroll_position(cx), initial_scroll_position);
assert!(follower.scroll_manager.has_autoscroll_request());
follower.update(cx, |follower, cx| {
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)
.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)
.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";

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 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
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 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])
})
});
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 {
unreachable!()
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,
);
}
}
} else {
return None;
};
let buffer = project.update(cx, |project, cx| {
project.open_buffer_by_id(state.buffer_id, cx)
});
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)
})
})
.unwrap_or_else(|| {
pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| Editor::for_buffer(buffer, 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;
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.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)
})
.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);
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),
},
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() {
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(())
})
}
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>() {

View File

@ -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, _| {
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));

View File

@ -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,
})

View File

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

View File

@ -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();

View File

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

View File

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

View File

@ -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,12 +1611,10 @@ 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);
}
}
}
}
cx.notify();
}
@ -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)
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,10 +1711,8 @@ 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);
}
}
if states_by_pane.is_empty() {
self.follower_states_by_leader.remove(&leader_id);
@ -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;
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);
}
}
});
Ok::<_, anyhow::Error>(())
})
}
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
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
.entry(update_view.id)
.or_insert(FollowerItem::Loading(Vec::new()))
.get(&ViewId::from_proto(id.clone()))
{
FollowerItem::Loaded(item) => {
item.apply_update_proto(variant, cx).log_err();
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>;
}

View File

@ -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::*;

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",
].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,