Introduce Editor::insert_flaps and Editor::remove_flaps (#12096)

This pull request introduces the ability to add flaps, custom foldable
regions whose first foldable line can be associated with:

- A toggle in the gutter
- A trailer showed at the end of the line, before the inline blame
information


https://github.com/zed-industries/zed/assets/482957/c53a9148-f31a-4743-af64-18afa73c404c

To achieve this, we changed `FoldMap::fold` to accept a piece of text to
display when the range is folded. We use this capability in flaps to
avoid displaying the ellipsis character.

We want to use this new API in the assistant to fold context while still
giving visual cues as to what that context is.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Antonio Scandurra
2024-05-21 20:23:37 +02:00
committed by GitHub
co-authored by Nathan Sobo Mikayla Max
parent b89f360199
commit f3710877f1
11 changed files with 868 additions and 239 deletions
+107 -53
View File
@@ -523,6 +523,7 @@ pub struct EditorSnapshot {
scroll_anchor: ScrollAnchor,
ongoing_scroll: OngoingScroll,
current_line_highlight: CurrentLineHighlight,
gutter_hovered: bool,
}
const GIT_BLAME_GUTTER_WIDTH_CHARS: f32 = 53.;
@@ -1886,6 +1887,7 @@ impl Editor {
placeholder_text: self.placeholder_text.clone(),
is_focused: self.focus_handle.is_focused(cx),
current_line_highlight: self.current_line_highlight,
gutter_hovered: self.gutter_hovered,
}
}
@@ -4639,44 +4641,6 @@ impl Editor {
}))
}
pub fn render_fold_indicators(
&mut self,
fold_data: Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
_style: &EditorStyle,
gutter_hovered: bool,
_line_height: Pixels,
_gutter_margin: Pixels,
cx: &mut ViewContext<Self>,
) -> Vec<Option<AnyElement>> {
fold_data
.iter()
.enumerate()
.map(|(ix, fold_data)| {
fold_data
.map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
IconButton::new(ix, ui::IconName::ChevronDown)
.on_click(cx.listener(move |this, _e, cx| match fold_status {
FoldStatus::Folded => {
this.unfold_at(&UnfoldAt { buffer_row }, cx);
}
FoldStatus::Foldable => {
this.fold_at(&FoldAt { buffer_row }, cx);
}
}))
.icon_color(ui::Color::Muted)
.icon_size(ui::IconSize::Small)
.selected(fold_status == FoldStatus::Folded)
.selected_icon(ui::IconName::ChevronRight)
.size(ui::ButtonSize::None)
.into_any_element()
})
})
.flatten()
})
.collect()
}
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.read()
@@ -5830,7 +5794,7 @@ impl Editor {
let mut end = fold.range.end.to_point(&buffer);
start.row -= row_delta;
end.row -= row_delta;
refold_ranges.push(start..end);
refold_ranges.push((start..end, fold.text));
}
}
}
@@ -5924,7 +5888,7 @@ impl Editor {
let mut end = fold.range.end.to_point(&buffer);
start.row += row_delta;
end.row += row_delta;
refold_ranges.push(start..end);
refold_ranges.push((start..end, fold.text));
}
}
}
@@ -9282,11 +9246,11 @@ impl Editor {
let buffer_start_row = range.start.row;
for row in (0..=range.end.row).rev() {
let fold_range = display_map.foldable_range(MultiBufferRow(row));
if let Some(fold_range) = fold_range {
if fold_range.end.row >= buffer_start_row {
fold_ranges.push(fold_range);
if let Some((foldable_range, fold_text)) =
display_map.foldable_range(MultiBufferRow(row))
{
if foldable_range.end.row >= buffer_start_row {
fold_ranges.push((foldable_range, fold_text));
if row <= range.start.row {
break;
}
@@ -9302,14 +9266,14 @@ impl Editor {
let buffer_row = fold_at.buffer_row;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
if let Some(fold_range) = display_map.foldable_range(buffer_row) {
if let Some((fold_range, fold_text)) = display_map.foldable_range(buffer_row) {
let autoscroll = self
.selections
.all::<Point>(cx)
.iter()
.any(|selection| fold_range.overlaps(&selection.range()));
self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
self.fold_ranges([(fold_range, fold_text)], autoscroll, cx);
}
}
@@ -9363,9 +9327,9 @@ impl Editor {
.buffer_snapshot
.line_len(MultiBufferRow(s.end.row)),
);
start..end
(start..end, "⋯")
} else {
s.start..s.end
(s.start..s.end, "⋯")
}
});
self.fold_ranges(ranges, true, cx);
@@ -9373,18 +9337,20 @@ impl Editor {
pub fn fold_ranges<T: ToOffset + Clone>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
let mut fold_ranges = Vec::new();
let mut buffers_affected = HashMap::default();
let multi_buffer = self.buffer().read(cx);
for range in ranges {
if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) {
for (fold_range, fold_text) in ranges {
if let Some((_, buffer, _)) =
multi_buffer.excerpt_containing(fold_range.start.clone(), cx)
{
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
};
fold_ranges.push(range);
fold_ranges.push((fold_range, fold_text));
}
let mut ranges = fold_ranges.into_iter().peekable();
@@ -9500,6 +9466,24 @@ impl Editor {
}
}
pub fn insert_flaps(
&mut self,
flaps: impl IntoIterator<Item = Flap>,
cx: &mut ViewContext<Self>,
) -> Vec<FlapId> {
self.display_map
.update(cx, |map, cx| map.insert_flaps(flaps, cx))
}
pub fn remove_flaps(
&mut self,
ids: impl IntoIterator<Item = FlapId>,
cx: &mut ViewContext<Self>,
) {
self.display_map
.update(cx, |map, cx| map.remove_flaps(ids, cx));
}
pub fn longest_row(&self, cx: &mut AppContext) -> DisplayRow {
self.display_map
.update(cx, |map, cx| map.snapshot(cx))
@@ -11098,6 +11082,76 @@ impl EditorSnapshot {
git_blame_entries_width,
}
}
pub fn render_fold_toggle(
&self,
buffer_row: MultiBufferRow,
row_contains_cursor: bool,
editor: View<Editor>,
cx: &mut WindowContext,
) -> Option<AnyElement> {
let folded = self.is_line_folded(buffer_row);
if let Some(flap) = self
.flap_snapshot
.query_row(buffer_row, &self.buffer_snapshot)
{
let toggle_callback = Arc::new(move |folded, cx: &mut WindowContext| {
if folded {
editor.update(cx, |editor, cx| {
editor.fold_at(&crate::FoldAt { buffer_row }, cx)
});
} else {
editor.update(cx, |editor, cx| {
editor.unfold_at(&crate::UnfoldAt { buffer_row }, cx)
});
}
});
Some((flap.render_toggle)(
buffer_row,
folded,
toggle_callback,
cx,
))
} else if folded
|| (self.starts_indent(buffer_row) && (row_contains_cursor || self.gutter_hovered))
{
Some(
IconButton::new(
("indent-fold-indicator", buffer_row.0),
ui::IconName::ChevronDown,
)
.on_click(cx.listener_for(&editor, move |this, _e, cx| {
if folded {
this.unfold_at(&UnfoldAt { buffer_row }, cx);
} else {
this.fold_at(&FoldAt { buffer_row }, cx);
}
}))
.icon_color(ui::Color::Muted)
.icon_size(ui::IconSize::Small)
.selected(folded)
.selected_icon(ui::IconName::ChevronRight)
.size(ui::ButtonSize::None)
.into_any_element(),
)
} else {
None
}
}
pub fn render_flap_trailer(
&self,
buffer_row: MultiBufferRow,
cx: &mut WindowContext,
) -> Option<AnyElement> {
let folded = self.is_line_folded(buffer_row);
let flap = self
.flap_snapshot
.query_row(buffer_row, &self.buffer_snapshot)?;
Some((flap.render_trailer)(buffer_row, folded, cx))
}
}
impl Deref for EditorSnapshot {