diff --git a/assets/icons/file_code_off.svg b/assets/icons/file_code_off.svg
new file mode 100644
index 0000000000..c97b461853
--- /dev/null
+++ b/assets/icons/file_code_off.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 2fc3b3d0a2..78749a6be7 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1008,6 +1008,11 @@
// Default depth to expand outline items in the current file.
// Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper.
"expand_outlines_with_depth": 100,
+ // Whether to hide symbols, excerpts and search matches in the outline panel
+ // when a multi-buffer view (e.g. a diff or search results) is active,
+ // showing only files and directories.
+ // Does not affect single-file views.
+ "multi_buffer_hide_symbols": false,
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 64531a165a..22ca64a582 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -127,6 +127,7 @@ pub enum IconName {
FastForwardOff,
File,
FileCode,
+ FileCodeOff,
FileDiff,
FileDoc,
FileGeneric,
diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs
index b251aaab1d..c2a42c123a 100644
--- a/crates/outline_panel/src/outline_panel.rs
+++ b/crates/outline_panel/src/outline_panel.rs
@@ -95,6 +95,8 @@ actions!(
SelectParent,
/// Toggles the pin status of the active editor.
ToggleActiveEditorPin,
+ /// Toggles showing symbols, excerpts and search matches for multi-buffer views.
+ ToggleSymbols,
/// Unfolds the selected directory.
UnfoldDirectory,
/// Toggles the outline panel.
@@ -143,7 +145,9 @@ pub struct OutlinePanel {
max_width_item_index: Option,
preserve_selection_on_buffer_fold_toggles: HashSet,
pending_default_expansion_depth: Option,
+ default_depth_applied_buffers: HashSet,
outline_children_cache: HashMap, usize), bool>>,
+ hide_symbols_override: Option,
}
#[derive(Debug)]
@@ -660,6 +664,16 @@ pub fn init(cx: &mut App) {
workspace.close_panel::(window, cx);
}
});
+ workspace.register_action(|workspace, _: &ToggleSymbols, window, cx| {
+ if let Some(outline_panel) = workspace.panel::(cx) {
+ // Defer to avoid updating the panel while the workspace is still being updated.
+ window.defer(cx, move |window, cx| {
+ outline_panel.update(cx, |outline_panel, cx| {
+ outline_panel.toggle_symbols(&ToggleSymbols, window, cx);
+ });
+ });
+ }
+ });
})
.detach();
}
@@ -777,15 +791,33 @@ impl OutlinePanel {
}
} else if &outline_panel_settings != new_settings {
let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth;
+ let old_hide_symbols = outline_panel_settings.multi_buffer_hide_symbols;
outline_panel_settings = *new_settings;
- if old_expansion_depth != new_settings.expand_outlines_with_depth {
+ let mut update_cached_entries = false;
+ if old_hide_symbols != outline_panel_settings.multi_buffer_hide_symbols {
+ outline_panel.hide_symbols_override = None;
+ outline_panel.update_non_fs_items(window, cx);
+ outline_panel.remap_selection_for_hidden_symbols(window, cx);
+ update_cached_entries = true;
+ }
+ if old_expansion_depth != outline_panel_settings.expand_outlines_with_depth
+ {
let old_collapsed_entries = outline_panel.collapsed_entries.clone();
outline_panel
.collapsed_entries
.retain(|entry| !matches!(entry, CollapsedEntry::Outline(..)));
- let new_depth = new_settings.expand_outlines_with_depth;
+ let new_depth = outline_panel_settings.expand_outlines_with_depth;
+ outline_panel.pending_default_expansion_depth = Some(new_depth);
+ outline_panel.default_depth_applied_buffers = outline_panel
+ .buffers
+ .iter()
+ .filter(|(_, buffer)| {
+ matches!(buffer.outlines, OutlineState::Outlines(_))
+ })
+ .map(|(buffer_id, _)| *buffer_id)
+ .collect();
for (buffer_id, buffer) in &outline_panel.buffers {
if let OutlineState::Outlines(outlines) = &buffer.outlines {
@@ -810,12 +842,11 @@ impl OutlinePanel {
}
if old_collapsed_entries != outline_panel.collapsed_entries {
- outline_panel.update_cached_entries(
- Some(UPDATE_DEBOUNCE),
- window,
- cx,
- );
+ update_cached_entries = true;
}
+ }
+ if update_cached_entries {
+ outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
} else {
cx.notify();
}
@@ -879,6 +910,7 @@ impl OutlinePanel {
new_entries_for_fs_update: HashSet::default(),
preserve_selection_on_buffer_fold_toggles: HashSet::default(),
pending_default_expansion_depth: None,
+ default_depth_applied_buffers: HashSet::default(),
fs_entries_update_task: Task::ready(()),
fs_entries_update_pending: false,
cached_entries_update_task: Task::ready(()),
@@ -895,6 +927,7 @@ impl OutlinePanel {
filter_update_subscription,
],
outline_children_cache: HashMap::default(),
+ hide_symbols_override: None,
};
if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
outline_panel.replace_active_editor(item, editor, window, cx);
@@ -3139,7 +3172,7 @@ impl OutlinePanel {
// Only update cached entries if we don't have outlines to fetch
// If we do have outlines to fetch, let fetch_outdated_outlines handle the update
- if outline_panel.buffers_to_fetch().is_empty() {
+ if outline_panel.buffers_to_fetch(cx).is_empty() {
outline_panel.update_cached_entries(debounce, window, cx);
}
@@ -3219,6 +3252,7 @@ impl OutlinePanel {
self.pinned = false;
self.mode = ItemsDisplayMode::Outline;
self.pending_default_expansion_depth = None;
+ self.default_depth_applied_buffers.clear();
}
fn location_for_editor_selection(
@@ -3449,7 +3483,7 @@ impl OutlinePanel {
}
fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context) {
- let buffers_to_fetch = self.buffers_to_fetch();
+ let buffers_to_fetch = self.buffers_to_fetch(cx);
if buffers_to_fetch.is_empty() {
return;
}
@@ -3483,7 +3517,11 @@ impl OutlinePanel {
outline_panel
.update_in(cx, |outline_panel, window, cx| {
let pending_default_depth =
- outline_panel.pending_default_expansion_depth.take();
+ outline_panel.pending_default_expansion_depth.filter(|_| {
+ outline_panel
+ .default_depth_applied_buffers
+ .insert(buffer_id)
+ });
let debounce =
if first_update.fetch_and(false, atomic::Ordering::AcqRel) {
@@ -3528,6 +3566,61 @@ impl OutlinePanel {
.is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton())
}
+ fn multi_buffer_active(&self, cx: &App) -> bool {
+ self.active_editor()
+ .is_some_and(|active_editor| !active_editor.read(cx).buffer().read(cx).is_singleton())
+ }
+
+ fn hide_symbols_active(&self, cx: &App) -> bool {
+ self.hide_symbols_override
+ .unwrap_or(OutlinePanelSettings::get_global(cx).multi_buffer_hide_symbols)
+ }
+
+ fn remap_selection_for_hidden_symbols(&mut self, window: &mut Window, cx: &mut Context) {
+ if !self.multi_buffer_active(cx) || !self.hide_symbols_active(cx) {
+ return;
+ }
+ let buffer_id = match self.selected_entry() {
+ Some(PanelEntry::Outline(OutlineEntry::Excerpt(excerpt))) => {
+ Some(excerpt.context.start.buffer_id)
+ }
+ Some(PanelEntry::Outline(OutlineEntry::Outline(outline))) => {
+ Some(outline.range.start.buffer_id)
+ }
+ Some(PanelEntry::Search(search_entry)) => {
+ let match_start = search_entry.match_range.start;
+ self.active_editor().and_then(|editor| {
+ let multi_buffer_snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
+ multi_buffer_snapshot
+ .anchor_to_buffer_anchor(match_start)
+ .map(|(anchor, _)| anchor.buffer_id)
+ })
+ }
+ Some(PanelEntry::Fs(_) | PanelEntry::FoldedDirs(_)) | None => None,
+ };
+ let Some(buffer_id) = buffer_id else {
+ return;
+ };
+ let file_entry = self
+ .fs_entries
+ .iter()
+ .find(|fs_entry| match fs_entry {
+ FsEntry::File(FsEntryFile {
+ buffer_id: file_buffer_id,
+ ..
+ })
+ | FsEntry::ExternalFile(FsEntryExternalFile {
+ buffer_id: file_buffer_id,
+ ..
+ }) => *file_buffer_id == buffer_id,
+ FsEntry::Directory(..) => false,
+ })
+ .cloned();
+ if let Some(file_entry) = file_entry {
+ self.select_entry(PanelEntry::Fs(file_entry), false, window, cx);
+ }
+ }
+
fn invalidate_outlines(&mut self, ids: &[BufferId]) {
self.outline_fetch_tasks.clear();
let mut ids = ids.iter().collect::>();
@@ -3541,7 +3634,13 @@ impl OutlinePanel {
}
}
- fn buffers_to_fetch(&self) -> HashSet {
+ fn buffers_to_fetch(&self, cx: &App) -> HashSet {
+ // Hide-symbols mode never renders outline rows, so fetching outlines would be wasted
+ // work; `update_fs_entries`'s completion only refreshes cached entries once this is
+ // empty, so the gate has to live here rather than in `fetch_outdated_outlines` alone.
+ if self.multi_buffer_active(cx) && self.hide_symbols_active(cx) {
+ return HashSet::default();
+ }
self.fs_entries
.iter()
.fold(HashSet::default(), |mut buffers_to_fetch, fs_entry| {
@@ -3692,6 +3791,7 @@ impl OutlinePanel {
let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
+ let hide_symbols = !is_singleton && outline_panel.hide_symbols_active(cx);
let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
let track_matches = query.is_some();
@@ -3703,58 +3803,59 @@ impl OutlinePanel {
depth: usize,
}
- let search_precomputed =
- if let ItemsDisplayMode::Search(search_state) = &outline_panel.mode {
- let multi_buffer_snapshot =
- active_editor.read(cx).buffer().read(cx).snapshot(cx);
- let mut folded_buffers = HashSet::default();
- let mut not_folded_buffers = HashSet::default();
- let mut matches_by_buffer = HashMap::default();
+ let search_precomputed = if hide_symbols {
+ None
+ } else if let ItemsDisplayMode::Search(search_state) = &outline_panel.mode {
+ let multi_buffer_snapshot =
+ active_editor.read(cx).buffer().read(cx).snapshot(cx);
+ let mut folded_buffers = HashSet::default();
+ let mut not_folded_buffers = HashSet::default();
+ let mut matches_by_buffer = HashMap::default();
- for (match_range, search_data) in &search_state.matches {
- let Some((start_anchor, _)) =
- multi_buffer_snapshot.anchor_to_buffer_anchor(match_range.start)
- else {
- continue;
- };
- let start_buffer_id = start_anchor.buffer_id;
- let end_buffer_id = multi_buffer_snapshot
- .anchor_to_buffer_anchor(match_range.end)
- .map(|(anchor, _)| anchor.buffer_id);
+ for (match_range, search_data) in &search_state.matches {
+ let Some((start_anchor, _)) =
+ multi_buffer_snapshot.anchor_to_buffer_anchor(match_range.start)
+ else {
+ continue;
+ };
+ let start_buffer_id = start_anchor.buffer_id;
+ let end_buffer_id = multi_buffer_snapshot
+ .anchor_to_buffer_anchor(match_range.end)
+ .map(|(anchor, _)| anchor.buffer_id);
- let mut any_folded = false;
- for buffer_id in
- [Some(start_buffer_id), end_buffer_id].into_iter().flatten()
- {
- if folded_buffers.contains(&buffer_id) {
+ let mut any_folded = false;
+ for buffer_id in
+ [Some(start_buffer_id), end_buffer_id].into_iter().flatten()
+ {
+ if folded_buffers.contains(&buffer_id) {
+ any_folded = true;
+ } else if !not_folded_buffers.contains(&buffer_id) {
+ if active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
+ folded_buffers.insert(buffer_id);
any_folded = true;
- } else if !not_folded_buffers.contains(&buffer_id) {
- if active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
- folded_buffers.insert(buffer_id);
- any_folded = true;
- } else {
- not_folded_buffers.insert(buffer_id);
- }
+ } else {
+ not_folded_buffers.insert(buffer_id);
}
}
- if any_folded {
- continue;
- }
-
- matches_by_buffer
- .entry(start_buffer_id)
- .or_insert_with(Vec::new)
- .push((match_range.clone(), Arc::clone(search_data)));
+ }
+ if any_folded {
+ continue;
}
- Some(SearchPrecomputed {
- multi_buffer_snapshot,
- matches_by_buffer,
- folded_buffers,
- })
- } else {
- None
- };
+ matches_by_buffer
+ .entry(start_buffer_id)
+ .or_insert_with(Vec::new)
+ .push((match_range.clone(), Arc::clone(search_data)));
+ }
+
+ Some(SearchPrecomputed {
+ multi_buffer_snapshot,
+ matches_by_buffer,
+ folded_buffers,
+ })
+ } else {
+ None
+ };
let mut parent_dirs = Vec::::new();
for entry in outline_panel.fs_entries.clone() {
@@ -3983,25 +4084,28 @@ impl OutlinePanel {
);
}
- match outline_panel.mode {
- ItemsDisplayMode::Search(_) => {
- if (is_singleton || query.is_some() || (should_add && is_expanded))
- && let Some(search) = &search_precomputed
- {
- outline_panel.add_search_entries(
- &mut generation_state,
- search,
- &entry,
- depth,
- query.is_some(),
- is_singleton,
- cx,
- );
+ if !hide_symbols {
+ match outline_panel.mode {
+ ItemsDisplayMode::Search(_) => {
+ if (is_singleton || query.is_some() || (should_add && is_expanded))
+ && let Some(search) = &search_precomputed
+ {
+ outline_panel.add_search_entries(
+ &mut generation_state,
+ search,
+ &entry,
+ depth,
+ query.is_some(),
+ is_singleton,
+ cx,
+ );
+ }
}
- }
- ItemsDisplayMode::Outline => {
- let excerpts_to_consider =
- if is_singleton || query.is_some() || (should_add && is_expanded) {
+ ItemsDisplayMode::Outline => {
+ let excerpts_to_consider = if is_singleton
+ || query.is_some()
+ || (should_add && is_expanded)
+ {
match &entry {
FsEntry::File(FsEntryFile {
buffer_id,
@@ -4018,18 +4122,19 @@ impl OutlinePanel {
} else {
None
};
- if let Some((buffer_id, _entry_excerpts)) = excerpts_to_consider
- && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
- {
- outline_panel.add_buffer_entries(
- &mut generation_state,
- buffer_id,
- depth,
- track_matches,
- is_singleton,
- query.as_deref(),
- cx,
- );
+ if let Some((buffer_id, _entry_excerpts)) = excerpts_to_consider
+ && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
+ {
+ outline_panel.add_buffer_entries(
+ &mut generation_state,
+ buffer_id,
+ depth,
+ track_matches,
+ is_singleton,
+ query.as_deref(),
+ cx,
+ );
+ }
}
}
}
@@ -4387,7 +4492,24 @@ impl OutlinePanel {
let mut last_depth_at_level: Vec