diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d7d2cd5d0e..1a9861d13a 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,20 +1,17 @@ -use std::ffi::OsStr; use std::ops::Range; -use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; +use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId}; use futures::future::{Shared, try_join_all}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity}; -use http_client::HttpClientWithUrl; +use gpui::{App, Entity, ImageFormat, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; -use language_model::LanguageModelImage; use lsp::CompletionContext; use project::{ Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, @@ -43,7 +40,7 @@ use crate::context_picker::{ #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { - pub abs_path: Option>, + pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } @@ -88,6 +85,8 @@ impl MentionSet { window: &mut Window, cx: &mut App, ) -> Task>> { + let mut processed_image_creases = HashSet::default(); + let mut contents = self .uri_by_crease_id .iter() @@ -97,59 +96,27 @@ impl MentionSet { // TODO directories let uri = uri.clone(); let abs_path = abs_path.to_path_buf(); - let extension = abs_path.extension().and_then(OsStr::to_str).unwrap_or(""); - if Img::extensions().contains(&extension) && !extension.contains("svg") { - let open_image_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_image(path, cx)) + if let Some(task) = self.images.get(&crease_id).cloned() { + processed_image_creases.insert(crease_id); + return cx.spawn(async move |_| { + let image = task.await.map_err(|e| anyhow!("{e}"))?; + anyhow::Ok((crease_id, Mention::Image(image))) }); - - cx.spawn(async move |cx| { - let image_item = open_image_task?.await?; - let (data, format) = image_item.update(cx, |image_item, cx| { - let format = image_item.image.format; - ( - LanguageModelImage::from_image( - image_item.image.clone(), - cx, - ), - format, - ) - })?; - let data = cx.spawn(async move |_| { - if let Some(data) = data.await { - Ok(data.source) - } else { - anyhow::bail!("Failed to convert image") - } - }); - - anyhow::Ok(( - crease_id, - Mention::Image(MentionImage { - abs_path: Some(abs_path.as_path().into()), - data: data.await?, - format, - }), - )) - }) - } else { - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) } + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) } MentionUri::Symbol { path, line_range, .. @@ -243,15 +210,19 @@ impl MentionSet { }) .collect::>(); - contents.extend(self.images.iter().map(|(crease_id, image)| { + // Handle images that didn't have a mention URI (because they were added by the paste handler). + contents.extend(self.images.iter().filter_map(|(crease_id, image)| { + if processed_image_creases.contains(crease_id) { + return None; + } let crease_id = *crease_id; let image = image.clone(); - cx.spawn(async move |_| { + Some(cx.spawn(async move |_| { Ok(( crease_id, Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), )) - }) + })) })); cx.spawn(async move |_cx| { @@ -753,7 +724,6 @@ impl ContextPickerCompletionProvider { source_range: Range, url_to_fetch: SharedString, message_editor: WeakEntity, - http_client: Arc, cx: &mut App, ) -> Option { let new_text = format!("@fetch {} ", url_to_fetch.clone()); @@ -772,30 +742,13 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_path.clone()), insert_text_mode: None, - confirm: Some({ - Arc::new(move |_, window, cx| { - let url_to_fetch = url_to_fetch.clone(); - let source_range = source_range.clone(); - let message_editor = message_editor.clone(); - let new_text = new_text.clone(); - let http_client = http_client.clone(); - window.defer(cx, move |window, cx| { - message_editor - .update(cx, |message_editor, cx| { - message_editor.confirm_mention_for_fetch( - new_text, - source_range, - url_to_fetch, - http_client, - window, - cx, - ) - }) - .ok(); - }); - false - }) - }), + confirm: Some(confirm_completion_callback( + url_to_fetch.to_string().into(), + source_range.start, + new_text.len() - 1, + message_editor, + mention_uri, + )), }) } } @@ -843,7 +796,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { }; let project = workspace.read(cx).project().clone(); - let http_client = workspace.read(cx).client().http_client(); let snapshot = buffer.read(cx).snapshot(); let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); @@ -852,8 +804,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { let text_thread_store = self.text_thread_store.clone(); let editor = self.message_editor.clone(); let Ok((exclude_paths, exclude_threads)) = - self.message_editor.update(cx, |message_editor, cx| { - message_editor.mentioned_path_and_threads(cx) + self.message_editor.update(cx, |message_editor, _cx| { + message_editor.mentioned_path_and_threads() }) else { return Task::ready(Ok(Vec::new())); @@ -942,7 +894,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { source_range.clone(), url, editor.clone(), - http_client.clone(), cx, ), diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 90827e5514..a4d74db266 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -16,14 +16,14 @@ use editor::{ use futures::{FutureExt as _, TryFutureExt as _}; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, - ImageFormat, Task, TextStyle, WeakEntity, + ImageFormat, Img, Task, TextStyle, WeakEntity, }; -use http_client::HttpClientWithUrl; use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{CompletionIntent, Project}; use settings::Settings; use std::{ + ffi::OsStr, fmt::Write, ops::Range, path::{Path, PathBuf}, @@ -48,6 +48,7 @@ pub struct MessageEditor { mention_set: MentionSet, editor: Entity, project: Entity, + workspace: WeakEntity, thread_store: Entity, text_thread_store: Entity, } @@ -79,7 +80,7 @@ impl MessageEditor { None, ); let completion_provider = ContextPickerCompletionProvider::new( - workspace, + workspace.clone(), thread_store.downgrade(), text_thread_store.downgrade(), cx.weak_entity(), @@ -114,6 +115,7 @@ impl MessageEditor { mention_set, thread_store, text_thread_store, + workspace, } } @@ -131,7 +133,7 @@ impl MessageEditor { self.editor.read(cx).is_empty(cx) } - pub fn mentioned_path_and_threads(&self, _: &App) -> (HashSet, HashSet) { + pub fn mentioned_path_and_threads(&self) -> (HashSet, HashSet) { let mut excluded_paths = HashSet::default(); let mut excluded_threads = HashSet::default(); @@ -165,8 +167,14 @@ impl MessageEditor { let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { return; }; + let Some(anchor) = snapshot + .buffer_snapshot + .anchor_in_excerpt(*excerpt_id, start) + else { + return; + }; - if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( *excerpt_id, start, content_len, @@ -175,48 +183,83 @@ impl MessageEditor { self.editor.clone(), window, cx, - ) { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - } - } - - pub fn confirm_mention_for_fetch( - &mut self, - new_text: String, - source_range: Range, - url: url::Url, - http_client: Arc, - window: &mut Window, - cx: &mut Context, - ) { - let mention_uri = MentionUri::Fetch { url: url.clone() }; - let icon_path = mention_uri.icon_path(cx); - - let start = source_range.start; - let content_len = new_text.len() - 1; - - let snapshot = self - .editor - .update(cx, |editor, cx| editor.snapshot(window, cx)); - let Some((&excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { - return; - }; - - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - excerpt_id, - start, - content_len, - url.to_string().into(), - icon_path, - self.editor.clone(), - window, - cx, ) else { return; }; + self.mention_set.insert_uri(crease_id, mention_uri.clone()); - let http_client = http_client.clone(); - let source_range = source_range.clone(); + match mention_uri { + MentionUri::Fetch { url } => { + self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); + } + MentionUri::File { + abs_path, + is_directory, + } => { + self.confirm_mention_for_file( + crease_id, + anchor, + abs_path, + is_directory, + window, + cx, + ); + } + MentionUri::Symbol { .. } + | MentionUri::Thread { .. } + | MentionUri::TextThread { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => {} + } + } + + fn confirm_mention_for_file( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + abs_path: PathBuf, + _is_directory: bool, + window: &mut Window, + cx: &mut Context, + ) { + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return; + }; + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let image = cx.spawn(async move |_, cx| { + let image = project + .update(cx, |project, cx| project.open_image(project_path, cx))? + .await?; + image.read_with(cx, |image, _cx| image.image.clone()) + }); + self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); + } + } + + fn confirm_mention_for_fetch( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + url: url::Url, + window: &mut Window, + cx: &mut Context, + ) { + let Some(http_client) = self + .workspace + .update(cx, |workspace, _cx| workspace.client().http_client()) + .ok() + else { + return; + }; let url_string = url.to_string(); let fetch = cx @@ -227,22 +270,18 @@ impl MessageEditor { .await }) .shared(); - self.mention_set.add_fetch_result(url, fetch.clone()); + self.mention_set + .add_fetch_result(url.clone(), fetch.clone()); cx.spawn_in(window, async move |this, cx| { let fetch = fetch.await.notify_async_err(cx); this.update(cx, |this, cx| { + let mention_uri = MentionUri::Fetch { url }; if fetch.is_some() { this.mention_set.insert_uri(crease_id, mention_uri.clone()); } else { // Remove crease if we failed to fetch this.editor.update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = - snapshot.anchor_in_excerpt(excerpt_id, source_range.start) - else { - return; - }; editor.display_map.update(cx, |display_map, cx| { display_map.unfold_intersecting(vec![anchor..anchor], true, cx); }); @@ -424,27 +463,46 @@ impl MessageEditor { let replacement_text = "image"; for image in images { - let (excerpt_id, anchor) = self.editor.update(cx, |message_editor, cx| { - let snapshot = message_editor.snapshot(window, cx); - let (excerpt_id, _, snapshot) = snapshot.buffer_snapshot.as_singleton().unwrap(); + let (excerpt_id, text_anchor, multibuffer_anchor) = + self.editor.update(cx, |message_editor, cx| { + let snapshot = message_editor.snapshot(window, cx); + let (excerpt_id, _, buffer_snapshot) = + snapshot.buffer_snapshot.as_singleton().unwrap(); - let anchor = snapshot.anchor_before(snapshot.len()); - message_editor.edit( - [( - multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - format!("{replacement_text} "), - )], - cx, - ); - (*excerpt_id, anchor) - }); + let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len()); + let multibuffer_anchor = snapshot + .buffer_snapshot + .anchor_in_excerpt(*excerpt_id, text_anchor); + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + format!("{replacement_text} "), + )], + cx, + ); + (*excerpt_id, text_anchor, multibuffer_anchor) + }); - self.insert_image( + let content_len = replacement_text.len(); + let Some(anchor) = multibuffer_anchor else { + return; + }; + let Some(crease_id) = insert_crease_for_image( excerpt_id, + text_anchor, + content_len, + None.clone(), + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.confirm_mention_for_image( + crease_id, anchor, - replacement_text.len(), - Arc::new(image), None, + Task::ready(Ok(Arc::new(image))), window, cx, ); @@ -510,34 +568,25 @@ impl MessageEditor { }) } - fn insert_image( + fn confirm_mention_for_image( &mut self, - excerpt_id: ExcerptId, - crease_start: text::Anchor, - content_len: usize, - image: Arc, - abs_path: Option>, + crease_id: CreaseId, + anchor: Anchor, + abs_path: Option, + image: Task>>, window: &mut Window, cx: &mut Context, ) { - let Some(crease_id) = insert_crease_for_image( - excerpt_id, - crease_start, - content_len, - abs_path.clone(), - self.editor.clone(), - window, - cx, - ) else { - return; - }; self.editor.update(cx, |_editor, cx| { - let format = image.format; - let convert = LanguageModelImage::from_image(image, cx); - let task = cx .spawn_in(window, async move |editor, cx| { - if let Some(image) = convert.await { + let image = image.await.map_err(|e| e.to_string())?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { Ok(MentionImage { abs_path, data: image.source, @@ -546,12 +595,6 @@ impl MessageEditor { } else { editor .update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = - snapshot.anchor_in_excerpt(excerpt_id, crease_start) - else { - return; - }; editor.display_map.update(cx, |display_map, cx| { display_map.unfold_intersecting(vec![anchor..anchor], true, cx); });