Allow folding buffers inside multi buffers (#22046)

Closes https://github.com/zed-industries/zed/issues/4925


https://github.com/user-attachments/assets/e7b87375-893f-41ae-a2d9-d501499e40d1


Allows to fold any buffer inside multi buffers, either by clicking the
chevron icon on the header, or by using
`editor::Fold`/`editor::UnfoldLines`/`editor::ToggleFold`/`editor::FoldAll`
and `editor::UnfoldAll` actions inside the multi buffer (those were noop
there before).

Every fold has a fake line inside it, so it's possible to navigate into
that via the keyboard and unfold it with the corresponding editor
action.

The state is synchronized with the outline panel state: any fold inside
multi buffer folds the corresponding file entry; any file entry fold
inside the outline panel folds the corresponding buffer inside the multi
buffer, any directory fold inside the outline panel folds the
corresponding buffers inside the multi buffer for each nested file entry
in the panel.


Release Notes:

- Added a possibility to fold buffers inside multi buffers

---------

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Kirill Bulatov 2024-12-16 00:32:07 +02:00 committed by GitHub
parent f64fcedabb
commit af50261ae2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 2401 additions and 589 deletions

View file

@ -1050,6 +1050,7 @@ fn editor_blocks(
.ok()?
}
Block::FoldedBuffer { .. } => FILE_HEADER.into(),
Block::ExcerptBoundary {
starts_new_buffer, ..
} => {

View file

@ -269,7 +269,7 @@ impl DisplayMap {
let start = buffer_snapshot.anchor_before(range.start);
let end = buffer_snapshot.anchor_after(range.end);
BlockProperties {
placement: BlockPlacement::Replace(start..end),
placement: BlockPlacement::Replace(start..=end),
render,
height,
style,
@ -336,6 +336,38 @@ impl DisplayMap {
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
}
pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool {
self.block_map.folded_buffers.contains(&buffer_id)
}
pub fn insert_creases(
&mut self,
creases: impl IntoIterator<Item = Crease<Anchor>>,
@ -712,7 +744,11 @@ impl DisplaySnapshot {
}
}
pub fn next_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
pub fn next_line_boundary(
&self,
mut point: MultiBufferPoint,
) -> (MultiBufferPoint, DisplayPoint) {
let original_point = point;
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
@ -723,7 +759,7 @@ impl DisplaySnapshot {
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
let next_point = self.display_point_to_point(display_point, Bias::Right);
if next_point == point {
if next_point == point || original_point == point || original_point == next_point {
return (point, display_point);
}
point = next_point;
@ -1081,10 +1117,6 @@ impl DisplaySnapshot {
|| self.fold_snapshot.is_line_folded(buffer_row)
}
pub fn is_line_replaced(&self, buffer_row: MultiBufferRow) -> bool {
self.block_snapshot.is_line_replaced(buffer_row)
}
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
self.block_snapshot.is_block_line(BlockRow(display_row.0))
}
@ -2231,7 +2263,7 @@ pub mod tests {
[BlockProperties {
placement: BlockPlacement::Replace(
buffer_snapshot.anchor_before(Point::new(1, 2))
..buffer_snapshot.anchor_after(Point::new(2, 3)),
..=buffer_snapshot.anchor_after(Point::new(2, 3)),
),
height: 4,
style: BlockStyle::Fixed,

File diff suppressed because it is too large Load diff

View file

@ -678,6 +678,7 @@ pub struct Editor {
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
toggle_fold_multiple_buffers: Task<()>,
_scroll_cursor_center_top_bottom_task: Task<()>,
}
@ -1325,6 +1326,7 @@ impl Editor {
addons: HashMap::default(),
registered_buffers: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
toggle_fold_multiple_buffers: Task::ready(()),
text_style_refinement: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
@ -10311,22 +10313,53 @@ impl Editor {
}
pub fn toggle_fold(&mut self, _: &actions::ToggleFold, cx: &mut ViewContext<Self>) {
let selection = self.selections.newest::<Point>(cx);
if self.is_singleton(cx) {
let selection = self.selections.newest::<Point>(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let range = if selection.is_empty() {
let point = selection.head().to_display_point(&display_map);
let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
.to_point(&display_map);
start..end
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let range = if selection.is_empty() {
let point = selection.head().to_display_point(&display_map);
let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
.to_point(&display_map);
start..end
} else {
selection.range()
};
if display_map.folds_in_range(range).next().is_some() {
self.unfold_lines(&Default::default(), cx)
} else {
self.fold(&Default::default(), cx)
}
} else {
selection.range()
};
if display_map.folds_in_range(range).next().is_some() {
self.unfold_lines(&Default::default(), cx)
} else {
self.fold(&Default::default(), cx)
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
let mut toggled_buffers = HashSet::default();
for selection in selections {
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.head(), Bias::Right)
.buffer_id
{
if toggled_buffers.insert(buffer_id) {
if self.buffer_folded(buffer_id, cx) {
self.unfold_buffer(buffer_id, cx);
} else {
self.fold_buffer(buffer_id, cx);
}
}
}
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.tail(), Bias::Left)
.buffer_id
{
if toggled_buffers.insert(buffer_id) {
if self.buffer_folded(buffer_id, cx) {
self.unfold_buffer(buffer_id, cx);
} else {
self.fold_buffer(buffer_id, cx);
}
}
}
}
}
}
@ -10355,44 +10388,68 @@ impl Editor {
}
pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
let mut to_fold = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
if self.is_singleton(cx) {
let mut to_fold = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
for selection in selections {
let range = selection.range().sorted();
let buffer_start_row = range.start.row;
for selection in selections {
let range = selection.range().sorted();
let buffer_start_row = range.start.row;
if range.start.row != range.end.row {
let mut found = false;
let mut row = range.start.row;
while row <= range.end.row {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
found = true;
row = crease.range().end.row + 1;
to_fold.push(crease);
} else {
row += 1
if range.start.row != range.end.row {
let mut found = false;
let mut row = range.start.row;
while row <= range.end.row {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row))
{
found = true;
row = crease.range().end.row + 1;
to_fold.push(crease);
} else {
row += 1
}
}
if found {
continue;
}
}
if found {
continue;
}
}
for row in (0..=range.start.row).rev() {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
if crease.range().end.row >= buffer_start_row {
to_fold.push(crease);
if row <= range.start.row {
break;
for row in (0..=range.start.row).rev() {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
if crease.range().end.row >= buffer_start_row {
to_fold.push(crease);
if row <= range.start.row {
break;
}
}
}
}
}
}
self.fold_creases(to_fold, true, cx);
self.fold_creases(to_fold, true, cx);
} else {
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
let mut folded_buffers = HashSet::default();
for selection in selections {
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.head(), Bias::Right)
.buffer_id
{
if folded_buffers.insert(buffer_id) {
self.fold_buffer(buffer_id, cx);
}
}
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.tail(), Bias::Left)
.buffer_id
{
if folded_buffers.insert(buffer_id) {
self.fold_buffer(buffer_id, cx);
}
}
}
}
}
fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
@ -10432,22 +10489,30 @@ impl Editor {
}
pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
if !self.buffer.read(cx).is_singleton() {
return;
}
if self.buffer.read(cx).is_singleton() {
let mut fold_ranges = Vec::new();
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut fold_ranges = Vec::new();
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..snapshot.max_row().0 {
if let Some(foldable_range) =
self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
{
fold_ranges.push(foldable_range);
for row in 0..snapshot.max_row().0 {
if let Some(foldable_range) =
self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
{
fold_ranges.push(foldable_range);
}
}
}
self.fold_creases(fold_ranges, true, cx);
self.fold_creases(fold_ranges, true, cx);
} else {
self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
editor.fold_buffer(buffer_id, cx);
}
})
.ok();
});
}
}
pub fn fold_function_bodies(
@ -10519,22 +10584,45 @@ impl Editor {
}
pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let selections = self.selections.all::<Point>(cx);
let ranges = selections
.iter()
.map(|s| {
let range = s.display_range(&display_map).sorted();
let mut start = range.start.to_point(&display_map);
let mut end = range.end.to_point(&display_map);
start.column = 0;
end.column = buffer.line_len(MultiBufferRow(end.row));
start..end
})
.collect::<Vec<_>>();
if self.is_singleton(cx) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let selections = self.selections.all::<Point>(cx);
let ranges = selections
.iter()
.map(|s| {
let range = s.display_range(&display_map).sorted();
let mut start = range.start.to_point(&display_map);
let mut end = range.end.to_point(&display_map);
start.column = 0;
end.column = buffer.line_len(MultiBufferRow(end.row));
start..end
})
.collect::<Vec<_>>();
self.unfold_ranges(&ranges, true, true, cx);
self.unfold_ranges(&ranges, true, true, cx);
} else {
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
let mut unfolded_buffers = HashSet::default();
for selection in selections {
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.head(), Bias::Right)
.buffer_id
{
if unfolded_buffers.insert(buffer_id) {
self.unfold_buffer(buffer_id, cx);
}
}
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.tail(), Bias::Left)
.buffer_id
{
if unfolded_buffers.insert(buffer_id) {
self.unfold_buffer(buffer_id, cx);
}
}
}
}
}
pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
@ -10574,8 +10662,20 @@ impl Editor {
}
pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
if self.buffer.read(cx).is_singleton() {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
} else {
self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
editor.unfold_buffer(buffer_id, cx);
}
})
.ok();
});
}
}
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
@ -10662,6 +10762,45 @@ impl Editor {
});
}
pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) {
return;
}
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
return;
};
let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
self.display_map
.update(cx, |display_map, cx| display_map.fold_buffer(buffer_id, cx));
cx.emit(EditorEvent::BufferFoldToggled {
ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
folded: true,
});
cx.notify();
}
pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) {
return;
}
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
return;
};
let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
self.display_map.update(cx, |display_map, cx| {
display_map.unfold_buffer(buffer_id, cx);
});
cx.emit(EditorEvent::BufferFoldToggled {
ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(),
folded: false,
});
cx.notify();
}
pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool {
self.display_map.read(cx).buffer_folded(buffer)
}
/// Removes any folds with the given ranges.
pub fn remove_folds_with_type<T: ToOffset + Clone>(
&mut self,
@ -13820,6 +13959,10 @@ pub enum EditorEvent {
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
BufferFoldToggled {
ids: Vec<ExcerptId>,
folded: bool,
},
ExcerptsEdited {
ids: Vec<ExcerptId>,
},

View file

@ -4064,7 +4064,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
let snapshot = editor.snapshot(cx);
let snapshot = &snapshot.buffer_snapshot;
let placement = BlockPlacement::Replace(
snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)),
snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
);
editor.insert_blocks(
[BlockProperties {
@ -13905,6 +13905,412 @@ async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"first.rs": sample_text_1,
"second.rs": sample_text_2,
"third.rs": sample_text_3,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "first.rs"), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "second.rs"), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "third.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer.push_excerpts(
buffer_2.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer.push_excerpts(
buffer_3.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"After folding the first buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"After folding the second buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n",
"After folding the third buffer, its text should not be displayed"
);
// Emulate selection inside the fold logic, that should work
multi_buffer_editor.update(cx, |editor, cx| {
editor.snapshot(cx).next_line_boundary(Point::new(0, 4));
});
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"After unfolding the second buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"After unfolding the first buffer, its and 2nd buffer's text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
"After unfolding the all buffers, all original text should be displayed"
);
}
#[gpui::test]
async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "1111\n2222\n3333".to_string();
let sample_text_2 = "4444\n5555\n6666".to_string();
let sample_text_3 = "7777\n8888\n9999".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"first.rs": sample_text_1,
"second.rs": sample_text_2,
"third.rs": sample_text_3,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "first.rs"), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "second.rs"), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "third.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer.push_excerpts(
buffer_3.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
"After folding the first buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\n7777\n8888\n9999\n",
"After folding the second buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n",
"After folding the third buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n",
"After unfolding the second buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
"After unfolding the first buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
"After unfolding all buffers, all original text should be displayed"
);
}
#[gpui::test]
async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"main.rs": sample_text,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: Point::new(0, 0)
..Point::new(
sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
0,
),
primary: None,
}],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let selection_range = Point::new(1, 0)..Point::new(2, 0);
multi_buffer_editor.update(cx, |editor, cx| {
enum TestHighlight {}
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
editor.highlight_text::<TestHighlight>(
vec![highlight_range.clone()],
HighlightStyle::color(Hsla::green()),
cx,
);
editor.change_selections(None, cx, |s| s.select_ranges(Some(highlight_range)));
});
let full_text = format!("\n\n\n{sample_text}\n");
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View file

@ -27,18 +27,18 @@ use crate::{
};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons;
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
WeakView, WindowContext,
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClickEvent,
ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element,
ElementInputHandler, Entity, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla,
InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent,
ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription,
TextRun, TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
};
use gpui::{ClickEvent, Subscription};
use itertools::Itertools;
use language::{
language_settings::{
@ -49,8 +49,8 @@ use language::{
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, ToOffset,
Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint,
MultiBufferRow, MultiBufferSnapshot, ToOffset,
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
@ -1713,6 +1713,15 @@ impl EditorElement {
}
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
let buffer_folded = snapshot
.buffer_snapshot
.buffer_line_for_row(multibuffer_row)
.map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
.map(|buffer_id| editor.buffer_folded(buffer_id, cx))
.unwrap_or(false);
if buffer_folded {
return None;
}
if snapshot.is_line_folded(multibuffer_row) {
// Skip folded indicators, unless it's the starting line of a fold.
@ -2087,6 +2096,7 @@ impl EditorElement {
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> (AnyElement, Size<Pixels>) {
let header_padding = px(6.0);
let mut element = match block {
Block::Custom(block) => {
let block_start = block.start().to_point(&snapshot.buffer_snapshot);
@ -2136,21 +2146,58 @@ impl EditorElement {
.into_any()
}
Block::ExcerptBoundary {
Block::FoldedBuffer {
first_excerpt,
prev_excerpt,
next_excerpt,
show_excerpt_controls,
starts_new_buffer,
height,
..
} => {
let icon_offset = gutter_dimensions.width
- (gutter_dimensions.left_padding + gutter_dimensions.margin);
let header_padding = px(6.0);
let mut result = v_flex().id(block_id).w_full();
if let Some(prev_excerpt) = prev_excerpt {
if *show_excerpt_controls {
result = result.child(
h_flex()
.w(icon_offset)
.h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
.flex_none()
.justify_end()
.child(self.render_expand_excerpt_button(
prev_excerpt.id,
ExpandExcerptDirection::Down,
IconName::ArrowDownFromLine,
cx,
)),
);
}
}
let jump_data = jump_data(snapshot, block_row_start, *height, first_excerpt, cx);
result
.child(self.render_buffer_header(
first_excerpt,
header_padding,
true,
jump_data,
cx,
))
.into_any_element()
}
Block::ExcerptBoundary {
prev_excerpt,
next_excerpt,
show_excerpt_controls,
height,
starts_new_buffer,
..
} => {
let icon_offset = gutter_dimensions.width
- (gutter_dimensions.left_padding + gutter_dimensions.margin);
let mut result = v_flex().id(block_id).w_full();
if let Some(prev_excerpt) = prev_excerpt {
if *show_excerpt_controls {
result = result.child(
@ -2170,115 +2217,15 @@ impl EditorElement {
}
if let Some(next_excerpt) = next_excerpt {
let buffer = &next_excerpt.buffer;
let range = &next_excerpt.range;
let jump_data = {
let jump_path =
project::File::from_dyn(buffer.file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let excerpt_start = range.context.start;
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
let offset_from_excerpt_start = if jump_anchor == excerpt_start {
0
} else {
let excerpt_start_row =
language::ToPoint::to_point(&jump_anchor, buffer).row;
jump_position.row - excerpt_start_row
};
let line_offset_from_top =
block_row_start.0 + *height + offset_from_excerpt_start
- snapshot
.scroll_anchor
.scroll_position(&snapshot.display_snapshot)
.y as u32;
JumpData {
excerpt_id: next_excerpt.id,
anchor: jump_anchor,
position: language::ToPoint::to_point(&jump_anchor, buffer),
path: jump_path,
line_offset_from_top,
}
};
let jump_data = jump_data(snapshot, block_row_start, *height, next_excerpt, cx);
if *starts_new_buffer {
let include_root = self
.editor
.read(cx)
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let path = buffer.resolve_file_path(cx, include_root);
let filename = path
.as_ref()
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
let parent_path = path.as_ref().and_then(|path| {
Some(path.parent()?.to_string_lossy().to_string() + "/")
});
result = result.child(
div()
.px(header_padding)
.pt(header_padding)
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
.child(
h_flex()
.id("path header block")
.size_full()
.flex_basis(Length::Definite(DefiniteLength::Fraction(
0.667,
)))
.px(gpui::px(12.))
.rounded_md()
.shadow_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_subheader_background)
.justify_between()
.hover(|style| style.bg(cx.theme().colors().element_hover))
.child(
h_flex().gap_3().child(
h_flex()
.gap_2()
.child(
filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into()),
)
.when_some(parent_path, |then, path| {
then.child(div().child(path).text_color(
cx.theme().colors().text_muted,
))
}),
),
)
.child(Icon::new(IconName::ArrowUpRight))
.cursor_pointer()
.tooltip(|cx| {
Tooltip::for_action("Jump to File", &OpenExcerpts, cx)
})
.on_mouse_down(MouseButton::Left, |_, cx| {
cx.stop_propagation()
})
.on_click(cx.listener_for(&self.editor, {
move |editor, e: &ClickEvent, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.down.modifiers.secondary(),
cx,
);
}
})),
),
);
result = result.child(self.render_buffer_header(
next_excerpt,
header_padding,
false,
jump_data,
cx,
));
if *show_excerpt_controls {
result = result.child(
h_flex()
@ -2428,6 +2375,105 @@ impl EditorElement {
(element, final_size)
}
fn render_buffer_header(
&self,
for_excerpt: &ExcerptInfo,
header_padding: Pixels,
is_folded: bool,
jump_data: JumpData,
cx: &mut WindowContext,
) -> Div {
let include_root = self
.editor
.read(cx)
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
let filename = path
.as_ref()
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
let parent_path = path
.as_ref()
.and_then(|path| Some(path.parent()?.to_string_lossy().to_string() + "/"));
div()
.px(header_padding)
.pt(header_padding)
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
.child(
h_flex()
.id("path header block")
.size_full()
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
.px(gpui::px(12.))
.rounded_md()
.shadow_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_subheader_background)
.justify_between()
.hover(|style| style.bg(cx.theme().colors().element_hover))
.child(
h_flex()
.gap_3()
.map(|header| {
let editor = self.editor.clone();
let buffer_id = for_excerpt.buffer_id;
let toggle_chevron_icon =
FileIcons::get_chevron_icon(!is_folded, cx)
.map(Icon::from_path);
header.child(
ButtonLike::new("toggle-buffer-fold")
.children(toggle_chevron_icon)
.on_click(move |_, cx| {
if is_folded {
editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_id, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
}
}),
)
})
.child(
h_flex()
.gap_2()
.child(
filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into()),
)
.when_some(parent_path, |then, path| {
then.child(
div()
.child(path)
.text_color(cx.theme().colors().text_muted),
)
}),
),
)
.child(Icon::new(IconName::ArrowUpRight))
.cursor_pointer()
.tooltip(|cx| Tooltip::for_action("Jump to File", &OpenExcerpts, cx))
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(cx.listener_for(&self.editor, {
move |editor, e: &ClickEvent, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.down.modifiers.secondary(),
cx,
);
}
})),
)
}
fn render_expand_excerpt_button(
&self,
excerpt_id: ExcerptId,
@ -4314,6 +4360,46 @@ impl EditorElement {
}
}
fn jump_data(
snapshot: &EditorSnapshot,
block_row_start: DisplayRow,
height: u32,
for_excerpt: &ExcerptInfo,
cx: &mut WindowContext<'_>,
) -> JumpData {
let range = &for_excerpt.range;
let buffer = &for_excerpt.buffer;
let jump_path = project::File::from_dyn(buffer.file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let excerpt_start = range.context.start;
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
let offset_from_excerpt_start = if jump_anchor == excerpt_start {
0
} else {
let excerpt_start_row = language::ToPoint::to_point(&jump_anchor, buffer).row;
jump_position.row - excerpt_start_row
};
let line_offset_from_top = block_row_start.0 + height + offset_from_excerpt_start
- snapshot
.scroll_anchor
.scroll_position(&snapshot.display_snapshot)
.y as u32;
JumpData {
excerpt_id: for_excerpt.id,
anchor: jump_anchor,
position: language::ToPoint::to_point(&jump_anchor, buffer),
path: jump_path,
line_offset_from_top,
}
}
fn inline_completion_popover_text(
editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>,
@ -5757,29 +5843,33 @@ impl Element for EditorElement {
if !expanded_add_hunks_by_rows
.contains_key(&newest_selection_display_row)
{
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
MultiBufferRow(newest_selection_point.row),
);
if let Some((buffer, range)) = buffer {
let buffer_id = buffer.remote_id();
let row = range.start.row;
let has_test_indicator = self
.editor
.read(cx)
.tasks
.contains_key(&(buffer_id, row));
if !snapshot
.is_line_folded(MultiBufferRow(newest_selection_point.row))
{
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
MultiBufferRow(newest_selection_point.row),
);
if let Some((buffer, range)) = buffer {
let buffer_id = buffer.remote_id();
let row = range.start.row;
let has_test_indicator = self
.editor
.read(cx)
.tasks
.contains_key(&(buffer_id, row));
if !has_test_indicator {
code_actions_indicator = self
.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
&rows_with_hunk_bounds,
cx,
);
if !has_test_indicator {
code_actions_indicator = self
.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
&rows_with_hunk_bounds,
cx,
);
}
}
}
}

View file

@ -172,13 +172,7 @@ pub fn indent_guides_in_range(
let start =
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
!snapshot.is_line_folded(start)
})
.collect()
}

View file

@ -195,6 +195,7 @@ pub struct ExcerptInfo {
pub buffer: BufferSnapshot,
pub buffer_id: BufferId,
pub range: ExcerptRange<text::Anchor>,
pub text_summary: TextSummary,
}
impl std::fmt::Debug for ExcerptInfo {
@ -1546,6 +1547,33 @@ impl MultiBuffer {
excerpts
}
pub fn excerpt_ranges_for_buffer(
&self,
buffer_id: BufferId,
cx: &AppContext,
) -> Vec<Range<Point>> {
let snapshot = self.read(cx);
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&());
buffers
.get(&buffer_id)
.into_iter()
.flat_map(|state| &state.excerpts)
.filter_map(move |locator| {
cursor.seek_forward(&Some(locator), Bias::Left, &());
cursor.item().and_then(|excerpt| {
if excerpt.locator == *locator {
let excerpt_start = cursor.start().1;
let excerpt_end = excerpt_start + excerpt.text_summary.lines;
Some(excerpt_start..excerpt_end)
} else {
None
}
})
})
.collect()
}
pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
self.snapshot
.borrow()
@ -3559,6 +3587,7 @@ impl MultiBufferSnapshot {
buffer: excerpt.buffer.clone(),
buffer_id: excerpt.buffer_id,
range: excerpt.range.clone(),
text_summary: excerpt.text_summary.clone(),
});
if next.is_none() {
@ -3574,6 +3603,7 @@ impl MultiBufferSnapshot {
buffer: prev_excerpt.buffer.clone(),
buffer_id: prev_excerpt.buffer_id,
range: prev_excerpt.range.clone(),
text_summary: prev_excerpt.text_summary.clone(),
});
let row = MultiBufferRow(cursor.start().1.row);

File diff suppressed because it is too large Load diff