{
let settings = OutlinePanelSettings::get_global(cx);
- let is_active = match &self.selected_entry {
- Some(EntryOwned::FoldedDirs(selected_worktree_id, selected_entries)) => {
+ let is_active = match self.selected_entry() {
+ Some(PanelEntry::FoldedDirs(selected_worktree_id, selected_entries)) => {
selected_worktree_id == &worktree_id && selected_entries == dir_entries
}
_ => false,
@@ -1479,7 +1662,7 @@ impl OutlinePanel {
};
self.entry_element(
- EntryRef::FoldedDirs(worktree_id, dir_entries),
+ PanelEntry::FoldedDirs(worktree_id, dir_entries.to_vec()),
item_id,
depth,
Some(icon),
@@ -1489,10 +1672,66 @@ impl OutlinePanel {
)
}
+ fn render_search_match(
+ &self,
+ match_range: &Range
,
+ search_data: &SearchData,
+ kind: SearchKind,
+ depth: usize,
+ string_match: Option<&StringMatch>,
+ cx: &mut ViewContext,
+ ) -> Stateful {
+ let search_matches = string_match
+ .iter()
+ .flat_map(|string_match| string_match.ranges())
+ .collect::
>();
+ let match_ranges = if search_matches.is_empty() {
+ &search_data.search_match_indices
+ } else {
+ &search_matches
+ };
+ let label_element = language::render_item(
+ &OutlineItem {
+ depth,
+ annotation_range: None,
+ range: search_data.context_range.clone(),
+ text: search_data.context_text.clone(),
+ highlight_ranges: search_data.highlight_ranges.clone(),
+ name_ranges: search_data.search_match_indices.clone(),
+ body_range: Some(search_data.context_range.clone()),
+ },
+ match_ranges.into_iter().cloned(),
+ cx,
+ )
+ .into_any_element();
+
+ let is_active = match self.selected_entry() {
+ Some(PanelEntry::Search(SearchEntry {
+ match_range: selected_match_range,
+ ..
+ })) => match_range == selected_match_range,
+ _ => false,
+ };
+ self.entry_element(
+ PanelEntry::Search(SearchEntry {
+ kind,
+ match_range: match_range.clone(),
+ same_line_matches: Vec::new(),
+ render_data: Some(OnceCell::new()),
+ }),
+ ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
+ depth,
+ None,
+ is_active,
+ label_element,
+ cx,
+ )
+ }
+
#[allow(clippy::too_many_arguments)]
fn entry_element(
&self,
- rendered_entry: EntryRef<'_>,
+ rendered_entry: PanelEntry,
item_id: ElementId,
depth: usize,
icon_element: Option,
@@ -1501,7 +1740,6 @@ impl OutlinePanel {
cx: &mut ViewContext,
) -> Stateful {
let settings = OutlinePanelSettings::get_global(cx);
- let rendered_entry = rendered_entry.to_owned_entry();
div()
.text_ui(cx)
.id(item_id.clone())
@@ -1520,7 +1758,8 @@ impl OutlinePanel {
if event.down.button == MouseButton::Right || event.down.first_mouse {
return;
}
- outline_panel.open_entry(&clicked_entry, cx);
+ let change_selection = event.down.click_count > 1;
+ outline_panel.open_entry(&clicked_entry, change_selection, cx);
})
})
.on_secondary_mouse_down(cx.listener(
@@ -1530,7 +1769,7 @@ impl OutlinePanel {
cx.stop_propagation();
outline_panel.deploy_context_menu(
event.position,
- rendered_entry.to_ref_entry(),
+ rendered_entry.clone(),
cx,
)
},
@@ -1582,7 +1821,6 @@ impl OutlinePanel {
&mut self,
active_editor: &View
,
new_entries: HashSet,
- new_selected_entry: Option,
debounce: Option,
cx: &mut ViewContext,
) {
@@ -1665,11 +1903,11 @@ impl OutlinePanel {
match &worktree {
Some(worktree) => {
new_collapsed_entries
- .insert(CollapsedEntry::File(worktree.id(), buffer_id));
+ .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
}
None => {
new_collapsed_entries
- .insert(CollapsedEntry::ExternalFile(buffer_id));
+ .remove(&CollapsedEntry::ExternalFile(buffer_id));
}
}
}
@@ -1908,45 +2146,43 @@ impl OutlinePanel {
outline_panel.fs_entries_depth = new_depth_map;
outline_panel.fs_children_count = new_children_count;
outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
- if new_selected_entry.is_some() {
- outline_panel.selected_entry = new_selected_entry;
- }
- outline_panel.fetch_outdated_outlines(cx);
- outline_panel.autoscroll(cx);
+ outline_panel.update_non_fs_items(cx);
+
cx.notify();
})
.ok();
});
}
- fn replace_visible_entries(
+ fn replace_active_editor(
&mut self,
new_active_editor: View,
cx: &mut ViewContext,
) {
- let new_selected_entry = self.location_for_editor_selection(&new_active_editor, cx);
self.clear_previous(cx);
+ let buffer_search_subscription = cx.subscribe(
+ &new_active_editor,
+ |outline_panel: &mut Self, _, _: &SearchEvent, cx: &mut ViewContext<'_, Self>| {
+ outline_panel.update_search_matches(cx);
+ outline_panel.autoscroll(cx);
+ },
+ );
self.active_item = Some(ActiveItem {
- item_id: new_active_editor.item_id(),
+ _buffer_search_subscription: buffer_search_subscription,
_editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
active_editor: new_active_editor.downgrade(),
});
let new_entries =
HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
- self.update_fs_entries(
- &new_active_editor,
- new_entries,
- new_selected_entry,
- None,
- cx,
- );
+ self.selected_entry.invalidate();
+ self.update_fs_entries(&new_active_editor, new_entries, None, cx);
}
fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
self.filter_editor.update(cx, |editor, cx| editor.clear(cx));
self.collapsed_entries.clear();
self.unfolded_dirs.clear();
- self.selected_entry = None;
+ self.selected_entry = SelectedEntry::None;
self.fs_entries_update_task = Task::ready(());
self.cached_entries_update_task = Task::ready(());
self.active_item = None;
@@ -1955,14 +2191,17 @@ impl OutlinePanel {
self.fs_children_count.clear();
self.outline_fetch_tasks.clear();
self.excerpts.clear();
- self.cached_entries_with_depth = Vec::new();
+ self.cached_entries = Vec::new();
+ self.search_matches.clear();
+ self.search = None;
+ self.pinned = false;
}
fn location_for_editor_selection(
&mut self,
editor: &View,
cx: &mut ViewContext,
- ) -> Option {
+ ) -> Option {
let selection = editor
.read(cx)
.selections
@@ -1979,6 +2218,64 @@ impl OutlinePanel {
let buffer_id = buffer.read(cx).remote_id();
let selection_display_point = selection.to_display_point(&editor_snapshot);
+ match self.mode {
+ ItemsDisplayMode::Search => self
+ .search_matches
+ .iter()
+ .rev()
+ .min_by_key(|&match_range| {
+ let match_display_range =
+ match_range.clone().to_display_points(&editor_snapshot);
+ let start_distance = if selection_display_point < match_display_range.start {
+ match_display_range.start - selection_display_point
+ } else {
+ selection_display_point - match_display_range.start
+ };
+ let end_distance = if selection_display_point < match_display_range.end {
+ match_display_range.end - selection_display_point
+ } else {
+ selection_display_point - match_display_range.end
+ };
+ start_distance + end_distance
+ })
+ .and_then(|closest_range| {
+ self.cached_entries.iter().find_map(|cached_entry| {
+ if let PanelEntry::Search(SearchEntry {
+ match_range,
+ same_line_matches,
+ ..
+ }) = &cached_entry.entry
+ {
+ if match_range == closest_range
+ || same_line_matches.contains(&closest_range)
+ {
+ Some(cached_entry.entry.clone())
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ }),
+ ItemsDisplayMode::Outline => self.outline_location(
+ buffer_id,
+ excerpt_id,
+ multi_buffer_snapshot,
+ editor_snapshot,
+ selection_display_point,
+ ),
+ }
+ }
+
+ fn outline_location(
+ &mut self,
+ buffer_id: BufferId,
+ excerpt_id: ExcerptId,
+ multi_buffer_snapshot: editor::MultiBufferSnapshot,
+ editor_snapshot: editor::EditorSnapshot,
+ selection_display_point: DisplayPoint,
+ ) -> Option {
let excerpt_outlines = self
.excerpts
.get(&buffer_id)
@@ -2068,31 +2365,37 @@ impl OutlinePanel {
.cloned();
let closest_container = match outline_item {
- Some(outline) => EntryOwned::Outline(buffer_id, excerpt_id, outline),
- None => self
- .cached_entries_with_depth
- .iter()
- .rev()
- .find_map(|cached_entry| match &cached_entry.entry {
- EntryOwned::Excerpt(entry_buffer_id, entry_excerpt_id, _) => {
- if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
- Some(cached_entry.entry.clone())
- } else {
- None
+ Some(outline) => {
+ PanelEntry::Outline(OutlineEntry::Outline(buffer_id, excerpt_id, outline))
+ }
+ None => {
+ self.cached_entries.iter().rev().find_map(|cached_entry| {
+ match &cached_entry.entry {
+ PanelEntry::Outline(OutlineEntry::Excerpt(
+ entry_buffer_id,
+ entry_excerpt_id,
+ _,
+ )) => {
+ if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
+ Some(cached_entry.entry.clone())
+ } else {
+ None
+ }
}
- }
- EntryOwned::Entry(
- FsEntry::ExternalFile(file_buffer_id, file_excerpts)
- | FsEntry::File(_, _, file_buffer_id, file_excerpts),
- ) => {
- if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
- Some(cached_entry.entry.clone())
- } else {
- None
+ PanelEntry::Fs(
+ FsEntry::ExternalFile(file_buffer_id, file_excerpts)
+ | FsEntry::File(_, _, file_buffer_id, file_excerpts),
+ ) => {
+ if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
+ Some(cached_entry.entry.clone())
+ } else {
+ None
+ }
}
+ _ => None,
}
- _ => None,
- })?,
+ })?
+ }
};
Some(closest_container)
}
@@ -2143,20 +2446,9 @@ impl OutlinePanel {
}
fn is_singleton_active(&self, cx: &AppContext) -> bool {
- self.active_item
- .as_ref()
- .and_then(|active_item| {
- Some(
- active_item
- .active_editor
- .upgrade()?
- .read(cx)
- .buffer()
- .read(cx)
- .is_singleton(),
- )
- })
- .unwrap_or(false)
+ self.active_editor().map_or(false, |active_editor| {
+ active_editor.read(cx).buffer().read(cx).is_singleton()
+ })
}
fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
@@ -2227,7 +2519,7 @@ impl OutlinePanel {
buffer_id: BufferId,
cx: &AppContext,
) -> Option {
- let editor = self.active_item.as_ref()?.active_editor.upgrade()?;
+ let editor = self.active_editor()?;
Some(
editor
.read(cx)
@@ -2239,9 +2531,9 @@ impl OutlinePanel {
)
}
- fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option {
+ fn abs_path(&self, entry: &PanelEntry, cx: &AppContext) -> Option {
match entry {
- EntryOwned::Entry(
+ PanelEntry::Fs(
FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
) => self
.buffer_snapshot_for_id(*buffer_id, cx)
@@ -2249,20 +2541,20 @@ impl OutlinePanel {
let file = File::from_dyn(buffer_snapshot.file())?;
file.worktree.read(cx).absolutize(&file.path).ok()
}),
- EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self
+ PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
.project
.read(cx)
.worktree_for_id(*worktree_id, cx)?
.read(cx)
.absolutize(&entry.path)
.ok(),
- EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
+ PanelEntry::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
self.project
.read(cx)
.worktree_for_id(*worktree_id, cx)
.and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
}),
- EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
+ PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
}
}
@@ -2282,6 +2574,10 @@ impl OutlinePanel {
debounce: Option,
cx: &mut ViewContext,
) {
+ if !self.active {
+ return;
+ }
+
let is_singleton = self.is_singleton_active(cx);
let query = self.query(cx);
self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
@@ -2299,7 +2595,18 @@ impl OutlinePanel {
let new_cached_entries = new_cached_entries.await;
outline_panel
.update(&mut cx, |outline_panel, cx| {
- outline_panel.cached_entries_with_depth = new_cached_entries;
+ outline_panel.cached_entries = new_cached_entries;
+ if outline_panel.selected_entry.is_invalidated() {
+ if let Some(new_selected_entry) =
+ outline_panel.active_editor().and_then(|active_editor| {
+ outline_panel.location_for_editor_selection(&active_editor, cx)
+ })
+ {
+ outline_panel.select_entry(new_selected_entry, false, cx);
+ }
+ }
+
+ outline_panel.autoscroll(cx);
cx.notify();
})
.ok();
@@ -2405,9 +2712,9 @@ impl OutlinePanel {
folded_dirs_entry =
Some((folded_depth, folded_worktree_id, folded_dirs))
} else {
- if parent_expanded || query.is_some() {
+ if !is_singleton && (parent_expanded || query.is_some()) {
let new_folded_dirs =
- EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs);
+ PanelEntry::FoldedDirs(folded_worktree_id, folded_dirs);
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
@@ -2441,12 +2748,12 @@ impl OutlinePanel {
.all(|entry| entry.path.as_ref() != *parent_path)
})
.map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
- if parent_expanded || query.is_some() {
+ if !is_singleton && (parent_expanded || query.is_some()) {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
track_matches,
- EntryOwned::FoldedDirs(worktree_id, folded_dirs),
+ PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
cx,
);
@@ -2468,12 +2775,12 @@ impl OutlinePanel {
.all(|entry| entry.path.as_ref() != *parent_path)
})
.map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
- if parent_expanded || query.is_some() {
+ if !is_singleton && (parent_expanded || query.is_some()) {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
track_matches,
- EntryOwned::FoldedDirs(worktree_id, folded_dirs),
+ PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
cx,
);
@@ -2509,85 +2816,71 @@ impl OutlinePanel {
&mut entries,
&mut match_candidates,
track_matches,
- EntryOwned::Entry(entry.clone()),
+ PanelEntry::Fs(entry.clone()),
depth,
cx,
);
}
- let excerpts_to_consider =
- if is_singleton || query.is_some() || (should_add && is_expanded) {
- match entry {
- FsEntry::File(_, _, buffer_id, entry_excerpts) => {
- Some((*buffer_id, entry_excerpts))
- }
- FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
- Some((*buffer_id, entry_excerpts))
- }
- _ => None,
- }
- } else {
- None
- };
- if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
- if let Some(excerpts) = outline_panel.excerpts.get(&buffer_id) {
- for &entry_excerpt in entry_excerpts {
- let Some(excerpt) = excerpts.get(&entry_excerpt) else {
- continue;
- };
- let excerpt_depth = depth + 1;
- outline_panel.push_entry(
+ match outline_panel.mode {
+ ItemsDisplayMode::Search => {
+ if is_singleton || query.is_some() || (should_add && is_expanded) {
+ outline_panel.add_search_entries(
+ entry,
+ depth,
+ track_matches,
+ is_singleton,
&mut entries,
&mut match_candidates,
- track_matches,
- EntryOwned::Excerpt(
- buffer_id,
- entry_excerpt,
- excerpt.range.clone(),
- ),
- excerpt_depth,
cx,
);
-
- let mut outline_base_depth = excerpt_depth + 1;
- if is_singleton {
- outline_base_depth = 0;
- entries.clear();
- match_candidates.clear();
- } else if query.is_none()
- && outline_panel.collapsed_entries.contains(
- &CollapsedEntry::Excerpt(buffer_id, entry_excerpt),
- )
- {
- continue;
- }
-
- for outline in excerpt.iter_outlines() {
- outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
- track_matches,
- EntryOwned::Outline(
- buffer_id,
- entry_excerpt,
- outline.clone(),
- ),
- outline_base_depth + outline.depth,
- cx,
- );
- }
- if is_singleton && entries.is_empty() {
- outline_panel.push_entry(
- &mut entries,
- &mut match_candidates,
- track_matches,
- EntryOwned::Entry(entry.clone()),
- 0,
- cx,
- );
- }
}
}
+ ItemsDisplayMode::Outline => {
+ let excerpts_to_consider =
+ if is_singleton || query.is_some() || (should_add && is_expanded) {
+ match entry {
+ FsEntry::File(_, _, buffer_id, entry_excerpts) => {
+ Some((*buffer_id, entry_excerpts))
+ }
+ FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
+ Some((*buffer_id, entry_excerpts))
+ }
+ _ => None,
+ }
+ } else {
+ None
+ };
+ if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
+ outline_panel.add_excerpt_entries(
+ buffer_id,
+ entry_excerpts,
+ depth,
+ track_matches,
+ is_singleton,
+ query.as_deref(),
+ &mut entries,
+ &mut match_candidates,
+ cx,
+ );
+ }
+ }
+ }
+
+ if is_singleton
+ && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
+ && !entries.iter().any(|item| {
+ matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
+ })
+ {
+ outline_panel.push_entry(
+ &mut entries,
+ &mut match_candidates,
+ track_matches,
+ PanelEntry::Fs(entry.clone()),
+ 0,
+ cx,
+ );
}
}
@@ -2606,7 +2899,7 @@ impl OutlinePanel {
&mut entries,
&mut match_candidates,
track_matches,
- EntryOwned::FoldedDirs(worktree_id, folded_dirs),
+ PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
cx,
);
@@ -2654,14 +2947,14 @@ impl OutlinePanel {
entries: &mut Vec,
match_candidates: &mut Vec,
track_matches: bool,
- entry: EntryOwned,
+ entry: PanelEntry,
depth: usize,
- cx: &AppContext,
+ cx: &mut WindowContext,
) {
if track_matches {
let id = entries.len();
match &entry {
- EntryOwned::Entry(fs_entry) => {
+ PanelEntry::Fs(fs_entry) => {
if let Some(file_name) =
self.relative_path(fs_entry, cx).as_deref().map(file_name)
{
@@ -2672,7 +2965,7 @@ impl OutlinePanel {
});
}
}
- EntryOwned::FoldedDirs(worktree_id, entries) => {
+ PanelEntry::FoldedDirs(worktree_id, entries) => {
let dir_names = self.dir_names_string(entries, *worktree_id, cx);
{
match_candidates.push(StringMatchCandidate {
@@ -2682,12 +2975,29 @@ impl OutlinePanel {
});
}
}
- EntryOwned::Outline(_, _, outline) => match_candidates.push(StringMatchCandidate {
- id,
- string: outline.text.clone(),
- char_bag: outline.text.chars().collect(),
- }),
- EntryOwned::Excerpt(..) => {}
+ PanelEntry::Outline(outline_entry) => match outline_entry {
+ OutlineEntry::Outline(_, _, outline) => {
+ match_candidates.push(StringMatchCandidate {
+ id,
+ string: outline.text.clone(),
+ char_bag: outline.text.chars().collect(),
+ });
+ }
+ OutlineEntry::Excerpt(..) => {}
+ },
+ PanelEntry::Search(new_search_entry) => {
+ if let Some(search_data) = new_search_entry
+ .render_data
+ .as_ref()
+ .and_then(|data| data.get())
+ {
+ match_candidates.push(StringMatchCandidate {
+ id,
+ char_bag: search_data.context_text.chars().collect(),
+ string: search_data.context_text.clone(),
+ });
+ }
+ }
}
}
entries.push(CachedEntry {
@@ -2729,6 +3039,318 @@ impl OutlinePanel {
};
!self.collapsed_entries.contains(&entry_to_check)
}
+
+ fn update_non_fs_items(&mut self, cx: &mut ViewContext) {
+ if !self.active {
+ return;
+ }
+
+ self.update_search_matches(cx);
+ self.fetch_outdated_outlines(cx);
+ self.autoscroll(cx);
+ }
+
+ fn update_search_matches(&mut self, cx: &mut ViewContext) {
+ if !self.active {
+ return;
+ }
+
+ let active_editor = self.active_editor();
+ let project_search = self.active_project_search(active_editor.as_ref(), cx);
+ let project_search_matches = project_search
+ .as_ref()
+ .map(|project_search| project_search.read(cx).get_matches(cx))
+ .unwrap_or_default();
+
+ let buffer_search = active_editor
+ .as_ref()
+ .and_then(|active_editor| self.workspace.read(cx).pane_for(active_editor))
+ .and_then(|pane| {
+ pane.read(cx)
+ .toolbar()
+ .read(cx)
+ .item_of_type::()
+ });
+ let buffer_search_matches = active_editor
+ .map(|active_editor| active_editor.update(cx, |editor, cx| editor.get_matches(cx)))
+ .unwrap_or_default();
+
+ let mut update_cached_entries = false;
+ if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
+ self.search_matches.clear();
+ self.search = None;
+ if self.mode == ItemsDisplayMode::Search {
+ self.mode = ItemsDisplayMode::Outline;
+ update_cached_entries = true;
+ }
+ } else {
+ let new_search_matches = if buffer_search_matches.is_empty() {
+ self.search = project_search.map(|project_search| {
+ (
+ SearchKind::Project,
+ project_search.read(cx).search_query_text(cx),
+ )
+ });
+ project_search_matches
+ } else {
+ self.search = buffer_search
+ .map(|buffer_search| (SearchKind::Buffer, buffer_search.read(cx).query(cx)));
+ buffer_search_matches
+ };
+ update_cached_entries = self.mode != ItemsDisplayMode::Search
+ || self.search_matches.is_empty()
+ || self.search_matches != new_search_matches;
+ self.search_matches = new_search_matches;
+ self.mode = ItemsDisplayMode::Search;
+ }
+ if update_cached_entries {
+ self.selected_entry.invalidate();
+ self.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
+ }
+ }
+
+ fn active_project_search(
+ &mut self,
+ for_editor: Option<&View>,
+ cx: &mut ViewContext,
+ ) -> Option> {
+ let for_editor = for_editor?;
+ self.workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .items()
+ .filter_map(|item| item.downcast::())
+ .find(|project_search| {
+ let project_search_editor = project_search.boxed_clone().act_as::(cx);
+ Some(for_editor) == project_search_editor.as_ref()
+ })
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn add_excerpt_entries(
+ &self,
+ buffer_id: BufferId,
+ entries_to_add: &[ExcerptId],
+ parent_depth: usize,
+ track_matches: bool,
+ is_singleton: bool,
+ query: Option<&str>,
+ entries: &mut Vec,
+ match_candidates: &mut Vec,
+ cx: &mut ViewContext,
+ ) {
+ if let Some(excerpts) = self.excerpts.get(&buffer_id) {
+ for &excerpt_id in entries_to_add {
+ let Some(excerpt) = excerpts.get(&excerpt_id) else {
+ continue;
+ };
+ let excerpt_depth = parent_depth + 1;
+ self.push_entry(
+ entries,
+ match_candidates,
+ track_matches,
+ PanelEntry::Outline(OutlineEntry::Excerpt(
+ buffer_id,
+ excerpt_id,
+ excerpt.range.clone(),
+ )),
+ excerpt_depth,
+ cx,
+ );
+
+ let mut outline_base_depth = excerpt_depth + 1;
+ if is_singleton {
+ outline_base_depth = 0;
+ entries.clear();
+ match_candidates.clear();
+ } else if query.is_none()
+ && self
+ .collapsed_entries
+ .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
+ {
+ continue;
+ }
+
+ for outline in excerpt.iter_outlines() {
+ self.push_entry(
+ entries,
+ match_candidates,
+ track_matches,
+ PanelEntry::Outline(OutlineEntry::Outline(
+ buffer_id,
+ excerpt_id,
+ outline.clone(),
+ )),
+ outline_base_depth + outline.depth,
+ cx,
+ );
+ }
+ }
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn add_search_entries(
+ &self,
+ entry: &FsEntry,
+ parent_depth: usize,
+ track_matches: bool,
+ is_singleton: bool,
+ entries: &mut Vec,
+ match_candidates: &mut Vec,
+ cx: &mut ViewContext,
+ ) {
+ let related_excerpts = match entry {
+ FsEntry::Directory(_, _) => return,
+ FsEntry::ExternalFile(_, excerpts) => excerpts,
+ FsEntry::File(_, _, _, excerpts) => excerpts,
+ }
+ .iter()
+ .copied()
+ .collect::>();
+ if related_excerpts.is_empty() || self.search_matches.is_empty() {
+ return;
+ }
+ let Some(kind) = self.search.as_ref().map(|&(kind, _)| kind) else {
+ return;
+ };
+
+ for match_range in &self.search_matches {
+ if related_excerpts.contains(&match_range.start.excerpt_id)
+ || related_excerpts.contains(&match_range.end.excerpt_id)
+ {
+ let depth = if is_singleton { 0 } else { parent_depth + 1 };
+ let previous_search_entry = entries.last_mut().and_then(|entry| {
+ if let PanelEntry::Search(previous_search_entry) = &mut entry.entry {
+ Some(previous_search_entry)
+ } else {
+ None
+ }
+ });
+ let mut new_search_entry = SearchEntry {
+ kind,
+ match_range: match_range.clone(),
+ same_line_matches: Vec::new(),
+ render_data: Some(OnceCell::new()),
+ };
+ if self.init_search_data(previous_search_entry, &mut new_search_entry, cx) {
+ self.push_entry(
+ entries,
+ match_candidates,
+ track_matches,
+ PanelEntry::Search(new_search_entry),
+ depth,
+ cx,
+ );
+ }
+ }
+ }
+ }
+
+ fn active_editor(&self) -> Option> {
+ self.active_item.as_ref()?.active_editor.upgrade()
+ }
+
+ fn should_replace_active_editor(&self, new_active_editor: &View) -> bool {
+ self.active_editor().map_or(true, |active_editor| {
+ !self.pinned && active_editor.item_id() != new_active_editor.item_id()
+ })
+ }
+
+ pub fn toggle_active_editor_pin(
+ &mut self,
+ _: &ToggleActiveEditorPin,
+ cx: &mut ViewContext,
+ ) {
+ self.pinned = !self.pinned;
+ if !self.pinned {
+ if let Some(active_editor) = workspace_active_editor(self.workspace.read(cx), cx) {
+ if self.should_replace_active_editor(&active_editor) {
+ self.replace_active_editor(active_editor, cx);
+ }
+ }
+ }
+
+ cx.notify();
+ }
+
+ fn selected_entry(&self) -> Option<&PanelEntry> {
+ match &self.selected_entry {
+ SelectedEntry::Invalidated(entry) => entry.as_ref(),
+ SelectedEntry::Valid(entry) => Some(entry),
+ SelectedEntry::None => None,
+ }
+ }
+
+ fn init_search_data(
+ &self,
+ previous_search_entry: Option<&mut SearchEntry>,
+ new_search_entry: &mut SearchEntry,
+ cx: &WindowContext,
+ ) -> bool {
+ let Some(active_editor) = self.active_editor() else {
+ return false;
+ };
+ let multi_buffer_snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
+ let theme = cx.theme().syntax().clone();
+ let previous_search_data = previous_search_entry.and_then(|previous_search_entry| {
+ let previous_search_data = previous_search_entry.render_data.as_mut()?;
+ previous_search_data.get_or_init(|| {
+ SearchData::new(
+ new_search_entry.kind,
+ &previous_search_entry.match_range,
+ &multi_buffer_snapshot,
+ &theme,
+ )
+ });
+ previous_search_data.get_mut()
+ });
+ let new_search_data = new_search_entry.render_data.as_mut().and_then(|data| {
+ data.get_or_init(|| {
+ SearchData::new(
+ new_search_entry.kind,
+ &new_search_entry.match_range,
+ &multi_buffer_snapshot,
+ &theme,
+ )
+ });
+ data.get_mut()
+ });
+ match (previous_search_data, new_search_data) {
+ (_, None) => false,
+ (None, Some(_)) => true,
+ (Some(previous_search_data), Some(new_search_data)) => {
+ if previous_search_data.context_range == new_search_data.context_range {
+ previous_search_data
+ .highlight_ranges
+ .append(&mut new_search_data.highlight_ranges);
+ previous_search_data
+ .search_match_indices
+ .append(&mut new_search_data.search_match_indices);
+ false
+ } else {
+ true
+ }
+ }
+ }
+ }
+
+ fn select_entry(&mut self, entry: PanelEntry, focus: bool, cx: &mut ViewContext) {
+ if focus {
+ self.focus_handle.focus(cx);
+ }
+ self.selected_entry = SelectedEntry::Valid(entry);
+ self.autoscroll(cx);
+ cx.notify();
+ }
+}
+
+fn workspace_active_editor(workspace: &Workspace, cx: &AppContext) -> Option> {
+ workspace
+ .active_item(cx)?
+ .act_as::(cx)
+ .filter(|editor| editor.read(cx).mode() == EditorMode::Full)
}
fn back_to_common_visited_parent(
@@ -2825,31 +3447,34 @@ impl Panel for OutlinePanel {
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext) {
- let old_active = self.active;
- self.active = active;
- if active && old_active != active {
- if let Some(active_editor) = self
- .active_item
- .as_ref()
- .and_then(|item| item.active_editor.upgrade())
- {
- if self.active_item.as_ref().map(|item| item.item_id)
- == Some(active_editor.item_id())
- {
- let new_selected_entry = self.location_for_editor_selection(&active_editor, cx);
- self.update_fs_entries(
- &active_editor,
- HashSet::default(),
- new_selected_entry,
- None,
- cx,
- )
- } else {
- self.replace_visible_entries(active_editor, cx);
- }
- }
- }
- self.serialize(cx);
+ cx.spawn(|outline_panel, mut cx| async move {
+ outline_panel
+ .update(&mut cx, |outline_panel, cx| {
+ let old_active = outline_panel.active;
+ outline_panel.active = active;
+ if active && old_active != active {
+ if let Some(active_editor) =
+ workspace_active_editor(outline_panel.workspace.read(cx), cx)
+ {
+ if outline_panel.should_replace_active_editor(&active_editor) {
+ outline_panel.replace_active_editor(active_editor, cx);
+ } else {
+ outline_panel.update_fs_entries(
+ &active_editor,
+ HashSet::default(),
+ None,
+ cx,
+ )
+ }
+ } else if !outline_panel.pinned {
+ outline_panel.clear_previous(cx);
+ }
+ }
+ outline_panel.serialize(cx);
+ })
+ .ok();
+ })
+ .detach()
}
}
@@ -2867,6 +3492,8 @@ impl Render for OutlinePanel {
fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement {
let project = self.project.read(cx);
let query = self.query(cx);
+ let pinned = self.pinned;
+
let outline_panel = v_flex()
.id("outline-panel")
.size_full()
@@ -2885,6 +3512,7 @@ impl Render for OutlinePanel {
.on_action(cx.listener(Self::collapse_all_entries))
.on_action(cx.listener(Self::copy_path))
.on_action(cx.listener(Self::copy_relative_path))
+ .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_or_ssh(), |el| {
@@ -2894,20 +3522,16 @@ impl Render for OutlinePanel {
.on_mouse_down(
MouseButton::Right,
cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
- if let Some(entry) = outline_panel.selected_entry.clone() {
- outline_panel.deploy_context_menu(event.position, entry.to_ref_entry(), cx)
+ if let Some(entry) = outline_panel.selected_entry().cloned() {
+ outline_panel.deploy_context_menu(event.position, entry, cx)
} else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
- outline_panel.deploy_context_menu(
- event.position,
- EntryRef::Entry(&entry),
- cx,
- )
+ outline_panel.deploy_context_menu(event.position, PanelEntry::Fs(entry), cx)
}
}),
)
.track_focus(&self.focus_handle);
- if self.cached_entries_with_depth.is_empty() {
+ if self.cached_entries.is_empty() {
let header = if self.updating_fs_entries {
"Loading outlines"
} else if query.is_some() {
@@ -2945,57 +3569,89 @@ impl Render for OutlinePanel {
),
)
} else {
- outline_panel.child({
- let items_len = self.cached_entries_with_depth.len();
- uniform_list(cx.view().clone(), "entries", items_len, {
- move |outline_panel, range, cx| {
- let entries = outline_panel.cached_entries_with_depth.get(range);
- entries
- .map(|entries| entries.to_vec())
- .unwrap_or_default()
- .into_iter()
- .filter_map(|cached_entry| match cached_entry.entry {
- EntryOwned::Entry(entry) => Some(outline_panel.render_entry(
- &entry,
- cached_entry.depth,
- cached_entry.string_match.as_ref(),
- cx,
- )),
- EntryOwned::FoldedDirs(worktree_id, entries) => {
- Some(outline_panel.render_folded_dirs(
- worktree_id,
- &entries,
+ outline_panel
+ .when_some(self.search.as_ref(), |outline_panel, (_, search_query)| {
+ outline_panel.child(
+ div()
+ .mx_2()
+ .child(
+ Label::new(format!("Searching: '{search_query}'"))
+ .color(Color::Muted),
+ )
+ .child(horizontal_separator(cx)),
+ )
+ })
+ .child({
+ let items_len = self.cached_entries.len();
+ 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,
- ))
- }
- EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => {
- outline_panel.render_excerpt(
+ )),
+ 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,
- )
- }
- EntryOwned::Outline(buffer_id, excerpt_id, outline) => {
- Some(outline_panel.render_outline(
+ ),
+ 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,
- ))
- }
- })
- .collect()
- }
+ )),
+ PanelEntry::Search(SearchEntry {
+ match_range,
+ render_data,
+ kind,
+ same_line_matches: _,
+ }) => render_data.as_ref().and_then(|search_data| {
+ let search_data = search_data.get()?;
+ Some(outline_panel.render_search_match(
+ &match_range,
+ search_data,
+ kind,
+ cached_entry.depth,
+ cached_entry.string_match.as_ref(),
+ cx,
+ ))
+ }),
+ })
+ .collect()
+ }
+ })
+ .size_full()
+ .track_scroll(self.scroll_handle.clone())
})
- .size_full()
- .track_scroll(self.scroll_handle.clone())
- })
}
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred(
@@ -3007,9 +3663,27 @@ impl Render for OutlinePanel {
.with_priority(1)
}))
.child(
- v_flex()
- .child(div().mx_2().border_primary(cx).border_t_1())
- .child(v_flex().p_2().child(self.filter_editor.clone())),
+ v_flex().child(horizontal_separator(cx)).child(
+ h_flex().p_2().child(self.filter_editor.clone()).child(
+ div().border_1().child(
+ IconButton::new(
+ "outline-panel-menu",
+ if pinned {
+ IconName::Unpin
+ } else {
+ IconName::Pin
+ },
+ )
+ .tooltip(move |cx| {
+ Tooltip::text(if pinned { "Unpin" } else { "Pin active editor" }, cx)
+ })
+ .shape(IconButtonShape::Square)
+ .on_click(cx.listener(|outline_panel, _, cx| {
+ outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx);
+ })),
+ ),
+ ),
+ ),
)
}
}
@@ -3030,7 +3704,6 @@ fn subscribe_for_editor_events(
outline_panel.update_fs_entries(
&editor,
excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
- None,
debounce,
cx,
);
@@ -3043,15 +3716,15 @@ fn subscribe_for_editor_events(
break;
}
}
- outline_panel.update_fs_entries(&editor, HashSet::default(), None, debounce, cx);
+ outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx);
}
EditorEvent::ExcerptsExpanded { ids } => {
outline_panel.invalidate_outlines(ids);
- outline_panel.fetch_outdated_outlines(cx)
+ outline_panel.update_non_fs_items(cx);
}
EditorEvent::ExcerptsEdited { ids } => {
outline_panel.invalidate_outlines(ids);
- outline_panel.fetch_outdated_outlines(cx);
+ outline_panel.update_non_fs_items(cx);
}
EditorEvent::Reparsed(buffer_id) => {
if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
@@ -3059,7 +3732,7 @@ fn subscribe_for_editor_events(
excerpt.invalidate_outlines();
}
}
- outline_panel.fetch_outdated_outlines(cx);
+ outline_panel.update_non_fs_items(cx);
}
_ => {}
},
@@ -3073,3 +3746,7 @@ fn empty_icon() -> AnyElement {
.flex_none()
.into_any_element()
}
+
+fn horizontal_separator(cx: &mut WindowContext) -> Div {
+ div().mx_2().border_primary(cx).border_t_1()
+}
diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs
index cfd4ee1d9c..82e6254981 100644
--- a/crates/search/src/buffer_search.rs
+++ b/crates/search/src/buffer_search.rs
@@ -988,7 +988,11 @@ impl BufferSearchBar {
.searchable_items_with_matches
.get(&active_searchable_item.downgrade())
.unwrap();
- active_searchable_item.update_matches(matches, cx);
+ if matches.is_empty() {
+ active_searchable_item.clear_matches(cx);
+ } else {
+ active_searchable_item.update_matches(matches, cx);
+ }
let _ = done_tx.send(());
}
cx.notify();
diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs
index d0c24a6f8a..0dcc87a5e1 100644
--- a/crates/search/src/project_search.rs
+++ b/crates/search/src/project_search.rs
@@ -503,6 +503,10 @@ impl Item for ProjectSearchView {
}
impl ProjectSearchView {
+ pub fn get_matches(&self, cx: &AppContext) -> Vec> {
+ self.model.read(cx).match_ranges.clone()
+ }
+
fn toggle_filters(&mut self, cx: &mut ViewContext) {
self.filters_enabled = !self.filters_enabled;
ActiveSettings::update_global(cx, |settings, cx| {
@@ -836,6 +840,10 @@ impl ProjectSearchView {
}
}
+ pub fn search_query_text(&self, cx: &WindowContext) -> String {
+ self.query_editor.read(cx).text(cx)
+ }
+
fn build_search_query(&mut self, cx: &mut ViewContext) -> Option {
// Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
let text = self.query_editor.read(cx).text(cx);
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index 74c565988d..0d01a36e4e 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -212,6 +212,7 @@ pub enum IconName {
PageUp,
Pencil,
Person,
+ Pin,
Play,
Plus,
Public,
@@ -263,6 +264,7 @@ pub enum IconName {
Trash,
TriangleRight,
Undo,
+ Unpin,
Update,
WholeWord,
XCircle,
@@ -381,6 +383,7 @@ impl IconName {
IconName::PageUp => "icons/page_up.svg",
IconName::Pencil => "icons/pencil.svg",
IconName::Person => "icons/person.svg",
+ IconName::Pin => "icons/pin.svg",
IconName::Play => "icons/play.svg",
IconName::Plus => "icons/plus.svg",
IconName::Public => "icons/public.svg",
@@ -431,6 +434,7 @@ impl IconName {
IconName::TextSelect => "icons/text_select.svg",
IconName::Trash => "icons/trash.svg",
IconName::TriangleRight => "icons/triangle_right.svg",
+ IconName::Unpin => "icons/unpin.svg",
IconName::Update => "icons/update.svg",
IconName::Undo => "icons/undo.svg",
IconName::WholeWord => "icons/word_search.svg",
diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs
index b30b986c50..d8a690bee4 100644
--- a/crates/workspace/src/searchable.rs
+++ b/crates/workspace/src/searchable.rs
@@ -65,6 +65,9 @@ pub trait SearchableItem: Item + EventEmitter {
fn toggle_filtered_search_ranges(&mut self, _enabled: bool, _cx: &mut ViewContext) {}
+ fn get_matches(&self, _: &mut WindowContext) -> Vec {
+ Vec::new()
+ }
fn clear_matches(&mut self, cx: &mut ViewContext);
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext);
fn query_suggestion(&mut self, cx: &mut ViewContext) -> String;