Automatically unfollow when editing, scrolling or changing selections
This commit is contained in:
parent
c550fc3f01
commit
3117554568
14 changed files with 214 additions and 60 deletions
|
@ -1035,14 +1035,19 @@ impl Editor {
|
||||||
self.scroll_top_anchor = Some(anchor);
|
self.scroll_top_anchor = Some(anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(Event::ScrollPositionChanged);
|
cx.emit(Event::ScrollPositionChanged { local: true });
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_scroll_top_anchor(&mut self, anchor: Option<Anchor>, cx: &mut ViewContext<Self>) {
|
fn set_scroll_top_anchor(
|
||||||
|
&mut self,
|
||||||
|
anchor: Option<Anchor>,
|
||||||
|
local: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
self.scroll_position = Vector2F::zero();
|
self.scroll_position = Vector2F::zero();
|
||||||
self.scroll_top_anchor = anchor;
|
self.scroll_top_anchor = anchor;
|
||||||
cx.emit(Event::ScrollPositionChanged);
|
cx.emit(Event::ScrollPositionChanged { local });
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1267,7 +1272,7 @@ impl Editor {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_selections(self.selections.clone(), Some(pending), cx);
|
self.set_selections(self.selections.clone(), Some(pending), true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn begin_selection(
|
fn begin_selection(
|
||||||
|
@ -1347,7 +1352,12 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
selections = Arc::from([]);
|
selections = Arc::from([]);
|
||||||
}
|
}
|
||||||
self.set_selections(selections, Some(PendingSelection { selection, mode }), cx);
|
self.set_selections(
|
||||||
|
selections,
|
||||||
|
Some(PendingSelection { selection, mode }),
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -1461,7 +1471,7 @@ impl Editor {
|
||||||
pending.selection.end = buffer.anchor_before(head);
|
pending.selection.end = buffer.anchor_before(head);
|
||||||
pending.selection.reversed = false;
|
pending.selection.reversed = false;
|
||||||
}
|
}
|
||||||
self.set_selections(self.selections.clone(), Some(pending), cx);
|
self.set_selections(self.selections.clone(), Some(pending), true, cx);
|
||||||
} else {
|
} else {
|
||||||
log::error!("update_selection dispatched with no pending selection");
|
log::error!("update_selection dispatched with no pending selection");
|
||||||
return;
|
return;
|
||||||
|
@ -1548,7 +1558,7 @@ impl Editor {
|
||||||
if selections.is_empty() {
|
if selections.is_empty() {
|
||||||
selections = Arc::from([pending.selection]);
|
selections = Arc::from([pending.selection]);
|
||||||
}
|
}
|
||||||
self.set_selections(selections, None, cx);
|
self.set_selections(selections, None, true, cx);
|
||||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||||
} else {
|
} else {
|
||||||
let mut oldest_selection = self.oldest_selection::<usize>(&cx);
|
let mut oldest_selection = self.oldest_selection::<usize>(&cx);
|
||||||
|
@ -1895,7 +1905,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
drop(snapshot);
|
drop(snapshot);
|
||||||
|
|
||||||
self.set_selections(selections.into(), None, cx);
|
self.set_selections(selections.into(), None, true, cx);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -3294,7 +3304,7 @@ impl Editor {
|
||||||
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
|
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
|
||||||
if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() {
|
if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() {
|
||||||
self.set_selections(selections, None, cx);
|
self.set_selections(selections, None, true, cx);
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||||
}
|
}
|
||||||
|
@ -3303,7 +3313,7 @@ impl Editor {
|
||||||
pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
|
pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
|
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
|
||||||
if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() {
|
if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() {
|
||||||
self.set_selections(selections, None, cx);
|
self.set_selections(selections, None, true, cx);
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||||
}
|
}
|
||||||
|
@ -4967,6 +4977,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
None,
|
None,
|
||||||
|
true,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5027,6 +5038,7 @@ impl Editor {
|
||||||
&mut self,
|
&mut self,
|
||||||
selections: Arc<[Selection<Anchor>]>,
|
selections: Arc<[Selection<Anchor>]>,
|
||||||
pending_selection: Option<PendingSelection>,
|
pending_selection: Option<PendingSelection>,
|
||||||
|
local: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -5095,7 +5107,7 @@ impl Editor {
|
||||||
self.refresh_document_highlights(cx);
|
self.refresh_document_highlights(cx);
|
||||||
|
|
||||||
self.pause_cursor_blinking(cx);
|
self.pause_cursor_blinking(cx);
|
||||||
cx.emit(Event::SelectionsChanged);
|
cx.emit(Event::SelectionsChanged { local });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
|
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -5508,10 +5520,10 @@ impl Editor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
language::Event::Edited => {
|
language::Event::Edited { local } => {
|
||||||
self.refresh_active_diagnostics(cx);
|
self.refresh_active_diagnostics(cx);
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited { local: *local });
|
||||||
}
|
}
|
||||||
language::Event::Dirtied => cx.emit(Event::Dirtied),
|
language::Event::Dirtied => cx.emit(Event::Dirtied),
|
||||||
language::Event::Saved => cx.emit(Event::Saved),
|
language::Event::Saved => cx.emit(Event::Saved),
|
||||||
|
@ -5638,13 +5650,13 @@ fn compute_scroll_position(
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Activate,
|
Activate,
|
||||||
Edited,
|
Edited { local: bool },
|
||||||
Blurred,
|
Blurred,
|
||||||
Dirtied,
|
Dirtied,
|
||||||
Saved,
|
Saved,
|
||||||
TitleChanged,
|
TitleChanged,
|
||||||
SelectionsChanged,
|
SelectionsChanged { local: bool },
|
||||||
ScrollPositionChanged,
|
ScrollPositionChanged { local: bool },
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ impl FollowableItem for Editor {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
if !selections.is_empty() {
|
if !selections.is_empty() {
|
||||||
editor.set_selections(selections.into(), None, cx);
|
editor.set_selections(selections.into(), None, false, cx);
|
||||||
}
|
}
|
||||||
editor
|
editor
|
||||||
})
|
})
|
||||||
|
@ -104,7 +104,7 @@ impl FollowableItem for Editor {
|
||||||
_: &AppContext,
|
_: &AppContext,
|
||||||
) -> Option<update_view::Variant> {
|
) -> Option<update_view::Variant> {
|
||||||
match event {
|
match event {
|
||||||
Event::ScrollPositionChanged | Event::SelectionsChanged => {
|
Event::ScrollPositionChanged { .. } | Event::SelectionsChanged { .. } => {
|
||||||
Some(update_view::Variant::Editor(update_view::Editor {
|
Some(update_view::Variant::Editor(update_view::Editor {
|
||||||
scroll_top: self
|
scroll_top: self
|
||||||
.scroll_top_anchor
|
.scroll_top_anchor
|
||||||
|
@ -138,10 +138,11 @@ impl FollowableItem for Editor {
|
||||||
text_anchor: language::proto::deserialize_anchor(anchor)
|
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||||
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||||
}),
|
}),
|
||||||
|
false,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
self.set_scroll_top_anchor(None, cx);
|
self.set_scroll_top_anchor(None, false, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let selections = message
|
let selections = message
|
||||||
|
@ -152,15 +153,20 @@ impl FollowableItem for Editor {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if !selections.is_empty() {
|
if !selections.is_empty() {
|
||||||
self.set_selections(selections.into(), None, cx);
|
self.set_selections(selections.into(), None, false, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
|
||||||
false
|
match event {
|
||||||
|
Event::Edited { local } => *local,
|
||||||
|
Event::SelectionsChanged { local } => *local,
|
||||||
|
Event::ScrollPositionChanged { local } => *local,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -291,7 +291,7 @@ impl FileFinder {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
self.latest_search_id = post_inc(&mut self.search_count);
|
self.latest_search_id = post_inc(&mut self.search_count);
|
||||||
|
|
|
@ -102,7 +102,7 @@ impl GoToLine {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
|
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
|
||||||
let mut components = line_editor.trim().split(&[',', ':'][..]);
|
let mut components = line_editor.trim().split(&[',', ':'][..]);
|
||||||
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
||||||
|
|
|
@ -142,7 +142,7 @@ pub enum Operation {
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Operation(Operation),
|
Operation(Operation),
|
||||||
Edited,
|
Edited { local: bool },
|
||||||
Dirtied,
|
Dirtied,
|
||||||
Saved,
|
Saved,
|
||||||
FileHandleChanged,
|
FileHandleChanged,
|
||||||
|
@ -968,7 +968,7 @@ impl Buffer {
|
||||||
) -> Option<TransactionId> {
|
) -> Option<TransactionId> {
|
||||||
if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
|
if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
|
||||||
let was_dirty = start_version != self.saved_version;
|
let was_dirty = start_version != self.saved_version;
|
||||||
self.did_edit(&start_version, was_dirty, cx);
|
self.did_edit(&start_version, was_dirty, true, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1161,6 +1161,7 @@ impl Buffer {
|
||||||
&mut self,
|
&mut self,
|
||||||
old_version: &clock::Global,
|
old_version: &clock::Global,
|
||||||
was_dirty: bool,
|
was_dirty: bool,
|
||||||
|
local: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.edits_since::<usize>(old_version).next().is_none() {
|
if self.edits_since::<usize>(old_version).next().is_none() {
|
||||||
|
@ -1169,7 +1170,7 @@ impl Buffer {
|
||||||
|
|
||||||
self.reparse(cx);
|
self.reparse(cx);
|
||||||
|
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited { local });
|
||||||
if !was_dirty {
|
if !was_dirty {
|
||||||
cx.emit(Event::Dirtied);
|
cx.emit(Event::Dirtied);
|
||||||
}
|
}
|
||||||
|
@ -1206,7 +1207,7 @@ impl Buffer {
|
||||||
self.text.apply_ops(buffer_ops)?;
|
self.text.apply_ops(buffer_ops)?;
|
||||||
self.deferred_ops.insert(deferred_ops);
|
self.deferred_ops.insert(deferred_ops);
|
||||||
self.flush_deferred_ops(cx);
|
self.flush_deferred_ops(cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, false, cx);
|
||||||
// Notify independently of whether the buffer was edited as the operations could include a
|
// Notify independently of whether the buffer was edited as the operations could include a
|
||||||
// selection update.
|
// selection update.
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -1321,7 +1322,7 @@ impl Buffer {
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.undo() {
|
if let Some((transaction_id, operation)) = self.text.undo() {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, true, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1342,7 +1343,7 @@ impl Buffer {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
}
|
}
|
||||||
if undone {
|
if undone {
|
||||||
self.did_edit(&old_version, was_dirty, cx)
|
self.did_edit(&old_version, was_dirty, true, cx)
|
||||||
}
|
}
|
||||||
undone
|
undone
|
||||||
}
|
}
|
||||||
|
@ -1353,7 +1354,7 @@ impl Buffer {
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.redo() {
|
if let Some((transaction_id, operation)) = self.text.redo() {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, true, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1374,7 +1375,7 @@ impl Buffer {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
}
|
}
|
||||||
if redone {
|
if redone {
|
||||||
self.did_edit(&old_version, was_dirty, cx)
|
self.did_edit(&old_version, was_dirty, true, cx)
|
||||||
}
|
}
|
||||||
redone
|
redone
|
||||||
}
|
}
|
||||||
|
@ -1440,7 +1441,7 @@ impl Buffer {
|
||||||
if !ops.is_empty() {
|
if !ops.is_empty() {
|
||||||
for op in ops {
|
for op in ops {
|
||||||
self.send_operation(Operation::Buffer(op), cx);
|
self.send_operation(Operation::Buffer(op), cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, true, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,11 +122,19 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) {
|
||||||
let buffer_1_events = buffer_1_events.borrow();
|
let buffer_1_events = buffer_1_events.borrow();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*buffer_1_events,
|
*buffer_1_events,
|
||||||
vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
|
vec![
|
||||||
|
Event::Edited { local: true },
|
||||||
|
Event::Dirtied,
|
||||||
|
Event::Edited { local: true },
|
||||||
|
Event::Edited { local: true }
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
let buffer_2_events = buffer_2_events.borrow();
|
let buffer_2_events = buffer_2_events.borrow();
|
||||||
assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
|
assert_eq!(
|
||||||
|
*buffer_2_events,
|
||||||
|
vec![Event::Edited { local: false }, Event::Dirtied]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -224,7 +224,7 @@ impl OutlineView {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||||
editor::Event::Edited => self.update_matches(cx),
|
editor::Event::Edited { .. } => self.update_matches(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1178,7 +1178,7 @@ impl Project {
|
||||||
});
|
});
|
||||||
cx.background().spawn(request).detach_and_log_err(cx);
|
cx.background().spawn(request).detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
BufferEvent::Edited => {
|
BufferEvent::Edited { .. } => {
|
||||||
let language_server = self
|
let language_server = self
|
||||||
.language_server_for_buffer(buffer.read(cx), cx)?
|
.language_server_for_buffer(buffer.read(cx), cx)?
|
||||||
.clone();
|
.clone();
|
||||||
|
@ -6227,7 +6227,10 @@ mod tests {
|
||||||
assert!(buffer.is_dirty());
|
assert!(buffer.is_dirty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*events.borrow(),
|
*events.borrow(),
|
||||||
&[language::Event::Edited, language::Event::Dirtied]
|
&[
|
||||||
|
language::Event::Edited { local: true },
|
||||||
|
language::Event::Dirtied
|
||||||
|
]
|
||||||
);
|
);
|
||||||
events.borrow_mut().clear();
|
events.borrow_mut().clear();
|
||||||
buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx);
|
buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx);
|
||||||
|
@ -6250,9 +6253,9 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*events.borrow(),
|
*events.borrow(),
|
||||||
&[
|
&[
|
||||||
language::Event::Edited,
|
language::Event::Edited { local: true },
|
||||||
language::Event::Dirtied,
|
language::Event::Dirtied,
|
||||||
language::Event::Edited,
|
language::Event::Edited { local: true },
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
events.borrow_mut().clear();
|
events.borrow_mut().clear();
|
||||||
|
@ -6264,7 +6267,7 @@ mod tests {
|
||||||
assert!(buffer.is_dirty());
|
assert!(buffer.is_dirty());
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(*events.borrow(), &[language::Event::Edited]);
|
assert_eq!(*events.borrow(), &[language::Event::Edited { local: true }]);
|
||||||
|
|
||||||
// When a file is deleted, the buffer is considered dirty.
|
// When a file is deleted, the buffer is considered dirty.
|
||||||
let events = Rc::new(RefCell::new(Vec::new()));
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
|
@ -328,7 +328,7 @@ impl ProjectSymbolsView {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||||
editor::Event::Edited => self.update_matches(cx),
|
editor::Event::Edited { .. } => self.update_matches(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -360,7 +360,7 @@ impl SearchBar {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
self.query_contains_error = false;
|
self.query_contains_error = false;
|
||||||
self.clear_matches(cx);
|
self.clear_matches(cx);
|
||||||
self.update_matches(true, cx);
|
self.update_matches(true, cx);
|
||||||
|
@ -377,8 +377,8 @@ impl SearchBar {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => self.update_matches(false, cx),
|
editor::Event::Edited { .. } => self.update_matches(false, cx),
|
||||||
editor::Event::SelectionsChanged => self.update_match_index(cx),
|
editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -350,7 +350,7 @@ impl ProjectSearchView {
|
||||||
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
||||||
.detach();
|
.detach();
|
||||||
cx.subscribe(&results_editor, |this, _, event, cx| {
|
cx.subscribe(&results_editor, |this, _, event, cx| {
|
||||||
if matches!(event, editor::Event::SelectionsChanged) {
|
if matches!(event, editor::Event::SelectionsChanged { .. }) {
|
||||||
this.update_match_index(cx);
|
this.update_match_index(cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1086,7 +1086,7 @@ mod tests {
|
||||||
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
|
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
|
||||||
ToOffset, ToggleCodeActions, Undo,
|
ToOffset, ToggleCodeActions, Undo,
|
||||||
};
|
};
|
||||||
use gpui::{executor, ModelHandle, TestAppContext, ViewHandle};
|
use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
|
||||||
use language::{
|
use language::{
|
||||||
tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
|
tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
|
||||||
LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition,
|
LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition,
|
||||||
|
@ -4308,11 +4308,6 @@ mod tests {
|
||||||
.project_path(cx)),
|
.project_path(cx)),
|
||||||
Some((worktree_id, "2.txt").into())
|
Some((worktree_id, "2.txt").into())
|
||||||
);
|
);
|
||||||
let editor_b2 = workspace_b
|
|
||||||
.read_with(cx_b, |workspace, cx| workspace.active_item(cx))
|
|
||||||
.unwrap()
|
|
||||||
.downcast::<Editor>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// When client A activates a different editor, client B does so as well.
|
// When client A activates a different editor, client B does so as well.
|
||||||
workspace_a.update(cx_a, |workspace, cx| {
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
|
@ -4324,7 +4319,7 @@ mod tests {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// When client A selects something, client B does as well.
|
// Changes to client A's editor are reflected on client B.
|
||||||
editor_a1.update(cx_a, |editor, cx| {
|
editor_a1.update(cx_a, |editor, cx| {
|
||||||
editor.select_ranges([1..1, 2..2], None, cx);
|
editor.select_ranges([1..1, 2..2], None, cx);
|
||||||
});
|
});
|
||||||
|
@ -4334,17 +4329,26 @@ mod tests {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
|
||||||
|
editor_b1
|
||||||
|
.condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
editor_a1.update(cx_a, |editor, cx| {
|
||||||
|
editor.select_ranges([3..3], None, cx);
|
||||||
|
});
|
||||||
|
editor_b1
|
||||||
|
.condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3])
|
||||||
|
.await;
|
||||||
|
|
||||||
// After unfollowing, client B stops receiving updates from client A.
|
// After unfollowing, client B stops receiving updates from client A.
|
||||||
workspace_b.update(cx_b, |workspace, cx| {
|
workspace_b.update(cx_b, |workspace, cx| {
|
||||||
workspace.unfollow(&workspace.active_pane().clone(), cx)
|
workspace.unfollow(&workspace.active_pane().clone(), cx)
|
||||||
});
|
});
|
||||||
workspace_a.update(cx_a, |workspace, cx| {
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
workspace.activate_item(&editor_a2, cx);
|
workspace.activate_item(&editor_a2, cx)
|
||||||
editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx));
|
|
||||||
});
|
});
|
||||||
editor_b2
|
cx_a.foreground().run_until_parked();
|
||||||
.condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
|
|
||||||
.await;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workspace_b.read_with(cx_b, |workspace, cx| workspace
|
workspace_b.read_with(cx_b, |workspace, cx| workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
|
@ -4456,6 +4460,126 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
cx_a.foreground().forbid_parking();
|
||||||
|
let fs = FakeFs::new(cx_a.background());
|
||||||
|
|
||||||
|
// 2 clients connect to a server.
|
||||||
|
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
|
let mut client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let mut client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
cx_a.update(editor::init);
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
|
||||||
|
// Client A shares a project.
|
||||||
|
fs.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
".zed.toml": r#"collaborators = ["user_b"]"#,
|
||||||
|
"1.txt": "one",
|
||||||
|
"2.txt": "two",
|
||||||
|
"3.txt": "three",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
|
||||||
|
project_a
|
||||||
|
.update(cx_a, |project, cx| project.share(cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B joins the project.
|
||||||
|
let project_b = client_b
|
||||||
|
.build_remote_project(
|
||||||
|
project_a
|
||||||
|
.read_with(cx_a, |project, _| project.remote_id())
|
||||||
|
.unwrap(),
|
||||||
|
cx_b,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Client A opens some editors.
|
||||||
|
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||||
|
let _editor_a1 = workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "1.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B starts following client A.
|
||||||
|
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||||
|
let leader_id = project_b.read_with(cx_b, |project, _| {
|
||||||
|
project.collaborators().values().next().unwrap().peer_id
|
||||||
|
});
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&leader_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
// When client B moves, it automatically stops following client A.
|
||||||
|
editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&leader_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// When client B edits, it automatically stops following client A.
|
||||||
|
editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&leader_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// When client B scrolls, it automatically stops following client A.
|
||||||
|
editor_b2.update(cx_b, |editor, cx| {
|
||||||
|
editor.set_scroll_position(vec2f(0., 3.), cx)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 100)]
|
#[gpui::test(iterations = 100)]
|
||||||
async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
|
async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
|
@ -204,7 +204,7 @@ impl ThemeSelector {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
self.update_matches(cx);
|
self.update_matches(cx);
|
||||||
self.select_if_matching(&cx.global::<Settings>().theme.name);
|
self.select_if_matching(&cx.global::<Settings>().theme.name);
|
||||||
self.show_selected_theme(cx);
|
self.show_selected_theme(cx);
|
||||||
|
|
|
@ -1750,7 +1750,7 @@ impl Workspace {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
||||||
self.follower_states_by_leader
|
self.follower_states_by_leader
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(leader_id, state)| {
|
.find_map(|(leader_id, state)| {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue