file_finder.rs

  1#[cfg(test)]
  2mod file_finder_tests;
  3
  4use collections::HashMap;
  5use editor::{scroll::Autoscroll, Bias, Editor};
  6use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
  7use gpui::{
  8    actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
  9    ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
 10};
 11use picker::{Picker, PickerDelegate};
 12use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
 13use std::{
 14    path::{Path, PathBuf},
 15    sync::{
 16        atomic::{self, AtomicBool},
 17        Arc,
 18    },
 19};
 20use text::Point;
 21use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 22use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
 23use workspace::{ModalView, Workspace};
 24
 25actions!(file_finder, [Toggle]);
 26
 27impl ModalView for FileFinder {}
 28
 29pub struct FileFinder {
 30    picker: View<Picker<FileFinderDelegate>>,
 31}
 32
 33pub fn init(cx: &mut AppContext) {
 34    cx.observe_new_views(FileFinder::register).detach();
 35}
 36
 37impl FileFinder {
 38    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 39        workspace.register_action(|workspace, _: &Toggle, cx| {
 40            let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
 41                Self::open(workspace, cx);
 42                return;
 43            };
 44
 45            file_finder.update(cx, |file_finder, cx| {
 46                file_finder
 47                    .picker
 48                    .update(cx, |picker, cx| picker.cycle_selection(cx))
 49            });
 50        });
 51    }
 52
 53    fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 54        let project = workspace.project().read(cx);
 55
 56        let currently_opened_path = workspace
 57            .active_item(cx)
 58            .and_then(|item| item.project_path(cx))
 59            .map(|project_path| {
 60                let abs_path = project
 61                    .worktree_for_id(project_path.worktree_id, cx)
 62                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
 63                FoundPath::new(project_path, abs_path)
 64            });
 65
 66        // if exists, bubble the currently opened path to the top
 67        let history_items = currently_opened_path
 68            .clone()
 69            .into_iter()
 70            .chain(
 71                workspace
 72                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
 73                    .into_iter()
 74                    .filter(|(history_path, _)| {
 75                        Some(history_path)
 76                            != currently_opened_path
 77                                .as_ref()
 78                                .map(|found_path| &found_path.project)
 79                    })
 80                    .filter(|(_, history_abs_path)| {
 81                        history_abs_path.as_ref()
 82                            != currently_opened_path
 83                                .as_ref()
 84                                .and_then(|found_path| found_path.absolute.as_ref())
 85                    })
 86                    .filter(|(_, history_abs_path)| match history_abs_path {
 87                        Some(abs_path) => history_file_exists(abs_path),
 88                        None => true,
 89                    })
 90                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
 91            )
 92            .collect();
 93
 94        let project = workspace.project().clone();
 95        let weak_workspace = cx.view().downgrade();
 96        workspace.toggle_modal(cx, |cx| {
 97            let delegate = FileFinderDelegate::new(
 98                cx.view().downgrade(),
 99                weak_workspace,
100                project,
101                currently_opened_path,
102                history_items,
103                cx,
104            );
105
106            FileFinder::new(delegate, cx)
107        });
108    }
109
110    fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
111        Self {
112            picker: cx.new_view(|cx| Picker::new(delegate, cx)),
113        }
114    }
115}
116
117impl EventEmitter<DismissEvent> for FileFinder {}
118
119impl FocusableView for FileFinder {
120    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
121        self.picker.focus_handle(cx)
122    }
123}
124
125impl Render for FileFinder {
126    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
127        v_flex().w(rems(34.)).child(self.picker.clone())
128    }
129}
130
131pub struct FileFinderDelegate {
132    file_finder: WeakView<FileFinder>,
133    workspace: WeakView<Workspace>,
134    project: Model<Project>,
135    search_count: usize,
136    latest_search_id: usize,
137    latest_search_did_cancel: bool,
138    latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
139    currently_opened_path: Option<FoundPath>,
140    matches: Matches,
141    selected_index: Option<usize>,
142    cancel_flag: Arc<AtomicBool>,
143    history_items: Vec<FoundPath>,
144}
145
146#[derive(Debug, Default)]
147struct Matches {
148    history: Vec<(FoundPath, Option<PathMatch>)>,
149    search: Vec<PathMatch>,
150}
151
152#[derive(Debug)]
153enum Match<'a> {
154    History(&'a FoundPath, Option<&'a PathMatch>),
155    Search(&'a PathMatch),
156}
157
158impl Matches {
159    fn len(&self) -> usize {
160        self.history.len() + self.search.len()
161    }
162
163    fn get(&self, index: usize) -> Option<Match<'_>> {
164        if index < self.history.len() {
165            self.history
166                .get(index)
167                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
168        } else {
169            self.search
170                .get(index - self.history.len())
171                .map(Match::Search)
172        }
173    }
174
175    fn push_new_matches(
176        &mut self,
177        history_items: &Vec<FoundPath>,
178        query: &PathLikeWithPosition<FileSearchQuery>,
179        mut new_search_matches: Vec<PathMatch>,
180        extend_old_matches: bool,
181    ) {
182        let matching_history_paths = matching_history_item_paths(history_items, query);
183        new_search_matches
184            .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
185        let history_items_to_show = history_items
186            .iter()
187            .filter_map(|history_item| {
188                Some((
189                    history_item.clone(),
190                    Some(
191                        matching_history_paths
192                            .get(&history_item.project.path)?
193                            .clone(),
194                    ),
195                ))
196            })
197            .collect::<Vec<_>>();
198        self.history = history_items_to_show;
199        if extend_old_matches {
200            self.search
201                .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
202            util::extend_sorted(
203                &mut self.search,
204                new_search_matches.into_iter(),
205                100,
206                |a, b| b.cmp(a),
207            )
208        } else {
209            self.search = new_search_matches;
210        }
211    }
212}
213
214fn matching_history_item_paths(
215    history_items: &Vec<FoundPath>,
216    query: &PathLikeWithPosition<FileSearchQuery>,
217) -> HashMap<Arc<Path>, PathMatch> {
218    let history_items_by_worktrees = history_items
219        .iter()
220        .filter_map(|found_path| {
221            let candidate = PathMatchCandidate {
222                path: &found_path.project.path,
223                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
224                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
225                // it would be shown first always, despite the latter being a better match.
226                char_bag: CharBag::from_iter(
227                    found_path
228                        .project
229                        .path
230                        .file_name()?
231                        .to_string_lossy()
232                        .to_lowercase()
233                        .chars(),
234                ),
235            };
236            Some((found_path.project.worktree_id, candidate))
237        })
238        .fold(
239            HashMap::default(),
240            |mut candidates, (worktree_id, new_candidate)| {
241                candidates
242                    .entry(worktree_id)
243                    .or_insert_with(Vec::new)
244                    .push(new_candidate);
245                candidates
246            },
247        );
248    let mut matching_history_paths = HashMap::default();
249    for (worktree, candidates) in history_items_by_worktrees {
250        let max_results = candidates.len() + 1;
251        matching_history_paths.extend(
252            fuzzy::match_fixed_path_set(
253                candidates,
254                worktree.to_usize(),
255                query.path_like.path_query(),
256                false,
257                max_results,
258            )
259            .into_iter()
260            .map(|path_match| (Arc::clone(&path_match.path), path_match)),
261        );
262    }
263    matching_history_paths
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
267struct FoundPath {
268    project: ProjectPath,
269    absolute: Option<PathBuf>,
270}
271
272impl FoundPath {
273    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
274        Self { project, absolute }
275    }
276}
277
278const MAX_RECENT_SELECTIONS: usize = 20;
279
280#[cfg(not(test))]
281fn history_file_exists(abs_path: &PathBuf) -> bool {
282    abs_path.exists()
283}
284
285#[cfg(test)]
286fn history_file_exists(abs_path: &PathBuf) -> bool {
287    !abs_path.ends_with("nonexistent.rs")
288}
289
290pub enum Event {
291    Selected(ProjectPath),
292    Dismissed,
293}
294
295#[derive(Debug, Clone)]
296struct FileSearchQuery {
297    raw_query: String,
298    file_query_end: Option<usize>,
299}
300
301impl FileSearchQuery {
302    fn path_query(&self) -> &str {
303        match self.file_query_end {
304            Some(file_path_end) => &self.raw_query[..file_path_end],
305            None => &self.raw_query,
306        }
307    }
308}
309
310impl FileFinderDelegate {
311    fn new(
312        file_finder: WeakView<FileFinder>,
313        workspace: WeakView<Workspace>,
314        project: Model<Project>,
315        currently_opened_path: Option<FoundPath>,
316        history_items: Vec<FoundPath>,
317        cx: &mut ViewContext<FileFinder>,
318    ) -> Self {
319        cx.observe(&project, |file_finder, _, cx| {
320            //todo We should probably not re-render on every project anything
321            file_finder
322                .picker
323                .update(cx, |picker, cx| picker.refresh(cx))
324        })
325        .detach();
326
327        Self {
328            file_finder,
329            workspace,
330            project,
331            search_count: 0,
332            latest_search_id: 0,
333            latest_search_did_cancel: false,
334            latest_search_query: None,
335            currently_opened_path,
336            matches: Matches::default(),
337            selected_index: None,
338            cancel_flag: Arc::new(AtomicBool::new(false)),
339            history_items,
340        }
341    }
342
343    fn spawn_search(
344        &mut self,
345        query: PathLikeWithPosition<FileSearchQuery>,
346        cx: &mut ViewContext<Picker<Self>>,
347    ) -> Task<()> {
348        let relative_to = self
349            .currently_opened_path
350            .as_ref()
351            .map(|found_path| Arc::clone(&found_path.project.path));
352        let worktrees = self
353            .project
354            .read(cx)
355            .visible_worktrees(cx)
356            .collect::<Vec<_>>();
357        let include_root_name = worktrees.len() > 1;
358        let candidate_sets = worktrees
359            .into_iter()
360            .map(|worktree| {
361                let worktree = worktree.read(cx);
362                PathMatchCandidateSet {
363                    snapshot: worktree.snapshot(),
364                    include_ignored: worktree
365                        .root_entry()
366                        .map_or(false, |entry| entry.is_ignored),
367                    include_root_name,
368                }
369            })
370            .collect::<Vec<_>>();
371
372        let search_id = util::post_inc(&mut self.search_count);
373        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
374        self.cancel_flag = Arc::new(AtomicBool::new(false));
375        let cancel_flag = self.cancel_flag.clone();
376        cx.spawn(|picker, mut cx| async move {
377            let matches = fuzzy::match_path_sets(
378                candidate_sets.as_slice(),
379                query.path_like.path_query(),
380                relative_to,
381                false,
382                100,
383                &cancel_flag,
384                cx.background_executor().clone(),
385            )
386            .await;
387            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
388            picker
389                .update(&mut cx, |picker, cx| {
390                    picker.delegate.selected_index.take();
391                    picker
392                        .delegate
393                        .set_search_matches(search_id, did_cancel, query, matches, cx)
394                })
395                .log_err();
396        })
397    }
398
399    fn set_search_matches(
400        &mut self,
401        search_id: usize,
402        did_cancel: bool,
403        query: PathLikeWithPosition<FileSearchQuery>,
404        matches: Vec<PathMatch>,
405        cx: &mut ViewContext<Picker<Self>>,
406    ) {
407        if search_id >= self.latest_search_id {
408            self.latest_search_id = search_id;
409            let extend_old_matches = self.latest_search_did_cancel
410                && Some(query.path_like.path_query())
411                    == self
412                        .latest_search_query
413                        .as_ref()
414                        .map(|query| query.path_like.path_query());
415            self.matches
416                .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
417            self.latest_search_query = Some(query);
418            self.latest_search_did_cancel = did_cancel;
419            cx.notify();
420        }
421    }
422
423    fn labels_for_match(
424        &self,
425        path_match: Match,
426        cx: &AppContext,
427        ix: usize,
428    ) -> (String, Vec<usize>, String, Vec<usize>) {
429        let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
430            Match::History(found_path, found_path_match) => {
431                let worktree_id = found_path.project.worktree_id;
432                let project_relative_path = &found_path.project.path;
433                let has_worktree = self
434                    .project
435                    .read(cx)
436                    .worktree_for_id(worktree_id, cx)
437                    .is_some();
438
439                if !has_worktree {
440                    if let Some(absolute_path) = &found_path.absolute {
441                        return (
442                            absolute_path
443                                .file_name()
444                                .map_or_else(
445                                    || project_relative_path.to_string_lossy(),
446                                    |file_name| file_name.to_string_lossy(),
447                                )
448                                .to_string(),
449                            Vec::new(),
450                            absolute_path.to_string_lossy().to_string(),
451                            Vec::new(),
452                        );
453                    }
454                }
455
456                let mut path = Arc::clone(project_relative_path);
457                if project_relative_path.as_ref() == Path::new("") {
458                    if let Some(absolute_path) = &found_path.absolute {
459                        path = Arc::from(absolute_path.as_path());
460                    }
461                }
462
463                let mut path_match = PathMatch {
464                    score: ix as f64,
465                    positions: Vec::new(),
466                    worktree_id: worktree_id.to_usize(),
467                    path,
468                    path_prefix: "".into(),
469                    distance_to_relative_ancestor: usize::MAX,
470                };
471                if let Some(found_path_match) = found_path_match {
472                    path_match
473                        .positions
474                        .extend(found_path_match.positions.iter())
475                }
476
477                self.labels_for_path_match(&path_match)
478            }
479            Match::Search(path_match) => self.labels_for_path_match(path_match),
480        };
481
482        if file_name_positions.is_empty() {
483            if let Some(user_home_path) = std::env::var("HOME").ok() {
484                let user_home_path = user_home_path.trim();
485                if !user_home_path.is_empty() {
486                    if (&full_path).starts_with(user_home_path) {
487                        return (
488                            file_name,
489                            file_name_positions,
490                            full_path.replace(user_home_path, "~"),
491                            full_path_positions,
492                        );
493                    }
494                }
495            }
496        }
497
498        (
499            file_name,
500            file_name_positions,
501            full_path,
502            full_path_positions,
503        )
504    }
505
506    fn labels_for_path_match(
507        &self,
508        path_match: &PathMatch,
509    ) -> (String, Vec<usize>, String, Vec<usize>) {
510        let path = &path_match.path;
511        let path_string = path.to_string_lossy();
512        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
513        let path_positions = path_match.positions.clone();
514
515        let file_name = path.file_name().map_or_else(
516            || path_match.path_prefix.to_string(),
517            |file_name| file_name.to_string_lossy().to_string(),
518        );
519        let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
520        let file_name_positions = path_positions
521            .iter()
522            .filter_map(|pos| {
523                if pos >= &file_name_start {
524                    Some(pos - file_name_start)
525                } else {
526                    None
527                }
528            })
529            .collect();
530
531        (file_name, file_name_positions, full_path, path_positions)
532    }
533
534    fn lookup_absolute_path(
535        &self,
536        query: PathLikeWithPosition<FileSearchQuery>,
537        cx: &mut ViewContext<'_, Picker<Self>>,
538    ) -> Task<()> {
539        cx.spawn(|picker, mut cx| async move {
540            let Some((project, fs)) = picker
541                .update(&mut cx, |picker, cx| {
542                    let fs = Arc::clone(&picker.delegate.project.read(cx).fs());
543                    (picker.delegate.project.clone(), fs)
544                })
545                .log_err()
546            else {
547                return;
548            };
549
550            let query_path = Path::new(query.path_like.path_query());
551            let mut path_matches = Vec::new();
552            match fs.metadata(query_path).await.log_err() {
553                Some(Some(_metadata)) => {
554                    let update_result = project
555                        .update(&mut cx, |project, cx| {
556                            if let Some((worktree, relative_path)) =
557                                project.find_local_worktree(query_path, cx)
558                            {
559                                path_matches.push(PathMatch {
560                                    score: 0.0,
561                                    positions: Vec::new(),
562                                    worktree_id: worktree.read(cx).id().to_usize(),
563                                    path: Arc::from(relative_path),
564                                    path_prefix: "".into(),
565                                    distance_to_relative_ancestor: usize::MAX,
566                                });
567                            }
568                        })
569                        .log_err();
570                    if update_result.is_none() {
571                        return;
572                    }
573                }
574                Some(None) => {}
575                None => return,
576            }
577
578            picker
579                .update(&mut cx, |picker, cx| {
580                    let picker_delegate = &mut picker.delegate;
581                    let search_id = util::post_inc(&mut picker_delegate.search_count);
582                    picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
583
584                    anyhow::Ok(())
585                })
586                .log_err();
587        })
588    }
589}
590
591impl PickerDelegate for FileFinderDelegate {
592    type ListItem = ListItem;
593
594    fn placeholder_text(&self) -> Arc<str> {
595        "Search project files...".into()
596    }
597
598    fn match_count(&self) -> usize {
599        self.matches.len()
600    }
601
602    fn selected_index(&self) -> usize {
603        self.selected_index.unwrap_or(0)
604    }
605
606    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
607        self.selected_index = Some(ix);
608        cx.notify();
609    }
610
611    fn separators_after_indices(&self) -> Vec<usize> {
612        let history_items = self.matches.history.len();
613        if history_items == 0 || self.matches.search.is_empty() {
614            Vec::new()
615        } else {
616            vec![history_items - 1]
617        }
618    }
619
620    fn update_matches(
621        &mut self,
622        raw_query: String,
623        cx: &mut ViewContext<Picker<Self>>,
624    ) -> Task<()> {
625        let raw_query = raw_query.trim();
626        if raw_query.is_empty() {
627            let project = self.project.read(cx);
628            self.latest_search_id = post_inc(&mut self.search_count);
629            self.selected_index.take();
630            self.matches = Matches {
631                history: self
632                    .history_items
633                    .iter()
634                    .filter(|history_item| {
635                        project
636                            .worktree_for_id(history_item.project.worktree_id, cx)
637                            .is_some()
638                            || (project.is_local() && history_item.absolute.is_some())
639                    })
640                    .cloned()
641                    .map(|p| (p, None))
642                    .collect(),
643                search: Vec::new(),
644            };
645            cx.notify();
646            Task::ready(())
647        } else {
648            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
649                Ok::<_, std::convert::Infallible>(FileSearchQuery {
650                    raw_query: raw_query.to_owned(),
651                    file_query_end: if path_like_str == raw_query {
652                        None
653                    } else {
654                        Some(path_like_str.len())
655                    },
656                })
657            })
658            .expect("infallible");
659
660            if Path::new(query.path_like.path_query()).is_absolute() {
661                self.lookup_absolute_path(query, cx)
662            } else {
663                self.spawn_search(query, cx)
664            }
665        }
666    }
667
668    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
669        if let Some(m) = self.matches.get(self.selected_index()) {
670            if let Some(workspace) = self.workspace.upgrade() {
671                let open_task = workspace.update(cx, move |workspace, cx| {
672                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
673                        if secondary {
674                            workspace.split_path(project_path, cx)
675                        } else {
676                            workspace.open_path(project_path, None, true, cx)
677                        }
678                    };
679                    match m {
680                        Match::History(history_match, _) => {
681                            let worktree_id = history_match.project.worktree_id;
682                            if workspace
683                                .project()
684                                .read(cx)
685                                .worktree_for_id(worktree_id, cx)
686                                .is_some()
687                            {
688                                split_or_open(
689                                    workspace,
690                                    ProjectPath {
691                                        worktree_id,
692                                        path: Arc::clone(&history_match.project.path),
693                                    },
694                                    cx,
695                                )
696                            } else {
697                                match history_match.absolute.as_ref() {
698                                    Some(abs_path) => {
699                                        if secondary {
700                                            workspace.split_abs_path(
701                                                abs_path.to_path_buf(),
702                                                false,
703                                                cx,
704                                            )
705                                        } else {
706                                            workspace.open_abs_path(
707                                                abs_path.to_path_buf(),
708                                                false,
709                                                cx,
710                                            )
711                                        }
712                                    }
713                                    None => split_or_open(
714                                        workspace,
715                                        ProjectPath {
716                                            worktree_id,
717                                            path: Arc::clone(&history_match.project.path),
718                                        },
719                                        cx,
720                                    ),
721                                }
722                            }
723                        }
724                        Match::Search(m) => split_or_open(
725                            workspace,
726                            ProjectPath {
727                                worktree_id: WorktreeId::from_usize(m.worktree_id),
728                                path: m.path.clone(),
729                            },
730                            cx,
731                        ),
732                    }
733                });
734
735                let row = self
736                    .latest_search_query
737                    .as_ref()
738                    .and_then(|query| query.row)
739                    .map(|row| row.saturating_sub(1));
740                let col = self
741                    .latest_search_query
742                    .as_ref()
743                    .and_then(|query| query.column)
744                    .unwrap_or(0)
745                    .saturating_sub(1);
746                let finder = self.file_finder.clone();
747
748                cx.spawn(|_, mut cx| async move {
749                    let item = open_task.await.log_err()?;
750                    if let Some(row) = row {
751                        if let Some(active_editor) = item.downcast::<Editor>() {
752                            active_editor
753                                .downgrade()
754                                .update(&mut cx, |editor, cx| {
755                                    let snapshot = editor.snapshot(cx).display_snapshot;
756                                    let point = snapshot
757                                        .buffer_snapshot
758                                        .clip_point(Point::new(row, col), Bias::Left);
759                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
760                                        s.select_ranges([point..point])
761                                    });
762                                })
763                                .log_err();
764                        }
765                    }
766                    finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
767
768                    Some(())
769                })
770                .detach();
771            }
772        }
773    }
774
775    fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
776        self.file_finder
777            .update(cx, |_, cx| cx.emit(DismissEvent))
778            .log_err();
779    }
780
781    fn render_match(
782        &self,
783        ix: usize,
784        selected: bool,
785        cx: &mut ViewContext<Picker<Self>>,
786    ) -> Option<Self::ListItem> {
787        let path_match = self
788            .matches
789            .get(ix)
790            .expect("Invalid matches state: no element for index {ix}");
791
792        let (file_name, file_name_positions, full_path, full_path_positions) =
793            self.labels_for_match(path_match, cx, ix);
794
795        Some(
796            ListItem::new(ix)
797                .spacing(ListItemSpacing::Sparse)
798                .inset(true)
799                .selected(selected)
800                .child(
801                    v_flex()
802                        .child(HighlightedLabel::new(file_name, file_name_positions))
803                        .child(HighlightedLabel::new(full_path, full_path_positions)),
804                ),
805        )
806    }
807}