diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 99a74fe7f2..2097f594eb 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -23,6 +23,12 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; +#[derive(Copy, Clone, Debug)] +pub enum FoldStatus { + Folded, + Foldable, +} + pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } @@ -591,6 +597,57 @@ impl DisplaySnapshot { self.blocks_snapshot.longest_row() } + pub fn fold_for_line(self: &Self, display_row: u32) -> Option { + if self.is_line_foldable(display_row) { + Some(FoldStatus::Foldable) + } else if self.is_line_folded(display_row) { + Some(FoldStatus::Folded) + } else { + None + } + } + + pub fn is_line_foldable(self: &Self, display_row: u32) -> bool { + let max_point = self.max_point(); + if display_row >= max_point.row() { + false + } else { + let (start_indent, is_blank) = self.line_indent(display_row); + if is_blank { + false + } else { + for display_row in display_row + 1..=max_point.row() { + let (indent, is_blank) = self.line_indent(display_row); + if !is_blank { + return indent > start_indent; + } + } + false + } + } + } + + pub fn foldable_range_for_line(self: &Self, start_row: u32) -> Option> { + if self.is_line_foldable(start_row) && !self.is_line_folded(start_row) { + let max_point = self.max_point(); + let (start_indent, _) = self.line_indent(start_row); + let start = DisplayPoint::new(start_row, self.line_len(start_row)); + let mut end = None; + for row in start_row + 1..=max_point.row() { + let (indent, is_blank) = self.line_indent(row); + if !is_blank && indent <= start_indent { + end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1))); + break; + } + } + + let end = end.unwrap_or(max_point); + Some(start.to_point(self)..end.to_point(self)) + } else { + return None; + } + } + #[cfg(any(test, feature = "test-support"))] pub fn highlight_ranges( &self, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2cc26c26ed..76ce6f1e4b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1,6 +1,7 @@ mod blink_manager; pub mod display_map; mod element; + mod git; mod highlight_matching_bracket; mod hover_popover; @@ -160,6 +161,16 @@ pub struct ToggleComments { pub advance_downwards: bool, } +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct FoldAt { + pub display_row: u32, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct UnfoldAt { + pub display_row: u32, +} + actions!( editor, [ @@ -258,6 +269,8 @@ impl_actions!( ConfirmCompletion, ConfirmCodeAction, ToggleComments, + FoldAt, + UnfoldAt ] ); @@ -348,7 +361,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::go_to_definition); cx.add_action(Editor::go_to_type_definition); cx.add_action(Editor::fold); + cx.add_action(Editor::fold_at); cx.add_action(Editor::unfold_lines); + cx.add_action(Editor::unfold_at); cx.add_action(Editor::fold_selected_ranges); cx.add_action(Editor::show_completions); cx.add_action(Editor::toggle_code_actions); @@ -2648,9 +2663,9 @@ impl Editor { cx: &mut RenderContext, ) -> Option { if self.available_code_actions.is_some() { - enum Tag {} + enum CodeActions {} Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::::new(0, cx, |_, _| { Svg::new("icons/bolt_8.svg") .with_color(style.code_actions.indicator) .boxed() @@ -2669,6 +2684,51 @@ impl Editor { } } + pub fn render_fold_indicators( + &self, + fold_data: Vec<(u32, FoldStatus)>, + fold_indicators: &mut Vec<(u32, ElementBox)>, + style: &EditorStyle, + cx: &mut RenderContext, + ) { + enum FoldIndicators {} + + for (fold_location, fold_status) in fold_data.iter() { + fold_indicators.push(( + *fold_location, + MouseEventHandler::::new( + *fold_location as usize, + cx, + |_, _| -> ElementBox { + Svg::new(match *fold_status { + FoldStatus::Folded => "icons/chevron_right_8.svg", + FoldStatus::Foldable => "icons/chevron_down_8.svg", + }) + .with_color(style.folds.indicator) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(3.)) + .on_down(MouseButton::Left, { + let fold_location = *fold_location; + let fold_status = *fold_status; + move |_, cx| { + cx.dispatch_any_action(match fold_status { + FoldStatus::Folded => Box::new(UnfoldAt { + display_row: fold_location, + }), + FoldStatus::Foldable => Box::new(FoldAt { + display_row: fold_location, + }), + }); + } + }) + .boxed(), + )) + } + } + pub fn context_menu_visible(&self) -> bool { self.context_menu .as_ref() @@ -3251,26 +3311,12 @@ impl Editor { while let Some(selection) = selections.next() { // Find all the selections that span a contiguous row range - contiguous_row_selections.push(selection.clone()); - let start_row = selection.start.row; - let mut end_row = if selection.end.column > 0 || selection.is_empty() { - display_map.next_line_boundary(selection.end).0.row + 1 - } else { - selection.end.row - }; - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row { - end_row = if next_selection.end.column > 0 || next_selection.is_empty() { - display_map.next_line_boundary(next_selection.end).0.row + 1 - } else { - next_selection.end.row - }; - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); // Move the text spanned by the row range to be before the line preceding the row range if start_row > 0 { @@ -3363,26 +3409,12 @@ impl Editor { while let Some(selection) = selections.next() { // Find all the selections that span a contiguous row range - contiguous_row_selections.push(selection.clone()); - let start_row = selection.start.row; - let mut end_row = if selection.end.column > 0 || selection.is_empty() { - display_map.next_line_boundary(selection.end).0.row + 1 - } else { - selection.end.row - }; - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row { - end_row = if next_selection.end.column > 0 || next_selection.is_empty() { - display_map.next_line_boundary(next_selection.end).0.row + 1 - } else { - next_selection.end.row - }; - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); // Move the text spanned by the row range to be after the last line of the row range if end_row <= buffer.max_point().row { @@ -5676,14 +5708,14 @@ impl Editor { let mut fold_ranges = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); for selection in selections { let range = selection.display_range(&display_map).sorted(); let buffer_start_row = range.start.to_point(&display_map).row; for row in (0..=range.end.row()).rev() { - if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) { - let fold_range = self.foldable_range_for_line(&display_map, row); + if let Some(fold_range) = display_map.foldable_range_for_line(row) { if fold_range.end.row >= buffer_start_row { fold_ranges.push(fold_range); if row <= range.start.row() { @@ -5697,6 +5729,16 @@ impl Editor { self.fold_ranges(fold_ranges, cx); } + pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext) { + let display_row = fold_at.display_row; + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(fold_range) = display_map.foldable_range_for_line(display_row) { + self.fold_ranges(std::iter::once(fold_range), cx); + } + } + pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -5715,46 +5757,11 @@ impl Editor { self.unfold_ranges(ranges, true, cx); } - fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool { - let max_point = display_map.max_point(); - if display_row >= max_point.row() { - false - } else { - let (start_indent, is_blank) = display_map.line_indent(display_row); - if is_blank { - false - } else { - for display_row in display_row + 1..=max_point.row() { - let (indent, is_blank) = display_map.line_indent(display_row); - if !is_blank { - return indent > start_indent; - } - } - false - } - } - } + pub fn unfold_at(&mut self, fold_at: &UnfoldAt, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let unfold_range = display_map.buffer_snapshot.row_span(fold_at.display_row); - fn foldable_range_for_line( - &self, - display_map: &DisplaySnapshot, - start_row: u32, - ) -> Range { - let max_point = display_map.max_point(); - - let (start_indent, _) = display_map.line_indent(start_row); - let start = DisplayPoint::new(start_row, display_map.line_len(start_row)); - let mut end = None; - for row in start_row + 1..=max_point.row() { - let (indent, is_blank) = display_map.line_indent(row); - if !is_blank && indent <= start_indent { - end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1))); - break; - } - } - - let end = end.unwrap_or(max_point); - start.to_point(display_map)..end.to_point(display_map) + self.unfold_ranges(std::iter::once(unfold_range), true, cx) } pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { @@ -6252,6 +6259,35 @@ impl Editor { } } +fn consume_contiguous_rows( + contiguous_row_selections: &mut Vec>, + selection: &Selection, + display_map: &DisplaySnapshot, + selections: &mut std::iter::Peekable>>, +) -> (u32, u32) { + contiguous_row_selections.push(selection.clone()); + let start_row = selection.start.row; + let mut end_row = ending_row(selection, display_map); + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row { + end_row = ending_row(next_selection, display_map); + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + (start_row, end_row) +} + +fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> u32 { + if next_selection.end.column > 0 || next_selection.is_empty() { + display_map.next_line_boundary(next_selection.end).0.row + 1 + } else { + next_selection.end.row + } +} + impl EditorSnapshot { pub fn language_at(&self, position: T) -> Option<&Arc> { self.display_snapshot.buffer_snapshot.language_at(position) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 57e0f18035..68f136bddb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ ToPoint, MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, @@ -575,6 +575,16 @@ impl EditorElement { y += (line_height - indicator.size().y()) / 2.; indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); } + + for (line, fold_indicator) in layout.fold_indicators.iter_mut() { + let mut x = bounds.width() - layout.gutter_padding; + let mut y = *line as f32 * line_height - scroll_top; + + x += ((layout.gutter_padding + layout.gutter_margin) - fold_indicator.size().x()) / 2.; + y += (line_height - fold_indicator.size().y()) / 2.; + + fold_indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); + } } fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { @@ -1118,6 +1128,21 @@ impl EditorElement { .width() } + fn get_fold_indicators( + &self, + display_rows: Range, + snapshot: &EditorSnapshot, + ) -> Vec<(u32, FoldStatus)> { + display_rows + .into_iter() + .filter_map(|display_row| { + snapshot + .fold_for_line(display_row) + .map(|fold_status| (display_row, fold_status)) + }) + .collect() + } + //Folds contained in a hunk are ignored apart from shrinking visual size //If a fold contains any hunks then that fold line is marked as modified fn layout_git_gutters( @@ -1689,6 +1714,8 @@ impl Element for EditorElement { let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); + let folds = self.get_fold_indicators(start_row..end_row, &snapshot); + let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines); let mut max_visible_line_width = 0.0; @@ -1751,6 +1778,7 @@ impl Element for EditorElement { } }); + let mut fold_indicators = Vec::with_capacity(folds.len()); let mut context_menu = None; let mut code_actions_indicator = None; let mut hover = None; @@ -1774,6 +1802,8 @@ impl Element for EditorElement { .map(|indicator| (newest_selection_head.row(), indicator)); } + view.render_fold_indicators(folds, &mut fold_indicators, &style, cx); + let visible_rows = start_row..start_row + line_layouts.len() as u32; hover = view.hover_state.render(&snapshot, &style, visible_rows, cx); mode = view.mode; @@ -1802,6 +1832,16 @@ impl Element for EditorElement { ); } + for (_, indicator) in fold_indicators.iter_mut() { + indicator.layout( + SizeConstraint::strict_along( + Axis::Vertical, + line_height * style.code_actions.vertical_scale, + ), + cx, + ); + } + if let Some((_, hover_popovers)) = hover.as_mut() { for hover_popover in hover_popovers.iter_mut() { hover_popover.layout( @@ -1851,6 +1891,7 @@ impl Element for EditorElement { selections, context_menu, code_actions_indicator, + fold_indicators, hover_popovers: hover, }, ) @@ -1979,6 +2020,7 @@ pub struct LayoutState { context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, hover_popovers: Option<(DisplayPoint, Vec)>, + fold_indicators: Vec<(u32, ElementBox)>, } pub struct PositionMap { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index da3c6bc4bd..541c63ff1c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1916,6 +1916,10 @@ impl MultiBufferSnapshot { } } + pub fn row_span(&self, display_row: u32) -> Range { + Point::new(display_row, 0)..Point::new(display_row, self.line_len(display_row)) + } + pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows { let mut result = MultiBufferRows { buffer_row_range: 0..0, diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index b77d46536d..a42dc1cfa8 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -296,7 +296,10 @@ impl AnyElement for Lifecycle { paint, } } - _ => panic!("invalid element lifecycle state"), + Lifecycle::Empty => panic!("invalid element lifecycle state"), + Lifecycle::Init { .. } => { + panic!("invalid element lifecycle state, paint called before layout") + } } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index efe64cbc5c..c9695d4f36 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -562,6 +562,7 @@ pub struct Editor { pub invalid_hint_diagnostic: DiagnosticStyle, pub autocomplete: AutocompleteStyle, pub code_actions: CodeActions, + pub folds: Folds, pub unnecessary_code_fade: f32, pub hover_popover: HoverPopover, pub link_definition: HighlightStyle, @@ -638,6 +639,13 @@ pub struct CodeActions { pub vertical_scale: f32, } +#[derive(Clone, Deserialize, Default)] +pub struct Folds { + #[serde(default)] + pub indicator: Color, + pub fold_background: Color, +} + #[derive(Clone, Deserialize, Default)] pub struct DiffStyle { pub inserted: Color, diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 85c9ccec95..3518c2aba5 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -47,6 +47,10 @@ export default function editor(colorScheme: ColorScheme) { indicator: foreground(layer, "variant"), verticalScale: 0.55, }, + folds: { + indicator: foreground(layer, "variant"), + fold_background: foreground(layer, "variant"), + }, diff: { deleted: foreground(layer, "negative"), modified: foreground(layer, "warning"),