Skip to repository content2301 lines · 84.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:54:36.559Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
file_finder.rs
1#[cfg(test)]
2mod file_finder_tests;
3#[cfg(test)]
4mod multi_select_tests;
5
6use futures::future::join_all;
7pub use open_path_prompt::OpenPathDelegate;
8
9use channel::ChannelStore;
10use client::ChannelId;
11use collections::HashMap;
12use editor::Editor;
13use file_icons::FileIcons;
14use fuzzy::{StringMatch, StringMatchCandidate};
15use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
16use gpui::{
17 Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
18 KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, TaskExt,
19 WeakEntity, Window, actions, rems,
20};
21use language::{BufferSnapshot, Point};
22use open_path_prompt::{
23 OpenPathPrompt,
24 file_finder_settings::{FileFinderSettings, FileFinderWidth},
25};
26use picker::{Picker, PickerDelegate};
27use project::{
28 PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore,
29};
30use project_panel::project_panel_settings::ProjectPanelSettings;
31use settings::Settings;
32use std::{
33 borrow::Cow,
34 cmp, mem,
35 ops::{Range, RangeInclusive},
36 path::{Component, Path, PathBuf},
37 sync::{
38 Arc,
39 atomic::{self, AtomicBool},
40 },
41 time::Duration,
42};
43use ui::{Checkbox, HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*};
44use util::{
45 ResultExt, maybe,
46 paths::{PathStyle, PathWithPosition},
47 post_inc,
48 rel_path::RelPath,
49};
50use workspace::{
51 ModalView, OpenChannelNotesById, OpenOptions, OpenVisible, SplitDirection, Workspace,
52 item::PreviewTabsSettings, notifications::NotifyResultExt, pane,
53};
54use zed_actions::search::ToggleIncludeIgnored;
55
56actions!(
57 file_finder,
58 [
59 /// Selects the previous item in the file finder.
60 SelectPrevious,
61 /// Opens the selected file in the editor without dismissing the file finder,
62 /// so additional files can be opened in sequence.
63 OpenWithoutDismiss
64 ]
65);
66
67impl ModalView for FileFinder {}
68
69pub struct FileFinder {
70 picker: Entity<Picker<FileFinderDelegate>>,
71 picker_focus_handle: FocusHandle,
72 init_modifiers: Option<Modifiers>,
73}
74
75pub fn init(cx: &mut App) {
76 cx.observe_new(FileFinder::register).detach();
77 cx.observe_new(OpenPathPrompt::register).detach();
78 cx.observe_new(OpenPathPrompt::register_new_path).detach();
79}
80
81impl FileFinder {
82 fn register(
83 workspace: &mut Workspace,
84 _window: Option<&mut Window>,
85 _: &mut Context<Workspace>,
86 ) {
87 workspace.register_action(
88 |workspace, action: &workspace::ToggleFileFinder, window, cx| {
89 let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
90 Self::open(
91 workspace,
92 action.separate_history,
93 action.include_ignored,
94 window,
95 cx,
96 )
97 .detach();
98 return;
99 };
100
101 file_finder.update(cx, |file_finder, cx| {
102 file_finder.init_modifiers = Some(window.modifiers());
103 file_finder.picker.update(cx, |picker, cx| {
104 picker.cycle_selection(window, cx);
105 });
106 });
107 },
108 );
109 }
110
111 fn open(
112 workspace: &mut Workspace,
113 separate_history: bool,
114 include_ignored: Option<bool>,
115 window: &mut Window,
116 cx: &mut Context<Workspace>,
117 ) -> Task<()> {
118 let project = workspace.project().read(cx);
119 let fs = project.fs();
120
121 let currently_opened_path = workspace.active_item(cx).and_then(|item| {
122 let project_path = item.project_path(cx)?;
123 let abs_path = project
124 .worktree_for_id(project_path.worktree_id, cx)?
125 .read(cx)
126 .absolutize(&project_path.path);
127 Some(FoundPath::new(project_path, abs_path))
128 });
129
130 let history_items = workspace
131 .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
132 .into_iter()
133 .filter_map(|(project_path, abs_path)| {
134 if project.entry_for_path(&project_path, cx).is_some() {
135 return Some(Task::ready(Some(FoundPath::new(project_path, abs_path?))));
136 }
137 let abs_path = abs_path?;
138 if project.is_local() {
139 let fs = fs.clone();
140 Some(cx.background_spawn(async move {
141 if fs.is_file(&abs_path).await {
142 Some(FoundPath::new(project_path, abs_path))
143 } else {
144 None
145 }
146 }))
147 } else {
148 Some(Task::ready(Some(FoundPath::new(project_path, abs_path))))
149 }
150 })
151 .collect::<Vec<_>>();
152 cx.spawn_in(window, async move |workspace, cx| {
153 let history_items = join_all(history_items).await.into_iter().flatten();
154
155 workspace
156 .update_in(cx, |workspace, window, cx| {
157 let project = workspace.project().clone();
158 let weak_workspace = cx.entity().downgrade();
159 workspace.toggle_modal(window, cx, |window, cx| {
160 let delegate = FileFinderDelegate::new(
161 cx.entity().downgrade(),
162 weak_workspace,
163 project,
164 currently_opened_path,
165 history_items.collect(),
166 separate_history,
167 include_ignored,
168 window,
169 cx,
170 );
171
172 FileFinder::new(delegate, window, cx)
173 });
174 })
175 .ok();
176 })
177 }
178
179 fn new(delegate: FileFinderDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
180 let modal_max_width_setting = FileFinderSettings::get_global(cx).modal_max_width;
181
182 let project = delegate.project.clone();
183 let modal_max_width = Self::modal_max_width(modal_max_width_setting, window);
184 let preview = picker_preview::editor_preview(project, window, cx);
185 let picker = cx.new(|cx| {
186 Picker::uniform_list_with_preview(delegate, preview, window, cx)
187 .initial_width(Rems::from_pixels(modal_max_width, window))
188 });
189 let picker_focus_handle = picker.focus_handle(cx);
190 picker.update(cx, |picker, _| {
191 picker.delegate.focus_handle = picker_focus_handle.clone();
192 });
193 Self {
194 picker,
195 picker_focus_handle,
196 init_modifiers: window.modifiers().modified().then_some(window.modifiers()),
197 }
198 }
199
200 fn handle_modifiers_changed(
201 &mut self,
202 event: &ModifiersChangedEvent,
203 window: &mut Window,
204 cx: &mut Context<Self>,
205 ) {
206 let Some(init_modifiers) = self.init_modifiers.take() else {
207 return;
208 };
209 if self.picker.read(cx).delegate.has_changed_selected_index
210 && (!event.modified() || !init_modifiers.is_subset_of(event))
211 {
212 self.init_modifiers = None;
213 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
214 }
215 }
216
217 fn handle_select_prev(
218 &mut self,
219 _: &SelectPrevious,
220 window: &mut Window,
221 cx: &mut Context<Self>,
222 ) {
223 self.init_modifiers = Some(window.modifiers());
224 window.dispatch_action(Box::new(menu::SelectPrevious), cx);
225 }
226
227 fn handle_toggle_ignored(
228 &mut self,
229 _: &ToggleIncludeIgnored,
230 window: &mut Window,
231 cx: &mut Context<Self>,
232 ) {
233 self.picker.update(cx, |picker, cx| {
234 picker.delegate.include_ignored = match picker.delegate.include_ignored {
235 Some(true) => FileFinderSettings::get_global(cx)
236 .include_ignored
237 .map(|_| false),
238 Some(false) => Some(true),
239 None => Some(true),
240 };
241 picker.delegate.include_ignored_refresh =
242 picker.delegate.update_matches(picker.query(cx), window, cx);
243 });
244 }
245
246 fn go_to_file_split_left(
247 &mut self,
248 _: &pane::SplitLeft,
249 window: &mut Window,
250 cx: &mut Context<Self>,
251 ) {
252 self.go_to_file_split_inner(SplitDirection::Left, window, cx)
253 }
254
255 fn go_to_file_split_right(
256 &mut self,
257 _: &pane::SplitRight,
258 window: &mut Window,
259 cx: &mut Context<Self>,
260 ) {
261 self.go_to_file_split_inner(SplitDirection::Right, window, cx)
262 }
263
264 fn go_to_file_split_up(
265 &mut self,
266 _: &pane::SplitUp,
267 window: &mut Window,
268 cx: &mut Context<Self>,
269 ) {
270 self.go_to_file_split_inner(SplitDirection::Up, window, cx)
271 }
272
273 fn go_to_file_split_down(
274 &mut self,
275 _: &pane::SplitDown,
276 window: &mut Window,
277 cx: &mut Context<Self>,
278 ) {
279 self.go_to_file_split_inner(SplitDirection::Down, window, cx)
280 }
281
282 fn go_to_file_split_inner(
283 &mut self,
284 split_direction: SplitDirection,
285 window: &mut Window,
286 cx: &mut Context<Self>,
287 ) {
288 self.picker.update(cx, |picker, cx| {
289 let delegate = &mut picker.delegate;
290 if !delegate.selected_matches.is_empty() {
291 delegate.open_selected_in_one_split(split_direction, window, cx);
292 return;
293 }
294 if let Some(workspace) = delegate.workspace.upgrade()
295 && let Some(m) = delegate.matches.get(delegate.selected_index())
296 {
297 let path = match m {
298 Match::History { path, .. } => {
299 let worktree_id = path.project.worktree_id;
300 ProjectPath {
301 worktree_id,
302 path: Arc::clone(&path.project.path),
303 }
304 }
305 Match::Search(m) => project_path_for_search_match(&delegate.project, &m.0, cx),
306 Match::CreateNew(p) => p.clone(),
307 Match::Channel { .. } => return,
308 };
309 let open_task = workspace.update(cx, move |workspace, cx| {
310 workspace.split_path_preview(path, false, Some(split_direction), window, cx)
311 });
312 open_task.detach_and_log_err(cx);
313 }
314 })
315 }
316
317 fn open_without_dismiss(
318 &mut self,
319 _: &OpenWithoutDismiss,
320 window: &mut Window,
321 cx: &mut Context<Self>,
322 ) {
323 self.picker.update(cx, |picker, cx| {
324 picker.delegate.confirm_without_dismiss(window, cx);
325 });
326 }
327
328 pub fn modal_max_width(width_setting: FileFinderWidth, window: &mut Window) -> Pixels {
329 let window_width = window.viewport_size().width;
330 let small_width = rems(34.).to_pixels(window.rem_size());
331
332 match width_setting {
333 FileFinderWidth::Small => small_width,
334 FileFinderWidth::Full => window_width,
335 FileFinderWidth::XLarge => (window_width - px(512.)).max(small_width),
336 FileFinderWidth::Large => (window_width - px(768.)).max(small_width),
337 FileFinderWidth::Medium => (window_width - px(1024.)).max(small_width),
338 }
339 }
340}
341
342impl EventEmitter<DismissEvent> for FileFinder {}
343
344impl Focusable for FileFinder {
345 fn focus_handle(&self, _: &App) -> FocusHandle {
346 self.picker_focus_handle.clone()
347 }
348}
349
350impl Render for FileFinder {
351 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
352 let key_context = self.picker.read(cx).delegate.key_context(window, cx);
353
354 v_flex()
355 .key_context(key_context)
356 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
357 .on_action(cx.listener(Self::handle_select_prev))
358 .on_action(cx.listener(Self::handle_toggle_ignored))
359 .on_action(cx.listener(Self::go_to_file_split_left))
360 .on_action(cx.listener(Self::go_to_file_split_right))
361 .on_action(cx.listener(Self::go_to_file_split_up))
362 .on_action(cx.listener(Self::go_to_file_split_down))
363 .on_action(cx.listener(Self::open_without_dismiss))
364 .child(self.picker.clone())
365 }
366}
367
368pub struct FileFinderDelegate {
369 file_finder: WeakEntity<FileFinder>,
370 workspace: WeakEntity<Workspace>,
371 project: Entity<Project>,
372 channel_store: Option<Entity<ChannelStore>>,
373 search_count: usize,
374 latest_search_id: usize,
375 latest_search_did_cancel: bool,
376 latest_search_query: Option<FileSearchQuery>,
377 currently_opened_path: Option<FoundPath>,
378 matches: Matches,
379 selected_matches: Vec<SelectedMatch>,
380 selected_index: usize,
381 has_changed_selected_index: bool,
382 cancel_flag: Arc<AtomicBool>,
383 search_in_flight: Arc<AtomicBool>,
384 history_items: Vec<FoundPath>,
385 separate_history: bool,
386 first_update: bool,
387 focus_handle: FocusHandle,
388 include_ignored: Option<bool>,
389 include_ignored_refresh: Task<()>,
390 debounce_next_refresh: bool,
391}
392
393/// Use a custom ordering for file finder: the regular one
394/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
395/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
396///
397/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
398/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
399/// as the files are shown in the project panel lists.
400#[derive(Debug, Clone, PartialEq, Eq)]
401struct ProjectPanelOrdMatch(PathMatch);
402
403impl Ord for ProjectPanelOrdMatch {
404 fn cmp(&self, other: &Self) -> cmp::Ordering {
405 self.0
406 .score
407 .partial_cmp(&other.0.score)
408 .unwrap_or(cmp::Ordering::Equal)
409 .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
410 .then_with(|| {
411 other
412 .0
413 .distance_to_relative_ancestor
414 .cmp(&self.0.distance_to_relative_ancestor)
415 })
416 .then_with(|| self.0.path.cmp(&other.0.path).reverse())
417 }
418}
419
420impl PartialOrd for ProjectPanelOrdMatch {
421 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
422 Some(self.cmp(other))
423 }
424}
425
426#[derive(Debug, Default)]
427struct Matches {
428 separate_history: bool,
429 matches: Vec<Match>,
430}
431
432#[derive(Debug, Clone)]
433enum Match {
434 History {
435 path: FoundPath,
436 panel_match: Option<ProjectPanelOrdMatch>,
437 },
438 Search(ProjectPanelOrdMatch),
439 Channel {
440 channel_id: ChannelId,
441 channel_name: SharedString,
442 string_match: StringMatch,
443 },
444 CreateNew(ProjectPath),
445}
446
447impl Match {
448 fn relative_path(&self) -> Option<&Arc<RelPath>> {
449 match self {
450 Match::History { path, .. } => Some(&path.project.path),
451 Match::Search(panel_match) => Some(&panel_match.0.path),
452 Match::Channel { .. } | Match::CreateNew(_) => None,
453 }
454 }
455
456 fn abs_path(&self, project: &Entity<Project>, cx: &App) -> Option<PathBuf> {
457 match self {
458 Match::History { path, .. } => Some(path.absolute.clone()),
459 Match::Search(ProjectPanelOrdMatch(path_match)) => Some(
460 project
461 .read(cx)
462 .worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
463 .read(cx)
464 .absolutize(&path_match.path),
465 ),
466 Match::Channel { .. } | Match::CreateNew(_) => None,
467 }
468 }
469
470 fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {
471 match self {
472 Match::History { panel_match, .. } => panel_match.as_ref(),
473 Match::Search(panel_match) => Some(panel_match),
474 Match::Channel { .. } | Match::CreateNew(_) => None,
475 }
476 }
477}
478
479/// Wrapper with Eq comparing the worktree-qualified path of file matches
480#[derive(Clone)]
481struct SelectedMatch(pub Match);
482
483impl SelectedMatch {
484 fn new(m: Match) -> Option<Self> {
485 match m {
486 Match::History { .. } | Match::Search(_) => Some(Self(m)),
487 Match::Channel { .. } | Match::CreateNew(_) => None,
488 }
489 }
490}
491
492impl PartialEq for SelectedMatch {
493 fn eq(&self, other: &Self) -> bool {
494 *self == other.0
495 }
496}
497
498impl Eq for SelectedMatch {}
499
500impl PartialEq<Match> for SelectedMatch {
501 fn eq(&self, other: &Match) -> bool {
502 match (&self.0, other) {
503 (Match::History { path: a, .. }, Match::History { path: b, .. }) => {
504 a.project == b.project
505 }
506 (Match::Search(a), Match::Search(b)) => {
507 a.0.worktree_id == b.0.worktree_id && a.0.path == b.0.path
508 }
509 (Match::History { path: h, .. }, Match::Search(s))
510 | (Match::Search(s), Match::History { path: h, .. }) => {
511 h.project.worktree_id.to_usize() == s.0.worktree_id && h.project.path == s.0.path
512 }
513 _ => false,
514 }
515 }
516}
517
518impl Matches {
519 fn len(&self) -> usize {
520 self.matches.len()
521 }
522
523 fn get(&self, index: usize) -> Option<&Match> {
524 self.matches.get(index)
525 }
526
527 fn position(
528 &self,
529 entry: &Match,
530 currently_opened: Option<&FoundPath>,
531 ) -> Result<usize, usize> {
532 if let Match::History {
533 path,
534 panel_match: None,
535 } = entry
536 {
537 // Slow case: linear search by path. Should not happen actually,
538 // since we call `position` only if matches set changed, but the query has not changed.
539 // And History entries do not have panel_match if query is empty, so there's no
540 // reason for the matches set to change.
541 self.matches
542 .iter()
543 .position(|m| match m.relative_path() {
544 Some(p) => path.project.path == *p,
545 None => false,
546 })
547 .ok_or(0)
548 } else {
549 self.matches.binary_search_by(|m| {
550 // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b.
551 // And we want the better entries go first.
552 Self::cmp_matches(self.separate_history, currently_opened, m, entry).reverse()
553 })
554 }
555 }
556
557 fn push_new_matches<'a>(
558 &'a mut self,
559 worktree_store: Entity<WorktreeStore>,
560 cx: &'a App,
561 history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,
562 currently_opened: Option<&'a FoundPath>,
563 query: Option<&FileSearchQuery>,
564 new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
565 extend_old_matches: bool,
566 path_style: PathStyle,
567 ) {
568 let Some(query) = query else {
569 // assuming that if there's no query, then there's no search matches.
570 self.matches.clear();
571 let path_to_entry = |found_path: &FoundPath| Match::History {
572 path: found_path.clone(),
573 panel_match: None,
574 };
575
576 self.matches
577 .extend(history_items.into_iter().map(path_to_entry));
578 return;
579 };
580
581 let worktree_name_by_id = worktree_names_for_history_matching(&worktree_store, cx);
582 let new_history_matches = matching_history_items(
583 history_items,
584 currently_opened,
585 worktree_name_by_id,
586 query,
587 path_style,
588 );
589 let new_search_matches: Vec<Match> = new_search_matches
590 .filter(|path_match| {
591 !new_history_matches.contains_key(&ProjectPath {
592 path: path_match.0.path.clone(),
593 worktree_id: WorktreeId::from_usize(path_match.0.worktree_id),
594 })
595 })
596 .map(Match::Search)
597 .collect();
598
599 if extend_old_matches {
600 // since we take history matches instead of new search matches
601 // and history matches has not changed(since the query has not changed and we do not extend old matches otherwise),
602 // old matches can't contain paths present in history_matches as well.
603 self.matches.retain(|m| matches!(m, Match::Search(_)));
604 } else {
605 self.matches.clear();
606 }
607
608 // At this point we have an unsorted set of new history matches, an unsorted set of new search matches
609 // and a sorted set of old search matches.
610 // It is possible that the new search matches' paths contain some of the old search matches' paths.
611 // History matches' paths are unique, since store in a HashMap by path.
612 // We build a sorted Vec<Match>, eliminating duplicate search matches.
613 // Search matches with the same paths should have equal `ProjectPanelOrdMatch`, so we should
614 // not have any duplicates after building the final list.
615 for new_match in new_history_matches.into_values().chain(new_search_matches) {
616 match self.position(&new_match, currently_opened) {
617 Ok(_duplicate) => continue,
618 Err(i) => {
619 self.matches.insert(i, new_match);
620 if self.matches.len() == 100 {
621 break;
622 }
623 }
624 }
625 }
626 }
627
628 /// If a < b, then a is a worse match, aligning with the `ProjectPanelOrdMatch` ordering.
629 fn cmp_matches(
630 separate_history: bool,
631 currently_opened: Option<&FoundPath>,
632 a: &Match,
633 b: &Match,
634 ) -> cmp::Ordering {
635 // Handle CreateNew variant - always put it at the end
636 match (a, b) {
637 (Match::CreateNew(_), _) => return cmp::Ordering::Less,
638 (_, Match::CreateNew(_)) => return cmp::Ordering::Greater,
639 _ => {}
640 }
641
642 match (&a, &b) {
643 // bubble currently opened files to the top
644 (Match::History { path, .. }, _) if Some(path) == currently_opened => {
645 return cmp::Ordering::Greater;
646 }
647 (_, Match::History { path, .. }) if Some(path) == currently_opened => {
648 return cmp::Ordering::Less;
649 }
650
651 _ => {}
652 }
653
654 if separate_history {
655 match (a, b) {
656 (Match::History { .. }, Match::Search(_)) => return cmp::Ordering::Greater,
657 (Match::Search(_), Match::History { .. }) => return cmp::Ordering::Less,
658
659 _ => {}
660 }
661 }
662
663 // For file-vs-file matches, use the existing detailed comparison.
664 if let (Some(a_panel), Some(b_panel)) = (a.panel_match(), b.panel_match()) {
665 return a_panel.cmp(b_panel);
666 }
667
668 let a_score = Self::match_score(a);
669 let b_score = Self::match_score(b);
670 // When at least one side is a channel, compare by raw score.
671 a_score
672 .partial_cmp(&b_score)
673 .unwrap_or(cmp::Ordering::Equal)
674 }
675
676 fn match_score(m: &Match) -> f64 {
677 match m {
678 Match::History { panel_match, .. } => panel_match.as_ref().map_or(0.0, |pm| pm.0.score),
679 Match::Search(pm) => pm.0.score,
680 Match::Channel { string_match, .. } => string_match.score,
681 Match::CreateNew(_) => 0.0,
682 }
683 }
684}
685
686fn matching_history_items<'a>(
687 history_items: impl IntoIterator<Item = &'a FoundPath>,
688 currently_opened: Option<&'a FoundPath>,
689 worktree_name_by_id: Option<HashMap<WorktreeId, Arc<RelPath>>>,
690 query: &FileSearchQuery,
691 path_style: PathStyle,
692) -> HashMap<ProjectPath, Match> {
693 let mut candidates_paths = HashMap::default();
694
695 let history_items_by_worktrees = history_items
696 .into_iter()
697 .chain(currently_opened)
698 .map(|found_path| {
699 // Only match history items names, otherwise their paths may match too many queries,
700 // producing false positives. E.g. `foo` would match both `something/foo/bar.rs` and
701 // `something/foo/foo.rs` and if the former is a history item, it would be shown first
702 // always, despite the latter being a better match.
703 let candidate = PathMatchCandidate::new(
704 &found_path.project.path,
705 false,
706 worktree_name_by_id
707 .as_ref()
708 .and_then(|m| m.get(&found_path.project.worktree_id))
709 .map(|prefix| prefix.as_ref()),
710 );
711 candidates_paths.insert(&found_path.project, found_path);
712 (found_path.project.worktree_id, candidate)
713 })
714 .fold(
715 HashMap::default(),
716 |mut candidates, (worktree_id, new_candidate)| {
717 candidates
718 .entry(worktree_id)
719 .or_insert_with(Vec::new)
720 .push(new_candidate);
721 candidates
722 },
723 );
724 let mut matching_history_paths = HashMap::default();
725 for (worktree, candidates) in history_items_by_worktrees {
726 let max_results = candidates.len() + 1;
727 let worktree_root_name = worktree_name_by_id
728 .as_ref()
729 .and_then(|w| w.get(&worktree).cloned());
730
731 matching_history_paths.extend(
732 fuzzy_nucleo::match_fixed_path_set(
733 candidates,
734 worktree.to_usize(),
735 worktree_root_name,
736 query.path_query(),
737 fuzzy_nucleo::Case::Ignore,
738 max_results,
739 path_style,
740 )
741 .into_iter()
742 // filter matches where at least one matched position is in filename portion, to prevent directory matches, nucleo scores them higher as history items are matched against their full path
743 .filter(|path_match| {
744 if let Some(filename) = path_match.path.file_name() {
745 let filename_start = path_match.path.as_unix_str().len() - filename.len();
746 path_match
747 .positions
748 .iter()
749 .any(|&pos| pos >= filename_start)
750 } else {
751 true
752 }
753 })
754 .filter_map(|path_match| {
755 let worktree_id = WorktreeId::from_usize(path_match.worktree_id);
756 let project_path = ProjectPath {
757 worktree_id,
758 path: Arc::clone(&path_match.path),
759 };
760 // For single-file worktrees, fuzzy_nucleo moves the worktree root name
761 // into path_match.path (root_is_file handling), so the stored key of ""
762 // won't match. Fall back to an empty-path lookup for those entries.
763 let (_, found_path) =
764 candidates_paths.remove_entry(&project_path).or_else(|| {
765 candidates_paths.remove_entry(&ProjectPath {
766 worktree_id,
767 path: RelPath::empty_arc(),
768 })
769 })?;
770 // Key with path_match.path so the deduplication check in push_new_matches
771 // (which also uses path_match.path) correctly suppresses the search duplicate.
772 Some((
773 project_path,
774 Match::History {
775 path: found_path.clone(),
776 panel_match: Some(ProjectPanelOrdMatch(path_match)),
777 },
778 ))
779 }),
780 );
781 }
782 matching_history_paths
783}
784
785fn should_hide_root_in_entry_path(worktree_store: &Entity<WorktreeStore>, cx: &App) -> bool {
786 let multiple_worktrees = worktree_store
787 .read(cx)
788 .visible_worktrees(cx)
789 .filter(|worktree| !worktree.read(cx).is_single_file())
790 .nth(1)
791 .is_some();
792 ProjectPanelSettings::get_global(cx).hide_root && !multiple_worktrees
793}
794
795fn worktree_names_for_history_matching(
796 worktree_store: &Entity<WorktreeStore>,
797 cx: &App,
798) -> Option<HashMap<WorktreeId, Arc<RelPath>>> {
799 let hide_root = should_hide_root_in_entry_path(worktree_store, cx);
800 let names = worktree_store
801 .read(cx)
802 .worktrees()
803 .filter_map(|worktree| {
804 let worktree = worktree.read(cx);
805 if hide_root && !worktree.is_single_file() {
806 None
807 } else {
808 Some((worktree.id(), worktree.root_name().into()))
809 }
810 })
811 .collect::<HashMap<_, _>>();
812
813 if names.is_empty() { None } else { Some(names) }
814}
815
816fn project_path_for_search_match(
817 project: &Entity<Project>,
818 path_match: &PathMatch,
819 cx: &App,
820) -> ProjectPath {
821 let worktree_id = WorktreeId::from_usize(path_match.worktree_id);
822 let path = if project
823 .read(cx)
824 .worktree_for_id(worktree_id, cx)
825 .is_some_and(|worktree| worktree.read(cx).is_single_file())
826 {
827 RelPath::empty_arc()
828 } else {
829 path_match.path.clone()
830 };
831
832 ProjectPath { worktree_id, path }
833}
834
835#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
836struct FoundPath {
837 project: ProjectPath,
838 absolute: PathBuf,
839}
840
841impl FoundPath {
842 fn new(project: ProjectPath, absolute: PathBuf) -> Self {
843 Self { project, absolute }
844 }
845}
846
847const MAX_RECENT_SELECTIONS: usize = 20;
848const SEARCH_DEBOUNCE: Duration = Duration::from_millis(100);
849const WORKTREE_UPDATE_REFRESH_DEBOUNCE: Duration = Duration::from_millis(200);
850
851pub enum Event {
852 Selected(ProjectPath),
853 Dismissed,
854}
855
856#[derive(Debug, Clone)]
857struct FileSearchQuery {
858 raw_query: String,
859 file_query_end: Option<usize>,
860 path_position: PathWithPosition,
861 line_range: Option<RangeInclusive<u32>>,
862}
863
864impl FileSearchQuery {
865 fn path_query(&self) -> &str {
866 match self.file_query_end {
867 Some(file_path_end) => &self.raw_query[..file_path_end],
868 None => &self.raw_query,
869 }
870 }
871
872 fn selection_range(&self, buffer_snapshot: &BufferSnapshot) -> Option<Range<Point>> {
873 if let Some(line_range) = self.line_range.clone() {
874 return Some(buffer_range_for_line_range(buffer_snapshot, line_range));
875 }
876
877 let row = self.path_position.row.map(|row| row.saturating_sub(1))?;
878 let col = self.path_position.column.unwrap_or(0).saturating_sub(1);
879 let point = buffer_snapshot.point_from_external_input(row, col);
880 Some(point..point)
881 }
882}
883
884fn parse_file_search_query(raw_query: &str) -> FileSearchQuery {
885 let raw_query = raw_query.trim().trim_end_matches(':').to_owned();
886
887 if let Some((path_query, start_line, end_line)) = parse_line_range_query(&raw_query) {
888 let path_query = path_query.to_owned();
889 return FileSearchQuery {
890 raw_query,
891 file_query_end: Some(path_query.len()),
892 path_position: PathWithPosition {
893 path: PathBuf::from(&path_query),
894 row: Some(start_line),
895 column: None,
896 },
897 line_range: end_line.map(|end| start_line..=end),
898 };
899 }
900
901 let path_position = PathWithPosition::parse_str(&raw_query);
902 let path_str = path_position.path.to_str();
903 let path_trimmed = path_str.unwrap_or(&raw_query).trim_end_matches(':');
904 let file_query_end = if path_trimmed == raw_query {
905 None
906 } else {
907 path_str.map(str::len)
908 };
909
910 FileSearchQuery {
911 raw_query,
912 file_query_end,
913 path_position,
914 line_range: None,
915 }
916}
917
918fn parse_line_range_query(raw_query: &str) -> Option<(&str, u32, Option<u32>)> {
919 let (path_query, line_range) = raw_query.rsplit_once(':')?;
920 if path_query.is_empty() {
921 return None;
922 }
923
924 let (start_line, end_line) = line_range.split_once('-')?;
925 let start_line = start_line.parse::<u32>().ok()?;
926 if start_line == 0 {
927 return None;
928 }
929
930 let end_line = end_line
931 .parse::<u32>()
932 .ok()
933 .filter(|&end| end != 0 && end >= start_line);
934
935 Some((path_query, start_line, end_line))
936}
937
938fn buffer_range_for_line_range(
939 buffer_snapshot: &BufferSnapshot,
940 line_range: RangeInclusive<u32>,
941) -> Range<Point> {
942 let max_point = buffer_snapshot.max_point();
943 let start_line = line_range.start().saturating_sub(1);
944 let start_point = if start_line > max_point.row {
945 max_point
946 } else {
947 Point::new(start_line, 0)
948 };
949 let end_line = *line_range.end();
950 let end_point = if end_line > max_point.row {
951 max_point
952 } else {
953 Point::new(end_line, 0)
954 };
955
956 start_point..end_point
957}
958
959impl FileFinderDelegate {
960 fn new(
961 file_finder: WeakEntity<FileFinder>,
962 workspace: WeakEntity<Workspace>,
963 project: Entity<Project>,
964 currently_opened_path: Option<FoundPath>,
965 history_items: Vec<FoundPath>,
966 separate_history: bool,
967 include_ignored: Option<bool>,
968 window: &mut Window,
969 cx: &mut Context<FileFinder>,
970 ) -> Self {
971 Self::subscribe_to_updates(&project, window, cx);
972 let channel_store = if FileFinderSettings::get_global(cx).include_channels {
973 ChannelStore::try_global(cx)
974 } else {
975 None
976 };
977 Self {
978 file_finder,
979 workspace,
980 project,
981 channel_store,
982 search_count: 0,
983 latest_search_id: 0,
984 latest_search_did_cancel: false,
985 latest_search_query: None,
986 currently_opened_path,
987 matches: Matches::default(),
988 selected_matches: Vec::new(),
989 has_changed_selected_index: false,
990 selected_index: 0,
991 cancel_flag: Arc::new(AtomicBool::new(false)),
992 search_in_flight: Arc::new(AtomicBool::new(false)),
993 history_items,
994 separate_history,
995 first_update: true,
996 focus_handle: cx.focus_handle(),
997 include_ignored: include_ignored.or(FileFinderSettings::get_global(cx).include_ignored),
998 include_ignored_refresh: Task::ready(()),
999 debounce_next_refresh: false,
1000 }
1001 }
1002
1003 fn subscribe_to_updates(
1004 project: &Entity<Project>,
1005 window: &mut Window,
1006 cx: &mut Context<FileFinder>,
1007 ) {
1008 cx.subscribe_in(project, window, |file_finder, _, event, window, cx| {
1009 match event {
1010 project::Event::WorktreeUpdatedEntries(_, _) => {
1011 file_finder.picker.update(cx, |picker, cx| {
1012 picker.delegate.debounce_next_refresh = true;
1013 picker.refresh(window, cx);
1014 })
1015 }
1016 project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
1017 file_finder
1018 .picker
1019 .update(cx, |picker, cx| picker.refresh(window, cx))
1020 }
1021 _ => {}
1022 };
1023 })
1024 .detach();
1025 }
1026
1027 fn prepend_selected_matches(&mut self) {
1028 if self.selected_matches.is_empty() {
1029 return;
1030 }
1031 self.matches
1032 .matches
1033 .retain(|m| !self.selected_matches.iter().any(|selected| selected == m));
1034 let mut new: Vec<Match> = self
1035 .selected_matches
1036 .iter()
1037 .map(|selected| selected.0.clone())
1038 .collect();
1039 new.append(&mut self.matches.matches);
1040 self.matches.matches = new;
1041 }
1042
1043 fn spawn_search(
1044 &mut self,
1045 query: FileSearchQuery,
1046 window: &mut Window,
1047 cx: &mut Context<Picker<Self>>,
1048 ) -> Task<()> {
1049 let relative_to = self
1050 .currently_opened_path
1051 .as_ref()
1052 .map(|found_path| Arc::clone(&found_path.project.path));
1053 let worktree_store = self.project.read(cx).worktree_store();
1054 let worktrees = worktree_store
1055 .read(cx)
1056 .visible_worktrees_and_single_files(cx)
1057 .collect::<Vec<_>>();
1058 let include_root_name = !should_hide_root_in_entry_path(&worktree_store, cx);
1059 let candidate_sets = worktrees
1060 .into_iter()
1061 .map(|worktree| {
1062 let worktree = worktree.read(cx);
1063 PathMatchCandidateSet {
1064 snapshot: worktree.snapshot(),
1065 include_ignored: self.include_ignored.unwrap_or_else(|| {
1066 worktree.root_entry().is_some_and(|entry| entry.is_ignored)
1067 }),
1068 include_root_name,
1069 candidates: project::Candidates::Files,
1070 }
1071 })
1072 .collect::<Vec<_>>();
1073
1074 let search_id = util::post_inc(&mut self.search_count);
1075 self.cancel_flag.store(true, atomic::Ordering::Release);
1076 self.cancel_flag = Arc::new(AtomicBool::new(false));
1077 let cancel_flag = self.cancel_flag.clone();
1078 cx.spawn_in(window, async move |picker, cx| {
1079 let matches = fuzzy_nucleo::match_path_sets(
1080 candidate_sets.as_slice(),
1081 query.path_query(),
1082 &relative_to,
1083 fuzzy_nucleo::Case::Ignore,
1084 100,
1085 &cancel_flag,
1086 cx.background_executor().clone(),
1087 )
1088 .await
1089 .into_iter()
1090 .map(ProjectPanelOrdMatch);
1091 let did_cancel = cancel_flag.load(atomic::Ordering::Acquire);
1092 picker
1093 .update(cx, |picker, cx| {
1094 picker
1095 .delegate
1096 .set_search_matches(search_id, did_cancel, query, matches, cx)
1097 })
1098 .log_err();
1099 })
1100 }
1101
1102 fn set_search_matches(
1103 &mut self,
1104 search_id: usize,
1105 did_cancel: bool,
1106 query: FileSearchQuery,
1107 matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
1108 cx: &mut Context<Picker<Self>>,
1109 ) {
1110 if search_id >= self.latest_search_id {
1111 self.latest_search_id = search_id;
1112 let query_changed = Some(query.path_query())
1113 != self
1114 .latest_search_query
1115 .as_ref()
1116 .map(|query| query.path_query());
1117 let extend_old_matches = self.latest_search_did_cancel && !query_changed;
1118
1119 let selected_match = if query_changed {
1120 None
1121 } else {
1122 self.matches.get(self.selected_index).cloned()
1123 };
1124
1125 let path_style = self.project.read(cx).path_style(cx);
1126 self.matches.push_new_matches(
1127 self.project.read(cx).worktree_store(),
1128 cx,
1129 &self.history_items,
1130 self.currently_opened_path.as_ref(),
1131 Some(&query),
1132 matches.into_iter(),
1133 extend_old_matches,
1134 path_style,
1135 );
1136
1137 // Add channel matches
1138 if let Some(channel_store) = &self.channel_store {
1139 let channel_store = channel_store.read(cx);
1140 let channels: Vec<_> = channel_store.channels().cloned().collect();
1141 if !channels.is_empty() {
1142 let candidates = channels
1143 .iter()
1144 .enumerate()
1145 .map(|(id, channel)| StringMatchCandidate::new(id, &channel.name));
1146 let channel_query = query.path_query();
1147 let query_lower = channel_query.to_lowercase();
1148 let mut channel_matches = Vec::new();
1149 for candidate in candidates {
1150 let channel_name = candidate.string;
1151 let name_lower = channel_name.to_lowercase();
1152
1153 let mut positions = Vec::new();
1154 let mut query_idx = 0;
1155 for (name_idx, name_char) in name_lower.char_indices() {
1156 if query_idx < query_lower.len() {
1157 let query_char =
1158 query_lower[query_idx..].chars().next().unwrap_or_default();
1159 if name_char == query_char {
1160 positions.push(name_idx);
1161 query_idx += query_char.len_utf8();
1162 }
1163 }
1164 }
1165
1166 if query_idx == query_lower.len() {
1167 let channel = &channels[candidate.id];
1168 let score = if name_lower == query_lower {
1169 1.0
1170 } else if name_lower.starts_with(&query_lower) {
1171 0.8
1172 } else {
1173 0.5 * (query_lower.len() as f64 / name_lower.len() as f64)
1174 };
1175 channel_matches.push(Match::Channel {
1176 channel_id: channel.id,
1177 channel_name: channel.name.clone(),
1178 string_match: StringMatch {
1179 candidate_id: candidate.id,
1180 score,
1181 positions,
1182 string: channel_name,
1183 },
1184 });
1185 }
1186 }
1187 for channel_match in channel_matches {
1188 match self
1189 .matches
1190 .position(&channel_match, self.currently_opened_path.as_ref())
1191 {
1192 Ok(_duplicate) => {}
1193 Err(ix) => self.matches.matches.insert(ix, channel_match),
1194 }
1195 }
1196 }
1197 }
1198
1199 let query_path = query.raw_query.as_str();
1200 if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) {
1201 let available_worktree = self
1202 .project
1203 .read(cx)
1204 .visible_worktrees(cx)
1205 .filter(|worktree| !worktree.read(cx).is_single_file())
1206 .collect::<Vec<_>>();
1207 let worktree_count = available_worktree.len();
1208 let mut expect_worktree = available_worktree.first().cloned();
1209 for worktree in &available_worktree {
1210 let worktree_root = worktree.read(cx).root_name();
1211 if worktree_count > 1 {
1212 if let Ok(suffix) = query_path.strip_prefix(worktree_root) {
1213 query_path = Cow::Owned(suffix.to_owned());
1214 expect_worktree = Some(worktree.clone());
1215 break;
1216 }
1217 }
1218 }
1219
1220 if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
1221 let worktree_id = project.worktree_id;
1222 let focused_file_in_available_worktree = available_worktree
1223 .iter()
1224 .any(|wt| wt.read(cx).id() == worktree_id);
1225
1226 if focused_file_in_available_worktree {
1227 expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
1228 }
1229 }
1230
1231 if let Some(worktree) = expect_worktree {
1232 let worktree = worktree.read(cx);
1233 if worktree.entry_for_path(&query_path).is_none()
1234 && !query.raw_query.ends_with("/")
1235 && !(path_style.is_windows() && query.raw_query.ends_with("\\"))
1236 {
1237 self.matches.matches.push(Match::CreateNew(ProjectPath {
1238 worktree_id: worktree.id(),
1239 path: query_path.into_arc(),
1240 }));
1241 }
1242 }
1243 }
1244
1245 self.prepend_selected_matches();
1246
1247 self.selected_index = if !self.selected_matches.is_empty() {
1248 0
1249 } else {
1250 selected_match.map_or_else(
1251 || self.calculate_selected_index(cx),
1252 |m| {
1253 self.matches
1254 .position(&m, self.currently_opened_path.as_ref())
1255 .unwrap_or(0)
1256 },
1257 )
1258 };
1259
1260 self.latest_search_query = Some(query);
1261 self.latest_search_did_cancel = did_cancel;
1262
1263 cx.notify();
1264 }
1265 }
1266
1267 fn labels_for_match(
1268 &self,
1269 path_match: &Match,
1270 window: &mut Window,
1271 cx: &App,
1272 ) -> (HighlightedLabel, HighlightedLabel) {
1273 let path_style = self.project.read(cx).path_style(cx);
1274 let (file_name, file_name_positions, mut full_path, mut full_path_positions) =
1275 match &path_match {
1276 Match::History {
1277 path: entry_path,
1278 panel_match,
1279 } => {
1280 let worktree_id = entry_path.project.worktree_id;
1281 let worktree = self
1282 .project
1283 .read(cx)
1284 .worktree_for_id(worktree_id, cx)
1285 .filter(|worktree| worktree.read(cx).is_visible());
1286
1287 if let Some(panel_match) = panel_match {
1288 self.labels_for_path_match(&panel_match.0, path_style)
1289 } else if let Some(worktree) = worktree {
1290 let worktree_store = self.project.read(cx).worktree_store();
1291 let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
1292 entry_path.project.path.clone()
1293 } else {
1294 worktree
1295 .read(cx)
1296 .root_name()
1297 .join(&entry_path.project.path)
1298 .into()
1299 };
1300 let mut components = full_path.components();
1301 let filename = components.next_back().unwrap_or("");
1302 let prefix = components.rest();
1303 (
1304 filename.to_string(),
1305 Vec::new(),
1306 prefix.display(path_style).to_string() + path_style.primary_separator(),
1307 Vec::new(),
1308 )
1309 } else {
1310 (
1311 entry_path
1312 .absolute
1313 .file_name()
1314 .map_or(String::new(), |f| f.to_string_lossy().into_owned()),
1315 Vec::new(),
1316 entry_path.absolute.parent().map_or(String::new(), |path| {
1317 path.to_string_lossy().into_owned() + path_style.primary_separator()
1318 }),
1319 Vec::new(),
1320 )
1321 }
1322 }
1323 Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style),
1324 Match::Channel {
1325 channel_name,
1326 string_match,
1327 ..
1328 } => (
1329 channel_name.to_string(),
1330 string_match.positions.clone(),
1331 "Channel Notes".to_string(),
1332 vec![],
1333 ),
1334 Match::CreateNew(project_path) => (
1335 format!("Create File: {}", project_path.path.display(path_style)),
1336 vec![],
1337 String::from(""),
1338 vec![],
1339 ),
1340 };
1341
1342 if file_name_positions.is_empty() {
1343 let user_home_path = util::paths::home_dir().to_string_lossy();
1344 if !user_home_path.is_empty() && full_path.starts_with(&*user_home_path) {
1345 full_path.replace_range(0..user_home_path.len(), "~");
1346 full_path_positions.retain_mut(|pos| {
1347 if *pos >= user_home_path.len() {
1348 *pos -= user_home_path.len();
1349 *pos += 1;
1350 true
1351 } else {
1352 false
1353 }
1354 })
1355 }
1356 }
1357
1358 if full_path.is_ascii() {
1359 let file_finder_settings = FileFinderSettings::get_global(cx);
1360 let max_width =
1361 FileFinder::modal_max_width(file_finder_settings.modal_max_width, window);
1362 let (normal_em, small_em) = {
1363 let style = window.text_style();
1364 let font_id = window.text_system().resolve_font(&style.font());
1365 let font_size = TextSize::Default.rems(cx).to_pixels(window.rem_size());
1366 let normal = cx
1367 .text_system()
1368 .em_width(font_id, font_size)
1369 .unwrap_or(px(16.));
1370 let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
1371 let small = cx
1372 .text_system()
1373 .em_width(font_id, font_size)
1374 .unwrap_or(px(10.));
1375 (normal, small)
1376 };
1377 let budget = full_path_budget(&file_name, normal_em, small_em, max_width);
1378 // If the computed budget is zero, we certainly won't be able to achieve it,
1379 // so no point trying to elide the path.
1380 if budget > 0 && full_path.len() > budget {
1381 let components = PathComponentSlice::new(&full_path);
1382 if let Some(elided_range) =
1383 components.elision_range(budget - 1, &full_path_positions)
1384 {
1385 let elided_len = elided_range.end - elided_range.start;
1386 let placeholder = "…";
1387 full_path_positions.retain_mut(|mat| {
1388 if *mat >= elided_range.end {
1389 *mat -= elided_len;
1390 *mat += placeholder.len();
1391 } else if *mat >= elided_range.start {
1392 return false;
1393 }
1394 true
1395 });
1396 full_path.replace_range(elided_range, placeholder);
1397 }
1398 }
1399 }
1400
1401 (
1402 HighlightedLabel::new(file_name, file_name_positions),
1403 HighlightedLabel::new(full_path, full_path_positions)
1404 .size(LabelSize::Small)
1405 .color(Color::Muted),
1406 )
1407 }
1408
1409 fn labels_for_path_match(
1410 &self,
1411 path_match: &PathMatch,
1412 path_style: PathStyle,
1413 ) -> (String, Vec<usize>, String, Vec<usize>) {
1414 let full_path = path_match.path_prefix.join(&path_match.path);
1415 let mut path_positions = path_match.positions.clone();
1416
1417 let file_name = full_path.file_name().unwrap_or("");
1418 let file_name_start = full_path.as_unix_str().len() - file_name.len();
1419 let file_name_positions = path_positions
1420 .iter()
1421 .filter_map(|pos| {
1422 if pos >= &file_name_start {
1423 Some(pos - file_name_start)
1424 } else {
1425 None
1426 }
1427 })
1428 .collect::<Vec<_>>();
1429
1430 let full_path = full_path
1431 .display(path_style)
1432 .trim_end_matches(&file_name)
1433 .to_string();
1434 path_positions.retain(|idx| *idx < full_path.len());
1435
1436 debug_assert!(
1437 file_name_positions
1438 .iter()
1439 .all(|ix| file_name[*ix..].chars().next().is_some()),
1440 "invalid file name positions {file_name:?} {file_name_positions:?}"
1441 );
1442 debug_assert!(
1443 path_positions
1444 .iter()
1445 .all(|ix| full_path[*ix..].chars().next().is_some()),
1446 "invalid path positions {full_path:?} {path_positions:?}"
1447 );
1448
1449 (
1450 file_name.to_string(),
1451 file_name_positions,
1452 full_path,
1453 path_positions,
1454 )
1455 }
1456
1457 /// Attempts to resolve an absolute file path and update the search matches if found.
1458 ///
1459 /// If the query path resolves to an absolute file that exists in the project,
1460 /// this method will find the corresponding worktree and relative path, create a
1461 /// match for it, and update the picker's search results.
1462 ///
1463 /// Returns `true` if the absolute path exists, otherwise returns `false`.
1464 fn lookup_absolute_path(
1465 &self,
1466 query: FileSearchQuery,
1467 window: &mut Window,
1468 cx: &mut Context<Picker<Self>>,
1469 ) -> Task<bool> {
1470 cx.spawn_in(window, async move |picker, cx| {
1471 let Some(project) = picker
1472 .read_with(cx, |picker, _| picker.delegate.project.clone())
1473 .log_err()
1474 else {
1475 return false;
1476 };
1477
1478 let query_path = Path::new(query.path_query());
1479 let mut path_matches = Vec::new();
1480
1481 let abs_file_exists = project
1482 .update(cx, |this, cx| {
1483 this.resolve_abs_file_path(query.path_query(), cx)
1484 })
1485 .await
1486 .is_some();
1487
1488 if abs_file_exists {
1489 project.update(cx, |project, cx| {
1490 if let Some((worktree, relative_path)) = project.find_worktree(query_path, cx) {
1491 path_matches.push(ProjectPanelOrdMatch(PathMatch {
1492 score: 1.0,
1493 positions: Vec::new(),
1494 worktree_id: worktree.read(cx).id().to_usize(),
1495 path: relative_path,
1496 path_prefix: RelPath::empty_arc(),
1497 is_dir: false, // File finder doesn't support directories
1498 distance_to_relative_ancestor: usize::MAX,
1499 }));
1500 }
1501 });
1502 }
1503
1504 picker
1505 .update_in(cx, |picker, _, cx| {
1506 let picker_delegate = &mut picker.delegate;
1507 let search_id = util::post_inc(&mut picker_delegate.search_count);
1508 picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
1509
1510 anyhow::Ok(())
1511 })
1512 .log_err();
1513 abs_file_exists
1514 })
1515 }
1516
1517 /// Skips first history match (that is displayed topmost) if it's currently opened.
1518 fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize {
1519 if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search
1520 && let Some(Match::History { path, .. }) = self.matches.get(0)
1521 && Some(path) == self.currently_opened_path.as_ref()
1522 {
1523 let elements_after_first = self.matches.len() - 1;
1524 if elements_after_first > 0 {
1525 return 1;
1526 }
1527 }
1528
1529 0
1530 }
1531
1532 fn key_context(&self, _window: &Window, _cx: &App) -> KeyContext {
1533 let mut key_context = KeyContext::new_with_defaults();
1534 key_context.add("FileFinder");
1535 key_context
1536 }
1537
1538 /// Shared file-opening logic for both `confirm` and `confirm_without_dismiss`.
1539 ///
1540 /// When `dismiss_after_open` is true this behaves like a normal confirm: the file is focused
1541 /// and the finder is dismissed. When false the finder stays open so the user can continue
1542 /// opening more files.
1543 fn open_selected_file(
1544 &mut self,
1545 secondary: bool,
1546 dismiss_after_open: bool,
1547 window: &mut Window,
1548 cx: &mut Context<Picker<FileFinderDelegate>>,
1549 ) {
1550 let Some(m) = self.matches.get(self.selected_index()).cloned() else {
1551 return;
1552 };
1553 let allow_preview = PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
1554 self.open_match(m, secondary, dismiss_after_open, allow_preview, window, cx);
1555 }
1556
1557 /// `allow_preview` is forced off for batch opens so every file gets its
1558 /// own tab instead of consecutive opens reusing one preview tab.
1559 fn open_match(
1560 &mut self,
1561 m: Match,
1562 secondary: bool,
1563 dismiss_after_open: bool,
1564 allow_preview: bool,
1565 window: &mut Window,
1566 cx: &mut Context<Picker<FileFinderDelegate>>,
1567 ) {
1568 let Some(workspace) = self.workspace.upgrade() else {
1569 return;
1570 };
1571
1572 // Channel matches always dismiss the finder.
1573 if let Match::Channel { channel_id, .. } = &m {
1574 let channel_id = channel_id.0;
1575 let finder = self.file_finder.clone();
1576 window.dispatch_action(OpenChannelNotesById { channel_id }.boxed_clone(), cx);
1577 finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err();
1578 return;
1579 }
1580
1581 // Focus the new item only when dismissing — this avoids stealing focus from the modal.
1582 // Always activate (make the tab current) so every opened file is visually reflected.
1583 let focus_item = dismiss_after_open;
1584
1585 let open_task = workspace.update(cx, |workspace, cx| {
1586 let split_or_open = |workspace: &mut Workspace,
1587 project_path,
1588 window: &mut Window,
1589 cx: &mut Context<Workspace>| {
1590 if secondary {
1591 workspace.split_path_preview(project_path, allow_preview, None, window, cx)
1592 } else {
1593 workspace.open_path_preview(
1594 project_path,
1595 None,
1596 focus_item,
1597 allow_preview,
1598 true,
1599 window,
1600 cx,
1601 )
1602 }
1603 };
1604
1605 match &m {
1606 Match::CreateNew(project_path) => {
1607 if secondary {
1608 workspace.split_path_preview(project_path.clone(), false, None, window, cx)
1609 } else {
1610 workspace.open_path_preview(
1611 project_path.clone(),
1612 None,
1613 focus_item,
1614 false,
1615 true,
1616 window,
1617 cx,
1618 )
1619 }
1620 }
1621 Match::History { path, .. } => {
1622 let worktree_id = path.project.worktree_id;
1623 if workspace
1624 .project()
1625 .read(cx)
1626 .worktree_for_id(worktree_id, cx)
1627 .is_some()
1628 {
1629 split_or_open(
1630 workspace,
1631 ProjectPath {
1632 worktree_id,
1633 path: Arc::clone(&path.project.path),
1634 },
1635 window,
1636 cx,
1637 )
1638 } else if secondary {
1639 workspace.split_abs_path(path.absolute.clone(), false, window, cx)
1640 } else {
1641 workspace.open_abs_path(
1642 path.absolute.clone(),
1643 OpenOptions {
1644 visible: Some(OpenVisible::None),
1645 ..Default::default()
1646 },
1647 window,
1648 cx,
1649 )
1650 }
1651 }
1652 Match::Search(path_match) => {
1653 let project_path =
1654 project_path_for_search_match(workspace.project(), &path_match.0, cx);
1655 split_or_open(workspace, project_path, window, cx)
1656 }
1657 Match::Channel { .. } => unreachable!("handled above"),
1658 }
1659 });
1660
1661 let selection_query = self.latest_search_query.clone();
1662 let finder = self.file_finder.clone();
1663 let workspace = self.workspace.clone();
1664
1665 cx.spawn_in(window, async move |_, mut cx| {
1666 let item = open_task
1667 .await
1668 .notify_workspace_async_err(workspace, &mut cx)?;
1669 if let Some(active_editor) = item.downcast::<Editor>() {
1670 active_editor
1671 .downgrade()
1672 .update_in(cx, |editor, window, cx| {
1673 let Some(buffer) = editor.buffer().read(cx).as_singleton() else {
1674 return;
1675 };
1676 let buffer_snapshot = buffer.read(cx).snapshot();
1677 let Some(selection_query) = selection_query.as_ref() else {
1678 return;
1679 };
1680 let Some(selection_range) =
1681 selection_query.selection_range(&buffer_snapshot)
1682 else {
1683 return;
1684 };
1685 editor.go_to_singleton_buffer_range(selection_range, window, cx);
1686 })
1687 .log_err();
1688 }
1689 if dismiss_after_open {
1690 finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
1691 }
1692 Some(())
1693 })
1694 .detach();
1695 }
1696
1697 fn confirm_without_dismiss(
1698 &mut self,
1699 window: &mut Window,
1700 cx: &mut Context<Picker<FileFinderDelegate>>,
1701 ) {
1702 self.open_selected_file(false, false, window, cx);
1703 }
1704
1705 /// Opens every multi-selected file as a tab in a single new split, then
1706 /// dismisses the finder.
1707 fn open_selected_in_one_split(
1708 &mut self,
1709 split_direction: SplitDirection,
1710 window: &mut Window,
1711 cx: &mut Context<Picker<FileFinderDelegate>>,
1712 ) {
1713 let Some(workspace) = self.workspace.upgrade() else {
1714 return;
1715 };
1716 let selected = std::mem::take(&mut self.selected_matches);
1717 let paths: Vec<ProjectPath> = selected
1718 .iter()
1719 .filter_map(|selected| match &selected.0 {
1720 Match::History { path, .. } => Some(ProjectPath {
1721 worktree_id: path.project.worktree_id,
1722 path: Arc::clone(&path.project.path),
1723 }),
1724 Match::Search(m) => Some(project_path_for_search_match(&self.project, &m.0, cx)),
1725 Match::Channel { .. } | Match::CreateNew(_) => None,
1726 })
1727 .collect();
1728 if paths.is_empty() {
1729 return;
1730 }
1731 workspace.update(cx, |workspace, cx| {
1732 let new_pane =
1733 workspace.split_pane(workspace.active_pane().clone(), split_direction, window, cx);
1734 let count = paths.len();
1735 for (i, path) in paths.into_iter().enumerate() {
1736 let focus_item = i + 1 == count;
1737 workspace
1738 .open_path_preview(
1739 path,
1740 Some(new_pane.downgrade()),
1741 focus_item,
1742 false,
1743 true,
1744 window,
1745 cx,
1746 )
1747 .detach_and_log_err(cx);
1748 }
1749 });
1750 // Deferred because this runs from a `FileFinder` action handler, so
1751 // the entity is already being updated.
1752 let finder = self.file_finder.clone();
1753 cx.defer(move |cx| {
1754 finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err();
1755 });
1756 }
1757}
1758
1759fn full_path_budget(
1760 file_name: &str,
1761 normal_em: Pixels,
1762 small_em: Pixels,
1763 max_width: Pixels,
1764) -> usize {
1765 (((max_width / 0.8) - file_name.len() * normal_em) / small_em) as usize
1766}
1767
1768impl PickerDelegate for FileFinderDelegate {
1769 type ListItem = ListItem;
1770
1771 fn name() -> &'static str {
1772 "file finder"
1773 }
1774
1775 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1776 "Search project files...".into()
1777 }
1778
1779 fn searchbar_trailer(
1780 &self,
1781 _window: &mut Window,
1782 cx: &mut Context<Picker<Self>>,
1783 ) -> Option<AnyElement> {
1784 let focus_handle = self.focus_handle.clone();
1785 let including_ignored = self.include_ignored == Some(true);
1786 // Clicking includes ignored files unless they're already included, in
1787 // which case it excludes them again (see `handle_toggle_ignored`).
1788 let tooltip_label = if including_ignored {
1789 "Exclude Ignored Files"
1790 } else {
1791 "Include Ignored Files"
1792 };
1793
1794 let filter_button = IconButton::new("filter-ignored", IconName::FileIgnored)
1795 .icon_size(IconSize::Small)
1796 .toggle_state(including_ignored)
1797 .tooltip(move |_window, cx| {
1798 Tooltip::for_action_in(tooltip_label, &ToggleIncludeIgnored, &focus_handle, cx)
1799 })
1800 .on_click(|_, window, cx| {
1801 window.dispatch_action(ToggleIncludeIgnored.boxed_clone(), cx)
1802 });
1803 Some(
1804 h_flex()
1805 .gap_1()
1806 .child(filter_button)
1807 .children(picker::parts::project_scan_indicator(
1808 self.latest_search_query.is_some(),
1809 &self.project,
1810 cx,
1811 ))
1812 .into_any_element(),
1813 )
1814 }
1815
1816 fn match_count(&self) -> usize {
1817 self.matches.len()
1818 }
1819
1820 fn selected_index(&self) -> usize {
1821 self.selected_index
1822 }
1823
1824 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1825 self.has_changed_selected_index = true;
1826 self.selected_index = ix;
1827 cx.notify();
1828 }
1829
1830 fn separators_after_indices(&self) -> Vec<usize> {
1831 if self.separate_history {
1832 let first_non_history_index = self
1833 .matches
1834 .matches
1835 .iter()
1836 .enumerate()
1837 .find(|(_, m)| !matches!(m, Match::History { .. }))
1838 .map(|(i, _)| i);
1839 if let Some(first_non_history_index) = first_non_history_index
1840 && first_non_history_index > 0
1841 {
1842 return vec![first_non_history_index - 1];
1843 }
1844 }
1845 Vec::new()
1846 }
1847
1848 fn update_matches(
1849 &mut self,
1850 raw_query: String,
1851 window: &mut Window,
1852 cx: &mut Context<Picker<Self>>,
1853 ) -> Task<()> {
1854 let debounce_refresh = mem::take(&mut self.debounce_next_refresh);
1855 let raw_query = raw_query.trim();
1856
1857 let raw_query = match &raw_query.get(0..2) {
1858 Some(".\\" | "./") => &raw_query[2..],
1859 Some(prefix @ ("a\\" | "a/" | "b\\" | "b/")) => {
1860 if self
1861 .workspace
1862 .upgrade()
1863 .into_iter()
1864 .flat_map(|workspace| workspace.read(cx).worktrees(cx))
1865 .all(|worktree| {
1866 worktree
1867 .read(cx)
1868 .entry_for_path(RelPath::from_unix_str(prefix.split_at(1).0).unwrap())
1869 .is_none_or(|entry| !entry.is_dir())
1870 })
1871 {
1872 &raw_query[2..]
1873 } else {
1874 raw_query
1875 }
1876 }
1877 _ => raw_query,
1878 };
1879
1880 if raw_query.is_empty() {
1881 // if there was no query before, and we already have some (history) matches
1882 // there's no need to update anything, since nothing has changed.
1883 // We also want to populate matches set from history entries on the first update.
1884 if self.latest_search_query.is_some() || self.first_update {
1885 let project = self.project.read(cx);
1886
1887 self.latest_search_id = post_inc(&mut self.search_count);
1888 self.latest_search_query = None;
1889 self.matches = Matches {
1890 separate_history: self.separate_history,
1891 ..Matches::default()
1892 };
1893 let path_style = self.project.read(cx).path_style(cx);
1894
1895 self.matches.push_new_matches(
1896 project.worktree_store(),
1897 cx,
1898 self.history_items.iter().filter(|history_item| {
1899 project
1900 .worktree_for_id(history_item.project.worktree_id, cx)
1901 .is_some()
1902 || project.is_local()
1903 || project.is_via_remote_server()
1904 }),
1905 self.currently_opened_path.as_ref(),
1906 None,
1907 None.into_iter(),
1908 false,
1909 path_style,
1910 );
1911
1912 self.first_update = false;
1913 self.selected_index = 0;
1914 }
1915 self.prepend_selected_matches();
1916 cx.notify();
1917 self.search_in_flight
1918 .store(false, atomic::Ordering::Release);
1919 Task::ready(())
1920 } else {
1921 let query = parse_file_search_query(raw_query);
1922 let path = query.path_position.path.clone();
1923
1924 let search_in_flight = self.search_in_flight.clone();
1925 let was_in_flight = search_in_flight.swap(true, atomic::Ordering::Relaxed);
1926
1927 cx.spawn_in(window, async move |this, cx| {
1928 if debounce_refresh {
1929 cx.background_executor()
1930 .timer(WORKTREE_UPDATE_REFRESH_DEBOUNCE)
1931 .await;
1932 } else if was_in_flight {
1933 cx.background_executor().timer(SEARCH_DEBOUNCE).await;
1934 }
1935 let _ = maybe!(async move {
1936 let is_absolute_path = path.is_absolute();
1937 let did_resolve_abs_path = is_absolute_path
1938 && this
1939 .update_in(cx, |this, window, cx| {
1940 this.delegate
1941 .lookup_absolute_path(query.clone(), window, cx)
1942 })?
1943 .await;
1944
1945 // Only check for relative paths if no absolute paths were
1946 // found.
1947 if !did_resolve_abs_path {
1948 this.update_in(cx, |this, window, cx| {
1949 this.delegate.spawn_search(query, window, cx)
1950 })?
1951 .await;
1952 }
1953 anyhow::Ok(())
1954 })
1955 .await;
1956 search_in_flight.store(false, atomic::Ordering::Relaxed);
1957 })
1958 }
1959 }
1960
1961 fn confirm(
1962 &mut self,
1963 secondary: bool,
1964 window: &mut Window,
1965 cx: &mut Context<Picker<FileFinderDelegate>>,
1966 ) {
1967 self.open_selected_file(secondary, true, window, cx);
1968 }
1969
1970 fn supports_multi_select(&self) -> bool {
1971 true
1972 }
1973
1974 fn is_item_selected(&self, ix: usize) -> bool {
1975 let Some(m) = self.matches.get(ix) else {
1976 return false;
1977 };
1978 self.selected_matches.iter().any(|selected| selected == m)
1979 }
1980
1981 fn toggle_item_selected(
1982 &mut self,
1983 ix: usize,
1984 _window: &mut Window,
1985 cx: &mut Context<Picker<FileFinderDelegate>>,
1986 ) {
1987 let Some(m) = self.matches.get(ix).cloned() else {
1988 return;
1989 };
1990 // `new` rejects channels and the `create-new` placeholder.
1991 let Some(selected) = SelectedMatch::new(m) else {
1992 return;
1993 };
1994 if let Some(position) = self
1995 .selected_matches
1996 .iter()
1997 .position(|existing| *existing == selected)
1998 {
1999 self.selected_matches.remove(position);
2000 } else {
2001 self.selected_matches.push(selected);
2002 }
2003 cx.notify();
2004 }
2005
2006 fn selected_item_count(&self) -> usize {
2007 self.selected_matches.len()
2008 }
2009
2010 fn clear_selection(&mut self, _cx: &mut Context<Picker<FileFinderDelegate>>) {
2011 self.selected_matches.clear();
2012 }
2013
2014 fn confirm_multi(
2015 &mut self,
2016 secondary: bool,
2017 window: &mut Window,
2018 cx: &mut Context<Picker<FileFinderDelegate>>,
2019 ) {
2020 let selected = std::mem::take(&mut self.selected_matches);
2021 let count = selected.len();
2022 for (i, selected_match) in selected.into_iter().enumerate() {
2023 let is_last = i + 1 == count;
2024 self.open_match(selected_match.0, secondary, is_last, false, window, cx);
2025 }
2026 }
2027
2028 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>) {
2029 self.file_finder
2030 .update(cx, |_, cx| cx.emit(DismissEvent))
2031 .log_err();
2032 }
2033
2034 fn try_get_preview_data_for_match(&self, cx: &App) -> Option<picker::PreviewUpdate> {
2035 let m = self.matches.get(self.selected_index)?;
2036 match m {
2037 Match::CreateNew(project_path) => {
2038 let path_style = self.project.read(cx).path_style(cx);
2039 let path_highlight = gpui::HighlightStyle {
2040 color: Some(cx.theme().colors().text_accent),
2041 ..Default::default()
2042 };
2043 let mut message = picker::HighlightedTextBuilder::default();
2044 message.push_plain("Create file ");
2045 message.push_styled(project_path.path.display(path_style), path_highlight);
2046 message.push_plain("?");
2047 Some(picker::PreviewUpdate::message(message.build()))
2048 }
2049 _ => Some(picker::PreviewUpdate::from_path(
2050 m.abs_path(&self.project, cx)?,
2051 )),
2052 }
2053 }
2054
2055 fn render_match(
2056 &self,
2057 ix: usize,
2058 selected: bool,
2059 window: &mut Window,
2060 cx: &mut Context<Picker<Self>>,
2061 ) -> Option<Self::ListItem> {
2062 self.render_match_impl(ix, selected, None, window, cx)
2063 }
2064
2065 fn render_match_with_checkbox(
2066 &self,
2067 ix: usize,
2068 selected: bool,
2069 checkbox: AnyElement,
2070 window: &mut Window,
2071 cx: &mut Context<Picker<Self>>,
2072 ) -> Option<Self::ListItem> {
2073 self.render_match_impl(ix, selected, Some(checkbox), window, cx)
2074 }
2075
2076 fn actions_menu(
2077 &self,
2078 _window: &mut Window,
2079 _cx: &mut Context<Picker<Self>>,
2080 ) -> Vec<picker::PickerAction> {
2081 let open_label: SharedString = if self.selected_matches.len() > 1 {
2082 "Open multiple".into()
2083 } else {
2084 "Open File".into()
2085 };
2086 vec![
2087 picker::PickerAction::header("Split…"),
2088 picker::PickerAction::button("Left", pane::SplitLeft::default().boxed_clone()),
2089 picker::PickerAction::button("Right", pane::SplitRight::default().boxed_clone()),
2090 picker::PickerAction::button("Up", pane::SplitUp::default().boxed_clone()),
2091 picker::PickerAction::button("Down", pane::SplitDown::default().boxed_clone()),
2092 picker::PickerAction::separator(),
2093 picker::PickerAction::button(open_label, menu::Confirm.boxed_clone()),
2094 ]
2095 }
2096}
2097
2098impl FileFinderDelegate {
2099 fn render_match_impl(
2100 &self,
2101 ix: usize,
2102 selected: bool,
2103 checkbox: Option<AnyElement>,
2104 window: &mut Window,
2105 cx: &mut Context<Picker<Self>>,
2106 ) -> Option<ListItem> {
2107 let settings = FileFinderSettings::get_global(cx);
2108
2109 let path_match = self.matches.get(ix)?;
2110
2111 let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx);
2112
2113 let start_icon = match path_match {
2114 Match::CreateNew(_) => Some(Icon::new(IconName::Plus).size(IconSize::Small)),
2115 Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)),
2116 _ => maybe!({
2117 if !settings.file_icons {
2118 return None;
2119 }
2120 let abs_path = path_match.abs_path(&self.project, cx)?;
2121 let file_name = abs_path.file_name()?;
2122 let icon = FileIcons::get_icon(file_name.as_ref(), cx)?;
2123 Some(Icon::from_path(icon).color(Color::Muted))
2124 }),
2125 };
2126
2127 let checkbox = checkbox.map(|checkbox| {
2128 if matches!(path_match, Match::CreateNew(_) | Match::Channel { .. }) {
2129 div()
2130 .flex_none()
2131 .size(Checkbox::container_size())
2132 .into_any_element()
2133 } else {
2134 checkbox
2135 }
2136 });
2137
2138 let start_slot: Option<AnyElement> = match (checkbox, start_icon) {
2139 (Some(checkbox), icon) => Some(
2140 h_flex()
2141 .gap_1p5()
2142 .child(checkbox)
2143 .children(icon)
2144 .into_any_element(),
2145 ),
2146 (None, icon) => icon.map(IntoElement::into_any_element),
2147 };
2148
2149 let end_slot: Option<AnyElement> = match path_match {
2150 Match::History { .. } => Some(
2151 Icon::new(IconName::HistoryRerun)
2152 .color(Color::Muted)
2153 .size(IconSize::Small)
2154 .into_any_element(),
2155 ),
2156 Match::Search(_) | Match::Channel { .. } => Some(
2157 div()
2158 .flex_none()
2159 .size(IconSize::Small.rems())
2160 .into_any_element(),
2161 ),
2162 Match::CreateNew(_) => None,
2163 };
2164
2165 Some(
2166 ListItem::new(ix)
2167 .spacing(ListItemSpacing::Sparse)
2168 .inset(true)
2169 .toggle_state(selected)
2170 .start_slot::<AnyElement>(start_slot)
2171 .child(
2172 h_flex()
2173 .w_full()
2174 .min_w_0()
2175 .gap_1p5()
2176 .child(file_name_label.truncate_middle())
2177 .child(full_path_label.truncate_start()),
2178 )
2179 .end_slot::<AnyElement>(end_slot),
2180 )
2181 }
2182}
2183
2184#[derive(Clone, Debug, PartialEq, Eq)]
2185struct PathComponentSlice<'a> {
2186 path: Cow<'a, Path>,
2187 path_str: Cow<'a, str>,
2188 component_ranges: Vec<(Component<'a>, Range<usize>)>,
2189}
2190
2191impl<'a> PathComponentSlice<'a> {
2192 fn new(path: &'a str) -> Self {
2193 let trimmed_path = Path::new(path).components().as_path().as_os_str();
2194 let mut component_ranges = Vec::new();
2195 let mut components = Path::new(trimmed_path).components();
2196 let len = trimmed_path.as_encoded_bytes().len();
2197 let mut pos = 0;
2198 while let Some(component) = components.next() {
2199 component_ranges.push((component, pos..0));
2200 pos = len - components.as_path().as_os_str().as_encoded_bytes().len();
2201 }
2202 for ((_, range), ancestor) in component_ranges
2203 .iter_mut()
2204 .rev()
2205 .zip(Path::new(trimmed_path).ancestors())
2206 {
2207 range.end = ancestor.as_os_str().as_encoded_bytes().len();
2208 }
2209 Self {
2210 path: Cow::Borrowed(Path::new(path)),
2211 path_str: Cow::Borrowed(path),
2212 component_ranges,
2213 }
2214 }
2215
2216 fn elision_range(&self, budget: usize, matches: &[usize]) -> Option<Range<usize>> {
2217 let eligible_range = {
2218 assert!(matches.is_sorted());
2219 let mut matches = matches.iter().copied().peekable();
2220 let mut longest: Option<Range<usize>> = None;
2221 let mut cur = 0..0;
2222 let mut seen_normal = false;
2223 for (i, (component, range)) in self.component_ranges.iter().enumerate() {
2224 let is_normal = matches!(component, Component::Normal(_));
2225 let is_first_normal = is_normal && !seen_normal;
2226 seen_normal |= is_normal;
2227 let is_last = i == self.component_ranges.len() - 1;
2228 let contains_match = matches.peek().is_some_and(|mat| range.contains(mat));
2229 if contains_match {
2230 matches.next();
2231 }
2232 if is_first_normal || is_last || !is_normal || contains_match {
2233 if longest
2234 .as_ref()
2235 .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
2236 {
2237 longest = Some(cur);
2238 }
2239 cur = i + 1..i + 1;
2240 } else {
2241 cur.end = i + 1;
2242 }
2243 }
2244 if longest
2245 .as_ref()
2246 .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
2247 {
2248 longest = Some(cur);
2249 }
2250 longest
2251 };
2252
2253 let eligible_range = eligible_range?;
2254 assert!(eligible_range.start <= eligible_range.end);
2255 if eligible_range.is_empty() {
2256 return None;
2257 }
2258
2259 let elided_range: Range<usize> = {
2260 let byte_range = self.component_ranges[eligible_range.start].1.start
2261 ..self.component_ranges[eligible_range.end - 1].1.end;
2262 let midpoint = self.path_str.len() / 2;
2263 let distance_from_start = byte_range.start.abs_diff(midpoint);
2264 let distance_from_end = byte_range.end.abs_diff(midpoint);
2265 let pick_from_end = distance_from_start > distance_from_end;
2266 let mut len_with_elision = self.path_str.len();
2267 let mut i = eligible_range.start;
2268 while i < eligible_range.end {
2269 let x = if pick_from_end {
2270 eligible_range.end - i + eligible_range.start - 1
2271 } else {
2272 i
2273 };
2274 len_with_elision -= self.component_ranges[x]
2275 .0
2276 .as_os_str()
2277 .as_encoded_bytes()
2278 .len()
2279 + 1;
2280 if len_with_elision <= budget {
2281 break;
2282 }
2283 i += 1;
2284 }
2285 if len_with_elision > budget {
2286 return None;
2287 } else if pick_from_end {
2288 let x = eligible_range.end - i + eligible_range.start - 1;
2289 x..eligible_range.end
2290 } else {
2291 let x = i;
2292 eligible_range.start..x + 1
2293 }
2294 };
2295
2296 let byte_range = self.component_ranges[elided_range.start].1.start
2297 ..self.component_ranges[elided_range.end - 1].1.end;
2298 Some(byte_range)
2299 }
2300}
2301