diff --git a/Cargo.lock b/Cargo.lock index 7c81f692ee1fe1b90b13f44475585bdb7142e23b..91b76f33e8d8f715a3aefc9d6bc64f4cfc62b00f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7728,8 +7728,10 @@ dependencies = [ "serde", "serde_json", "settings", + "smallvec", "smol", "theme", + "ui", "util", "workspace", "worktree", diff --git a/assets/settings/default.json b/assets/settings/default.json index 32f46ce714379157dfb54ae06d6d507514421b16..cd4e3db15c2f6742510aa8f07d68b26dd602ff3f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -388,6 +388,8 @@ "git_status": true, // Amount of indentation for nested items. "indent_size": 20, + // Whether to show indent guides in the outline panel. + "indent_guides": true, // Whether to reveal it in the outline panel automatically, // when a corresponding outline entry becomes active. // Gitignored entries are never auto revealed. diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 9ce85aab232cd7407ced619ba1f3feecdf73c9e6..2379ee9f8123e726ffb01e77858bb4feea5e92ea 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -340,6 +340,7 @@ impl Element for UniformList { visible_range.clone(), bounds, item_height, + self.item_count, cx, ); let available_space = size( @@ -396,6 +397,7 @@ pub trait UniformListDecoration { visible_range: Range, bounds: Bounds, item_height: Pixels, + item_count: usize, cx: &mut WindowContext, ) -> AnyElement; } diff --git a/crates/outline_panel/Cargo.toml b/crates/outline_panel/Cargo.toml index 824ea70735d9ca58e3904d447de919c7857c12eb..be7653db685e969e7183a023ddba63afbbbf5754 100644 --- a/crates/outline_panel/Cargo.toml +++ b/crates/outline_panel/Cargo.toml @@ -30,8 +30,10 @@ search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +smallvec.workspace = true smol.workspace = true theme.workspace = true +ui.workspace = true util.workspace = true worktree.workspace = true workspace.workspace = true diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 72b97c8f69ea797037f3104619d6f9323c2b0394..6def76bb38d50eb16ff664478035b0cb1cdc3d94 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -24,12 +24,12 @@ use editor::{ use file_icons::FileIcons; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement, - AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, - EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement, IntoElement, - KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, - SharedString, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, - VisualContext, WeakView, WindowContext, + actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action, + AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, + Div, ElementId, EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement, + IntoElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, + Render, SharedString, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use itertools::Itertools; use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; @@ -42,6 +42,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use smol::channel; use theme::{SyntaxTheme, ThemeSettings}; +use ui::{IndentGuideColors, IndentGuideLayout}; use util::{debug_panic, RangeExt, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -254,14 +255,14 @@ impl SearchState { #[derive(Debug)] enum SelectedEntry { Invalidated(Option), - Valid(PanelEntry), + Valid(PanelEntry, usize), None, } impl SelectedEntry { fn invalidate(&mut self) { match std::mem::replace(self, SelectedEntry::None) { - Self::Valid(entry) => *self = Self::Invalidated(Some(entry)), + Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)), Self::None => *self = Self::Invalidated(None), other => *self = other, } @@ -3568,7 +3569,7 @@ impl OutlinePanel { fn selected_entry(&self) -> Option<&PanelEntry> { match &self.selected_entry { SelectedEntry::Invalidated(entry) => entry.as_ref(), - SelectedEntry::Valid(entry) => Some(entry), + SelectedEntry::Valid(entry, _) => Some(entry), SelectedEntry::None => None, } } @@ -3577,7 +3578,16 @@ impl OutlinePanel { if focus { self.focus_handle.focus(cx); } - self.selected_entry = SelectedEntry::Valid(entry); + let ix = self + .cached_entries + .iter() + .enumerate() + .find(|(_, cached_entry)| &cached_entry.entry == &entry) + .map(|(i, _)| i) + .unwrap_or_default(); + + self.selected_entry = SelectedEntry::Valid(entry, ix); + self.autoscroll(cx); cx.notify(); } @@ -3736,6 +3746,9 @@ impl Render for OutlinePanel { let project = self.project.read(cx); let query = self.query(cx); let pinned = self.pinned; + let settings = OutlinePanelSettings::get_global(cx); + let indent_size = settings.indent_size; + let show_indent_guides = settings.indent_guides; let outline_panel = v_flex() .id("outline-panel") @@ -3901,6 +3914,61 @@ impl Render for OutlinePanel { }) .size_full() .track_scroll(self.scroll_handle.clone()) + .when(show_indent_guides, |list| { + list.with_decoration( + ui::indent_guides( + cx.view().clone(), + px(indent_size), + IndentGuideColors::panel(cx), + |outline_panel, range, _| { + let entries = outline_panel.cached_entries.get(range); + if let Some(entries) = entries { + entries.into_iter().map(|item| item.depth).collect() + } else { + smallvec::SmallVec::new() + } + }, + ) + .with_render_fn( + cx.view().clone(), + move |outline_panel, params, _| { + const LEFT_OFFSET: f32 = 14.; + + let indent_size = params.indent_size; + let item_height = params.item_height; + let active_indent_guide_ix = find_active_indent_guide_ix( + outline_panel, + ¶ms.indent_guides, + ); + + params + .indent_guides + .into_iter() + .enumerate() + .map(|(ix, layout)| { + let bounds = Bounds::new( + point( + px(layout.offset.x as f32) * indent_size + + px(LEFT_OFFSET), + px(layout.offset.y as f32) * item_height, + ), + size( + px(1.), + px(layout.length as f32) * item_height, + ), + ); + ui::RenderedIndentGuide { + bounds, + layout, + is_active: active_indent_guide_ix == Some(ix), + hitbox: None, + } + }) + .collect() + }, + ), + ) + }) }) } .children(self.context_menu.as_ref().map(|(menu, position, _)| { @@ -3945,6 +4013,40 @@ impl Render for OutlinePanel { } } +fn find_active_indent_guide_ix( + outline_panel: &OutlinePanel, + candidates: &[IndentGuideLayout], +) -> Option { + let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else { + return None; + }; + let target_depth = outline_panel + .cached_entries + .get(*target_ix) + .map(|cached_entry| cached_entry.depth)?; + + let (target_ix, target_depth) = if let Some(target_depth) = outline_panel + .cached_entries + .get(target_ix + 1) + .filter(|cached_entry| cached_entry.depth > target_depth) + .map(|entry| entry.depth) + { + (target_ix + 1, target_depth.saturating_sub(1)) + } else { + (*target_ix, target_depth.saturating_sub(1)) + }; + + candidates + .iter() + .enumerate() + .find(|(_, guide)| { + guide.offset.y <= target_ix + && target_ix < guide.offset.y + guide.length + && guide.offset.x == target_depth + }) + .map(|(ix, _)| ix) +} + fn subscribe_for_editor_events( editor: &View, cx: &mut ViewContext, diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index e19fc3c008494745d417d2b836d6d603b9f84ffa..e165978fc758efe51c0bc9eacd1d7fa37858b6ca 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -19,6 +19,7 @@ pub struct OutlinePanelSettings { pub folder_icons: bool, pub git_status: bool, pub indent_size: f32, + pub indent_guides: bool, pub auto_reveal_entries: bool, pub auto_fold_dirs: bool, } @@ -53,6 +54,10 @@ pub struct OutlinePanelSettingsContent { /// /// Default: 20 pub indent_size: Option, + /// Whether to show indent guides in the outline panel. + /// + /// Default: true + pub indent_guides: Option, /// Whether to reveal it in the outline panel automatically, /// when a corresponding project entry becomes active. /// Gitignored entries are never auto revealed. diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index e45404429ce4f92897491b8981e9d19dafbe5d03..caab92053c58b884ec31edb1e8f0949b8b4b3402 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -140,13 +140,18 @@ mod uniform_list { visible_range: Range, bounds: Bounds, item_height: Pixels, + item_count: usize, cx: &mut WindowContext, ) -> AnyElement { let mut visible_range = visible_range.clone(); - visible_range.end += 1; + let includes_trailing_indent = visible_range.end < item_count; + // Check if we have entries after the visible range, + // if so extend the visible range so we can fetch a trailing indent, + // which is needed to compute indent guides correctly. + if includes_trailing_indent { + visible_range.end += 1; + } let visible_entries = &(self.compute_indents_fn)(visible_range.clone(), cx); - // Check if we have an additional indent that is outside of the visible range - let includes_trailing_indent = visible_entries.len() == visible_range.len(); let indent_guides = compute_indent_guides( &visible_entries, visible_range.start, @@ -198,8 +203,12 @@ mod uniform_list { on_hovered_indent_guide_click: Option>, } - struct IndentGuidesElementPrepaintState { - hitboxes: SmallVec<[Hitbox; 12]>, + enum IndentGuidesElementPrepaintState { + Static, + Interactive { + hitboxes: Rc>, + on_hovered_indent_guide_click: Rc, + }, } impl Element for IndentGuidesElement { @@ -225,11 +234,21 @@ mod uniform_list { _request_layout: &mut Self::RequestLayoutState, cx: &mut WindowContext, ) -> Self::PrepaintState { - let mut hitboxes = SmallVec::new(); - for guide in self.indent_guides.as_ref().iter() { - hitboxes.push(cx.insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), false)); + if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() + { + let hitboxes = self + .indent_guides + .as_ref() + .iter() + .map(|guide| cx.insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), false)) + .collect(); + Self::PrepaintState::Interactive { + hitboxes: Rc::new(hitboxes), + on_hovered_indent_guide_click, + } + } else { + Self::PrepaintState::Static } - Self::PrepaintState { hitboxes } } fn paint( @@ -240,81 +259,96 @@ mod uniform_list { prepaint: &mut Self::PrepaintState, cx: &mut WindowContext, ) { - let callback = self.on_hovered_indent_guide_click.clone(); - if let Some(callback) = callback { - cx.on_mouse_event({ - let hitboxes = prepaint.hitboxes.clone(); - let indent_guides = self.indent_guides.clone(); - move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble && event.button == MouseButton::Left { - let mut active_hitbox_ix = None; - for (i, hitbox) in hitboxes.iter().enumerate() { - if hitbox.is_hovered(cx) { - active_hitbox_ix = Some(i); - break; + match prepaint { + IndentGuidesElementPrepaintState::Static => { + for indent_guide in self.indent_guides.as_ref() { + let fill_color = if indent_guide.is_active { + self.colors.active + } else { + self.colors.default + }; + + cx.paint_quad(fill(indent_guide.bounds, fill_color)); + } + } + IndentGuidesElementPrepaintState::Interactive { + hitboxes, + on_hovered_indent_guide_click, + } => { + cx.on_mouse_event({ + let hitboxes = hitboxes.clone(); + let indent_guides = self.indent_guides.clone(); + let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone(); + move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && event.button == MouseButton::Left { + let mut active_hitbox_ix = None; + for (i, hitbox) in hitboxes.iter().enumerate() { + if hitbox.is_hovered(cx) { + active_hitbox_ix = Some(i); + break; + } } - } - let Some(active_hitbox_ix) = active_hitbox_ix else { - return; - }; + let Some(active_hitbox_ix) = active_hitbox_ix else { + return; + }; - let active_indent_guide = &indent_guides[active_hitbox_ix].layout; - callback(active_indent_guide, cx); + let active_indent_guide = &indent_guides[active_hitbox_ix].layout; + on_hovered_indent_guide_click(active_indent_guide, cx); - cx.stop_propagation(); - cx.prevent_default(); + cx.stop_propagation(); + cx.prevent_default(); + } } - } - }); - } - - let mut hovered_hitbox_id = None; - for (i, hitbox) in prepaint.hitboxes.iter().enumerate() { - cx.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); - let indent_guide = &self.indent_guides[i]; - let fill_color = if hitbox.is_hovered(cx) { - hovered_hitbox_id = Some(hitbox.id); - self.colors.hover - } else if indent_guide.is_active { - self.colors.active - } else { - self.colors.default - }; - - cx.paint_quad(fill(indent_guide.bounds, fill_color)); - } - - cx.on_mouse_event({ - let prev_hovered_hitbox_id = hovered_hitbox_id; - let hitboxes = prepaint.hitboxes.clone(); - move |_: &MouseMoveEvent, phase, cx| { + }); let mut hovered_hitbox_id = None; - for hitbox in &hitboxes { - if hitbox.is_hovered(cx) { + for (i, hitbox) in hitboxes.iter().enumerate() { + cx.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); + let indent_guide = &self.indent_guides[i]; + let fill_color = if hitbox.is_hovered(cx) { hovered_hitbox_id = Some(hitbox.id); - break; - } + self.colors.hover + } else if indent_guide.is_active { + self.colors.active + } else { + self.colors.default + }; + + cx.paint_quad(fill(indent_guide.bounds, fill_color)); } - if phase == DispatchPhase::Capture { - // If the hovered hitbox has changed, we need to re-paint the indent guides. - match (prev_hovered_hitbox_id, hovered_hitbox_id) { - (Some(prev_id), Some(id)) => { - if prev_id != id { - cx.refresh(); + + cx.on_mouse_event({ + let prev_hovered_hitbox_id = hovered_hitbox_id; + let hitboxes = hitboxes.clone(); + move |_: &MouseMoveEvent, phase, cx| { + let mut hovered_hitbox_id = None; + for hitbox in hitboxes.as_ref() { + if hitbox.is_hovered(cx) { + hovered_hitbox_id = Some(hitbox.id); + break; } } - (None, Some(_)) => { - cx.refresh(); - } - (Some(_), None) => { - cx.refresh(); + if phase == DispatchPhase::Capture { + // If the hovered hitbox has changed, we need to re-paint the indent guides. + match (prev_hovered_hitbox_id, hovered_hitbox_id) { + (Some(prev_id), Some(id)) => { + if prev_id != id { + cx.refresh(); + } + } + (None, Some(_)) => { + cx.refresh(); + } + (Some(_), None) => { + cx.refresh(); + } + (None, None) => {} + } } - (None, None) => {} } - } + }); } - }); + } } } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 784cb631ca275214380808302d212056aeab29f1..f149fa5cf1b59dfcdd5805824846d2a1b9c31501 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2237,6 +2237,7 @@ Run the `theme selector: toggle` action in the command palette to see a current "folder_icons": true, "git_status": true, "indent_size": 20, + "indent_guides": true, "auto_reveal_entries": true, "auto_fold_dirs": true, }