diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 229ecc34feeefbbfa9c3251d9e515234761d26bd..500f3626e348aeaf94cfa0efeff71ce5454ad01d 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -8,6 +8,7 @@ use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; use fold_map::FoldMap; use gpui::{ + color::Color, fonts::{FontId, HighlightStyle}, Entity, ModelContext, ModelHandle, }; @@ -218,6 +219,10 @@ impl DisplayMap { .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) } + pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool { + self.fold_map.set_ellipses_color(color) + } + pub fn set_wrap_width(&self, width: Option, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) @@ -643,7 +648,6 @@ impl DisplaySnapshot { break; } } - let end = end.unwrap_or(max_point); Some(start..end) } else { diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 44de95fe32448e26e6af449cc06f9c8e1c230bb1..6323461987aa58c1e280ee4fb1e869c34ddab4b4 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -4,7 +4,7 @@ use crate::{ ToOffset, }; use collections::BTreeMap; -use gpui::fonts::HighlightStyle; +use gpui::{color::Color, fonts::HighlightStyle}; use language::{Chunk, Edit, Point, TextSummary}; use parking_lot::Mutex; use std::{ @@ -133,6 +133,7 @@ impl<'a> FoldMapWriter<'a> { folds: self.0.folds.clone(), buffer_snapshot: buffer, version: self.0.version.load(SeqCst), + ellipses_color: self.0.ellipses_color, }; (snapshot, edits) } @@ -182,6 +183,7 @@ impl<'a> FoldMapWriter<'a> { folds: self.0.folds.clone(), buffer_snapshot: buffer, version: self.0.version.load(SeqCst), + ellipses_color: self.0.ellipses_color, }; (snapshot, edits) } @@ -192,6 +194,7 @@ pub struct FoldMap { transforms: Mutex>, folds: SumTree, version: AtomicUsize, + ellipses_color: Option, } impl FoldMap { @@ -209,6 +212,7 @@ impl FoldMap { }, &(), )), + ellipses_color: None, version: Default::default(), }; @@ -217,6 +221,7 @@ impl FoldMap { folds: this.folds.clone(), buffer_snapshot: this.buffer.lock().clone(), version: this.version.load(SeqCst), + ellipses_color: None, }; (this, snapshot) } @@ -233,6 +238,7 @@ impl FoldMap { folds: self.folds.clone(), buffer_snapshot: self.buffer.lock().clone(), version: self.version.load(SeqCst), + ellipses_color: self.ellipses_color, }; (snapshot, edits) } @@ -246,6 +252,15 @@ impl FoldMap { (FoldMapWriter(self), snapshot, edits) } + pub fn set_ellipses_color(&mut self, color: Color) -> bool { + if self.ellipses_color != Some(color) { + self.ellipses_color = Some(color); + true + } else { + false + } + } + fn check_invariants(&self) { if cfg!(test) { assert_eq!( @@ -477,6 +492,7 @@ pub struct FoldSnapshot { folds: SumTree, buffer_snapshot: MultiBufferSnapshot, pub version: usize, + pub ellipses_color: Option, } impl FoldSnapshot { @@ -739,6 +755,7 @@ impl FoldSnapshot { max_output_offset: range.end.0, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), + ellipses_color: self.ellipses_color, } } @@ -1029,6 +1046,7 @@ pub struct FoldChunks<'a> { max_output_offset: usize, highlight_endpoints: Peekable>, active_highlights: BTreeMap, HighlightStyle>, + ellipses_color: Option, } impl<'a> Iterator for FoldChunks<'a> { @@ -1058,7 +1076,10 @@ impl<'a> Iterator for FoldChunks<'a> { return Some(Chunk { text: output_text, syntax_highlight_id: None, - highlight_style: None, + highlight_style: self.ellipses_color.map(|color| HighlightStyle { + color: Some(color), + ..Default::default() + }), diagnostic_severity: None, is_unnecessary: false, }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cb9bf4da6c36fbf9778fd801b7389fc9222f819d..3e7a14d2aea29312bb6b6853f18957ca116eb5f6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -39,11 +39,10 @@ use gpui::{ impl_actions, impl_internal_actions, keymap_matcher::KeymapContext, platform::CursorStyle, - scene::MouseClick, serde_json::json, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, - EventContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, - View, ViewContext, ViewHandle, WeakViewHandle, + 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}; @@ -458,8 +457,6 @@ 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, @@ -488,7 +485,6 @@ 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, @@ -1159,7 +1155,6 @@ 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), @@ -5853,34 +5848,12 @@ impl Editor { ) { let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_some() { - let ranges = ranges.collect_vec(); - - self.display_map - .update(cx, |map, cx| map.fold(ranges.iter().cloned(), cx)); + self.display_map.update(cx, |map, cx| map.fold(ranges, 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(); } } @@ -5894,41 +5867,12 @@ impl Editor { ) { let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_some() { - let ranges = ranges.collect_vec(); - - self.display_map.update(cx, |map, cx| { - map.unfold(ranges.iter().cloned(), inclusive, cx) - }); + self.display_map + .update(cx, |map, cx| map.unfold(ranges, 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(); } } @@ -6054,69 +5998,6 @@ 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; } @@ -6495,25 +6376,6 @@ fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) } } -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) @@ -6585,6 +6447,7 @@ impl View for Editor { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let style = self.style(cx); let font_changed = self.display_map.update(cx, |map, cx| { + map.set_fold_ellipses_color(style.folds.ellipses.text_color); map.set_font(style.text.font_id, style.text.font_size, cx) }); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 239be5246163f18f09f41f7f941c5322ccefbbe6..f69902e53d993fbe60470a2b42f3d553c93a6c1f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -14,7 +14,7 @@ use crate::{ }, mouse_context_menu::DeployMouseContextMenu, scroll::actions::Scroll, - EditorStyle, GutterHover, TextClickedCallback, UnfoldAt, + EditorStyle, GutterHover, UnfoldAt, }; use clock::ReplicaId; use collections::{BTreeMap, HashMap}; @@ -30,7 +30,6 @@ 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, @@ -51,6 +50,8 @@ use std::{ }; use workspace::item::Item; +enum FoldMarkers {} + struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, @@ -116,7 +117,6 @@ impl EditorElement { fn attach_mouse_handlers( view: &WeakViewHandle, position_map: &Arc, - click_ranges: Arc, TextClickedCallback)>>, has_popovers: bool, visible_bounds: RectF, text_bounds: RectF, @@ -213,21 +213,6 @@ 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() { - // Range -> RangeInclusive - if range.contains(&point) || range.end == point { - callback(&e, range, &position_map.snapshot, cx) - } - } - } - } }), ); @@ -723,9 +708,24 @@ impl EditorElement { }, }); - for (range, _) in layout.click_ranges.iter() { + let fold_corner_radius = + self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height; + for (id, range, color) in layout.fold_ranges.iter() { + self.paint_highlighted_range( + range.clone(), + *color, + fold_corner_radius, + fold_corner_radius * 2., + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); + for bound in range_to_bounds( - range, + &range, content_origin, scroll_left, scroll_top, @@ -737,6 +737,16 @@ impl EditorElement { bounds: bound, style: CursorStyle::PointingHand, }); + + let display_row = range.start.row(); + cx.scene.push_mouse_region( + MouseRegion::new::(self.view.id(), *id as usize, bound) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(UnfoldAt { display_row }) + }) + .with_notify_on_hover(true) + .with_notify_on_click(true), + ) } } @@ -756,9 +766,10 @@ impl EditorElement { } let mut cursors = SmallVec::<[Cursor; 32]>::new(); + let corner_radius = 0.15 * layout.position_map.line_height; + for (replica_id, selections) in &layout.selections { let selection_style = style.replica_selection_style(*replica_id); - let corner_radius = 0.15 * layout.position_map.line_height; for selection in selections { self.paint_highlighted_range( @@ -1676,7 +1687,7 @@ 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 fold_ranges = Vec::new(); let mut show_scrollbars = false; let mut include_root = false; let mut is_singleton = false; @@ -1689,7 +1700,19 @@ impl Element for EditorElement { 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); + + fold_ranges.extend( + snapshot + .folds_in_range(start_anchor..end_anchor) + .map(|anchor| { + let start = anchor.start.to_point(&snapshot.buffer_snapshot); + ( + start.row, + start.to_display_point(&snapshot.display_snapshot) + ..anchor.end.to_display_point(&snapshot), + ) + }), + ); let mut remote_selections = HashMap::default(); for (replica_id, line_mode, cursor_shape, selection) in display_map @@ -1759,6 +1782,21 @@ impl Element for EditorElement { .unwrap_or_default() }); + let fold_ranges: Vec<(BufferRow, Range, Color)> = fold_ranges + .into_iter() + .map(|(id, fold)| { + let color = self + .style + .folds + .ellipses + .background + .style_for(&mut cx.mouse_state::(id as usize), false) + .color; + + (id, fold, color) + }) + .collect(); + let line_number_layouts = self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx); @@ -1946,7 +1984,7 @@ impl Element for EditorElement { active_rows, highlighted_rows, highlighted_ranges, - click_ranges: Arc::new(click_ranges), + fold_ranges, line_number_layouts, display_hunks, blocks, @@ -1978,7 +2016,6 @@ 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, @@ -2062,6 +2099,8 @@ impl Element for EditorElement { } } +type BufferRow = u32; + pub struct LayoutState { position_map: Arc, gutter_size: Vector2F, @@ -2076,7 +2115,7 @@ pub struct LayoutState { display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, - click_ranges: Arc, TextClickedCallback)>>, + fold_ranges: Vec<(BufferRow, Range, Color)>, selections: Vec<(ReplicaId, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, @@ -2383,29 +2422,6 @@ 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, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 35c91f1ff233be1ac3fdf4d7ccdc945ef4d67674..31563010b723d88b4f834084fba91cb3ecd9a15c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4159,10 +4159,10 @@ pub struct RenderContext<'a, T: View> { #[derive(Debug, Clone, Default)] pub struct MouseState { - hovered: bool, - clicked: Option, - accessed_hovered: bool, - accessed_clicked: bool, + pub(crate) hovered: bool, + pub(crate) clicked: Option, + pub(crate) accessed_hovered: bool, + pub(crate) accessed_clicked: bool, } impl MouseState { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index f774532930e9f4c90032ba17412c4471cd72895c..ce20b02005d82e5bba42df27e1a9785f06b2fc61 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -12,9 +12,9 @@ use crate::{ text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, Appearance, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent, - MouseRegion, MouseRegionId, ParentId, ReadModel, ReadView, RenderContext, RenderParams, - SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, - WeakViewHandle, + MouseRegion, MouseRegionId, MouseState, ParentId, ReadModel, ReadView, RenderContext, + RenderParams, SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, + WeakModelHandle, WeakViewHandle, }; use anyhow::bail; use collections::{HashMap, HashSet}; @@ -605,6 +605,24 @@ pub struct LayoutContext<'a> { } impl<'a> LayoutContext<'a> { + pub fn mouse_state(&self, region_id: usize) -> MouseState { + let view_id = self.view_stack.last().unwrap(); + + let region_id = MouseRegionId::new::(*view_id, region_id); + MouseState { + hovered: self.hovered_region_ids.contains(®ion_id), + clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| { + if ids.contains(®ion_id) { + Some(*button) + } else { + None + } + }), + accessed_hovered: false, + accessed_clicked: false, + } + } + fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F { let print_error = |view_id| { format!( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cc1781d8b2433a269cec63b52be1293455a8fcf4..484c542edeffce8b22c8f2f29ae4f672c0693399 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -633,16 +633,22 @@ pub struct FieldEditor { pub selection: SelectionStyle, } +#[derive(Clone, Deserialize, Default)] +pub struct InteractiveColor { + pub color: Color, +} + #[derive(Clone, Deserialize, Default)] pub struct CodeActions { #[serde(default)] - pub indicator: Interactive, + pub indicator: Interactive, pub vertical_scale: f32, } #[derive(Clone, Deserialize, Default)] pub struct Folds { - pub indicator: Interactive, + pub indicator: Interactive, + pub ellipses: FoldEllipses, pub fold_background: Color, pub icon_width: f32, pub folded_icon: String, @@ -650,8 +656,10 @@ pub struct Folds { } #[derive(Clone, Deserialize, Default)] -pub struct Indicator { - pub color: Color, +pub struct FoldEllipses { + pub text_color: Color, + pub background: Interactive, + pub corner_radius_factor: f32, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index c8334d1cc0bea5f59a6e7655ad8f2839944fcbfe..799363a349191ffe9bd53748d818c7b0cb33177c 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -76,6 +76,22 @@ export default function editor(colorScheme: ColorScheme) { color: foreground(layer, "on"), }, }, + ellipses: { + textColor: colorScheme.ramps.neutral(0.71).hex(), + cornerRadiusFactor: 0.15, + background: { + // Copied from hover_popover highlight + color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(), + + hover: { + color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(), + }, + + clicked: { + color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(), + }, + } + }, foldBackground: foreground(layer, "variant"), }, diff: {