@@ -5,7 +5,7 @@ use std::{
cmp,
hash::Hash,
ops::Range,
- path::{Path, PathBuf},
+ path::{Path, PathBuf, MAIN_SEPARATOR_STR},
sync::{atomic::AtomicBool, Arc, OnceLock},
time::Duration,
u32,
@@ -17,9 +17,9 @@ use db::kvp::KEY_VALUE_STORE;
use editor::{
display_map::ToDisplayPoint,
items::{entry_git_aware_label_color, entry_label_color},
- scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
- AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, ExcerptId, ExcerptRange,
- MultiBufferSnapshot, RangeToAnchorExt,
+ scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor, ScrollbarAutoHide},
+ AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, EditorSettings, ExcerptId,
+ ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, ShowScrollbar,
};
use file_icons::FileIcons;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
@@ -27,8 +27,9 @@ use gpui::{
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,
+ IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Model, MouseButton,
+ MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Stateful,
+ StatefulInteractiveElement as _, Styled, Subscription, Task, UniformListScrollHandle, View,
ViewContext, VisualContext, WeakView, WindowContext,
};
use itertools::Itertools;
@@ -51,7 +52,8 @@ use workspace::{
ui::{
h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,
HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
- LabelCommon, ListItem, Selectable, Spacing, StyledExt, StyledTypography, Tooltip,
+ LabelCommon, ListItem, Scrollbar, ScrollbarState, Selectable, Spacing, StyledExt,
+ StyledTypography, Tooltip,
},
OpenInTerminal, WeakItemHandle, Workspace,
};
@@ -116,6 +118,11 @@ pub struct OutlinePanel {
cached_entries: Vec<CachedEntry>,
filter_editor: View<Editor>,
mode: ItemsDisplayMode,
+ show_scrollbar: bool,
+ vertical_scrollbar_state: ScrollbarState,
+ horizontal_scrollbar_state: ScrollbarState,
+ hide_scrollbar_task: Option<Task<()>>,
+ max_width_item_index: Option<usize>,
}
enum ItemsDisplayMode {
@@ -624,6 +631,9 @@ impl OutlinePanel {
let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in);
+ let focus_out_subscription = cx.on_focus_out(&focus_handle, |outline_panel, _, cx| {
+ outline_panel.hide_scrollbar(cx);
+ });
let workspace_subscription = cx.subscribe(
&workspace
.weak_handle()
@@ -674,6 +684,8 @@ impl OutlinePanel {
}
});
+ let scroll_handle = UniformListScrollHandle::new();
+
let mut outline_panel = Self {
mode: ItemsDisplayMode::Outline,
active: false,
@@ -681,7 +693,14 @@ impl OutlinePanel {
workspace: workspace_handle,
project,
fs: workspace.app_state().fs.clone(),
- scroll_handle: UniformListScrollHandle::new(),
+ show_scrollbar: !Self::should_autohide_scrollbar(cx),
+ hide_scrollbar_task: None,
+ vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
+ .parent_view(cx.view()),
+ horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
+ .parent_view(cx.view()),
+ max_width_item_index: None,
+ scroll_handle,
focus_handle,
filter_editor,
fs_entries: Vec::new(),
@@ -705,6 +724,7 @@ impl OutlinePanel {
settings_subscription,
icons_subscription,
focus_subscription,
+ focus_out_subscription,
workspace_subscription,
filter_update_subscription,
],
@@ -1606,16 +1626,11 @@ impl OutlinePanel {
}
.unwrap_or_else(empty_icon);
- let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
- let excerpt_range = range.context.to_point(&buffer_snapshot);
- let label_element = Label::new(format!(
- "Lines {}- {}",
- excerpt_range.start.row + 1,
- excerpt_range.end.row + 1,
- ))
- .single_line()
- .color(color)
- .into_any_element();
+ let label = self.excerpt_label(buffer_id, range, cx)?;
+ let label_element = Label::new(label)
+ .single_line()
+ .color(color)
+ .into_any_element();
Some(self.entry_element(
PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, range.clone())),
@@ -1628,6 +1643,21 @@ impl OutlinePanel {
))
}
+ fn excerpt_label(
+ &self,
+ buffer_id: BufferId,
+ range: &ExcerptRange<language::Anchor>,
+ cx: &AppContext,
+ ) -> Option<String> {
+ let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
+ let excerpt_range = range.context.to_point(&buffer_snapshot);
+ Some(format!(
+ "Lines {}- {}",
+ excerpt_range.start.row + 1,
+ excerpt_range.end.row + 1,
+ ))
+ }
+
fn render_outline(
&self,
buffer_id: BufferId,
@@ -2793,10 +2823,11 @@ impl OutlinePanel {
else {
return;
};
- let new_cached_entries = new_cached_entries.await;
+ let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
outline_panel
.update(&mut cx, |outline_panel, cx| {
outline_panel.cached_entries = new_cached_entries;
+ outline_panel.max_width_item_index = max_width_item_index;
if outline_panel.selected_entry.is_invalidated() {
if let Some(new_selected_entry) =
outline_panel.active_editor().and_then(|active_editor| {
@@ -2819,11 +2850,10 @@ impl OutlinePanel {
is_singleton: bool,
query: Option<String>,
cx: &mut ViewContext<'_, Self>,
- ) -> Task<Vec<CachedEntry>> {
+ ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
let project = self.project.clone();
cx.spawn(|outline_panel, mut cx| async move {
- let mut entries = Vec::new();
- let mut match_candidates = Vec::new();
+ let mut generation_state = GenerationState::default();
let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
@@ -2943,8 +2973,7 @@ impl OutlinePanel {
folded_dirs,
);
outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
+ &mut generation_state,
track_matches,
new_folded_dirs,
folded_depth,
@@ -2981,8 +3010,7 @@ impl OutlinePanel {
.map_or(true, |parent| parent.expanded);
if !is_singleton && (parent_expanded || query.is_some()) {
outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
+ &mut generation_state,
track_matches,
PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
@@ -3006,8 +3034,7 @@ impl OutlinePanel {
.map_or(true, |parent| parent.expanded);
if !is_singleton && (parent_expanded || query.is_some()) {
outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
+ &mut generation_state,
track_matches,
PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
@@ -3042,8 +3069,7 @@ impl OutlinePanel {
&& (should_add || (query.is_some() && folded_dirs_entry.is_none()))
{
outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
+ &mut generation_state,
track_matches,
PanelEntry::Fs(entry.clone()),
depth,
@@ -3055,8 +3081,7 @@ impl OutlinePanel {
ItemsDisplayMode::Search(_) => {
if is_singleton || query.is_some() || (should_add && is_expanded) {
outline_panel.add_search_entries(
- &mut entries,
- &mut match_candidates,
+ &mut generation_state,
entry.clone(),
depth,
query.clone(),
@@ -3082,14 +3107,13 @@ impl OutlinePanel {
};
if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
outline_panel.add_excerpt_entries(
+ &mut generation_state,
buffer_id,
entry_excerpts,
depth,
track_matches,
is_singleton,
query.as_deref(),
- &mut entries,
- &mut match_candidates,
cx,
);
}
@@ -3098,13 +3122,12 @@ impl OutlinePanel {
if is_singleton
&& matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
- && !entries.iter().any(|item| {
+ && !generation_state.entries.iter().any(|item| {
matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
})
{
outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
+ &mut generation_state,
track_matches,
PanelEntry::Fs(entry.clone()),
0,
@@ -3121,8 +3144,7 @@ impl OutlinePanel {
.map_or(true, |parent| parent.expanded);
if parent_expanded || query.is_some() {
outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
+ &mut generation_state,
track_matches,
PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
@@ -3131,15 +3153,20 @@ impl OutlinePanel {
}
}
}) else {
- return Vec::new();
+ return (Vec::new(), None);
};
let Some(query) = query else {
- return entries;
+ return (
+ generation_state.entries,
+ generation_state
+ .max_width_estimate_and_index
+ .map(|(_, index)| index),
+ );
};
let mut matched_ids = match_strings(
- &match_candidates,
+ &generation_state.match_candidates,
&query,
true,
usize::MAX,
@@ -3152,7 +3179,7 @@ impl OutlinePanel {
.collect::<HashMap<_, _>>();
let mut id = 0;
- entries.retain_mut(|cached_entry| {
+ generation_state.entries.retain_mut(|cached_entry| {
let retain = match matched_ids.remove(&id) {
Some(string_match) => {
cached_entry.string_match = Some(string_match);
@@ -3164,15 +3191,19 @@ impl OutlinePanel {
retain
});
- entries
+ (
+ generation_state.entries,
+ generation_state
+ .max_width_estimate_and_index
+ .map(|(_, index)| index),
+ )
})
}
#[allow(clippy::too_many_arguments)]
fn push_entry(
&self,
- entries: &mut Vec<CachedEntry>,
- match_candidates: &mut Vec<StringMatchCandidate>,
+ state: &mut GenerationState,
track_matches: bool,
entry: PanelEntry,
depth: usize,
@@ -3192,13 +3223,13 @@ impl OutlinePanel {
};
if track_matches {
- let id = entries.len();
+ let id = state.entries.len();
match &entry {
PanelEntry::Fs(fs_entry) => {
if let Some(file_name) =
self.relative_path(fs_entry, cx).as_deref().map(file_name)
{
- match_candidates.push(StringMatchCandidate {
+ state.match_candidates.push(StringMatchCandidate {
id,
string: file_name.to_string(),
char_bag: file_name.chars().collect(),
@@ -3208,7 +3239,7 @@ impl OutlinePanel {
PanelEntry::FoldedDirs(worktree_id, entries) => {
let dir_names = self.dir_names_string(entries, *worktree_id, cx);
{
- match_candidates.push(StringMatchCandidate {
+ state.match_candidates.push(StringMatchCandidate {
id,
string: dir_names.clone(),
char_bag: dir_names.chars().collect(),
@@ -3217,7 +3248,7 @@ impl OutlinePanel {
}
PanelEntry::Outline(outline_entry) => match outline_entry {
OutlineEntry::Outline(_, _, outline) => {
- match_candidates.push(StringMatchCandidate {
+ state.match_candidates.push(StringMatchCandidate {
id,
string: outline.text.clone(),
char_bag: outline.text.chars().collect(),
@@ -3226,7 +3257,7 @@ impl OutlinePanel {
OutlineEntry::Excerpt(..) => {}
},
PanelEntry::Search(new_search_entry) => {
- match_candidates.push(StringMatchCandidate {
+ state.match_candidates.push(StringMatchCandidate {
id,
char_bag: new_search_entry.render_data.context_text.chars().collect(),
string: new_search_entry.render_data.context_text.clone(),
@@ -3234,7 +3265,16 @@ impl OutlinePanel {
}
}
}
- entries.push(CachedEntry {
+
+ let width_estimate = self.width_estimate(depth, &entry, cx);
+ if Some(width_estimate)
+ > state
+ .max_width_estimate_and_index
+ .map(|(estimate, _)| estimate)
+ {
+ state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
+ }
+ state.entries.push(CachedEntry {
depth,
entry,
string_match: None,
@@ -3369,14 +3409,13 @@ impl OutlinePanel {
#[allow(clippy::too_many_arguments)]
fn add_excerpt_entries(
&self,
+ state: &mut GenerationState,
buffer_id: BufferId,
entries_to_add: &[ExcerptId],
parent_depth: usize,
track_matches: bool,
is_singleton: bool,
query: Option<&str>,
- entries: &mut Vec<CachedEntry>,
- match_candidates: &mut Vec<StringMatchCandidate>,
cx: &mut ViewContext<Self>,
) {
if let Some(excerpts) = self.excerpts.get(&buffer_id) {
@@ -3386,8 +3425,7 @@ impl OutlinePanel {
};
let excerpt_depth = parent_depth + 1;
self.push_entry(
- entries,
- match_candidates,
+ state,
track_matches,
PanelEntry::Outline(OutlineEntry::Excerpt(
buffer_id,
@@ -3401,8 +3439,7 @@ impl OutlinePanel {
let mut outline_base_depth = excerpt_depth + 1;
if is_singleton {
outline_base_depth = 0;
- entries.clear();
- match_candidates.clear();
+ state.clear();
} else if query.is_none()
&& self
.collapsed_entries
@@ -3413,8 +3450,7 @@ impl OutlinePanel {
for outline in excerpt.iter_outlines() {
self.push_entry(
- entries,
- match_candidates,
+ state,
track_matches,
PanelEntry::Outline(OutlineEntry::Outline(
buffer_id,
@@ -3432,8 +3468,7 @@ impl OutlinePanel {
#[allow(clippy::too_many_arguments)]
fn add_search_entries(
&mut self,
- entries: &mut Vec<CachedEntry>,
- match_candidates: &mut Vec<StringMatchCandidate>,
+ state: &mut GenerationState,
parent_entry: FsEntry,
parent_depth: usize,
filter_query: Option<String>,
@@ -3464,7 +3499,8 @@ impl OutlinePanel {
|| related_excerpts.contains(&match_range.end.excerpt_id)
});
- let previous_search_matches = entries
+ let previous_search_matches = state
+ .entries
.iter()
.skip_while(|entry| {
if let PanelEntry::Fs(entry) = &entry.entry {
@@ -3519,8 +3555,7 @@ impl OutlinePanel {
.collect::<Vec<_>>();
for new_search_entry in new_search_entries {
self.push_entry(
- entries,
- match_candidates,
+ state,
filter_query.is_some(),
PanelEntry::Search(new_search_entry),
depth,
@@ -3589,6 +3624,430 @@ impl OutlinePanel {
self.autoscroll(cx);
cx.notify();
}
+
+ fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
+ if !Self::should_show_scrollbar(cx)
+ || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
+ {
+ return None;
+ }
+ Some(
+ div()
+ .occlude()
+ .id("project-panel-vertical-scroll")
+ .on_mouse_move(cx.listener(|_, _, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, cx| {
+ cx.stop_propagation();
+ })
+ .on_any_mouse_down(|_, cx| {
+ cx.stop_propagation();
+ })
+ .on_mouse_up(
+ MouseButton::Left,
+ cx.listener(|outline_panel, _, cx| {
+ if !outline_panel.vertical_scrollbar_state.is_dragging()
+ && !outline_panel.focus_handle.contains_focused(cx)
+ {
+ outline_panel.hide_scrollbar(cx);
+ cx.notify();
+ }
+
+ cx.stop_propagation();
+ }),
+ )
+ .on_scroll_wheel(cx.listener(|_, _, cx| {
+ cx.notify();
+ }))
+ .h_full()
+ .absolute()
+ .right_1()
+ .top_1()
+ .bottom_0()
+ .w(px(12.))
+ .cursor_default()
+ .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
+ )
+ }
+
+ fn render_horizontal_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
+ if !Self::should_show_scrollbar(cx)
+ || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
+ {
+ return None;
+ }
+
+ let scroll_handle = self.scroll_handle.0.borrow();
+ let longest_item_width = scroll_handle
+ .last_item_size
+ .filter(|size| size.contents.width > size.item.width)?
+ .contents
+ .width
+ .0 as f64;
+ if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
+ return None;
+ }
+
+ Some(
+ div()
+ .occlude()
+ .id("project-panel-horizontal-scroll")
+ .on_mouse_move(cx.listener(|_, _, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, cx| {
+ cx.stop_propagation();
+ })
+ .on_any_mouse_down(|_, cx| {
+ cx.stop_propagation();
+ })
+ .on_mouse_up(
+ MouseButton::Left,
+ cx.listener(|outline_panel, _, cx| {
+ if !outline_panel.horizontal_scrollbar_state.is_dragging()
+ && !outline_panel.focus_handle.contains_focused(cx)
+ {
+ outline_panel.hide_scrollbar(cx);
+ cx.notify();
+ }
+
+ cx.stop_propagation();
+ }),
+ )
+ .on_scroll_wheel(cx.listener(|_, _, cx| {
+ cx.notify();
+ }))
+ .w_full()
+ .absolute()
+ .right_1()
+ .left_1()
+ .bottom_0()
+ .h(px(12.))
+ .cursor_default()
+ .when(self.width.is_some(), |this| {
+ this.children(Scrollbar::horizontal(
+ self.horizontal_scrollbar_state.clone(),
+ ))
+ }),
+ )
+ }
+
+ fn should_show_scrollbar(cx: &AppContext) -> bool {
+ let show = OutlinePanelSettings::get_global(cx)
+ .scrollbar
+ .show
+ .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
+ match show {
+ ShowScrollbar::Auto => true,
+ ShowScrollbar::System => true,
+ ShowScrollbar::Always => true,
+ ShowScrollbar::Never => false,
+ }
+ }
+
+ fn should_autohide_scrollbar(cx: &AppContext) -> bool {
+ let show = OutlinePanelSettings::get_global(cx)
+ .scrollbar
+ .show
+ .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
+ match show {
+ ShowScrollbar::Auto => true,
+ ShowScrollbar::System => cx
+ .try_global::<ScrollbarAutoHide>()
+ .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
+ ShowScrollbar::Always => false,
+ ShowScrollbar::Never => true,
+ }
+ }
+
+ fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
+ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
+ if !Self::should_autohide_scrollbar(cx) {
+ return;
+ }
+ self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
+ cx.background_executor()
+ .timer(SCROLLBAR_SHOW_INTERVAL)
+ .await;
+ panel
+ .update(&mut cx, |panel, cx| {
+ panel.show_scrollbar = false;
+ cx.notify();
+ })
+ .log_err();
+ }))
+ }
+
+ fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &AppContext) -> u64 {
+ let item_text_chars = match entry {
+ PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => self
+ .buffer_snapshot_for_id(*buffer_id, cx)
+ .and_then(|snapshot| {
+ Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
+ })
+ .unwrap_or_default(),
+ PanelEntry::Fs(FsEntry::Directory(_, directory)) => directory
+ .path
+ .file_name()
+ .map(|name| name.to_string_lossy().len())
+ .unwrap_or_default(),
+ PanelEntry::Fs(FsEntry::File(_, file, _, _)) => file
+ .path
+ .file_name()
+ .map(|name| name.to_string_lossy().len())
+ .unwrap_or_default(),
+ PanelEntry::FoldedDirs(_, dirs) => {
+ dirs.iter()
+ .map(|dir| {
+ dir.path
+ .file_name()
+ .map(|name| name.to_string_lossy().len())
+ .unwrap_or_default()
+ })
+ .sum::<usize>()
+ + dirs.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
+ }
+ PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, _, range)) => self
+ .excerpt_label(*buffer_id, range, cx)
+ .map(|label| label.len())
+ .unwrap_or_default(),
+ PanelEntry::Outline(OutlineEntry::Outline(_, _, outline)) => outline.text.len(),
+ PanelEntry::Search(search) => search.render_data.context_text.len(),
+ };
+
+ (item_text_chars + depth) as u64
+ }
+
+ fn render_main_contents(
+ &mut self,
+ query: Option<String>,
+ show_indent_guides: bool,
+ indent_size: f32,
+ cx: &mut ViewContext<'_, Self>,
+ ) -> Div {
+ let contents = if self.cached_entries.is_empty() {
+ let header = if self.updating_fs_entries {
+ "Loading outlines"
+ } else if query.is_some() {
+ "No matches for query"
+ } else {
+ "No outlines available"
+ };
+
+ v_flex()
+ .flex_1()
+ .justify_center()
+ .size_full()
+ .child(h_flex().justify_center().child(Label::new(header)))
+ .when_some(query.clone(), |panel, query| {
+ panel.child(h_flex().justify_center().child(Label::new(query)))
+ })
+ .child(
+ h_flex()
+ .pt(Spacing::Small.rems(cx))
+ .justify_center()
+ .child({
+ let keystroke = match self.position(cx) {
+ DockPosition::Left => {
+ cx.keystroke_text_for(&workspace::ToggleLeftDock)
+ }
+ DockPosition::Bottom => {
+ cx.keystroke_text_for(&workspace::ToggleBottomDock)
+ }
+ DockPosition::Right => {
+ cx.keystroke_text_for(&workspace::ToggleRightDock)
+ }
+ };
+ Label::new(format!("Toggle this panel with {keystroke}"))
+ }),
+ )
+ } else {
+ let list_contents = {
+ let items_len = self.cached_entries.len();
+ let multi_buffer_snapshot = self
+ .active_editor()
+ .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
+ uniform_list(cx.view().clone(), "entries", items_len, {
+ move |outline_panel, range, cx| {
+ let entries = outline_panel.cached_entries.get(range);
+ entries
+ .map(|entries| entries.to_vec())
+ .unwrap_or_default()
+ .into_iter()
+ .filter_map(|cached_entry| match cached_entry.entry {
+ PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
+ &entry,
+ cached_entry.depth,
+ cached_entry.string_match.as_ref(),
+ cx,
+ )),
+ PanelEntry::FoldedDirs(worktree_id, entries) => {
+ Some(outline_panel.render_folded_dirs(
+ worktree_id,
+ &entries,
+ cached_entry.depth,
+ cached_entry.string_match.as_ref(),
+ cx,
+ ))
+ }
+ PanelEntry::Outline(OutlineEntry::Excerpt(
+ buffer_id,
+ excerpt_id,
+ excerpt,
+ )) => outline_panel.render_excerpt(
+ buffer_id,
+ excerpt_id,
+ &excerpt,
+ cached_entry.depth,
+ cx,
+ ),
+ PanelEntry::Outline(OutlineEntry::Outline(
+ buffer_id,
+ excerpt_id,
+ outline,
+ )) => Some(outline_panel.render_outline(
+ buffer_id,
+ excerpt_id,
+ &outline,
+ cached_entry.depth,
+ cached_entry.string_match.as_ref(),
+ cx,
+ )),
+ PanelEntry::Search(SearchEntry {
+ match_range,
+ render_data,
+ kind,
+ ..
+ }) => Some(outline_panel.render_search_match(
+ multi_buffer_snapshot.as_ref(),
+ &match_range,
+ &render_data,
+ kind,
+ cached_entry.depth,
+ cached_entry.string_match.as_ref(),
+ cx,
+ )),
+ })
+ .collect()
+ }
+ })
+ .with_sizing_behavior(ListSizingBehavior::Infer)
+ .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
+ .with_width_from_item(self.max_width_item_index)
+ .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()
+ },
+ ),
+ )
+ })
+ };
+
+ v_flex()
+ .flex_shrink()
+ .size_full()
+ .child(list_contents.size_full().flex_shrink())
+ .children(self.render_vertical_scrollbar(cx))
+ .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
+ this.pb_4().child(scrollbar)
+ })
+ }
+ .children(self.context_menu.as_ref().map(|(menu, position, _)| {
+ deferred(
+ anchored()
+ .position(*position)
+ .anchor(gpui::AnchorCorner::TopLeft)
+ .child(menu.clone()),
+ )
+ .with_priority(1)
+ }));
+
+ v_flex().w_full().flex_1().overflow_hidden().child(contents)
+ }
+
+ fn render_filter_footer(&mut self, pinned: bool, cx: &mut ViewContext<'_, Self>) -> Div {
+ v_flex().flex_none().child(horizontal_separator(cx)).child(
+ h_flex()
+ .p_2()
+ .w_full()
+ .child(self.filter_editor.clone())
+ .child(
+ div().child(
+ IconButton::new(
+ "outline-panel-menu",
+ if pinned {
+ IconName::Unpin
+ } else {
+ IconName::Pin
+ },
+ )
+ .tooltip(move |cx| {
+ Tooltip::text(
+ if pinned {
+ "Unpin Outline"
+ } else {
+ "Pin Active Outline"
+ },
+ cx,
+ )
+ })
+ .shape(IconButtonShape::Square)
+ .on_click(cx.listener(|outline_panel, _, cx| {
+ outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx);
+ })),
+ ),
+ ),
+ )
+ }
}
fn workspace_active_editor(
@@ -3741,17 +4200,34 @@ impl EventEmitter<PanelEvent> for OutlinePanel {}
impl Render for OutlinePanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let project = self.project.read(cx);
+ let (is_local, is_via_ssh) = self
+ .project
+ .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
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.show == ShowIndentGuides::Always;
- let outline_panel = v_flex()
+ let search_query = match &self.mode {
+ ItemsDisplayMode::Search(search_query) => Some(search_query),
+ _ => None,
+ };
+
+ v_flex()
.id("outline-panel")
.size_full()
+ .overflow_hidden()
.relative()
+ .on_hover(cx.listener(|this, hovered, cx| {
+ if *hovered {
+ this.show_scrollbar = true;
+ this.hide_scrollbar_task.take();
+ cx.notify();
+ } else if !this.focus_handle.contains_focused(cx) {
+ this.hide_scrollbar(cx);
+ }
+ }))
.key_context(self.dispatch_context(cx))
.on_action(cx.listener(Self::open))
.on_action(cx.listener(Self::cancel))
@@ -3769,10 +4245,10 @@ impl Render for OutlinePanel {
.on_action(cx.listener(Self::toggle_active_editor_pin))
.on_action(cx.listener(Self::unfold_directory))
.on_action(cx.listener(Self::fold_directory))
- .when(project.is_local(), |el| {
+ .when(is_local, |el| {
el.on_action(cx.listener(Self::reveal_in_finder))
})
- .when(project.is_local() || project.is_via_ssh(), |el| {
+ .when(is_local || is_via_ssh, |el| {
el.on_action(cx.listener(Self::open_in_terminal))
})
.on_mouse_down(
@@ -3785,229 +4261,20 @@ impl Render for OutlinePanel {
}
}),
)
- .track_focus(&self.focus_handle(cx));
-
- if self.cached_entries.is_empty() {
- let header = if self.updating_fs_entries {
- "Loading outlines"
- } else if query.is_some() {
- "No matches for query"
- } else {
- "No outlines available"
- };
-
- outline_panel.child(
- v_flex()
- .justify_center()
- .size_full()
- .child(h_flex().justify_center().child(Label::new(header)))
- .when_some(query.clone(), |panel, query| {
- panel.child(h_flex().justify_center().child(Label::new(query)))
- })
- .child(
- h_flex()
- .pt(Spacing::Small.rems(cx))
- .justify_center()
- .child({
- let keystroke = match self.position(cx) {
- DockPosition::Left => {
- cx.keystroke_text_for(&workspace::ToggleLeftDock)
- }
- DockPosition::Bottom => {
- cx.keystroke_text_for(&workspace::ToggleBottomDock)
- }
- DockPosition::Right => {
- cx.keystroke_text_for(&workspace::ToggleRightDock)
- }
- };
- Label::new(format!("Toggle this panel with {keystroke}"))
- }),
- ),
- )
- } else {
- let search_query = match &self.mode {
- ItemsDisplayMode::Search(search_query) => Some(search_query),
- _ => None,
- };
- outline_panel
- .when_some(search_query, |outline_panel, search_state| {
- outline_panel.child(
- div()
- .mx_2()
- .child(
- Label::new(format!("Searching: '{}'", search_state.query))
- .color(Color::Muted),
- )
- .child(horizontal_separator(cx)),
- )
- })
- .child({
- let items_len = self.cached_entries.len();
- let multi_buffer_snapshot = self
- .active_editor()
- .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
- uniform_list(cx.view().clone(), "entries", items_len, {
- move |outline_panel, range, cx| {
- let entries = outline_panel.cached_entries.get(range);
- entries
- .map(|entries| entries.to_vec())
- .unwrap_or_default()
- .into_iter()
- .filter_map(|cached_entry| match cached_entry.entry {
- PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
- &entry,
- cached_entry.depth,
- cached_entry.string_match.as_ref(),
- cx,
- )),
- PanelEntry::FoldedDirs(worktree_id, entries) => {
- Some(outline_panel.render_folded_dirs(
- worktree_id,
- &entries,
- cached_entry.depth,
- cached_entry.string_match.as_ref(),
- cx,
- ))
- }
- PanelEntry::Outline(OutlineEntry::Excerpt(
- buffer_id,
- excerpt_id,
- excerpt,
- )) => outline_panel.render_excerpt(
- buffer_id,
- excerpt_id,
- &excerpt,
- cached_entry.depth,
- cx,
- ),
- PanelEntry::Outline(OutlineEntry::Outline(
- buffer_id,
- excerpt_id,
- outline,
- )) => Some(outline_panel.render_outline(
- buffer_id,
- excerpt_id,
- &outline,
- cached_entry.depth,
- cached_entry.string_match.as_ref(),
- cx,
- )),
- PanelEntry::Search(SearchEntry {
- match_range,
- render_data,
- kind,
- ..
- }) => Some(outline_panel.render_search_match(
- multi_buffer_snapshot.as_ref(),
- &match_range,
- &render_data,
- kind,
- cached_entry.depth,
- cached_entry.string_match.as_ref(),
- cx,
- )),
- })
- .collect()
- }
- })
- .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, _)| {
- deferred(
- anchored()
- .position(*position)
- .anchor(gpui::AnchorCorner::TopLeft)
- .child(menu.clone()),
- )
- .with_priority(1)
- }))
- .child(
- v_flex().child(horizontal_separator(cx)).child(
- h_flex().p_2().child(self.filter_editor.clone()).child(
- div().child(
- IconButton::new(
- "outline-panel-menu",
- if pinned {
- IconName::Unpin
- } else {
- IconName::Pin
- },
+ .track_focus(&self.focus_handle(cx))
+ .when_some(search_query, |outline_panel, search_state| {
+ outline_panel.child(
+ v_flex()
+ .child(
+ Label::new(format!("Searching: '{}'", search_state.query))
+ .color(Color::Muted)
+ .mx_2(),
)
- .tooltip(move |cx| {
- Tooltip::text(
- if pinned {
- "Unpin Outline"
- } else {
- "Pin Active Outline"
- },
- cx,
- )
- })
- .shape(IconButtonShape::Square)
- .on_click(cx.listener(|outline_panel, _, cx| {
- outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx);
- })),
- ),
- ),
- ),
- )
+ .child(horizontal_separator(cx)),
+ )
+ })
+ .child(self.render_main_contents(query, show_indent_guides, indent_size, cx))
+ .child(self.render_filter_footer(pinned, cx))
}
}