diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 17eb051e35ad6e2ef0c2358cd0664cdba93af013..223dbb776550e949d0ce86dca6f68aff6482433d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3449,6 +3449,21 @@ impl Editor { Subscription::join(other_subscription, this_subscription) } + fn unfold_buffers_with_selections(&mut self, cx: &mut Context) { + if self.buffer().read(cx).is_singleton() { + return; + } + let snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids: HashSet = self + .selections + .disjoint_anchor_ranges() + .flat_map(|range| snapshot.buffer_ids_for_range(range)) + .collect(); + for buffer_id in buffer_ids { + self.unfold_buffer(buffer_id, cx); + } + } + /// Changes selections using the provided mutation function. Changes to `self.selections` occur /// immediately, but when run within `transact` or `with_selection_effects_deferred` other /// effects of selection change occur at the end of the transaction. @@ -4190,6 +4205,8 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + self.unfold_buffers_with_selections(cx); + let selections = self.selections.all_adjusted(&self.display_snapshot(cx)); let mut bracket_inserted = false; let mut edits = Vec::new(); @@ -18879,10 +18896,17 @@ impl Editor { if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { return; } + let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); self.display_map.update(cx, |display_map, cx| { display_map.fold_buffers([buffer_id], cx) }); + + let snapshot = self.display_snapshot(cx); + self.selections.change_with(&snapshot, |selections| { + selections.remove_selections_from_buffer(buffer_id); + }); + cx.emit(EditorEvent::BufferFoldToggled { ids: folded_excerpts.iter().map(|&(id, _)| id).collect(), folded: true, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 598d1383726a9610bb5a2c851cd1d56a709546ec..4510e61b74c9bd9ca8ace634f7554f63c4981dd7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -22378,7 +22378,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) { assert_eq!( multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)), - "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n", + "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n", "After unfolding the first buffer, its and 2nd buffer's text should be displayed" ); @@ -22387,7 +22387,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) { }); assert_eq!( multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)), - "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555", + "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555", "After unfolding the all buffers, all original text should be displayed" ); } @@ -27453,3 +27453,213 @@ async fn test_next_prev_reference(cx: &mut TestAppContext) { _move(Direction::Prev, 2, &mut cx).await; cx.assert_editor_state(CYCLE_POSITIONS[1]); } + +#[gpui::test] +async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let (editor, cx) = cx.add_window_view(|window, cx| { + let multi_buffer = MultiBuffer::build_multi( + [ + ("1\n2\n3\n", vec![Point::row_range(0..3)]), + ("1\n2\n3\n", vec![Point::row_range(0..3)]), + ], + cx, + ); + Editor::new(EditorMode::full(), multi_buffer, None, window, cx) + }); + + let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await; + let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids()); + + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + ˇ1 + 2 + 3 + [EXCERPT] + 1 + 2 + 3 + "}); + + // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert + cx.update_editor(|editor, window, cx| { + editor.change_selections(None.into(), window, cx, |s| { + s.select_ranges([2..3]); + }); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + 1 + 2ˇ + 3 + [EXCERPT] + 1 + 2 + 3 + "}); + + cx.update_editor(|editor, window, cx| { + editor + .select_all_matches(&SelectAllMatches, window, cx) + .unwrap(); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + 1 + 2ˇ + 3 + [EXCERPT] + 1 + 2ˇ + 3 + "}); + + cx.update_editor(|editor, window, cx| { + editor.handle_input("X", window, cx); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + 1 + Xˇ + 3 + [EXCERPT] + 1 + Xˇ + 3 + "}); + + // Scenario 2: Select "2", then fold second buffer before insertion + cx.update_multibuffer(|mb, cx| { + for buffer_id in buffer_ids.iter() { + let buffer = mb.buffer(*buffer_id).unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx); + }); + } + }); + + // Select "2" and select all matches + cx.update_editor(|editor, window, cx| { + editor.change_selections(None.into(), window, cx, |s| { + s.select_ranges([2..3]); + }); + editor + .select_all_matches(&SelectAllMatches, window, cx) + .unwrap(); + }); + + // Fold second buffer - should remove selections from folded buffer + cx.update_editor(|editor, _, cx| { + editor.fold_buffer(buffer_ids[1], cx); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + 1 + 2ˇ + 3 + [EXCERPT] + [FOLDED] + "}); + + // Insert text - should only affect first buffer + cx.update_editor(|editor, window, cx| { + editor.handle_input("Y", window, cx); + }); + cx.update_editor(|editor, _, cx| { + editor.unfold_buffer(buffer_ids[1], cx); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + 1 + Yˇ + 3 + [EXCERPT] + 1 + 2 + 3 + "}); + + // Scenario 3: Select "2", then fold first buffer before insertion + cx.update_multibuffer(|mb, cx| { + for buffer_id in buffer_ids.iter() { + let buffer = mb.buffer(*buffer_id).unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx); + }); + } + }); + + // Select "2" and select all matches + cx.update_editor(|editor, window, cx| { + editor.change_selections(None.into(), window, cx, |s| { + s.select_ranges([2..3]); + }); + editor + .select_all_matches(&SelectAllMatches, window, cx) + .unwrap(); + }); + + // Fold first buffer - should remove selections from folded buffer + cx.update_editor(|editor, _, cx| { + editor.fold_buffer(buffer_ids[0], cx); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + [FOLDED] + [EXCERPT] + 1 + 2ˇ + 3 + "}); + + // Insert text - should only affect second buffer + cx.update_editor(|editor, window, cx| { + editor.handle_input("Z", window, cx); + }); + cx.update_editor(|editor, _, cx| { + editor.unfold_buffer(buffer_ids[0], cx); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + 1 + 2 + 3 + [EXCERPT] + 1 + Zˇ + 3 + "}); + + // Edge case scenario: fold all buffers, then try to insert + cx.update_editor(|editor, _, cx| { + editor.fold_buffer(buffer_ids[0], cx); + editor.fold_buffer(buffer_ids[1], cx); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + ˇ[FOLDED] + [EXCERPT] + [FOLDED] + "}); + + // Insert should work via default selection + cx.update_editor(|editor, window, cx| { + editor.handle_input("W", window, cx); + }); + cx.update_editor(|editor, _, cx| { + editor.unfold_buffer(buffer_ids[0], cx); + editor.unfold_buffer(buffer_ids[1], cx); + }); + cx.assert_excerpts_with_selections(indoc! {" + [EXCERPT] + Wˇ1 + 2 + 3 + [EXCERPT] + 1 + Z + 3 + "}); +} diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index eeecfffa70a30705174b64f698d2965f9540fe0b..75fffdc7fea17fe35f9942125499ba15c9a77422 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -487,6 +487,43 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> { self.selections_changed |= changed; } + pub fn remove_selections_from_buffer(&mut self, buffer_id: language::BufferId) { + let mut changed = false; + + let filtered_selections: Arc<[Selection]> = { + self.disjoint + .iter() + .filter(|selection| { + if let Some(selection_buffer_id) = + self.snapshot.buffer_id_for_anchor(selection.start) + { + let should_remove = selection_buffer_id == buffer_id; + changed |= should_remove; + !should_remove + } else { + true + } + }) + .cloned() + .collect() + }; + + if filtered_selections.is_empty() { + let default_anchor = self.snapshot.anchor_before(0); + self.collection.disjoint = Arc::from([Selection { + id: post_inc(&mut self.collection.next_selection_id), + start: default_anchor, + end: default_anchor, + reversed: false, + goal: SelectionGoal::None, + }]); + } else { + self.collection.disjoint = filtered_selections; + } + + self.selections_changed |= changed; + } + pub fn clear_pending(&mut self) { if self.collection.pending.is_some() { self.collection.pending = None; diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index ee271c9ad92cde8fe8f7da54fb1cc1ae74d20ea9..a7fe9ea679d565b2a8a2a26bf86306b93dd62e78 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -6619,13 +6619,11 @@ outline: struct OutlineEntryExcerpt format!( r#"frontend-project/ public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} + syntax-tree.json <==== selected src/ app/(site)/ components/ - ErrorBoundary.tsx <==== selected - search: «static»"# + ErrorBoundary.tsx"# ) ); }); @@ -6667,7 +6665,7 @@ outline: struct OutlineEntryExcerpt format!( r#"frontend-project/ public/lottie/ - syntax-tree.json + syntax-tree.json <==== selected search: {{ "something": "«static»" }} src/ app/(site)/ @@ -6678,7 +6676,7 @@ outline: struct OutlineEntryExcerpt page.tsx search: «static» components/ - ErrorBoundary.tsx <==== selected + ErrorBoundary.tsx search: «static»"# ) );