agent: Fuzzy match on paths and symbols when typing @ (#28357)

Release Notes:

- agent: Improve fuzzy matching when using @-mentions
This commit is contained in:
Bennet Bo Fenner 2025-04-09 13:00:23 -06:00 committed by GitHub
parent 088d7c1342
commit 780143298a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 415 additions and 328 deletions

View file

@ -18,16 +18,133 @@ use text::{Anchor, ToPoint};
use ui::prelude::*; use ui::prelude::*;
use workspace::Workspace; 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::context_store::ContextStore;
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use super::fetch_context_picker::fetch_url_content; 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::{ 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 { pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
@ -50,97 +167,20 @@ impl ContextPickerCompletionProvider {
} }
} }
fn default_completions( fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
excerpt_id: ExcerptId, Completion {
source_range: Range<Anchor>, replace_range: source_range.clone(),
context_store: Entity<ContextStore>, new_text: format!("@{} ", mode.mention_prefix()),
thread_store: Option<WeakEntity<ThreadStore>>, label: CodeLabel::plain(mode.label().to_string(), None),
editor: Entity<Editor>, icon_path: Some(mode.icon().path().into()),
workspace: Entity<Workspace>, documentation: None,
cx: &App, source: project::CompletionSource::Custom,
) -> Vec<Completion> { insert_text_mode: None,
let mut completions = Vec::new(); // This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
completions.extend( // inserted
recent_context_picker_entries( confirm: Some(Arc::new(|_, _, _| true)),
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);
} }
label.filter_range = 0..label.text().len();
label
} }
fn completion_for_thread( fn completion_for_thread(
@ -261,11 +301,8 @@ impl ContextPickerCompletionProvider {
path_prefix, path_prefix,
); );
let label = Self::build_code_label_for_full_path( let label =
&file_name, build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
directory.as_ref().map(|s| s.as_ref()),
cx,
);
let full_path = if let Some(directory) = directory { let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name) format!("{}{}", directory, file_name)
} else { } 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 { impl CompletionProvider for ContextPickerCompletionProvider {
fn completions( fn completions(
&self, &self,
@ -404,10 +457,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(None)); return Task::ready(Ok(None));
}; };
let Some(workspace) = self.workspace.upgrade() else { let Some((workspace, context_store)) =
return Task::ready(Ok(None)); self.workspace.upgrade().zip(self.context_store.upgrade())
}; else {
let Some(context_store) = self.context_store.upgrade() else {
return Task::ready(Ok(None)); return Task::ready(Ok(None));
}; };
@ -419,154 +471,89 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let editor = self.editor.clone(); let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client().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| { cx.spawn(async move |_, cx| {
let mut completions = Vec::new(); let matches = search_task.await;
let Some(editor) = editor.upgrade() else {
return Ok(None);
};
let MentionCompletion { mode, argument, .. } = state; Ok(Some(cx.update(|cx| {
matches
let query = argument.unwrap_or_else(|| "".to_string()); .into_iter()
match mode { .filter_map(|mat| match mat {
Some(ContextPickerMode::File) => { Match::File(FileMatch { mat, is_recent }) => {
let path_matches = cx Some(Self::completion_for_path(
.update(|cx| { ProjectPath {
super::file_context_picker::search_paths( worktree_id: WorktreeId::from_usize(mat.worktree_id),
query, path: mat.path.clone(),
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,
)
}, },
)); &mat.path_prefix,
})?; is_recent,
} mat.is_dir,
}
Some(ContextPickerMode::Fetch) => {
if let Some(editor) = editor.upgrade() {
if !query.is_empty() {
completions.push(Self::completion_for_fetch(
source_range.clone(),
query.into(),
excerpt_id, excerpt_id,
source_range.clone(),
editor.clone(), editor.clone(),
context_store.clone(), context_store.clone(),
http_client.clone(),
));
}
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(),
excerpt_id,
source_range.clone(),
false,
editor.clone(),
context_store.clone(),
thread_store.clone(),
));
}
}
}
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, cx,
)); ))
} }
})?; Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
} symbol,
} excerpt_id,
Ok(Some(completions)) 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(),
is_recent,
editor.clone(),
context_store.clone(),
thread_store,
))
}
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))
}
})
.collect()
})?))
}) })
} }
@ -676,7 +663,12 @@ impl MentionCompletion {
let mut end = last_mention_start + 1; let mut end = last_mention_start + 1;
if let Some(mode_text) = parts.next() { if let Some(mode_text) = parts.next() {
end += mode_text.len(); 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()) { match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
Some(whitespace_count) => { Some(whitespace_count) => {
if let Some(argument_text) = parts.next() { if let Some(argument_text) = parts.next() {
@ -702,13 +694,13 @@ impl MentionCompletion {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use gpui::{Focusable, TestAppContext, VisualTestContext}; use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::{ops::Deref, path::PathBuf}; use std::ops::Deref;
use util::{path, separator}; use util::{path, separator};
use workspace::AppState; use workspace::{AppState, Item};
#[test] #[test]
fn test_mention_completion_parse() { 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); 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] #[gpui::test]
async fn test_context_completion_provider(cx: &mut TestAppContext) { async fn test_context_completion_provider(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@ -846,25 +871,27 @@ mod tests {
.unwrap(); .unwrap();
} }
let item = workspace let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
.update_in(&mut cx, |workspace, window, cx| { let editor = cx.new(|cx| {
workspace.open_path( Editor::new(
ProjectPath { editor::EditorMode::Full,
worktree_id, multi_buffer::MultiBuffer::build_simple("", cx),
path: PathBuf::from("editor").into(),
},
None, None,
true,
window, window,
cx, cx,
) )
}) });
.await workspace.active_pane().update(cx, |pane, cx| {
.expect("Could not open test file"); pane.add_item(
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
let editor = cx.update(|_, cx| { true,
item.act_as::<Editor>(cx) true,
.expect("Opened test file wasn't an editor") None,
window,
cx,
);
});
editor
}); });
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None)); let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
@ -895,10 +922,10 @@ mod tests {
assert_eq!( assert_eq!(
current_completion_labels(editor), current_completion_labels(editor),
&[ &[
"editor dir/",
"seven.txt dir/b/", "seven.txt dir/b/",
"six.txt dir/b/", "six.txt dir/b/",
"five.txt dir/b/", "five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories", "Files & Directories",
"Symbols", "Symbols",
"Fetch" "Fetch"
@ -993,14 +1020,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| { editor.update(&mut cx, |editor, cx| {
assert_eq!( assert_eq!(
editor.text(cx), 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!(!editor.has_visible_completions_menu());
assert_eq!( assert_eq!(
crease_ranges(editor, cx), crease_ranges(editor, cx),
vec![ vec![
Point::new(0, 6)..Point::new(0, 37), 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| { editor.update(&mut cx, |editor, cx| {
assert_eq!( assert_eq!(
editor.text(cx), 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!(editor.has_visible_completions_menu());
assert_eq!( assert_eq!(
crease_ranges(editor, cx), crease_ranges(editor, cx),
vec![ vec![
Point::new(0, 6)..Point::new(0, 37), 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| { editor.update(&mut cx, |editor, cx| {
assert_eq!( assert_eq!(
editor.text(cx), 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!(!editor.has_visible_completions_menu());
assert_eq!( assert_eq!(
crease_ranges(editor, cx), crease_ranges(editor, cx),
vec![ vec![
Point::new(0, 6)..Point::new(0, 37), Point::new(0, 6)..Point::new(0, 37),
Point::new(0, 44)..Point::new(0, 71), Point::new(0, 44)..Point::new(0, 79),
Point::new(1, 0)..Point::new(1, 35) Point::new(1, 0)..Point::new(1, 31)
] ]
); );
}); });

View file

@ -58,7 +58,7 @@ pub struct FileContextPickerDelegate {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior, confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>, matches: Vec<FileMatch>,
selected_index: usize, selected_index: usize,
} }
@ -114,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return Task::ready(()); 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| { cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background. // 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>>) { 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; return;
}; };
@ -181,7 +181,7 @@ impl PickerDelegate for FileContextPickerDelegate {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Picker<Self>>, cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let path_match = &self.matches[ix]; let FileMatch { mat, .. } = &self.matches[ix];
Some( Some(
ListItem::new(ix) ListItem::new(ix)
@ -189,9 +189,9 @@ impl PickerDelegate for FileContextPickerDelegate {
.toggle_state(selected) .toggle_state(selected)
.child(render_file_context_entry( .child(render_file_context_entry(
ElementId::NamedInteger("file-ctx-picker".into(), ix), ElementId::NamedInteger("file-ctx-picker".into(), ix),
&path_match.path, &mat.path,
&path_match.path_prefix, &mat.path_prefix,
path_match.is_dir, mat.is_dir,
self.context_store.clone(), self.context_store.clone(),
cx, 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, query: String,
cancellation_flag: Arc<AtomicBool>, cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>, workspace: &Entity<Workspace>,
cx: &App, cx: &App,
) -> Task<Vec<PathMatch>> { ) -> Task<Vec<FileMatch>> {
if query.is_empty() { if query.is_empty() {
let workspace = workspace.read(cx); let workspace = workspace.read(cx);
let project = workspace.project().read(cx); let project = workspace.project().read(cx);
@ -213,28 +218,34 @@ pub(crate) fn search_paths(
.into_iter() .into_iter()
.filter_map(|(project_path, _)| { .filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?; let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(PathMatch { Some(FileMatch {
score: 0., mat: PathMatch {
positions: Vec::new(), score: 0.,
worktree_id: project_path.worktree_id.to_usize(), positions: Vec::new(),
path: project_path.path, worktree_id: project_path.worktree_id.to_usize(),
path_prefix: worktree.read(cx).root_name().into(), path: project_path.path,
distance_to_relative_ancestor: 0, path_prefix: worktree.read(cx).root_name().into(),
is_dir: false, distance_to_relative_ancestor: 0,
is_dir: false,
},
is_recent: true,
}) })
}); });
let file_matches = project.worktrees(cx).flat_map(|worktree| { let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into(); let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| PathMatch { worktree.entries(false, 0).map(move |entry| FileMatch {
score: 0., mat: PathMatch {
positions: Vec::new(), score: 0.,
worktree_id: worktree.id().to_usize(), positions: Vec::new(),
path: entry.path.clone(), worktree_id: worktree.id().to_usize(),
path_prefix: path_prefix.clone(), path: entry.path.clone(),
distance_to_relative_ancestor: 0, path_prefix: path_prefix.clone(),
is_dir: entry.is_dir(), distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
is_recent: false,
}) })
}); });
@ -269,6 +280,12 @@ pub(crate) fn search_paths(
executor, executor,
) )
.await .await
.into_iter()
.map(|mat| FileMatch {
mat,
is_recent: false,
})
.collect::<Vec<_>>()
}) })
} }
} }

View file

@ -2,7 +2,7 @@ use std::cmp::Reverse;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use anyhow::{Context as _, Result}; use anyhow::Result;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity, App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
@ -119,11 +119,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx); let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
let context_store = self.context_store.clone(); let context_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let symbols = search_task let symbols = search_task.await;
.await
.context("Failed to load symbols")
.log_err()
.unwrap_or_default();
let symbol_entries = context_store let symbol_entries = context_store
.read_with(cx, |context_store, cx| { .read_with(cx, |context_store, cx| {
@ -285,12 +281,16 @@ fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Optio
} }
} }
pub struct SymbolMatch {
pub symbol: Symbol,
}
pub(crate) fn search_symbols( pub(crate) fn search_symbols(
query: String, query: String,
cancellation_flag: Arc<AtomicBool>, cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>, workspace: &Entity<Workspace>,
cx: &mut App, cx: &mut App,
) -> Task<Result<Vec<(StringMatch, Symbol)>>> { ) -> Task<Vec<SymbolMatch>> {
let symbols_task = workspace.update(cx, |workspace, cx| { let symbols_task = workspace.update(cx, |workspace, cx| {
workspace workspace
.project() .project()
@ -298,19 +298,28 @@ pub(crate) fn search_symbols(
}); });
let project = workspace.read(cx).project().clone(); let project = workspace.read(cx).project().clone();
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
let symbols = symbols_task.await?; let Some(symbols) = symbols_task.await.log_err() else {
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project return Vec::new();
.update(cx, |project, cx| { };
symbols let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
.iter() project
.enumerate() .update(cx, |project, cx| {
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text())) symbols
.partition(|candidate| { .iter()
project .enumerate()
.entry_for_path(&symbols[candidate.id].path, cx) .map(|(id, symbol)| {
.map_or(false, |e| !e.is_ignored) StringMatchCandidate::new(id, &symbol.label.filter_text())
}) })
})?; .partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.map_or(false, |e| !e.is_ignored)
})
})
.log_err()
else {
return Vec::new();
};
const MAX_MATCHES: usize = 100; const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
@ -339,7 +348,7 @@ pub(crate) fn search_symbols(
let mut matches = visible_matches; let mut matches = visible_matches;
matches.append(&mut external_matches); matches.append(&mut external_matches);
Ok(matches matches
.into_iter() .into_iter()
.map(|mut mat| { .map(|mut mat| {
let symbol = symbols[mat.candidate_id].clone(); let symbol = symbols[mat.candidate_id].clone();
@ -347,19 +356,19 @@ pub(crate) fn search_symbols(
for position in &mut mat.positions { for position in &mut mat.positions {
*position += filter_start; *position += filter_start;
} }
(mat, symbol) SymbolMatch { symbol }
}) })
.collect()) .collect()
}) })
} }
fn compute_symbol_entries( fn compute_symbol_entries(
symbols: Vec<(StringMatch, Symbol)>, symbols: Vec<SymbolMatch>,
context_store: &ContextStore, context_store: &ContextStore,
cx: &App, cx: &App,
) -> Vec<SymbolEntry> { ) -> Vec<SymbolEntry> {
let mut symbol_entries = Vec::with_capacity(symbols.len()); let mut symbol_entries = Vec::with_capacity(symbols.len());
for (_, symbol) in symbols { for SymbolMatch { symbol, .. } in symbols {
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path); let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
let is_included = if let Some(symbols_for_path) = symbols_for_path { let is_included = if let Some(symbols_for_path) = symbols_for_path {
let mut is_included = false; let mut is_included = false;

View file

@ -1,4 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
@ -114,11 +115,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
return Task::ready(()); return Task::ready(());
}; };
let search_task = search_threads(query, threads, cx); let search_task = search_threads(query, Arc::new(AtomicBool::default()), threads, cx);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await; let matches = search_task.await;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.delegate.matches = matches; this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
this.delegate.selected_index = 0; this.delegate.selected_index = 0;
cx.notify(); cx.notify();
}) })
@ -217,11 +218,18 @@ pub fn render_thread_context_entry(
}) })
} }
#[derive(Clone)]
pub struct ThreadMatch {
pub thread: ThreadContextEntry,
pub is_recent: bool,
}
pub(crate) fn search_threads( pub(crate) fn search_threads(
query: String, query: String,
cancellation_flag: Arc<AtomicBool>,
thread_store: Entity<ThreadStore>, thread_store: Entity<ThreadStore>,
cx: &mut App, cx: &mut App,
) -> Task<Vec<ThreadContextEntry>> { ) -> Task<Vec<ThreadMatch>> {
let threads = thread_store.update(cx, |this, _cx| { let threads = thread_store.update(cx, |this, _cx| {
this.threads() this.threads()
.into_iter() .into_iter()
@ -236,6 +244,12 @@ pub(crate) fn search_threads(
cx.background_spawn(async move { cx.background_spawn(async move {
if query.is_empty() { if query.is_empty() {
threads threads
.into_iter()
.map(|thread| ThreadMatch {
thread,
is_recent: false,
})
.collect()
} else { } else {
let candidates = threads let candidates = threads
.iter() .iter()
@ -247,14 +261,17 @@ pub(crate) fn search_threads(
&query, &query,
false, false,
100, 100,
&Default::default(), &cancellation_flag,
executor, executor,
) )
.await; .await;
matches matches
.into_iter() .into_iter()
.map(|mat| threads[mat.candidate_id].clone()) .map(|mat| ThreadMatch {
thread: threads[mat.candidate_id].clone(),
is_recent: false,
})
.collect() .collect()
} }
}) })

View file

@ -3,14 +3,16 @@ use std::sync::Arc;
use crate::assistant_model_selector::ModelType; use crate::assistant_model_selector::ModelType;
use collections::HashSet; use collections::HashSet;
use editor::actions::MoveUp; use editor::actions::MoveUp;
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle}; use editor::{
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle, MultiBuffer,
};
use file_icons::FileIcons; use file_icons::FileIcons;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
}; };
use language::Buffer; use language::{Buffer, Language};
use language_model::{ConfiguredModel, LanguageModelRegistry}; use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector; use language_model_selector::ToggleModelSelector;
use multi_buffer; use multi_buffer;
@ -66,8 +68,24 @@ impl MessageEditor {
let inline_context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();
let language = Language::new(
language::LanguageConfig {
completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
..Default::default()
},
None,
);
let editor = cx.new(|cx| { let editor = cx.new(|cx| {
let mut editor = Editor::auto_height(10, window, cx); let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(
editor::EditorMode::AutoHeight { max_lines: 10 },
buffer,
None,
window,
cx,
);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx); editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx); editor.set_show_indent_guides(false, cx);
editor.set_context_menu_options(ContextMenuOptions { editor.set_context_menu_options(ContextMenuOptions {
@ -75,7 +93,6 @@ impl MessageEditor {
max_entries_visible: 12, max_entries_visible: 12,
placement: Some(ContextMenuPlacement::Above), placement: Some(ContextMenuPlacement::Above),
}); });
editor editor
}); });