diff --git a/crates/agent/src/context_picker/completion_provider.rs b/crates/agent/src/context_picker/completion_provider.rs index a24713af9a..89ad7300e3 100644 --- a/crates/agent/src/context_picker/completion_provider.rs +++ b/crates/agent/src/context_picker/completion_provider.rs @@ -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, + query: String, + cancellation_flag: Arc, + recent_entries: Vec, + thread_store: Option>, + workspace: Entity, + cx: &mut App, +) -> Task> { + 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::>(); + + 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, context_store: WeakEntity, @@ -50,97 +167,20 @@ impl ContextPickerCompletionProvider { } } - fn default_completions( - excerpt_id: ExcerptId, - source_range: Range, - context_store: Entity, - thread_store: Option>, - editor: Entity, - workspace: Entity, - cx: &App, - ) -> Vec { - 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, 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::::default(), + recent_entries, + thread_store.clone(), + workspace.clone(), + 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; - - 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::::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, - ) + 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(), - )); - } - - 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, - )); + )) } - })?; - } - } - Ok(Some(completions)) + 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(), + 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; 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); + + 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) -> 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::(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) ] ); }); diff --git a/crates/agent/src/context_picker/file_context_picker.rs b/crates/agent/src/context_picker/file_context_picker.rs index 8dca313fd2..7ea4e8f639 100644 --- a/crates/agent/src/context_picker/file_context_picker.rs +++ b/crates/agent/src/context_picker/file_context_picker.rs @@ -58,7 +58,7 @@ pub struct FileContextPickerDelegate { workspace: WeakEntity, context_store: WeakEntity, confirm_behavior: ConfirmBehavior, - matches: Vec, + matches: Vec, selected_index: usize, } @@ -114,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate { return Task::ready(()); }; - let search_task = search_paths(query, Arc::::default(), &workspace, cx); + let search_task = search_files(query, Arc::::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>) { - 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>, ) -> Option { - 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, workspace: &Entity, cx: &App, -) -> Task> { +) -> Task> { 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 = 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::>() }) } } diff --git a/crates/agent/src/context_picker/symbol_context_picker.rs b/crates/agent/src/context_picker/symbol_context_picker.rs index 9c7056deca..608accc098 100644 --- a/crates/agent/src/context_picker/symbol_context_picker.rs +++ b/crates/agent/src/context_picker/symbol_context_picker.rs @@ -2,7 +2,7 @@ use std::cmp::Reverse; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use anyhow::{Context as _, Result}; +use anyhow::Result; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity, @@ -119,11 +119,7 @@ impl PickerDelegate for SymbolContextPickerDelegate { let search_task = search_symbols(query, Arc::::default(), &workspace, cx); let context_store = self.context_store.clone(); cx.spawn_in(window, async move |this, cx| { - let symbols = search_task - .await - .context("Failed to load symbols") - .log_err() - .unwrap_or_default(); + let symbols = search_task.await; let symbol_entries = context_store .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( query: String, cancellation_flag: Arc, workspace: &Entity, cx: &mut App, -) -> Task>> { +) -> Task> { let symbols_task = workspace.update(cx, |workspace, cx| { workspace .project() @@ -298,19 +298,28 @@ pub(crate) fn search_symbols( }); let project = workspace.read(cx).project().clone(); cx.spawn(async move |cx| { - let symbols = symbols_task.await?; - let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project - .update(cx, |project, cx| { - symbols - .iter() - .enumerate() - .map(|(id, symbol)| 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) - }) - })?; + let Some(symbols) = symbols_task.await.log_err() else { + return Vec::new(); + }; + let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> = + project + .update(cx, |project, cx| { + symbols + .iter() + .enumerate() + .map(|(id, symbol)| { + 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; 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; matches.append(&mut external_matches); - Ok(matches + matches .into_iter() .map(|mut mat| { let symbol = symbols[mat.candidate_id].clone(); @@ -347,19 +356,19 @@ pub(crate) fn search_symbols( for position in &mut mat.positions { *position += filter_start; } - (mat, symbol) + SymbolMatch { symbol } }) - .collect()) + .collect() }) } fn compute_symbol_entries( - symbols: Vec<(StringMatch, Symbol)>, + symbols: Vec, context_store: &ContextStore, cx: &App, ) -> Vec { 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 is_included = if let Some(symbols_for_path) = symbols_for_path { let mut is_included = false; diff --git a/crates/agent/src/context_picker/thread_context_picker.rs b/crates/agent/src/context_picker/thread_context_picker.rs index 094209e9cf..98f62b3073 100644 --- a/crates/agent/src/context_picker/thread_context_picker.rs +++ b/crates/agent/src/context_picker/thread_context_picker.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::sync::atomic::AtomicBool; use fuzzy::StringMatchCandidate; use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; @@ -114,11 +115,11 @@ impl PickerDelegate for ThreadContextPickerDelegate { 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| { let matches = search_task.await; 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; 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( query: String, + cancellation_flag: Arc, thread_store: Entity, cx: &mut App, -) -> Task> { +) -> Task> { let threads = thread_store.update(cx, |this, _cx| { this.threads() .into_iter() @@ -236,6 +244,12 @@ pub(crate) fn search_threads( cx.background_spawn(async move { if query.is_empty() { threads + .into_iter() + .map(|thread| ThreadMatch { + thread, + is_recent: false, + }) + .collect() } else { let candidates = threads .iter() @@ -247,14 +261,17 @@ pub(crate) fn search_threads( &query, false, 100, - &Default::default(), + &cancellation_flag, executor, ) .await; matches .into_iter() - .map(|mat| threads[mat.candidate_id].clone()) + .map(|mat| ThreadMatch { + thread: threads[mat.candidate_id].clone(), + is_recent: false, + }) .collect() } }) diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 541a56eb14..85cb62e24a 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -3,14 +3,16 @@ use std::sync::Arc; use crate::assistant_model_selector::ModelType; use collections::HashSet; use editor::actions::MoveUp; -use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle}; +use editor::{ + ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle, MultiBuffer, +}; use file_icons::FileIcons; use fs::Fs; use gpui::{ Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, }; -use language::Buffer; +use language::{Buffer, Language}; use language_model::{ConfiguredModel, LanguageModelRegistry}; use language_model_selector::ToggleModelSelector; use multi_buffer; @@ -66,8 +68,24 @@ impl MessageEditor { let inline_context_picker_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 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_show_indent_guides(false, cx); editor.set_context_menu_options(ContextMenuOptions { @@ -75,7 +93,6 @@ impl MessageEditor { max_entries_visible: 12, placement: Some(ContextMenuPlacement::Above), }); - editor });