outline panel: Add indent guides (#19719)
See #12673 | File | Search | |--------|--------| | <img width="302" alt="image" src="https://github.com/user-attachments/assets/44b8d5f9-8446-41b5-8c0f-e438050f0ac9"> | <img width="301" alt="image" src="https://github.com/user-attachments/assets/a2e6f77b-6d3b-4f1c-8fcb-16bd35274807"> | Release Notes: - Added indent guides to the outline panel
This commit is contained in:
parent
e86b096b92
commit
888fec9299
8 changed files with 236 additions and 86 deletions
|
@ -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
|
||||
|
|
|
@ -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<PanelEntry>),
|
||||
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<usize> {
|
||||
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<Editor>,
|
||||
cx: &mut ViewContext<OutlinePanel>,
|
||||
|
|
|
@ -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<f32>,
|
||||
/// Whether to show indent guides in the outline panel.
|
||||
///
|
||||
/// Default: true
|
||||
pub indent_guides: Option<bool>,
|
||||
/// Whether to reveal it in the outline panel automatically,
|
||||
/// when a corresponding project entry becomes active.
|
||||
/// Gitignored entries are never auto revealed.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue