diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 99a74fe7f2..229ecc34fe 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, PartialEq, Eq)] +pub enum FoldStatus { + Folded, + Foldable, +} + pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } @@ -591,6 +597,60 @@ impl DisplaySnapshot { self.blocks_snapshot.longest_row() } + pub fn fold_for_line(self: &Self, display_row: u32) -> Option { + if self.is_foldable(display_row) { + Some(FoldStatus::Foldable) + } else if self.is_line_folded(display_row) { + Some(FoldStatus::Folded) + } else { + None + } + } + + pub fn is_foldable(self: &Self, row: u32) -> bool { + let max_point = self.max_point(); + if row >= max_point.row() { + return false; + } + + let (start_indent, is_blank) = self.line_indent(row); + if is_blank { + return false; + } + + for display_row in next_rows(row, self) { + let (indent, is_blank) = self.line_indent(display_row); + if !is_blank { + return indent > start_indent; + } + } + + return false; + } + + pub fn foldable_range(self: &Self, row: u32) -> Option> { + let start = DisplayPoint::new(row, self.line_len(row)); + + if self.is_foldable(row) && !self.is_line_folded(start.row()) { + let (start_indent, _) = self.line_indent(row); + let max_point = self.max_point(); + let mut end = None; + + for row in next_rows(row, self) { + 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..end) + } else { + None + } + } + #[cfg(any(test, feature = "test-support"))] pub fn highlight_ranges( &self, @@ -678,6 +738,24 @@ impl ToDisplayPoint for Anchor { } } +pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator { + let max_row = display_map.max_point().row(); + let start_row = display_row + 1; + let mut current = None; + std::iter::from_fn(move || { + if current == None { + current = Some(start_row); + } else { + current = Some(current.unwrap() + 1) + } + if current.unwrap() > max_row { + None + } else { + current + } + }) +} + #[cfg(test)] pub mod tests { use super::*; @@ -1167,7 +1245,7 @@ pub mod tests { vec![ ("fn ".to_string(), None), ("out".to_string(), Some(Color::blue())), - ("…".to_string(), None), + ("⋯".to_string(), None), (" fn ".to_string(), Some(Color::red())), ("inner".to_string(), Some(Color::blue())), ("() {}\n}".to_string(), Some(Color::red())), @@ -1248,7 +1326,7 @@ pub mod tests { cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), [ ("out".to_string(), Some(Color::blue())), - ("…\n".to_string(), None), + ("⋯\n".to_string(), None), (" \nfn ".to_string(), Some(Color::red())), ("i\n".to_string(), Some(Color::blue())) ] diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index a2f7005e3d..44de95fe32 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -370,7 +370,7 @@ impl FoldMap { } if fold.end > fold.start { - let output_text = "…"; + let output_text = "⋯"; new_transforms.push( Transform { summary: TransformSummary { @@ -1214,7 +1214,7 @@ mod tests { Point::new(0, 2)..Point::new(2, 2), Point::new(2, 4)..Point::new(4, 1), ]); - assert_eq!(snapshot2.text(), "aa…cc…eeeee"); + assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee"); assert_eq!( edits, &[ @@ -1241,7 +1241,7 @@ mod tests { buffer.snapshot(cx) }); let (snapshot3, edits) = map.read(buffer_snapshot, subscription.consume().into_inner()); - assert_eq!(snapshot3.text(), "123a…c123c…eeeee"); + assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee"); assert_eq!( edits, &[ @@ -1261,12 +1261,12 @@ mod tests { buffer.snapshot(cx) }); let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); - assert_eq!(snapshot4.text(), "123a…c123456eee"); + assert_eq!(snapshot4.text(), "123a⋯c123456eee"); let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false); let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]); - assert_eq!(snapshot5.text(), "123a…c123456eee"); + assert_eq!(snapshot5.text(), "123a⋯c123456eee"); let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true); @@ -1287,19 +1287,19 @@ mod tests { let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); writer.fold(vec![5..8]); let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); - assert_eq!(snapshot.text(), "abcde…ijkl"); + assert_eq!(snapshot.text(), "abcde⋯ijkl"); // Create an fold adjacent to the start of the first fold. let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); writer.fold(vec![0..1, 2..5]); let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); - assert_eq!(snapshot.text(), "…b…ijkl"); + assert_eq!(snapshot.text(), "⋯b⋯ijkl"); // Create an fold adjacent to the end of the first fold. let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); writer.fold(vec![11..11, 8..10]); let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); - assert_eq!(snapshot.text(), "…b…kl"); + assert_eq!(snapshot.text(), "⋯b⋯kl"); } { @@ -1309,7 +1309,7 @@ mod tests { let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); writer.fold(vec![0..2, 2..5]); let (snapshot, _) = map.read(buffer_snapshot, vec![]); - assert_eq!(snapshot.text(), "…fghijkl"); + assert_eq!(snapshot.text(), "⋯fghijkl"); // Edit within one of the folds. let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -1317,7 +1317,7 @@ mod tests { buffer.snapshot(cx) }); let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner()); - assert_eq!(snapshot.text(), "12345…fghijkl"); + assert_eq!(snapshot.text(), "12345⋯fghijkl"); } } @@ -1334,7 +1334,7 @@ mod tests { Point::new(3, 1)..Point::new(4, 1), ]); let (snapshot, _) = map.read(buffer_snapshot, vec![]); - assert_eq!(snapshot.text(), "aa…eeeee"); + assert_eq!(snapshot.text(), "aa⋯eeeee"); } #[gpui::test] @@ -1351,14 +1351,14 @@ mod tests { Point::new(3, 1)..Point::new(4, 1), ]); let (snapshot, _) = map.read(buffer_snapshot, vec![]); - assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee"); + assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx); buffer.snapshot(cx) }); let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner()); - assert_eq!(snapshot.text(), "aa…eeeee"); + assert_eq!(snapshot.text(), "aa⋯eeeee"); } #[gpui::test] @@ -1450,7 +1450,7 @@ mod tests { let mut expected_text: String = buffer_snapshot.text().to_string(); for fold_range in map.merged_fold_ranges().into_iter().rev() { - expected_text.replace_range(fold_range.start..fold_range.end, "…"); + expected_text.replace_range(fold_range.start..fold_range.end, "⋯"); } assert_eq!(snapshot.text(), expected_text); @@ -1655,7 +1655,7 @@ mod tests { ]); let (snapshot, _) = map.read(buffer_snapshot, vec![]); - assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee\nffffff\n"); + assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n"); assert_eq!( snapshot.buffer_rows(0).collect::>(), [Some(0), Some(3), Some(5), Some(6)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2cc26c26ed..69bb298c22 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; @@ -38,10 +39,11 @@ use gpui::{ impl_actions, impl_internal_actions, keymap_matcher::KeymapContext, platform::CursorStyle, + scene::MouseClick, serde_json::json, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, - ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + EventContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HideHover, HoverState}; @@ -160,6 +162,21 @@ 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, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct GutterHover { + pub hovered: bool, +} + actions!( editor, [ @@ -258,6 +275,9 @@ impl_actions!( ConfirmCompletion, ConfirmCodeAction, ToggleComments, + FoldAt, + UnfoldAt, + GutterHover ] ); @@ -348,7 +368,10 @@ 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::gutter_hover); cx.add_action(Editor::fold_selected_ranges); cx.add_action(Editor::show_completions); cx.add_action(Editor::toggle_code_actions); @@ -435,6 +458,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; +type TextClickedCallback = + fn(&MouseClick, &Range, &EditorSnapshot, &mut EventContext); pub struct Editor { handle: WeakViewHandle, @@ -463,6 +488,7 @@ pub struct Editor { highlighted_rows: Option>, #[allow(clippy::type_complexity)] background_highlights: BTreeMap Color, Vec>)>, + clickable_text: BTreeMap>)>, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -480,6 +506,7 @@ pub struct Editor { leader_replica_id: Option, remote_id: Option, hover_state: HoverState, + gutter_hovered: bool, link_go_to_definition_state: LinkGoToDefinitionState, _subscriptions: Vec, } @@ -1132,6 +1159,7 @@ impl Editor { placeholder_text: None, highlighted_rows: None, background_highlights: Default::default(), + clickable_text: Default::default(), nav_history: None, context_menu: None, mouse_context_menu: cx.add_view(context_menu::ContextMenu::new), @@ -1151,6 +1179,7 @@ impl Editor { remote_id: None, hover_state: Default::default(), link_go_to_definition_state: Default::default(), + gutter_hovered: false, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), @@ -2645,14 +2674,15 @@ impl Editor { pub fn render_code_actions_indicator( &self, style: &EditorStyle, + active: bool, cx: &mut RenderContext, ) -> Option { if self.available_code_actions.is_some() { - enum Tag {} + enum CodeActions {} Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::::new(0, cx, |state, _| { Svg::new("icons/bolt_8.svg") - .with_color(style.code_actions.indicator) + .with_color(style.code_actions.indicator.style_for(state, active).color) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) @@ -2669,6 +2699,76 @@ impl Editor { } } + pub fn render_fold_indicators( + &self, + fold_data: Option>, + style: &EditorStyle, + gutter_hovered: bool, + line_height: f32, + gutter_margin: f32, + cx: &mut RenderContext, + ) -> Option> { + enum FoldIndicators {} + + let style = style.folds.clone(); + + fold_data.map(|fold_data| { + fold_data + .iter() + .copied() + .filter_map(|(fold_location, fold_status)| { + (gutter_hovered || fold_status == FoldStatus::Folded).then(|| { + ( + fold_location, + MouseEventHandler::::new( + fold_location as usize, + cx, + |mouse_state, _| -> ElementBox { + Svg::new(match fold_status { + FoldStatus::Folded => style.folded_icon.clone(), + FoldStatus::Foldable => style.foldable_icon.clone(), + }) + .with_color( + style + .indicator + .style_for( + mouse_state, + fold_status == FoldStatus::Folded, + ) + .color, + ) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_height(line_height) + .with_width(gutter_margin) + .aligned() + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(3.)) + .on_click(MouseButton::Left, { + 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(), + ) + }) + }) + .collect() + }) + } + pub fn context_menu_visible(&self) -> bool { self.context_menu .as_ref() @@ -3251,26 +3351,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 { @@ -3335,13 +3421,13 @@ impl Editor { } self.transact(cx, |this, cx| { - this.unfold_ranges(unfold_ranges, true, cx); + this.unfold_ranges(unfold_ranges, true, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([(range, text)], None, cx); } }); - this.fold_ranges(refold_ranges, cx); + this.fold_ranges(refold_ranges, true, cx); this.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select(new_selections); }) @@ -3363,26 +3449,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 { @@ -3440,13 +3512,13 @@ impl Editor { } self.transact(cx, |this, cx| { - this.unfold_ranges(unfold_ranges, true, cx); + this.unfold_ranges(unfold_ranges, true, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([(range, text)], None, cx); } }); - this.fold_ranges(refold_ranges, cx); + this.fold_ranges(refold_ranges, true, cx); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); }); } @@ -4274,7 +4346,7 @@ impl Editor { to_unfold.push(selection.start..selection.end); } } - self.unfold_ranges(to_unfold, true, cx); + self.unfold_ranges(to_unfold, true, true, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(new_selection_ranges); }); @@ -4423,7 +4495,7 @@ impl Editor { } if let Some(next_selected_range) = next_selected_range { - self.unfold_ranges([next_selected_range.clone()], false, cx); + self.unfold_ranges([next_selected_range.clone()], false, true, cx); self.change_selections(Some(Autoscroll::newest()), cx, |s| { if action.replace_newest { s.delete(s.newest_anchor().id); @@ -4456,7 +4528,7 @@ impl Editor { wordwise: true, done: false, }; - self.unfold_ranges([selection.start..selection.end], false, cx); + self.unfold_ranges([selection.start..selection.end], false, true, cx); self.change_selections(Some(Autoscroll::newest()), cx, |s| { s.select(selections); }); @@ -5676,14 +5748,18 @@ 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); + let fold_range = display_map.foldable_range(row).map(|range| { + range.start.to_point(&display_map)..range.end.to_point(&display_map) + }); + + if let Some(fold_range) = fold_range { if fold_range.end.row >= buffer_start_row { fold_ranges.push(fold_range); if row <= range.start.row() { @@ -5694,7 +5770,26 @@ impl Editor { } } - self.fold_ranges(fold_ranges, cx); + self.fold_ranges(fold_ranges, true, 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(display_row) { + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| fold_range.overlaps(&selection.display_range(&display_map))); + + let fold_range = + fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map); + + self.fold_ranges(std::iter::once(fold_range), autoscroll, cx); + } } pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext) { @@ -5712,85 +5807,137 @@ impl Editor { start..end }) .collect::>(); - self.unfold_ranges(ranges, true, cx); + + self.unfold_ranges(ranges, true, 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, unfold_at: &UnfoldAt, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - fn foldable_range_for_line( - &self, - display_map: &DisplaySnapshot, - start_row: u32, - ) -> Range { - let max_point = display_map.max_point(); + let intersection_range = DisplayPoint::new(unfold_at.display_row, 0) + ..DisplayPoint::new( + unfold_at.display_row, + display_map.line_len(unfold_at.display_row), + ); - 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 autoscroll = + self.selections.all::(cx).iter().any(|selection| { + intersection_range.overlaps(&selection.display_range(&display_map)) + }); - let end = end.unwrap_or(max_point); - start.to_point(display_map)..end.to_point(display_map) + let display_point = DisplayPoint::new(unfold_at.display_row, 0).to_point(&display_map); + + let mut point_range = display_point..display_point; + + point_range.start.column = 0; + point_range.end.column = display_map.buffer_snapshot.line_len(point_range.end.row); + + self.unfold_ranges(std::iter::once(point_range), true, autoscroll, cx) } pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { let selections = self.selections.all::(cx); let ranges = selections.into_iter().map(|s| s.start..s.end); - self.fold_ranges(ranges, cx); + self.fold_ranges(ranges, true, cx); } - pub fn fold_ranges( + pub fn fold_ranges( &mut self, ranges: impl IntoIterator>, + auto_scroll: bool, cx: &mut ViewContext, ) { let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_some() { - self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); - self.request_autoscroll(Autoscroll::fit(), cx); + let ranges = ranges.collect_vec(); + + self.display_map + .update(cx, |map, cx| map.fold(ranges.iter().cloned(), cx)); + + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + let snapshot = self.snapshot(cx); + let anchor_ranges = offset_to_anchors(ranges, &snapshot); + + self.change_click_ranges::(cx, |click_ranges| { + for range in anchor_ranges { + if let Err(idx) = click_ranges.binary_search_by(|click_range| { + click_range.cmp(&range, &snapshot.buffer_snapshot) + }) { + click_ranges.insert(idx, range) + } + } + }); + let click_ranges = self.clone_click_ranges::(); + self.highlight_background::( + click_ranges, + |theme| theme.editor.document_highlight_write_background, + cx, + ); + cx.notify(); } } - pub fn unfold_ranges( + pub fn unfold_ranges( &mut self, ranges: impl IntoIterator>, inclusive: bool, + auto_scroll: bool, cx: &mut ViewContext, ) { let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_some() { - self.display_map - .update(cx, |map, cx| map.unfold(ranges, inclusive, cx)); - self.request_autoscroll(Autoscroll::fit(), cx); + let ranges = ranges.collect_vec(); + + self.display_map.update(cx, |map, cx| { + map.unfold(ranges.iter().cloned(), inclusive, cx) + }); + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + let snapshot = self.snapshot(cx); + let anchor_ranges = offset_to_anchors(ranges, &snapshot); + + self.change_click_ranges::(cx, |click_ranges| { + for range in anchor_ranges { + let range_point = range.start.to_point(&snapshot.buffer_snapshot); + // Fold and unfold ranges start at different points in the row. + // But their rows do match, so we can use that to detect sameness. + if let Ok(idx) = click_ranges.binary_search_by(|click_range| { + click_range + .start + .to_point(&snapshot.buffer_snapshot) + .row + .cmp(&range_point.row) + }) { + click_ranges.remove(idx); + } + } + }); + let click_ranges = self.clone_click_ranges::(); + self.highlight_background::( + click_ranges, + |theme| theme.editor.document_highlight_write_background, + cx, + ); + cx.notify(); } } + pub fn gutter_hover( + &mut self, + GutterHover { hovered }: &GutterHover, + cx: &mut ViewContext, + ) { + self.gutter_hovered = *hovered; + cx.notify(); + } + pub fn insert_blocks( &mut self, blocks: impl IntoIterator>, @@ -5903,6 +6050,69 @@ impl Editor { } } + // FIXME: Consolidate the range styling APIs so that this clone isn't nescessary + pub fn clone_click_ranges(&self) -> Vec> { + self.clickable_text + .get(&TypeId::of::()) + .map(|click_range| click_range.1.clone()) + .unwrap_or_default() + } + + pub fn change_click_ranges( + &mut self, + cx: &mut ViewContext, + change: impl FnOnce(&mut Vec>), + ) { + let mut ranges = self + .clickable_text + .remove(&TypeId::of::()) + .map(|click_range| click_range.1) + .unwrap_or_default(); + + change(&mut ranges); + + self.clickable_text + .insert(TypeId::of::(), (T::click_handler, ranges)); + + cx.notify(); + } + + pub fn click_ranges_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + ) -> Vec<(Range, TextClickedCallback)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + for (callback, ranges) in self.clickable_text.values() { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, buffer); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, buffer).is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((start..end, *callback)) + } + } + results + } + pub fn highlight_rows(&mut self, rows: Option>) { self.highlighted_rows = rows; } @@ -6252,6 +6462,54 @@ 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 + } +} + +fn offset_to_anchors< + 'snapshot, + 'iter: 'snapshot, + T: ToOffset, + I: IntoIterator> + 'iter, +>( + ranges: I, + snapshot: &'snapshot EditorSnapshot, +) -> impl Iterator> + 'snapshot { + ranges.into_iter().map(|range| { + snapshot + .buffer_snapshot + .anchor_at(range.start.to_offset(&snapshot.buffer_snapshot), Bias::Left) + ..snapshot + .buffer_snapshot + .anchor_at(range.end.to_offset(&snapshot.buffer_snapshot), Bias::Right) + }) +} + 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/editor_tests.rs b/crates/editor/src/editor_tests.rs index b0ce4a68b9..8589bbcc79 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -447,6 +447,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) { Point::new(1, 0)..Point::new(2, 0), Point::new(3, 0)..Point::new(4, 0), ], + true, cx, ); }); @@ -669,10 +670,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) { 1 } - fn b() {… + fn b() {⋯ } - fn c() {… + fn c() {⋯ } } " @@ -683,7 +684,7 @@ fn test_fold(cx: &mut gpui::MutableAppContext) { assert_eq!( view.display_text(cx), " - impl Foo {… + impl Foo {⋯ } " .unindent(), @@ -700,10 +701,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) { 1 } - fn b() {… + fn b() {⋯ } - fn c() {… + fn c() {⋯ } } " @@ -807,9 +808,10 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { Point::new(1, 2)..Point::new(1, 4), Point::new(2, 4)..Point::new(2, 8), ], + true, cx, ); - assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); + assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε\n"); view.move_right(&MoveRight, cx); assert_eq!( @@ -824,13 +826,13 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { view.move_right(&MoveRight, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ…".len())] + &[empty_range(0, "ⓐⓑ⋯".len())] ); view.move_down(&MoveDown, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(1, "ab…".len())] + &[empty_range(1, "ab⋯".len())] ); view.move_left(&MoveLeft, cx); assert_eq!( @@ -856,28 +858,28 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { view.move_right(&MoveRight, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(2, "αβ…".len())] + &[empty_range(2, "αβ⋯".len())] ); view.move_right(&MoveRight, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(2, "αβ…ε".len())] + &[empty_range(2, "αβ⋯ε".len())] ); view.move_up(&MoveUp, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(1, "ab…e".len())] + &[empty_range(1, "ab⋯e".len())] ); view.move_up(&MoveUp, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ…ⓔ".len())] + &[empty_range(0, "ⓐⓑ⋯ⓔ".len())] ); view.move_left(&MoveLeft, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ…".len())] + &[empty_range(0, "ⓐⓑ⋯".len())] ); view.move_left(&MoveLeft, cx); assert_eq!( @@ -2119,6 +2121,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { Point::new(2, 3)..Point::new(4, 1), Point::new(7, 0)..Point::new(8, 4), ], + true, cx, ); view.change_selections(None, cx, |s| { @@ -2131,13 +2134,13 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { }); assert_eq!( view.display_text(cx), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj" ); view.move_line_up(&MoveLineUp, cx); assert_eq!( view.display_text(cx), - "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" + "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff" ); assert_eq!( view.selections.display_ranges(cx), @@ -2154,7 +2157,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { view.move_line_down(&MoveLineDown, cx); assert_eq!( view.display_text(cx), - "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" + "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj" ); assert_eq!( view.selections.display_ranges(cx), @@ -2171,7 +2174,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { view.move_line_down(&MoveLineDown, cx); assert_eq!( view.display_text(cx), - "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" + "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj" ); assert_eq!( view.selections.display_ranges(cx), @@ -2188,7 +2191,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { view.move_line_up(&MoveLineUp, cx); assert_eq!( view.display_text(cx), - "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff" ); assert_eq!( view.selections.display_ranges(cx), @@ -2586,6 +2589,7 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { Point::new(2, 3)..Point::new(4, 1), Point::new(7, 0)..Point::new(8, 4), ], + true, cx, ); view.change_selections(None, cx, |s| { @@ -2596,14 +2600,14 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), ]) }); - assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i"); + assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); }); view.update(cx, |view, cx| { view.split_selection_into_lines(&SplitSelectionIntoLines, cx); assert_eq!( view.display_text(cx), - "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i" + "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" ); assert_eq!( view.selections.display_ranges(cx), @@ -2983,6 +2987,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { Point::new(0, 21)..Point::new(0, 24), Point::new(3, 20)..Point::new(3, 22), ], + true, cx, ); view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 57e0f18035..78c3518d81 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, @@ -14,7 +14,7 @@ use crate::{ }, mouse_context_menu::DeployMouseContextMenu, scroll::actions::Scroll, - EditorStyle, + EditorStyle, GutterHover, TextClickedCallback, UnfoldAt, }; use clock::ReplicaId; use collections::{BTreeMap, HashMap}; @@ -30,6 +30,7 @@ use gpui::{ }, json::{self, ToJson}, platform::CursorStyle, + scene::MouseClick, text_layout::{self, Line, RunStyle, TextLayoutCache}, AppContext, Axis, Border, CursorRegion, Element, ElementBox, EventContext, LayoutContext, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MutableAppContext, @@ -48,6 +49,7 @@ use std::{ ops::{DerefMut, Range}, sync::Arc, }; +use workspace::item::Item; struct SelectionLayout { head: DisplayPoint, @@ -114,6 +116,7 @@ impl EditorElement { fn attach_mouse_handlers( view: &WeakViewHandle, position_map: &Arc, + click_ranges: Arc, TextClickedCallback)>>, has_popovers: bool, visible_bounds: RectF, text_bounds: RectF, @@ -210,8 +213,33 @@ impl EditorElement { cx.propagate_event() } } + }) + .on_click(MouseButton::Left, { + let position_map = position_map.clone(); + move |e, cx| { + let point = + position_to_display_point(e.position, text_bounds, &position_map); + if let Some(point) = point { + for (range, callback) in click_ranges.iter() { + if range.contains(&point) { + callback(&e, range, &position_map.snapshot, cx) + } + } + } + } }), ); + + enum GutterHandlers {} + cx.scene.push_mouse_region( + MouseRegion::new::(view.id(), view.id() + 1, gutter_bounds).on_hover( + |hover, cx| { + cx.dispatch_action(GutterHover { + hovered: hover.started, + }) + }, + ), + ) } fn mouse_down( @@ -400,16 +428,7 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let point = if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } - } else { - None - }; + let point = position_to_display_point(position, text_bounds, position_map); cx.dispatch_action(UpdateGoToDefinitionLink { point, @@ -418,6 +437,7 @@ impl EditorElement { }); cx.dispatch_action(HoverAt { point }); + true } @@ -569,12 +589,25 @@ impl EditorElement { } if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { - let mut x = bounds.width() - layout.gutter_padding; + let mut x = 0.; let mut y = *row as f32 * line_height - scroll_top; x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; y += (line_height - indicator.size().y()) / 2.; indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); } + + layout.fold_indicators.as_mut().map(|fold_indicators| { + for (line, fold_indicator) in 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) { @@ -676,6 +709,7 @@ impl EditorElement { let max_glyph_width = layout.position_map.em_width; let scroll_left = scroll_position.x() * max_glyph_width; let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); + let line_end_overshoot = 0.15 * layout.position_map.line_height; cx.scene.push_layer(Some(bounds)); @@ -688,12 +722,29 @@ impl EditorElement { }, }); + for (range, _) in layout.click_ranges.iter() { + for bound in range_to_bounds( + range, + content_origin, + scroll_left, + scroll_top, + &layout.visible_display_row_range, + line_end_overshoot, + &layout.position_map, + ) { + cx.scene.push_cursor_region(CursorRegion { + bounds: bound, + style: CursorStyle::PointingHand, + }); + } + } + for (range, color) in &layout.highlighted_ranges { self.paint_highlighted_range( range.clone(), *color, 0., - 0.15 * layout.position_map.line_height, + line_end_overshoot, layout, content_origin, scroll_top, @@ -1118,6 +1169,24 @@ impl EditorElement { .width() } + fn get_fold_indicators( + &self, + is_singleton: bool, + display_rows: Range, + snapshot: &EditorSnapshot, + ) -> Option> { + is_singleton.then(|| { + 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( @@ -1438,7 +1507,7 @@ impl EditorElement { } else { let text_style = self.style.text.clone(); Flex::row() - .with_child(Label::new("…", text_style).boxed()) + .with_child(Label::new("⋯", text_style).boxed()) .with_children(jump_icon) .contained() .with_padding_left(gutter_padding) @@ -1606,15 +1675,20 @@ impl Element for EditorElement { let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; let mut highlighted_ranges = Vec::new(); + let mut click_ranges = Vec::new(); let mut show_scrollbars = false; let mut include_root = false; + let mut is_singleton = false; self.update_view(cx.app, |view, cx| { + is_singleton = view.is_singleton(cx); + let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx)); highlighted_rows = view.highlighted_rows(); let theme = cx.global::().theme.as_ref(); highlighted_ranges = view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme); + click_ranges = view.click_ranges_in_range(start_anchor..end_anchor, &display_map); let mut remote_selections = HashMap::default(); for (replica_id, line_mode, cursor_shape, selection) in display_map @@ -1689,6 +1763,8 @@ impl Element for EditorElement { let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); + let folds = self.get_fold_indicators(is_singleton, 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; @@ -1755,7 +1831,7 @@ impl Element for EditorElement { let mut code_actions_indicator = None; let mut hover = None; let mut mode = EditorMode::Full; - cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| { + let mut fold_indicators = cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| { let newest_selection_head = view .selections .newest::(cx) @@ -1769,14 +1845,25 @@ impl Element for EditorElement { view.render_context_menu(newest_selection_head, style.clone(), cx); } + let active = matches!(view.context_menu, Some(crate::ContextMenu::CodeActions(_))); + code_actions_indicator = view - .render_code_actions_indicator(&style, cx) + .render_code_actions_indicator(&style, active, cx) .map(|indicator| (newest_selection_head.row(), indicator)); } 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; + + view.render_fold_indicators( + folds, + &style, + view.gutter_hovered, + line_height, + gutter_margin, + cx, + ) }); if let Some((_, context_menu)) = context_menu.as_mut() { @@ -1802,6 +1889,18 @@ impl Element for EditorElement { ); } + fold_indicators.as_mut().map(|fold_indicators| { + 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( @@ -1845,12 +1944,14 @@ impl Element for EditorElement { active_rows, highlighted_rows, highlighted_ranges, + click_ranges: Arc::new(click_ranges), line_number_layouts, display_hunks, blocks, selections, context_menu, code_actions_indicator, + fold_indicators, hover_popovers: hover, }, ) @@ -1875,6 +1976,7 @@ impl Element for EditorElement { Self::attach_mouse_handlers( &self.view, &layout.position_map, + layout.click_ranges.clone(), // No need to clone the vec layout.hover_popovers.is_some(), visible_bounds, text_bounds, @@ -1972,6 +2074,7 @@ pub struct LayoutState { display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, + click_ranges: Arc, TextClickedCallback)>>, selections: Vec<(ReplicaId, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, @@ -1979,6 +2082,7 @@ pub struct LayoutState { context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, hover_popovers: Option<(DisplayPoint, Vec)>, + fold_indicators: Option>, } pub struct PositionMap { @@ -2277,6 +2381,98 @@ impl HighlightedRange { } } +pub trait ClickRange: 'static { + fn click_handler( + click: &MouseClick, + range: &Range, + snapshot: &EditorSnapshot, + cx: &mut EventContext, + ); +} + +pub enum FoldMarker {} +impl ClickRange for FoldMarker { + fn click_handler( + _click: &MouseClick, + range: &Range, + _snapshot: &EditorSnapshot, + cx: &mut EventContext, + ) { + cx.dispatch_action(UnfoldAt { + display_row: range.start.row(), + }) + } +} + +pub fn position_to_display_point( + position: Vector2F, + text_bounds: RectF, + position_map: &PositionMap, +) -> Option { + if text_bounds.contains_point(position) { + let (point, target_point) = position_map.point_for_position(text_bounds, position); + if point == target_point { + Some(point) + } else { + None + } + } else { + None + } +} + +pub fn range_to_bounds( + range: &Range, + content_origin: Vector2F, + scroll_left: f32, + scroll_top: f32, + visible_row_range: &Range, + line_end_overshoot: f32, + position_map: &PositionMap, +) -> impl Iterator { + let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new(); + + if range.start == range.end { + return bounds.into_iter(); + } + + let start_row = visible_row_range.start; + let end_row = visible_row_range.end; + + let row_range = if range.end.column() == 0 { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) + } else { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) + }; + + let first_y = + content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top; + + for (idx, row) in row_range.enumerate() { + let line_layout = &position_map.line_layouts[(row - start_row) as usize]; + + let start_x = if row == range.start.row() { + content_origin.x() + line_layout.x_for_index(range.start.column() as usize) + - scroll_left + } else { + content_origin.x() - scroll_left + }; + + let end_x = if row == range.end.row() { + content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left + } else { + content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left + }; + + bounds.push(RectF::from_points( + vec2f(start_x, first_y + position_map.line_height * idx as f32), + vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32), + )) + } + + bounds.into_iter() +} + pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 { delta.powf(1.5) / 100.0 } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b0b368acd6..0d594a66ef 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -886,7 +886,7 @@ impl SearchableItem for Editor { matches: Vec>, cx: &mut ViewContext, ) { - self.unfold_ranges([matches[index].clone()], false, cx); + self.unfold_ranges([matches[index].clone()], false, true, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([matches[index].clone()]) }); 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/search/src/project_search.rs b/crates/search/src/project_search.rs index 04f4b1470a..e48d78e84f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -538,7 +538,7 @@ impl ProjectSearchView { let range_to_select = match_ranges[new_index].clone(); self.results_editor.update(cx, |editor, cx| { - editor.unfold_ranges([range_to_select.clone()], false, cx); + editor.unfold_ranges([range_to_select.clone()], false, true, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range_to_select]) }); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index efe64cbc5c..06122b9dda 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, @@ -634,10 +635,24 @@ pub struct FieldEditor { #[derive(Clone, Deserialize, Default)] pub struct CodeActions { #[serde(default)] - pub indicator: Color, + pub indicator: Interactive, pub vertical_scale: f32, } +#[derive(Clone, Deserialize, Default)] +pub struct Folds { + pub indicator: Interactive, + pub fold_background: Color, + pub icon_width: f32, + pub folded_icon: String, + pub foldable_icon: String, +} + +#[derive(Clone, Deserialize, Default)] +pub struct Indicator { + pub color: Color, +} + #[derive(Clone, Deserialize, Default)] pub struct DiffStyle { pub inserted: Color, diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 37e1f29ce2..5ecf889f9b 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -237,7 +237,7 @@ macro_rules! iife { }; } -/// Async lImmediately invoked function expression. Good for using the ? operator +/// Async Immediately invoked function expression. Good for using the ? operator /// in functions which do not return an Option or Result. Async version of above #[macro_export] macro_rules! async_iife { @@ -262,10 +262,7 @@ impl RangeExt for Range { } fn overlaps(&self, other: &Range) -> bool { - self.contains(&other.start) - || self.contains(&other.end) - || other.contains(&self.start) - || other.contains(&self.end) + self.start < other.end && other.start < self.end } } @@ -279,10 +276,7 @@ impl RangeExt for RangeInclusive { } fn overlaps(&self, other: &Range) -> bool { - self.contains(&other.start) - || self.contains(&other.end) - || other.contains(&self.start()) - || other.contains(&self.end()) + self.start() < &other.end && &other.start <= self.end() } } diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 85c9ccec95..c8334d1cc0 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -44,9 +44,40 @@ export default function editor(colorScheme: ColorScheme) { activeLineBackground: withOpacity(background(layer, "on"), 0.75), highlightedLineBackground: background(layer, "on"), codeActions: { - indicator: foreground(layer, "variant"), + indicator: { + color: foreground(layer, "variant"), + + clicked: { + color: foreground(layer, "base"), + }, + hover: { + color: foreground(layer, "on"), + }, + active: { + color: foreground(layer, "on"), + }, + }, verticalScale: 0.55, }, + folds: { + iconWidth: 8, + foldedIcon: "icons/chevron_right_8.svg", + foldableIcon: "icons/chevron_down_8.svg", + indicator: { + color: foreground(layer, "variant"), + + clicked: { + color: foreground(layer, "base"), + }, + hover: { + color: foreground(layer, "on"), + }, + active: { + color: foreground(layer, "on"), + }, + }, + foldBackground: foreground(layer, "variant"), + }, diff: { deleted: foreground(layer, "negative"), modified: foreground(layer, "warning"),