diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index f2e7ff4e04..781f8b5868 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,27 +1,49 @@ use std::ops::Range; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; +use agent::context_store::ContextStore; use anyhow::{Context as _, Result}; use collections::HashMap; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId}; use file_icons::FileIcons; use futures::future::try_join_all; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; use parking_lot::Mutex; -use project::{Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, WorktreeId}; +use project::{ + Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, +}; +use prompt_store::PromptStore; use rope::Point; use text::{Anchor, ToPoint}; use ui::prelude::*; +use util::ResultExt as _; use workspace::Workspace; -use crate::context_picker::MentionLink; -use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files}; +use agent::{ + Thread, + context::{AgentContextHandle, AgentContextKey, RULES_ICON}, + thread_store::{TextThreadStore, ThreadStore}, +}; + +use crate::context_picker::file_context_picker::{FileMatch, search_files}; +use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; +use crate::context_picker::symbol_context_picker::SymbolMatch; +use crate::context_picker::symbol_context_picker::search_symbols; +use crate::context_picker::thread_context_picker::{ + ThreadContextEntry, ThreadMatch, search_threads, +}; +use crate::context_picker::{ + ContextPickerEntry, ContextPickerMode, RecentEntry, available_context_picker_entries, + recent_context_picker_entries, +}; +use crate::message_editor::ContextCreasesAddon; #[derive(Default)] pub struct MentionSet { @@ -83,22 +105,479 @@ pub struct Mention { pub content: String, } +pub(crate) enum Match { + File(FileMatch), + Symbol(SymbolMatch), + Thread(ThreadMatch), + Rules(RulesContextEntry), + Entry(EntryMatch), +} + +pub struct EntryMatch { + mat: Option, + entry: ContextPickerEntry, +} + +impl Match { + pub fn score(&self) -> f64 { + match self { + Match::File(file) => file.mat.score, + Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.), + Match::Thread(_) => 1., + Match::Symbol(_) => 1., + Match::Rules(_) => 1., + } + } +} + +fn search( + mode: Option, + query: String, + cancellation_flag: Arc, + recent_entries: Vec, + prompt_store: Option>, + thread_store: Option>, + text_thread_context_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, context_store)) = thread_store + .as_ref() + .and_then(|t| t.upgrade()) + .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade())) + { + let search_threads_task = search_threads( + query.clone(), + cancellation_flag.clone(), + thread_store, + context_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) => { + // todo! make a new mode type for acp? + unreachable!() + } + + Some(ContextPickerMode::Rules) => { + if let Some(prompt_store) = prompt_store.as_ref() { + let search_rules_task = + search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + cx.background_spawn(async move { + search_rules_task + .await + .into_iter() + .map(Match::Rules) + .collect::>() + }) + } else { + Task::ready(Vec::new()) + } + } + + None => { + if query.is_empty() { + let mut matches = recent_entries + .into_iter() + .map(|entry| match entry { + 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, + }), + RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch { + thread: thread_context_entry, + is_recent: true, + }), + }) + .collect::>(); + + matches.extend( + available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx) + .into_iter() + .map(|mode| { + Match::Entry(EntryMatch { + entry: mode, + mat: None, + }) + }), + ); + + Task::ready(matches) + } else { + let executor = cx.background_executor().clone(); + + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + + let entries = + available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx); + let entry_candidates = entries + .iter() + .enumerate() + .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword())) + .collect::>(); + + cx.background_spawn(async move { + let mut matches = search_files_task + .await + .into_iter() + .map(Match::File) + .collect::>(); + + let entry_matches = fuzzy::match_strings( + &entry_candidates, + &query, + false, + true, + 100, + &Arc::new(AtomicBool::default()), + executor, + ) + .await; + + matches.extend(entry_matches.into_iter().map(|mat| { + Match::Entry(EntryMatch { + entry: entries[mat.candidate_id], + mat: Some(mat), + }) + })); + + matches.sort_by(|a, b| { + b.score() + .partial_cmp(&a.score()) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + matches + }) + } + } + } +} + pub struct ContextPickerCompletionProvider { - workspace: WeakEntity, - editor: WeakEntity, mention_set: Arc>, + workspace: WeakEntity, + thread_store: Option>, + text_thread_store: Option>, + editor: WeakEntity, + excluded_buffer: Option>, } impl ContextPickerCompletionProvider { pub fn new( mention_set: Arc>, workspace: WeakEntity, + thread_store: Option>, + text_thread_store: Option>, editor: WeakEntity, + exclude_buffer: Option>, ) -> Self { Self { mention_set, workspace, + thread_store, + text_thread_store, editor, + excluded_buffer: exclude_buffer, + } + } + + fn completion_for_entry( + entry: ContextPickerEntry, + excerpt_id: ExcerptId, + source_range: Range, + editor: Entity, + mention_set: Arc>, + workspace: &Entity, + cx: &mut App, + ) -> Option { + match entry { + ContextPickerEntry::Mode(mode) => Some(Completion { + replace_range: source_range.clone(), + new_text: format!("@{} ", mode.keyword()), + 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)), + }), + ContextPickerEntry::Action(action) => { + todo!() + // let (new_text, on_action) = match action { + // ContextPickerAction::AddSelections => { + // let selections = selection_ranges(workspace, cx); + + // let selection_infos = selections + // .iter() + // .map(|(buffer, range)| { + // let full_path = buffer + // .read(cx) + // .file() + // .map(|file| file.full_path(cx)) + // .unwrap_or_else(|| PathBuf::from("untitled")); + // let file_name = full_path + // .file_name() + // .unwrap_or_default() + // .to_string_lossy() + // .to_string(); + // let line_range = range.to_point(&buffer.read(cx).snapshot()); + + // let link = MentionLink::for_selection( + // &file_name, + // &full_path.to_string_lossy(), + // line_range.start.row as usize..line_range.end.row as usize, + // ); + // (file_name, link, line_range) + // }) + // .collect::>(); + + // let new_text = format!( + // "{} ", + // selection_infos.iter().map(|(_, link, _)| link).join(" ") + // ); + + // let callback = Arc::new({ + // let context_store = context_store.clone(); + // let selections = selections.clone(); + // let selection_infos = selection_infos.clone(); + // move |_, window: &mut Window, cx: &mut App| { + // context_store.update(cx, |context_store, cx| { + // for (buffer, range) in &selections { + // context_store.add_selection( + // buffer.clone(), + // range.clone(), + // cx, + // ); + // } + // }); + + // let editor = editor.clone(); + // let selection_infos = selection_infos.clone(); + // window.defer(cx, move |window, cx| { + // let mut current_offset = 0; + // for (file_name, link, line_range) in selection_infos.iter() { + // let snapshot = + // editor.read(cx).buffer().read(cx).snapshot(cx); + // let Some(start) = snapshot + // .anchor_in_excerpt(excerpt_id, source_range.start) + // else { + // return; + // }; + + // let offset = start.to_offset(&snapshot) + current_offset; + // let text_len = link.len(); + + // let range = snapshot.anchor_after(offset) + // ..snapshot.anchor_after(offset + text_len); + + // let crease = super::crease_for_mention( + // format!( + // "{} ({}-{})", + // file_name, + // line_range.start.row + 1, + // line_range.end.row + 1 + // ) + // .into(), + // IconName::Reader.path().into(), + // range, + // editor.downgrade(), + // ); + + // editor.update(cx, |editor, cx| { + // editor.insert_creases(vec![crease.clone()], cx); + // editor.fold_creases(vec![crease], false, window, cx); + // }); + + // current_offset += text_len + 1; + // } + // }); + + // false + // } + // }); + + // (new_text, callback) + // } + // }; + + // Some(Completion { + // replace_range: source_range.clone(), + // new_text, + // label: CodeLabel::plain(action.label().to_string(), None), + // icon_path: Some(action.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(on_action), + // }) + } + } + } + + fn completion_for_thread( + thread_entry: ThreadContextEntry, + excerpt_id: ExcerptId, + source_range: Range, + recent: bool, + editor: Entity, + mention_set: Arc>, + thread_store: Entity, + text_thread_store: Entity, + ) -> Completion { + todo!(); + // let icon_for_completion = if recent { + // IconName::HistoryRerun + // } else { + // IconName::Thread + // }; + + // let new_text = format!("{} ", MentionUri::Thread(thread_id)); + + // let new_text_len = new_text.len(); + // Completion { + // replace_range: source_range.clone(), + // new_text, + // label: CodeLabel::plain(thread_entry.title().to_string(), None), + // documentation: None, + // insert_text_mode: None, + // source: project::CompletionSource::Custom, + // icon_path: Some(icon_for_completion.path().into()), + // confirm: Some(confirm_completion_callback( + // IconName::Thread.path().into(), + // thread_entry.title().clone(), + // excerpt_id, + // source_range.start, + // new_text_len - 1, + // editor.clone(), + // context_store.clone(), + // move |window, cx| match &thread_entry { + // ThreadContextEntry::Thread { id, .. } => { + // let thread_id = id.clone(); + // let context_store = context_store.clone(); + // let thread_store = thread_store.clone(); + // window.spawn::<_, Option<_>>(cx, async move |cx| { + // let thread: Entity = thread_store + // .update_in(cx, |thread_store, window, cx| { + // thread_store.open_thread(&thread_id, window, cx) + // }) + // .ok()? + // .await + // .log_err()?; + // let context = context_store + // .update(cx, |context_store, cx| { + // context_store.add_thread(thread, false, cx) + // }) + // .ok()??; + // Some(context) + // }) + // } + // ThreadContextEntry::Context { path, .. } => { + // let path = path.clone(); + // let context_store = context_store.clone(); + // let text_thread_store = text_thread_store.clone(); + // cx.spawn::<_, Option<_>>(async move |cx| { + // let thread = text_thread_store + // .update(cx, |store, cx| store.open_local_context(path, cx)) + // .ok()? + // .await + // .log_err()?; + // let context = context_store + // .update(cx, |context_store, cx| { + // context_store.add_text_thread(thread, false, cx) + // }) + // .ok()??; + // Some(context) + // }) + // } + // }, + // )), + // } + } + + fn completion_for_rules( + rules: RulesContextEntry, + excerpt_id: ExcerptId, + source_range: Range, + editor: Entity, + mention_set: Arc>, + ) -> Completion { + let uri = MentionUri::Rule(rules.prompt_id.0.to_string()); + let new_text = uri.to_link(); + let new_text_len = new_text.len(); + Completion { + replace_range: source_range.clone(), + new_text, + label: CodeLabel::plain(rules.title.to_string(), None), + documentation: None, + insert_text_mode: None, + source: project::CompletionSource::Custom, + icon_path: Some(RULES_ICON.path().into()), + confirm: Some(confirm_completion_callback( + RULES_ICON.path().into(), + rules.title.clone(), + excerpt_id, + source_range.start, + new_text_len - 1, + editor.clone(), + mention_set, + uri, + )), } } @@ -113,9 +592,12 @@ impl ContextPickerCompletionProvider { mention_set: Arc>, project: Entity, cx: &App, - ) -> Completion { + ) -> Option { let (file_name, directory) = - extract_file_name_and_directory(&project_path.path, path_prefix); + crate::context_picker::file_context_picker::extract_file_name_and_directory( + &project_path.path, + path_prefix, + ); let label = build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx); @@ -137,9 +619,14 @@ impl ContextPickerCompletionProvider { crease_icon_path.clone() }; - let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path)); + let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) else { + return None; + }; + + let file_uri = MentionUri::File(abs_path.into()); + let new_text = file_uri.to_link(); let new_text_len = new_text.len(); - Completion { + Some(Completion { replace_range: source_range.clone(), new_text, label, @@ -150,15 +637,84 @@ impl ContextPickerCompletionProvider { confirm: Some(confirm_completion_callback( crease_icon_path, file_name, - project_path, excerpt_id, source_range.start, new_text_len - 1, editor, - mention_set, - project, + mention_set.clone(), + file_uri, )), - } + }) + } + + fn completion_for_symbol( + symbol: Symbol, + excerpt_id: ExcerptId, + source_range: Range, + editor: Entity, + mention_set: Arc>, + workspace: Entity, + cx: &mut App, + ) -> Option { + None + // let path_prefix = workspace + // .read(cx) + // .project() + // .read(cx) + // .worktree_for_id(symbol.path.worktree_id, cx)? + // .read(cx) + // .root_name(); + + // let (file_name, directory) = + // crate::context_picker::file_context_picker::extract_file_name_and_directory( + // &symbol.path.path, + // path_prefix, + // ); + // let full_path = if let Some(directory) = directory { + // format!("{}{}", directory, file_name) + // } else { + // file_name.to_string() + // }; + + // let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId); + // let mut label = CodeLabel::plain(symbol.name.clone(), None); + // label.push_str(" ", None); + // label.push_str(&file_name, comment_id); + // label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id); + + // let new_text = MentionUri::Symbol(full_path.into(), symbol.name.clone()).to_link(); + // let new_text_len = new_text.len(); + // Some(Completion { + // replace_range: source_range.clone(), + // new_text, + // label, + // documentation: None, + // source: project::CompletionSource::Custom, + // icon_path: Some(IconName::Code.path().into()), + // insert_text_mode: None, + // confirm: Some(confirm_completion_callback( + // IconName::Code.path().into(), + // symbol.name.clone().into(), + // excerpt_id, + // source_range.start, + // new_text_len - 1, + // editor.clone(), + // context_store.clone(), + // move |_, cx| { + // let symbol = symbol.clone(); + // let context_store = context_store.clone(); + // let workspace = workspace.clone(); + // let result = crate::context_picker::symbol_context_picker::add_symbol( + // symbol.clone(), + // false, + // workspace.clone(), + // context_store.downgrade(), + // cx, + // ); + // cx.spawn(async move |_| result.await.log_err()?.0) + // }, + // )), + // }) } } @@ -209,12 +765,51 @@ impl CompletionProvider for ContextPickerCompletionProvider { let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); + let thread_store = self.thread_store.clone(); + let text_thread_store = self.text_thread_store.clone(); let editor = self.editor.clone(); - let mention_set = self.mention_set.clone(); - let MentionCompletion { argument, .. } = state; + + let MentionCompletion { mode, argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); - let search_task = search_files(query.clone(), Arc::::default(), &workspace, cx); + let excluded_path = self + .excluded_buffer + .as_ref() + .and_then(WeakEntity::upgrade) + .and_then(|b| b.read(cx).file()) + .map(|file| ProjectPath::from_file(file.as_ref(), cx)); + + // let recent_entries = recent_context_picker_entries( + // context_store.clone(), + // thread_store.clone(), + // text_thread_store.clone(), + // workspace.clone(), + // excluded_path.clone(), + // cx, + // ); + + let prompt_store = thread_store.as_ref().and_then(|thread_store| { + thread_store + .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) + .ok() + .flatten() + }); + + let search_task = search( + mode, + query, + Arc::::default(), + // todo! + // recent_entries, + vec![], + prompt_store, + thread_store.clone(), + text_thread_store.clone(), + workspace.clone(), + cx, + ); + + let mention_set = self.mention_set.clone(); cx.spawn(async move |_, cx| { let matches = search_task.await; @@ -225,25 +820,76 @@ impl CompletionProvider for ContextPickerCompletionProvider { let completions = cx.update(|cx| { matches .into_iter() - .map(|mat| { - let path_match = &mat.mat; - let project_path = ProjectPath { - worktree_id: WorktreeId::from_usize(path_match.worktree_id), - path: path_match.path.clone(), - }; + .filter_map(|mat| match mat { + Match::File(FileMatch { mat, is_recent }) => { + let project_path = ProjectPath { + worktree_id: WorktreeId::from_usize(mat.worktree_id), + path: mat.path.clone(), + }; - Self::completion_for_path( - project_path, - &path_match.path_prefix, - mat.is_recent, - path_match.is_dir, + if excluded_path.as_ref() == Some(&project_path) { + return None; + } + + Self::completion_for_path( + project_path, + &mat.path_prefix, + is_recent, + mat.is_dir, + excerpt_id, + source_range.clone(), + editor.clone(), + mention_set.clone(), + project.clone(), + cx, + ) + } + + Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol( + symbol, excerpt_id, source_range.clone(), editor.clone(), mention_set.clone(), - project.clone(), + workspace.clone(), cx, - ) + ), + + Match::Thread(ThreadMatch { + thread, is_recent, .. + }) => { + let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?; + let text_thread_store = + text_thread_store.as_ref().and_then(|t| t.upgrade())?; + Some(Self::completion_for_thread( + thread, + excerpt_id, + source_range.clone(), + is_recent, + editor.clone(), + mention_set.clone(), + thread_store, + text_thread_store, + )) + } + + Match::Rules(user_rules) => Some(Self::completion_for_rules( + user_rules, + excerpt_id, + source_range.clone(), + editor.clone(), + mention_set.clone(), + )), + + Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( + entry, + excerpt_id, + source_range.clone(), + editor.clone(), + mention_set.clone(), + &workspace, + cx, + ), }) .collect() })?; @@ -295,23 +941,21 @@ impl CompletionProvider for ContextPickerCompletionProvider { fn confirm_completion_callback( crease_icon_path: SharedString, crease_text: SharedString, - project_path: ProjectPath, excerpt_id: ExcerptId, start: Anchor, content_len: usize, editor: Entity, mention_set: Arc>, - project: Entity, + mention_uri: MentionUri, ) -> Arc bool + Send + Sync> { Arc::new(move |_, window, cx| { let crease_text = crease_text.clone(); let crease_icon_path = crease_icon_path.clone(); let editor = editor.clone(); - let project_path = project_path.clone(); let mention_set = mention_set.clone(); - let project = project.clone(); + let mention_uri = mention_uri.clone(); window.defer(cx, move |window, cx| { - let crease_id = crate::context_picker::insert_crease_for_mention( + if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( excerpt_id, start, content_len, @@ -320,14 +964,8 @@ fn confirm_completion_callback( editor.clone(), window, cx, - ); - - let Some(path) = project.read(cx).absolute_path(&project_path, cx) else { - return; - }; - - if let Some(crease_id) = crease_id { - mention_set.lock().insert(crease_id, MentionUri::File(path)); + ) { + mention_set.lock().insert(crease_id, mention_uri.clone()); } }); false @@ -337,6 +975,7 @@ fn confirm_completion_callback( #[derive(Debug, Default, PartialEq)] struct MentionCompletion { source_range: Range, + mode: Option, argument: Option, } @@ -356,273 +995,474 @@ impl MentionCompletion { } let rest_of_line = &line[last_mention_start + 1..]; + + let mut mode = None; let mut argument = None; let mut parts = rest_of_line.split_whitespace(); let mut end = last_mention_start + 1; - if let Some(argument_text) = parts.next() { - end += argument_text.len(); - argument = Some(argument_text.to_string()); + if let Some(mode_text) = parts.next() { + end += mode_text.len(); + + 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() { + argument = Some(argument_text.to_string()); + end += whitespace_count + argument_text.len(); + } + } + None => { + // Rest of line is entirely whitespace + end += rest_of_line.len() - mode_text.len(); + } + } } Some(Self { source_range: last_mention_start + offset_to_line..end + offset_to_line, + mode, argument, }) } } -#[cfg(test)] -mod tests { - use super::*; - use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; - use project::{Project, ProjectPath}; - use serde_json::json; - use settings::SettingsStore; - use std::{ops::Deref, rc::Rc}; - use util::path; - use workspace::{AppState, Item}; +// #[cfg(test)] +// mod tests { +// use super::*; +// use editor::AnchorRangeExt; +// use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; +// use project::{Project, ProjectPath}; +// use serde_json::json; +// use settings::SettingsStore; +// use std::{ops::Deref, rc::Rc}; +// use util::path; +// use workspace::{AppState, Item}; - #[test] - fn test_mention_completion_parse() { - assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); +// #[test] +// fn test_mention_completion_parse() { +// assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); - assert_eq!( - MentionCompletion::try_parse("Lorem @", 0), - Some(MentionCompletion { - source_range: 6..7, - argument: None, - }) - ); +// assert_eq!( +// MentionCompletion::try_parse("Lorem @", 0), +// Some(MentionCompletion { +// source_range: 6..7, +// mode: None, +// argument: None, +// }) +// ); - assert_eq!( - MentionCompletion::try_parse("Lorem @main", 0), - Some(MentionCompletion { - source_range: 6..11, - argument: Some("main".to_string()), - }) - ); +// assert_eq!( +// MentionCompletion::try_parse("Lorem @file", 0), +// Some(MentionCompletion { +// source_range: 6..11, +// mode: Some(ContextPickerMode::File), +// argument: None, +// }) +// ); - assert_eq!(MentionCompletion::try_parse("test@", 0), None); - } +// assert_eq!( +// MentionCompletion::try_parse("Lorem @file ", 0), +// Some(MentionCompletion { +// source_range: 6..12, +// mode: Some(ContextPickerMode::File), +// argument: None, +// }) +// ); - struct AtMentionEditor(Entity); +// assert_eq!( +// MentionCompletion::try_parse("Lorem @file main.rs", 0), +// Some(MentionCompletion { +// source_range: 6..19, +// mode: Some(ContextPickerMode::File), +// argument: Some("main.rs".to_string()), +// }) +// ); - impl Item for AtMentionEditor { - type Event = (); +// assert_eq!( +// MentionCompletion::try_parse("Lorem @file main.rs ", 0), +// Some(MentionCompletion { +// source_range: 6..19, +// mode: Some(ContextPickerMode::File), +// argument: Some("main.rs".to_string()), +// }) +// ); - fn include_in_nav_history() -> bool { - false - } +// assert_eq!( +// MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0), +// Some(MentionCompletion { +// source_range: 6..19, +// mode: Some(ContextPickerMode::File), +// argument: Some("main.rs".to_string()), +// }) +// ); - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } - } +// assert_eq!( +// MentionCompletion::try_parse("Lorem @main", 0), +// Some(MentionCompletion { +// source_range: 6..11, +// mode: None, +// argument: Some("main".to_string()), +// }) +// ); - impl EventEmitter<()> for AtMentionEditor {} +// assert_eq!(MentionCompletion::try_parse("test@", 0), None); +// } - impl Focusable for AtMentionEditor { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() - } - } +// struct AtMentionEditor(Entity); - impl Render for AtMentionEditor { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - self.0.clone().into_any_element() - } - } +// impl Item for AtMentionEditor { +// type Event = (); - #[gpui::test] - async fn test_context_completion_provider(cx: &mut TestAppContext) { - init_test(cx); +// fn include_in_nav_history() -> bool { +// false +// } - let app_state = cx.update(AppState::test); +// fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { +// "Test".into() +// } +// } - cx.update(|cx| { - language::init(cx); - editor::init(cx); - workspace::init(app_state.clone(), cx); - Project::init_settings(cx); - }); +// impl EventEmitter<()> for AtMentionEditor {} - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "editor": "", - "a": { - "one.txt": "", - "two.txt": "", - "three.txt": "", - "four.txt": "" - }, - "b": { - "five.txt": "", - "six.txt": "", - "seven.txt": "", - "eight.txt": "", - } - }), - ) - .await; +// impl Focusable for AtMentionEditor { +// fn focus_handle(&self, cx: &App) -> FocusHandle { +// self.0.read(cx).focus_handle(cx).clone() +// } +// } - let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); +// impl Render for AtMentionEditor { +// fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { +// self.0.clone().into_any_element() +// } +// } - let worktree = project.update(cx, |project, cx| { - let mut worktrees = project.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - worktrees.pop().unwrap() - }); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); +// #[gpui::test] +// async fn test_context_completion_provider(cx: &mut TestAppContext) { +// init_test(cx); - let mut cx = VisualTestContext::from_window(*window.deref(), cx); +// let app_state = cx.update(AppState::test); - let paths = vec![ - path!("a/one.txt"), - path!("a/two.txt"), - path!("a/three.txt"), - path!("a/four.txt"), - path!("b/five.txt"), - path!("b/six.txt"), - path!("b/seven.txt"), - path!("b/eight.txt"), - ]; +// cx.update(|cx| { +// language::init(cx); +// editor::init(cx); +// workspace::init(app_state.clone(), cx); +// Project::init_settings(cx); +// }); - let mut opened_editors = Vec::new(); - for path in paths { - let buffer = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: Path::new(path).into(), - }, - None, - false, - window, - cx, - ) - }) - .await - .unwrap(); - opened_editors.push(buffer); - } +// app_state +// .fs +// .as_fake() +// .insert_tree( +// path!("/dir"), +// json!({ +// "editor": "", +// "a": { +// "one.txt": "", +// "two.txt": "", +// "three.txt": "", +// "four.txt": "" +// }, +// "b": { +// "five.txt": "", +// "six.txt": "", +// "seven.txt": "", +// "eight.txt": "", +// } +// }), +// ) +// .await; - 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, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| AtMentionEditor(editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - editor - }); +// let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; +// let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); +// let workspace = window.root(cx).unwrap(); - let mention_set = Arc::new(Mutex::new(MentionSet::default())); +// let worktree = project.update(cx, |project, cx| { +// let mut worktrees = project.worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// worktrees.pop().unwrap() +// }); +// let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - let editor_entity = editor.downgrade(); - editor.update_in(&mut cx, |editor, window, cx| { - window.focus(&editor.focus_handle(cx)); - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - mention_set.clone(), - workspace.downgrade(), - editor_entity, - )))); - }); +// let mut cx = VisualTestContext::from_window(*window.deref(), cx); - cx.simulate_input("Lorem "); +// let paths = vec![ +// path!("a/one.txt"), +// path!("a/two.txt"), +// path!("a/three.txt"), +// path!("a/four.txt"), +// path!("b/five.txt"), +// path!("b/six.txt"), +// path!("b/seven.txt"), +// path!("b/eight.txt"), +// ]; - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem "); - assert!(!editor.has_visible_completions_menu()); - }); +// let mut opened_editors = Vec::new(); +// for path in paths { +// let buffer = workspace +// .update_in(&mut cx, |workspace, window, cx| { +// workspace.open_path( +// ProjectPath { +// worktree_id, +// path: Path::new(path).into(), +// }, +// None, +// false, +// window, +// cx, +// ) +// }) +// .await +// .unwrap(); +// opened_editors.push(buffer); +// } - cx.simulate_input("@"); +// 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, +// window, +// cx, +// ) +// }); +// workspace.active_pane().update(cx, |pane, cx| { +// pane.add_item( +// Box::new(cx.new(|_| AtMentionEditor(editor.clone()))), +// true, +// true, +// None, +// window, +// cx, +// ); +// }); +// editor +// }); - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @"); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - "eight.txt dir/b/", - "seven.txt dir/b/", - "six.txt dir/b/", - "five.txt dir/b/", - "four.txt dir/a/", - "three.txt dir/a/", - "two.txt dir/a/", - "one.txt dir/a/", - "dir ", - "a dir/", - "four.txt dir/a/", - "one.txt dir/a/", - "three.txt dir/a/", - "two.txt dir/a/", - "b dir/", - "eight.txt dir/b/", - "five.txt dir/b/", - "seven.txt dir/b/", - "six.txt dir/b/", - "editor dir/" - ] - ); - }); +// let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None)); - // Select and confirm "File" - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); +// let editor_entity = editor.downgrade(); +// editor.update_in(&mut cx, |editor, window, cx| { +// let last_opened_buffer = opened_editors.last().and_then(|editor| { +// editor +// .downcast::()? +// .read(cx) +// .buffer() +// .read(cx) +// .as_singleton() +// .as_ref() +// .map(Entity::downgrade) +// }); +// window.focus(&editor.focus_handle(cx)); +// editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( +// workspace.downgrade(), +// context_store.downgrade(), +// None, +// None, +// editor_entity, +// last_opened_buffer, +// )))); +// }); - cx.run_until_parked(); +// cx.simulate_input("Lorem "); - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@four.txt](@file:dir/a/four.txt) "); - }); - } +// editor.update(&mut cx, |editor, cx| { +// assert_eq!(editor.text(cx), "Lorem "); +// assert!(!editor.has_visible_completions_menu()); +// }); - fn current_completion_labels(editor: &Editor) -> Vec { - let completions = editor.current_completions().expect("Missing completions"); - completions - .into_iter() - .map(|completion| completion.label.text.to_string()) - .collect::>() - } +// cx.simulate_input("@"); - pub(crate) fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - editor::init_settings(cx); - }); - } -} +// editor.update(&mut cx, |editor, cx| { +// assert_eq!(editor.text(cx), "Lorem @"); +// assert!(editor.has_visible_completions_menu()); +// assert_eq!( +// current_completion_labels(editor), +// &[ +// "seven.txt dir/b/", +// "six.txt dir/b/", +// "five.txt dir/b/", +// "four.txt dir/a/", +// "Files & Directories", +// "Symbols", +// "Fetch" +// ] +// ); +// }); + +// // Select and confirm "File" +// editor.update_in(&mut cx, |editor, window, cx| { +// assert!(editor.has_visible_completions_menu()); +// editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); +// editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); +// editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); +// editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); +// editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); +// }); + +// cx.run_until_parked(); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!(editor.text(cx), "Lorem @file "); +// assert!(editor.has_visible_completions_menu()); +// }); + +// cx.simulate_input("one"); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!(editor.text(cx), "Lorem @file one"); +// assert!(editor.has_visible_completions_menu()); +// assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]); +// }); + +// editor.update_in(&mut cx, |editor, window, cx| { +// assert!(editor.has_visible_completions_menu()); +// editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); +// }); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) "); +// assert!(!editor.has_visible_completions_menu()); +// assert_eq!( +// fold_ranges(editor, cx), +// vec![Point::new(0, 6)..Point::new(0, 37)] +// ); +// }); + +// cx.simulate_input(" "); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) "); +// assert!(!editor.has_visible_completions_menu()); +// assert_eq!( +// fold_ranges(editor, cx), +// vec![Point::new(0, 6)..Point::new(0, 37)] +// ); +// }); + +// cx.simulate_input("Ipsum "); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!( +// editor.text(cx), +// "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ", +// ); +// assert!(!editor.has_visible_completions_menu()); +// assert_eq!( +// fold_ranges(editor, cx), +// vec![Point::new(0, 6)..Point::new(0, 37)] +// ); +// }); + +// cx.simulate_input("@file "); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!( +// editor.text(cx), +// "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ", +// ); +// assert!(editor.has_visible_completions_menu()); +// assert_eq!( +// fold_ranges(editor, cx), +// vec![Point::new(0, 6)..Point::new(0, 37)] +// ); +// }); + +// editor.update_in(&mut cx, |editor, window, cx| { +// editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); +// }); + +// cx.run_until_parked(); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!( +// editor.text(cx), +// "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) " +// ); +// assert!(!editor.has_visible_completions_menu()); +// assert_eq!( +// fold_ranges(editor, cx), +// vec![ +// Point::new(0, 6)..Point::new(0, 37), +// Point::new(0, 45)..Point::new(0, 80) +// ] +// ); +// }); + +// cx.simulate_input("\n@"); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!( +// editor.text(cx), +// "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!( +// fold_ranges(editor, cx), +// vec![ +// Point::new(0, 6)..Point::new(0, 37), +// Point::new(0, 45)..Point::new(0, 80) +// ] +// ); +// }); + +// editor.update_in(&mut cx, |editor, window, cx| { +// editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); +// }); + +// cx.run_until_parked(); + +// editor.update(&mut cx, |editor, cx| { +// assert_eq!( +// editor.text(cx), +// "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!( +// fold_ranges(editor, cx), +// vec![ +// Point::new(0, 6)..Point::new(0, 37), +// Point::new(0, 45)..Point::new(0, 80), +// Point::new(1, 0)..Point::new(1, 31) +// ] +// ); +// }); +// } + +// fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { +// let snapshot = editor.buffer().read(cx).snapshot(cx); +// editor.display_map.update(cx, |display_map, cx| { +// display_map +// .snapshot(cx) +// .folds_in_range(0..snapshot.len()) +// .map(|fold| fold.range.to_point(&snapshot)) +// .collect() +// }) +// } + +// fn current_completion_labels(editor: &Editor) -> Vec { +// let completions = editor.current_completions().expect("Missing completions"); +// completions +// .into_iter() +// .map(|completion| completion.label.text.to_string()) +// .collect::>() +// } + +// pub(crate) fn init_test(cx: &mut TestAppContext) { +// cx.update(|cx| { +// let store = SettingsStore::test(cx); +// cx.set_global(store); +// theme::init(theme::LoadThemes::JustBase, cx); +// client::init_settings(cx); +// language::init(cx); +// Project::init_settings(cx); +// workspace::init_settings(cx); +// editor::init_settings(cx); +// }); +// } +// } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index fcbfac2bd7..ab4d4bdbfe 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -141,7 +141,11 @@ impl AcpThreadView { editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( mention_set.clone(), workspace.clone(), + // todo! provide thread stores + None, + None, cx.weak_entity(), + None, )))); editor.set_context_menu_options(ContextMenuOptions { min_entries_visible: 12, @@ -3002,7 +3006,7 @@ impl AcpThreadView { .unwrap_or(path.path.as_os_str()) .display() .to_string(); - let completion = ContextPickerCompletionProvider::completion_for_path( + let Some(completion) = ContextPickerCompletionProvider::completion_for_path( path, &path_prefix, false, @@ -3013,7 +3017,9 @@ impl AcpThreadView { self.mention_set.clone(), self.project.clone(), cx, - ); + ) else { + continue; + }; self.message_editor.update(cx, |message_editor, cx| { message_editor.edit( diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 58f11313e6..cbf669de03 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -1,9 +1,9 @@ mod completion_provider; -mod fetch_context_picker; +pub(crate) mod fetch_context_picker; pub(crate) mod file_context_picker; -mod rules_context_picker; -mod symbol_context_picker; -mod thread_context_picker; +pub(crate) mod rules_context_picker; +pub(crate) mod symbol_context_picker; +pub(crate) mod thread_context_picker; use std::ops::Range; use std::path::{Path, PathBuf}; @@ -45,7 +45,7 @@ use agent::{ }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ContextPickerEntry { +pub(crate) enum ContextPickerEntry { Mode(ContextPickerMode), Action(ContextPickerAction), } @@ -74,7 +74,7 @@ impl ContextPickerEntry { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ContextPickerMode { +pub(crate) enum ContextPickerMode { File, Symbol, Fetch, @@ -83,7 +83,7 @@ enum ContextPickerMode { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ContextPickerAction { +pub(crate) enum ContextPickerAction { AddSelections, } @@ -585,7 +585,8 @@ impl Render for ContextPicker { }) } } -enum RecentEntry { + +pub(crate) enum RecentEntry { File { project_path: ProjectPath, path_prefix: Arc, @@ -593,7 +594,7 @@ enum RecentEntry { Thread(ThreadContextEntry), } -fn available_context_picker_entries( +pub(crate) fn available_context_picker_entries( prompt_store: &Option>, thread_store: &Option>, workspace: &Entity, @@ -630,7 +631,7 @@ fn available_context_picker_entries( entries } -fn recent_context_picker_entries( +pub(crate) fn recent_context_picker_entries( context_store: Entity, thread_store: Option>, text_thread_store: Option>,