@@ -18,16 +18,133 @@ use text::{Anchor, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
-use crate::context::AssistantContext;
+use crate::context_picker::file_context_picker::search_files;
+use crate::context_picker::symbol_context_picker::search_symbols;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
use super::fetch_context_picker::fetch_url_content;
-use super::thread_context_picker::ThreadContextEntry;
+use super::file_context_picker::FileMatch;
+use super::symbol_context_picker::SymbolMatch;
+use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::{
- ContextPickerMode, MentionLink, recent_context_picker_entries, supported_context_picker_modes,
+ ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
+ supported_context_picker_modes,
};
+pub(crate) enum Match {
+ Symbol(SymbolMatch),
+ File(FileMatch),
+ Thread(ThreadMatch),
+ Fetch(SharedString),
+ Mode(ContextPickerMode),
+}
+
+fn search(
+ mode: Option<ContextPickerMode>,
+ query: String,
+ cancellation_flag: Arc<AtomicBool>,
+ recent_entries: Vec<RecentEntry>,
+ thread_store: Option<WeakEntity<ThreadStore>>,
+ workspace: Entity<Workspace>,
+ cx: &mut App,
+) -> Task<Vec<Match>> {
+ match mode {
+ Some(ContextPickerMode::File) => {
+ let search_files_task =
+ search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+ cx.background_spawn(async move {
+ search_files_task
+ .await
+ .into_iter()
+ .map(Match::File)
+ .collect()
+ })
+ }
+ Some(ContextPickerMode::Symbol) => {
+ let search_symbols_task =
+ search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
+ cx.background_spawn(async move {
+ search_symbols_task
+ .await
+ .into_iter()
+ .map(Match::Symbol)
+ .collect()
+ })
+ }
+ Some(ContextPickerMode::Thread) => {
+ if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
+ let search_threads_task =
+ search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx);
+ cx.background_spawn(async move {
+ search_threads_task
+ .await
+ .into_iter()
+ .map(Match::Thread)
+ .collect()
+ })
+ } else {
+ Task::ready(Vec::new())
+ }
+ }
+ Some(ContextPickerMode::Fetch) => {
+ if !query.is_empty() {
+ Task::ready(vec![Match::Fetch(query.into())])
+ } else {
+ Task::ready(Vec::new())
+ }
+ }
+ None => {
+ if query.is_empty() {
+ let mut matches = recent_entries
+ .into_iter()
+ .map(|entry| match entry {
+ super::RecentEntry::File {
+ project_path,
+ path_prefix,
+ } => Match::File(FileMatch {
+ mat: fuzzy::PathMatch {
+ score: 1.,
+ positions: Vec::new(),
+ worktree_id: project_path.worktree_id.to_usize(),
+ path: project_path.path,
+ path_prefix,
+ is_dir: false,
+ distance_to_relative_ancestor: 0,
+ },
+ is_recent: true,
+ }),
+ super::RecentEntry::Thread(thread_context_entry) => {
+ Match::Thread(ThreadMatch {
+ thread: thread_context_entry,
+ is_recent: true,
+ })
+ }
+ })
+ .collect::<Vec<_>>();
+
+ matches.extend(
+ supported_context_picker_modes(&thread_store)
+ .into_iter()
+ .map(Match::Mode),
+ );
+
+ Task::ready(matches)
+ } else {
+ let search_files_task =
+ search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+ cx.background_spawn(async move {
+ search_files_task
+ .await
+ .into_iter()
+ .map(Match::File)
+ .collect()
+ })
+ }
+ }
+ }
+}
+
pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
@@ -50,97 +167,20 @@ impl ContextPickerCompletionProvider {
}
}
- fn default_completions(
- excerpt_id: ExcerptId,
- source_range: Range<Anchor>,
- context_store: Entity<ContextStore>,
- thread_store: Option<WeakEntity<ThreadStore>>,
- editor: Entity<Editor>,
- workspace: Entity<Workspace>,
- cx: &App,
- ) -> Vec<Completion> {
- let mut completions = Vec::new();
-
- completions.extend(
- recent_context_picker_entries(
- context_store.clone(),
- thread_store.clone(),
- workspace.clone(),
- cx,
- )
- .iter()
- .filter_map(|entry| match entry {
- super::RecentEntry::File {
- project_path,
- path_prefix,
- } => Some(Self::completion_for_path(
- project_path.clone(),
- path_prefix,
- true,
- false,
- excerpt_id,
- source_range.clone(),
- editor.clone(),
- context_store.clone(),
- cx,
- )),
- super::RecentEntry::Thread(thread_context_entry) => {
- let thread_store = thread_store
- .as_ref()
- .and_then(|thread_store| thread_store.upgrade())?;
- Some(Self::completion_for_thread(
- thread_context_entry.clone(),
- excerpt_id,
- source_range.clone(),
- true,
- editor.clone(),
- context_store.clone(),
- thread_store,
- ))
- }
- }),
- );
-
- completions.extend(
- supported_context_picker_modes(&thread_store)
- .iter()
- .map(|mode| {
- Completion {
- replace_range: source_range.clone(),
- new_text: format!("@{} ", mode.mention_prefix()),
- label: CodeLabel::plain(mode.label().to_string(), None),
- icon_path: Some(mode.icon().path().into()),
- documentation: None,
- source: project::CompletionSource::Custom,
- insert_text_mode: None,
- // This ensures that when a user accepts this completion, the
- // completion menu will still be shown after "@category " is
- // inserted
- confirm: Some(Arc::new(|_, _, _| true)),
- }
- }),
- );
- completions
- }
-
- fn build_code_label_for_full_path(
- file_name: &str,
- directory: Option<&str>,
- cx: &App,
- ) -> CodeLabel {
- let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
- let mut label = CodeLabel::default();
-
- label.push_str(&file_name, None);
- label.push_str(" ", None);
-
- if let Some(directory) = directory {
- label.push_str(&directory, comment_id);
+ fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
+ Completion {
+ replace_range: source_range.clone(),
+ new_text: format!("@{} ", mode.mention_prefix()),
+ label: CodeLabel::plain(mode.label().to_string(), None),
+ icon_path: Some(mode.icon().path().into()),
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ insert_text_mode: None,
+ // This ensures that when a user accepts this completion, the
+ // completion menu will still be shown after "@category " is
+ // inserted
+ confirm: Some(Arc::new(|_, _, _| true)),
}
-
- label.filter_range = 0..label.text().len();
-
- label
}
fn completion_for_thread(
@@ -261,11 +301,8 @@ impl ContextPickerCompletionProvider {
path_prefix,
);
- let label = Self::build_code_label_for_full_path(
- &file_name,
- directory.as_ref().map(|s| s.as_ref()),
- cx,
- );
+ let label =
+ build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
@@ -382,6 +419,22 @@ impl ContextPickerCompletionProvider {
}
}
+fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
+ let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
+ let mut label = CodeLabel::default();
+
+ label.push_str(&file_name, None);
+ label.push_str(" ", None);
+
+ if let Some(directory) = directory {
+ label.push_str(&directory, comment_id);
+ }
+
+ label.filter_range = 0..label.text().len();
+
+ label
+}
+
impl CompletionProvider for ContextPickerCompletionProvider {
fn completions(
&self,
@@ -404,10 +457,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(None));
};
- let Some(workspace) = self.workspace.upgrade() else {
- return Task::ready(Ok(None));
- };
- let Some(context_store) = self.context_store.upgrade() else {
+ let Some((workspace, context_store)) =
+ self.workspace.upgrade().zip(self.context_store.upgrade())
+ else {
return Task::ready(Ok(None));
};
@@ -419,154 +471,89 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client().clone();
+ let MentionCompletion { mode, argument, .. } = state;
+ let query = argument.unwrap_or_else(|| "".to_string());
+
+ let recent_entries = recent_context_picker_entries(
+ context_store.clone(),
+ thread_store.clone(),
+ workspace.clone(),
+ cx,
+ );
+
+ let search_task = search(
+ mode,
+ query,
+ Arc::<AtomicBool>::default(),
+ recent_entries,
+ thread_store.clone(),
+ workspace.clone(),
+ cx,
+ );
+
cx.spawn(async move |_, cx| {
- let mut completions = Vec::new();
-
- let MentionCompletion { mode, argument, .. } = state;
-
- let query = argument.unwrap_or_else(|| "".to_string());
- match mode {
- Some(ContextPickerMode::File) => {
- let path_matches = cx
- .update(|cx| {
- super::file_context_picker::search_paths(
- query,
- Arc::<AtomicBool>::default(),
- &workspace,
- cx,
- )
- })?
- .await;
-
- if let Some(editor) = editor.upgrade() {
- completions.reserve(path_matches.len());
- cx.update(|cx| {
- completions.extend(path_matches.iter().map(|mat| {
- Self::completion_for_path(
- ProjectPath {
- worktree_id: WorktreeId::from_usize(mat.worktree_id),
- path: mat.path.clone(),
- },
- &mat.path_prefix,
- false,
- mat.is_dir,
- excerpt_id,
- source_range.clone(),
- editor.clone(),
- context_store.clone(),
- cx,
- )
- }));
- })?;
- }
- }
- Some(ContextPickerMode::Symbol) => {
- if let Some(editor) = editor.upgrade() {
- let symbol_matches = cx
- .update(|cx| {
- super::symbol_context_picker::search_symbols(
- query,
- Arc::new(AtomicBool::default()),
- &workspace,
- cx,
- )
- })?
- .await?;
- cx.update(|cx| {
- completions.extend(symbol_matches.into_iter().filter_map(
- |(_, symbol)| {
- Self::completion_for_symbol(
- symbol,
- excerpt_id,
- source_range.clone(),
- editor.clone(),
- context_store.clone(),
- workspace.clone(),
- cx,
- )
+ let matches = search_task.await;
+ let Some(editor) = editor.upgrade() else {
+ return Ok(None);
+ };
+
+ Ok(Some(cx.update(|cx| {
+ matches
+ .into_iter()
+ .filter_map(|mat| match mat {
+ Match::File(FileMatch { mat, is_recent }) => {
+ Some(Self::completion_for_path(
+ ProjectPath {
+ worktree_id: WorktreeId::from_usize(mat.worktree_id),
+ path: mat.path.clone(),
},
- ));
- })?;
- }
- }
- Some(ContextPickerMode::Fetch) => {
- if let Some(editor) = editor.upgrade() {
- if !query.is_empty() {
- completions.push(Self::completion_for_fetch(
- source_range.clone(),
- query.into(),
+ &mat.path_prefix,
+ is_recent,
+ mat.is_dir,
excerpt_id,
+ source_range.clone(),
editor.clone(),
context_store.clone(),
- http_client.clone(),
- ));
+ cx,
+ ))
}
-
- context_store.update(cx, |store, _| {
- let urls = store.context().iter().filter_map(|context| {
- if let AssistantContext::FetchedUrl(context) = context {
- Some(context.url.clone())
- } else {
- None
- }
- });
- for url in urls {
- completions.push(Self::completion_for_fetch(
- source_range.clone(),
- url,
- excerpt_id,
- editor.clone(),
- context_store.clone(),
- http_client.clone(),
- ));
- }
- })?;
- }
- }
- Some(ContextPickerMode::Thread) => {
- if let Some((thread_store, editor)) = thread_store
- .and_then(|thread_store| thread_store.upgrade())
- .zip(editor.upgrade())
- {
- let threads = cx
- .update(|cx| {
- super::thread_context_picker::search_threads(
- query,
- thread_store.clone(),
- cx,
- )
- })?
- .await;
- for thread in threads {
- completions.push(Self::completion_for_thread(
- thread.clone(),
+ Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
+ symbol,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ context_store.clone(),
+ workspace.clone(),
+ cx,
+ ),
+ Match::Thread(ThreadMatch {
+ thread, is_recent, ..
+ }) => {
+ let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
+ Some(Self::completion_for_thread(
+ thread,
excerpt_id,
source_range.clone(),
- false,
+ is_recent,
editor.clone(),
context_store.clone(),
- thread_store.clone(),
- ));
+ thread_store,
+ ))
}
- }
- }
- None => {
- cx.update(|cx| {
- if let Some(editor) = editor.upgrade() {
- completions.extend(Self::default_completions(
- excerpt_id,
- source_range.clone(),
- context_store.clone(),
- thread_store.clone(),
- editor,
- workspace.clone(),
- cx,
- ));
+ Match::Fetch(url) => Some(Self::completion_for_fetch(
+ source_range.clone(),
+ url,
+ excerpt_id,
+ editor.clone(),
+ context_store.clone(),
+ http_client.clone(),
+ )),
+ Match::Mode(mode) => {
+ Some(Self::completion_for_mode(source_range.clone(), mode))
}
- })?;
- }
- }
- Ok(Some(completions))
+ })
+ .collect()
+ })?))
})
}
@@ -676,7 +663,12 @@ impl MentionCompletion {
let mut end = last_mention_start + 1;
if let Some(mode_text) = parts.next() {
end += mode_text.len();
- mode = ContextPickerMode::try_from(mode_text).ok();
+
+ if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
+ mode = Some(parsed_mode);
+ } else {
+ argument = Some(mode_text.to_string());
+ }
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
Some(whitespace_count) => {
if let Some(argument_text) = parts.next() {
@@ -702,13 +694,13 @@ impl MentionCompletion {
#[cfg(test)]
mod tests {
use super::*;
- use gpui::{Focusable, TestAppContext, VisualTestContext};
+ use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
use project::{Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
- use std::{ops::Deref, path::PathBuf};
+ use std::ops::Deref;
use util::{path, separator};
- use workspace::AppState;
+ use workspace::{AppState, Item};
#[test]
fn test_mention_completion_parse() {
@@ -768,9 +760,42 @@ mod tests {
})
);
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @main", 0),
+ Some(MentionCompletion {
+ source_range: 6..11,
+ mode: None,
+ argument: Some("main".to_string()),
+ })
+ );
+
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
}
+ struct AtMentionEditor(Entity<Editor>);
+
+ impl Item for AtMentionEditor {
+ type Event = ();
+
+ fn include_in_nav_history() -> bool {
+ false
+ }
+ }
+
+ impl EventEmitter<()> for AtMentionEditor {}
+
+ impl Focusable for AtMentionEditor {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.0.read(cx).focus_handle(cx).clone()
+ }
+ }
+
+ impl Render for AtMentionEditor {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ self.0.clone().into_any_element()
+ }
+ }
+
#[gpui::test]
async fn test_context_completion_provider(cx: &mut TestAppContext) {
init_test(cx);
@@ -846,25 +871,27 @@ mod tests {
.unwrap();
}
- let item = workspace
- .update_in(&mut cx, |workspace, window, cx| {
- workspace.open_path(
- ProjectPath {
- worktree_id,
- path: PathBuf::from("editor").into(),
- },
+ let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
+ let editor = cx.new(|cx| {
+ Editor::new(
+ editor::EditorMode::Full,
+ multi_buffer::MultiBuffer::build_simple("", cx),
None,
- true,
window,
cx,
)
- })
- .await
- .expect("Could not open test file");
-
- let editor = cx.update(|_, cx| {
- item.act_as::<Editor>(cx)
- .expect("Opened test file wasn't an editor")
+ });
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(
+ Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
+ true,
+ true,
+ None,
+ window,
+ cx,
+ );
+ });
+ editor
});
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
@@ -895,10 +922,10 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
- "editor dir/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
+ "four.txt dir/a/",
"Files & Directories",
"Symbols",
"Fetch"
@@ -993,14 +1020,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)"
+ "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
- Point::new(0, 44)..Point::new(0, 71)
+ Point::new(0, 44)..Point::new(0, 79)
]
);
});
@@ -1010,14 +1037,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n@"
+ "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
- Point::new(0, 44)..Point::new(0, 71)
+ Point::new(0, 44)..Point::new(0, 79)
]
);
});
@@ -1031,15 +1058,15 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n[@seven.txt](@file:dir/b/seven.txt)"
+ "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
- Point::new(0, 44)..Point::new(0, 71),
- Point::new(1, 0)..Point::new(1, 35)
+ Point::new(0, 44)..Point::new(0, 79),
+ Point::new(1, 0)..Point::new(1, 31)
]
);
});
@@ -58,7 +58,7 @@ pub struct FileContextPickerDelegate {
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
- matches: Vec<PathMatch>,
+ matches: Vec<FileMatch>,
selected_index: usize,
}
@@ -114,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return Task::ready(());
};
- let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
+ let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background.
@@ -128,7 +128,7 @@ impl PickerDelegate for FileContextPickerDelegate {
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
- let Some(mat) = self.matches.get(self.selected_index) else {
+ let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
return;
};
@@ -181,7 +181,7 @@ impl PickerDelegate for FileContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let path_match = &self.matches[ix];
+ let FileMatch { mat, .. } = &self.matches[ix];
Some(
ListItem::new(ix)
@@ -189,9 +189,9 @@ impl PickerDelegate for FileContextPickerDelegate {
.toggle_state(selected)
.child(render_file_context_entry(
ElementId::NamedInteger("file-ctx-picker".into(), ix),
- &path_match.path,
- &path_match.path_prefix,
- path_match.is_dir,
+ &mat.path,
+ &mat.path_prefix,
+ mat.is_dir,
self.context_store.clone(),
cx,
)),
@@ -199,12 +199,17 @@ impl PickerDelegate for FileContextPickerDelegate {
}
}
-pub(crate) fn search_paths(
+pub struct FileMatch {
+ pub mat: PathMatch,
+ pub is_recent: bool,
+}
+
+pub(crate) fn search_files(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &App,
-) -> Task<Vec<PathMatch>> {
+) -> Task<Vec<FileMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
@@ -213,28 +218,34 @@ pub(crate) fn search_paths(
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
- Some(PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: project_path.worktree_id.to_usize(),
- path: project_path.path,
- path_prefix: worktree.read(cx).root_name().into(),
- distance_to_relative_ancestor: 0,
- is_dir: false,
+ Some(FileMatch {
+ mat: PathMatch {
+ score: 0.,
+ positions: Vec::new(),
+ worktree_id: project_path.worktree_id.to_usize(),
+ path: project_path.path,
+ path_prefix: worktree.read(cx).root_name().into(),
+ distance_to_relative_ancestor: 0,
+ is_dir: false,
+ },
+ is_recent: true,
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
- worktree.entries(false, 0).map(move |entry| PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: worktree.id().to_usize(),
- path: entry.path.clone(),
- path_prefix: path_prefix.clone(),
- distance_to_relative_ancestor: 0,
- is_dir: entry.is_dir(),
+ worktree.entries(false, 0).map(move |entry| FileMatch {
+ mat: PathMatch {
+ score: 0.,
+ positions: Vec::new(),
+ worktree_id: worktree.id().to_usize(),
+ path: entry.path.clone(),
+ path_prefix: path_prefix.clone(),
+ distance_to_relative_ancestor: 0,
+ is_dir: entry.is_dir(),
+ },
+ is_recent: false,
})
});
@@ -269,6 +280,12 @@ pub(crate) fn search_paths(
executor,
)
.await
+ .into_iter()
+ .map(|mat| FileMatch {
+ mat,
+ is_recent: false,
+ })
+ .collect::<Vec<_>>()
})
}
}