mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
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:
commit
e08d6cd6de
@ -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.
|
||||
|
@ -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);
|
||||
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";
|
||||
|
@ -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>() {
|
||||
|
@ -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));
|
||||
|
@ -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,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>;
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
50
script/start-local-collaboration
Executable file
50
script/start-local-collaboration
Executable 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
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user