@@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId};
use serde::{Deserialize, Serialize};
use std::{
fmt,
- ops::Range,
+ ops::RangeInclusive,
path::{Path, PathBuf},
str::FromStr,
};
@@ -17,13 +17,14 @@ pub enum MentionUri {
File {
abs_path: PathBuf,
},
+ PastedImage,
Directory {
abs_path: PathBuf,
},
Symbol {
- path: PathBuf,
+ abs_path: PathBuf,
name: String,
- line_range: Range<u32>,
+ line_range: RangeInclusive<u32>,
},
Thread {
id: acp::SessionId,
@@ -38,8 +39,9 @@ pub enum MentionUri {
name: String,
},
Selection {
- path: PathBuf,
- line_range: Range<u32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ abs_path: Option<PathBuf>,
+ line_range: RangeInclusive<u32>,
},
Fetch {
url: Url,
@@ -48,36 +50,44 @@ pub enum MentionUri {
impl MentionUri {
pub fn parse(input: &str) -> Result<Self> {
+ fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
+ let range = fragment
+ .strip_prefix("L")
+ .context("Line range must start with \"L\"")?;
+ let (start, end) = range
+ .split_once(":")
+ .context("Line range must use colon as separator")?;
+ let range = start
+ .parse::<u32>()
+ .context("Parsing line range start")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?
+ ..=end
+ .parse::<u32>()
+ .context("Parsing line range end")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?;
+ Ok(range)
+ }
+
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
let path = url.to_file_path().ok().context("Extracting file path")?;
if let Some(fragment) = url.fragment() {
- let range = fragment
- .strip_prefix("L")
- .context("Line range must start with \"L\"")?;
- let (start, end) = range
- .split_once(":")
- .context("Line range must use colon as separator")?;
- let line_range = start
- .parse::<u32>()
- .context("Parsing line range start")?
- .checked_sub(1)
- .context("Line numbers should be 1-based")?
- ..end
- .parse::<u32>()
- .context("Parsing line range end")?
- .checked_sub(1)
- .context("Line numbers should be 1-based")?;
+ let line_range = parse_line_range(fragment)?;
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
- path,
+ abs_path: path,
line_range,
})
} else {
- Ok(Self::Selection { path, line_range })
+ Ok(Self::Selection {
+ abs_path: Some(path),
+ line_range,
+ })
}
} else if input.ends_with("/") {
Ok(Self::Directory { abs_path: path })
@@ -105,6 +115,17 @@ impl MentionUri {
id: rule_id.into(),
name,
})
+ } else if path.starts_with("/agent/pasted-image") {
+ Ok(Self::PastedImage)
+ } else if path.starts_with("/agent/untitled-buffer") {
+ let fragment = url
+ .fragment()
+ .context("Missing fragment for untitled buffer selection")?;
+ let line_range = parse_line_range(fragment)?;
+ Ok(Self::Selection {
+ abs_path: None,
+ line_range,
+ })
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -121,13 +142,16 @@ impl MentionUri {
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
+ MentionUri::PastedImage => "Image".to_string(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection {
- path, line_range, ..
- } => selection_name(path, line_range),
+ abs_path: path,
+ line_range,
+ ..
+ } => selection_name(path.as_deref(), line_range),
MentionUri::Fetch { url } => url.to_string(),
}
}
@@ -137,6 +161,7 @@ impl MentionUri {
MentionUri::File { abs_path } => {
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
}
+ MentionUri::PastedImage => IconName::Image.path().into(),
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
.unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(),
@@ -157,29 +182,40 @@ impl MentionUri {
MentionUri::File { abs_path } => {
Url::from_file_path(abs_path).expect("mention path should be absolute")
}
+ MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
MentionUri::Directory { abs_path } => {
Url::from_directory_path(abs_path).expect("mention path should be absolute")
}
MentionUri::Symbol {
- path,
+ abs_path,
name,
line_range,
} => {
- let mut url = Url::from_file_path(path).expect("mention path should be absolute");
+ let mut url =
+ Url::from_file_path(abs_path).expect("mention path should be absolute");
url.query_pairs_mut().append_pair("symbol", name);
url.set_fragment(Some(&format!(
"L{}:{}",
- line_range.start + 1,
- line_range.end + 1
+ line_range.start() + 1,
+ line_range.end() + 1
)));
url
}
- MentionUri::Selection { path, line_range } => {
- let mut url = Url::from_file_path(path).expect("mention path should be absolute");
+ MentionUri::Selection {
+ abs_path: path,
+ line_range,
+ } => {
+ let mut url = if let Some(path) = path {
+ Url::from_file_path(path).expect("mention path should be absolute")
+ } else {
+ let mut url = Url::parse("zed:///").unwrap();
+ url.set_path("/agent/untitled-buffer");
+ url
+ };
url.set_fragment(Some(&format!(
"L{}:{}",
- line_range.start + 1,
- line_range.end + 1
+ line_range.start() + 1,
+ line_range.end() + 1
)));
url
}
@@ -191,7 +227,10 @@ impl MentionUri {
}
MentionUri::TextThread { path, name } => {
let mut url = Url::parse("zed:///").unwrap();
- url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
+ url.set_path(&format!(
+ "/agent/text-thread/{}",
+ path.to_string_lossy().trim_start_matches('/')
+ ));
url.query_pairs_mut().append_pair("name", name);
url
}
@@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
}
}
-pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
+pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
format!(
"{} ({}:{})",
- path.file_name().unwrap_or_default().display(),
- line_range.start + 1,
- line_range.end + 1
+ path.and_then(|path| path.file_name())
+ .unwrap_or("Untitled".as_ref())
+ .display(),
+ *line_range.start() + 1,
+ *line_range.end() + 1
)
}
@@ -302,14 +343,14 @@ mod tests {
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol {
- path,
+ abs_path: path,
name,
line_range,
} => {
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
assert_eq!(name, "MySymbol");
- assert_eq!(line_range.start, 9);
- assert_eq!(line_range.end, 19);
+ assert_eq!(line_range.start(), &9);
+ assert_eq!(line_range.end(), &19);
}
_ => panic!("Expected Symbol variant"),
}
@@ -321,16 +362,39 @@ mod tests {
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
- MentionUri::Selection { path, line_range } => {
- assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
- assert_eq!(line_range.start, 4);
- assert_eq!(line_range.end, 14);
+ MentionUri::Selection {
+ abs_path: path,
+ line_range,
+ } => {
+ assert_eq!(
+ path.as_ref().unwrap().to_str().unwrap(),
+ path!("/path/to/file.rs")
+ );
+ assert_eq!(line_range.start(), &4);
+ assert_eq!(line_range.end(), &14);
}
_ => panic!("Expected Selection variant"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
+ #[test]
+ fn test_parse_untitled_selection_uri() {
+ let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
+ let parsed = MentionUri::parse(selection_uri).unwrap();
+ match &parsed {
+ MentionUri::Selection {
+ abs_path: None,
+ line_range,
+ } => {
+ assert_eq!(line_range.start(), &0);
+ assert_eq!(line_range.end(), &9);
+ }
+ _ => panic!("Expected Selection variant without path"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), selection_uri);
+ }
+
#[test]
fn test_parse_thread_uri() {
let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
@@ -6,7 +6,7 @@ use acp_thread::{MentionUri, selection_name};
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use agent2::HistoryStore;
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path;
use collections::{HashMap, HashSet};
use editor::{
@@ -17,8 +17,8 @@ use editor::{
display_map::{Crease, CreaseId, FoldId},
};
use futures::{
- FutureExt as _, TryFutureExt as _,
- future::{Shared, join_all, try_join_all},
+ FutureExt as _,
+ future::{Shared, join_all},
};
use gpui::{
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable,
@@ -28,14 +28,14 @@ use gpui::{
use language::{Buffer, Language};
use language_model::LanguageModelImage;
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
-use prompt_store::PromptStore;
+use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::Settings;
use std::{
cell::Cell,
ffi::OsStr,
fmt::Write,
- ops::Range,
+ ops::{Range, RangeInclusive},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
@@ -49,12 +49,8 @@ use ui::{
Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
h_flex, px,
};
-use url::Url;
-use util::ResultExt;
-use workspace::{
- Toast, Workspace,
- notifications::{NotificationId, NotifyResultExt as _},
-};
+use util::{ResultExt, debug_panic};
+use workspace::{Workspace, notifications::NotifyResultExt as _};
use zed_actions::agent::Chat;
const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50);
@@ -219,9 +215,9 @@ impl MessageEditor {
pub fn mentions(&self) -> HashSet<MentionUri> {
self.mention_set
- .uri_by_crease_id
+ .mentions
.values()
- .cloned()
+ .map(|(uri, _)| uri.clone())
.collect()
}
@@ -246,132 +242,168 @@ impl MessageEditor {
else {
return Task::ready(());
};
+ let end_anchor = snapshot
+ .buffer_snapshot
+ .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
- if let MentionUri::File { abs_path, .. } = &mention_uri {
- let extension = abs_path
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default();
-
- if Img::extensions().contains(&extension) && !extension.contains("svg") {
- if !self.prompt_capabilities.get().image {
- struct ImagesNotAllowed;
+ let crease_id = if let MentionUri::File { abs_path } = &mention_uri
+ && let Some(extension) = abs_path.extension()
+ && let Some(extension) = extension.to_str()
+ && Img::extensions().contains(&extension)
+ && !extension.contains("svg")
+ {
+ let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ log::error!("project path not found");
+ return Task::ready(());
+ };
+ let image = self
+ .project
+ .update(cx, |project, cx| project.open_image(project_path, cx));
+ let image = cx
+ .spawn(async move |_, cx| {
+ let image = image.await.map_err(|e| e.to_string())?;
+ let image = image
+ .update(cx, |image, _| image.image.clone())
+ .map_err(|e| e.to_string())?;
+ Ok(image)
+ })
+ .shared();
+ insert_crease_for_image(
+ *excerpt_id,
+ start,
+ content_len,
+ Some(abs_path.as_path().into()),
+ image,
+ self.editor.clone(),
+ window,
+ cx,
+ )
+ } else {
+ crate::context_picker::insert_crease_for_mention(
+ *excerpt_id,
+ start,
+ content_len,
+ crease_text,
+ mention_uri.icon_path(cx),
+ self.editor.clone(),
+ window,
+ cx,
+ )
+ };
+ let Some(crease_id) = crease_id else {
+ return Task::ready(());
+ };
- let end_anchor = snapshot.buffer_snapshot.anchor_before(
- start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1,
- );
+ let task = match mention_uri.clone() {
+ MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
+ MentionUri::Directory { abs_path } => self.confirm_mention_for_directory(abs_path, cx),
+ MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
+ MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
+ MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
+ MentionUri::Symbol {
+ abs_path,
+ line_range,
+ ..
+ } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
+ MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
+ MentionUri::PastedImage => {
+ debug_panic!("pasted image URI should not be included in completions");
+ Task::ready(Err(anyhow!(
+ "pasted imaged URI should not be included in completions"
+ )))
+ }
+ MentionUri::Selection { .. } => {
+ // Handled elsewhere
+ debug_panic!("unexpected selection URI");
+ Task::ready(Err(anyhow!("unexpected selection URI")))
+ }
+ };
+ let task = cx
+ .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
+ .shared();
+ self.mention_set
+ .mentions
+ .insert(crease_id, (mention_uri, task.clone()));
- self.editor.update(cx, |editor, cx| {
+ // Notify the user if we failed to load the mentioned context
+ cx.spawn_in(window, async move |this, cx| {
+ if task.await.notify_async_err(cx).is_none() {
+ this.update(cx, |this, cx| {
+ this.editor.update(cx, |editor, cx| {
// Remove mention
- editor.edit([((start_anchor..end_anchor), "")], cx);
+ editor.edit([(start_anchor..end_anchor, "")], cx);
});
-
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<ImagesNotAllowed>(),
- "This agent does not support images yet",
- )
- .autohide(),
- cx,
- );
- })
- .ok();
- return Task::ready(());
- }
-
- let project = self.project.clone();
- let Some(project_path) = project
- .read(cx)
- .project_path_for_absolute_path(abs_path, cx)
- else {
- return Task::ready(());
- };
- let image = cx
- .spawn(async move |_, cx| {
- let image = project
- .update(cx, |project, cx| project.open_image(project_path, cx))
- .map_err(|e| e.to_string())?
- .await
- .map_err(|e| e.to_string())?;
- image
- .read_with(cx, |image, _cx| image.image.clone())
- .map_err(|e| e.to_string())
- })
- .shared();
- let Some(crease_id) = insert_crease_for_image(
- *excerpt_id,
- start,
- content_len,
- Some(abs_path.as_path().into()),
- image.clone(),
- self.editor.clone(),
- window,
- cx,
- ) else {
- return Task::ready(());
- };
- return self.confirm_mention_for_image(
- crease_id,
- start_anchor,
- Some(abs_path.clone()),
- image,
- window,
- cx,
- );
+ this.mention_set.mentions.remove(&crease_id);
+ })
+ .ok();
}
- }
+ })
+ }
- let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
- *excerpt_id,
- start,
- content_len,
- crease_text,
- mention_uri.icon_path(cx),
- self.editor.clone(),
- window,
- cx,
- ) else {
- return Task::ready(());
+ fn confirm_mention_for_file(
+ &mut self,
+ abs_path: PathBuf,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
};
+ let extension = abs_path
+ .extension()
+ .and_then(OsStr::to_str)
+ .unwrap_or_default();
- match mention_uri {
- MentionUri::Fetch { url } => {
- self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx)
- }
- MentionUri::Directory { abs_path } => {
- self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx)
- }
- MentionUri::Thread { id, name } => {
- self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx)
- }
- MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread(
- crease_id,
- start_anchor,
- path,
- name,
- window,
- cx,
- ),
- MentionUri::File { .. }
- | MentionUri::Symbol { .. }
- | MentionUri::Rule { .. }
- | MentionUri::Selection { .. } => {
- self.mention_set.insert_uri(crease_id, mention_uri.clone());
- Task::ready(())
+ if Img::extensions().contains(&extension) && !extension.contains("svg") {
+ if !self.prompt_capabilities.get().image {
+ return Task::ready(Err(anyhow!("This agent does not support images yet")));
}
+ let task = self
+ .project
+ .update(cx, |project, cx| project.open_image(project_path, cx));
+ return cx.spawn(async move |_, cx| {
+ let image = task.await?;
+ let image = image.update(cx, |image, _| image.image.clone())?;
+ let format = image.format;
+ let image = cx
+ .update(|cx| LanguageModelImage::from_image(image, cx))?
+ .await;
+ if let Some(image) = image {
+ Ok(Mention::Image(MentionImage {
+ data: image.source,
+ format,
+ }))
+ } else {
+ Err(anyhow!("Failed to convert image"))
+ }
+ });
}
+
+ let buffer = self
+ .project
+ .update(cx, |project, cx| project.open_buffer(project_path, cx));
+ cx.spawn(async move |_, cx| {
+ let buffer = buffer.await?;
+ let mention = buffer.update(cx, |buffer, cx| Mention::Text {
+ content: buffer.text(),
+ tracked_buffers: vec![cx.entity()],
+ })?;
+ anyhow::Ok(mention)
+ })
}
fn confirm_mention_for_directory(
&mut self,
- crease_id: CreaseId,
- anchor: Anchor,
abs_path: PathBuf,
- window: &mut Window,
cx: &mut Context<Self>,
- ) -> Task<()> {
+ ) -> Task<Result<Mention>> {
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
let mut files = Vec::new();
@@ -386,24 +418,21 @@ impl MessageEditor {
files
}
- let uri = MentionUri::Directory {
- abs_path: abs_path.clone(),
- };
let Some(project_path) = self
.project
.read(cx)
.project_path_for_absolute_path(&abs_path, cx)
else {
- return Task::ready(());
+ return Task::ready(Err(anyhow!("project path not found")));
};
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
- return Task::ready(());
+ return Task::ready(Err(anyhow!("project entry not found")));
};
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
- return Task::ready(());
+ return Task::ready(Err(anyhow!("worktree not found")));
};
let project = self.project.clone();
- let task = cx.spawn(async move |_, cx| {
+ cx.spawn(async move |_, cx| {
let directory_path = entry.path.clone();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
@@ -453,89 +482,83 @@ impl MessageEditor {
((rel_path, full_path, rope), buffer)
})
.unzip();
- (render_directory_contents(contents), tracked_buffers)
+ Mention::Text {
+ content: render_directory_contents(contents),
+ tracked_buffers,
+ }
})
.await;
anyhow::Ok(contents)
- });
- let task = cx
- .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
- .shared();
-
- self.mention_set
- .directories
- .insert(abs_path.clone(), task.clone());
-
- let editor = self.editor.clone();
- cx.spawn_in(window, async move |this, cx| {
- if task.await.notify_async_err(cx).is_some() {
- this.update(cx, |this, _| {
- this.mention_set.insert_uri(crease_id, uri);
- })
- .ok();
- } else {
- editor
- .update(cx, |editor, cx| {
- editor.display_map.update(cx, |display_map, cx| {
- display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
- });
- editor.remove_creases([crease_id], cx);
- })
- .ok();
- this.update(cx, |this, _cx| {
- this.mention_set.directories.remove(&abs_path);
- })
- .ok();
- }
})
}
fn confirm_mention_for_fetch(
&mut self,
- crease_id: CreaseId,
- anchor: Anchor,
url: url::Url,
- window: &mut Window,
cx: &mut Context<Self>,
- ) -> Task<()> {
- let Some(http_client) = self
+ ) -> Task<Result<Mention>> {
+ let http_client = match self
.workspace
- .update(cx, |workspace, _cx| workspace.client().http_client())
- .ok()
- else {
- return Task::ready(());
+ .update(cx, |workspace, _| workspace.client().http_client())
+ {
+ Ok(http_client) => http_client,
+ Err(e) => return Task::ready(Err(e)),
};
-
- let url_string = url.to_string();
- let fetch = cx
- .background_executor()
- .spawn(async move {
- fetch_url_content(http_client, url_string)
- .map_err(|e| e.to_string())
- .await
+ cx.background_executor().spawn(async move {
+ let content = fetch_url_content(http_client, url.to_string()).await?;
+ Ok(Mention::Text {
+ content,
+ tracked_buffers: Vec::new(),
})
- .shared();
- 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| {
- if fetch.is_some() {
- this.mention_set
- .insert_uri(crease_id, MentionUri::Fetch { url });
- } else {
- // Remove crease if we failed to fetch
- this.editor.update(cx, |editor, cx| {
- editor.display_map.update(cx, |display_map, cx| {
- display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
- });
- editor.remove_creases([crease_id], cx);
- });
- this.mention_set.fetch_results.remove(&url);
+ fn confirm_mention_for_symbol(
+ &mut self,
+ abs_path: PathBuf,
+ line_range: RangeInclusive<u32>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
+ };
+ let buffer = self
+ .project
+ .update(cx, |project, cx| project.open_buffer(project_path, cx));
+ cx.spawn(async move |_, cx| {
+ let buffer = buffer.await?;
+ let mention = buffer.update(cx, |buffer, cx| {
+ let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
+ let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
+ let content = buffer.text_for_range(start..end).collect();
+ Mention::Text {
+ content,
+ tracked_buffers: vec![cx.entity()],
}
+ })?;
+ anyhow::Ok(mention)
+ })
+ }
+
+ fn confirm_mention_for_rule(
+ &mut self,
+ id: PromptId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(prompt_store) = self.prompt_store.clone() else {
+ return Task::ready(Err(anyhow!("missing prompt store")));
+ };
+ let prompt = prompt_store.read(cx).load(id, cx);
+ cx.spawn(async move |_, _| {
+ let prompt = prompt.await?;
+ Ok(Mention::Text {
+ content: prompt,
+ tracked_buffers: Vec::new(),
})
- .ok();
})
}
@@ -560,24 +583,24 @@ impl MessageEditor {
let range = snapshot.anchor_after(offset + range_to_fold.start)
..snapshot.anchor_after(offset + range_to_fold.end);
- // TODO support selections from buffers with no path
- let Some(project_path) = buffer.read(cx).project_path(cx) else {
- continue;
- };
- let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
- continue;
- };
+ let abs_path = buffer
+ .read(cx)
+ .project_path(cx)
+ .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx));
let snapshot = buffer.read(cx).snapshot();
+ let text = snapshot
+ .text_for_range(selection_range.clone())
+ .collect::<String>();
let point_range = selection_range.to_point(&snapshot);
- let line_range = point_range.start.row..point_range.end.row;
+ let line_range = point_range.start.row..=point_range.end.row;
let uri = MentionUri::Selection {
- path: abs_path.clone(),
+ abs_path: abs_path.clone(),
line_range: line_range.clone(),
};
let crease = crate::context_picker::crease_for_mention(
- selection_name(&abs_path, &line_range).into(),
+ selection_name(abs_path.as_deref(), &line_range).into(),
uri.icon_path(cx),
range,
self.editor.downgrade(),
@@ -589,132 +612,69 @@ impl MessageEditor {
crease_ids.first().copied().unwrap()
});
- self.mention_set.insert_uri(crease_id, uri);
+ self.mention_set.mentions.insert(
+ crease_id,
+ (
+ uri,
+ Task::ready(Ok(Mention::Text {
+ content: text,
+ tracked_buffers: vec![buffer],
+ }))
+ .shared(),
+ ),
+ );
}
}
fn confirm_mention_for_thread(
&mut self,
- crease_id: CreaseId,
- anchor: Anchor,
id: acp::SessionId,
- name: String,
- window: &mut Window,
cx: &mut Context<Self>,
- ) -> Task<()> {
- let uri = MentionUri::Thread {
- id: id.clone(),
- name,
- };
+ ) -> Task<Result<Mention>> {
let server = Rc::new(agent2::NativeAgentServer::new(
self.project.read(cx).fs().clone(),
self.history_store.clone(),
));
let connection = server.connect(Path::new(""), &self.project, cx);
- let load_summary = cx.spawn({
- let id = id.clone();
- async move |_, cx| {
- let agent = connection.await?;
- let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
- let summary = agent
- .0
- .update(cx, |agent, cx| agent.thread_summary(id, cx))?
- .await?;
- anyhow::Ok(summary)
- }
- });
- let task = cx
- .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}")))
- .shared();
-
- self.mention_set.insert_thread(id.clone(), task.clone());
- self.mention_set.insert_uri(crease_id, uri);
-
- let editor = self.editor.clone();
- cx.spawn_in(window, async move |this, cx| {
- if task.await.notify_async_err(cx).is_none() {
- editor
- .update(cx, |editor, cx| {
- editor.display_map.update(cx, |display_map, cx| {
- display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
- });
- editor.remove_creases([crease_id], cx);
- })
- .ok();
- this.update(cx, |this, _| {
- this.mention_set.thread_summaries.remove(&id);
- this.mention_set.uri_by_crease_id.remove(&crease_id);
- })
- .ok();
- }
+ cx.spawn(async move |_, cx| {
+ let agent = connection.await?;
+ let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
+ let summary = agent
+ .0
+ .update(cx, |agent, cx| agent.thread_summary(id, cx))?
+ .await?;
+ anyhow::Ok(Mention::Text {
+ content: summary.to_string(),
+ tracked_buffers: Vec::new(),
+ })
})
}
fn confirm_mention_for_text_thread(
&mut self,
- crease_id: CreaseId,
- anchor: Anchor,
path: PathBuf,
- name: String,
- window: &mut Window,
cx: &mut Context<Self>,
- ) -> Task<()> {
- let uri = MentionUri::TextThread {
- path: path.clone(),
- name,
- };
+ ) -> Task<Result<Mention>> {
let context = self.history_store.update(cx, |text_thread_store, cx| {
text_thread_store.load_text_thread(path.as_path().into(), cx)
});
- let task = cx
- .spawn(async move |_, cx| {
- let context = context.await.map_err(|e| e.to_string())?;
- let xml = context
- .update(cx, |context, cx| context.to_xml(cx))
- .map_err(|e| e.to_string())?;
- Ok(xml)
+ cx.spawn(async move |_, cx| {
+ let context = context.await?;
+ let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
+ Ok(Mention::Text {
+ content: xml,
+ tracked_buffers: Vec::new(),
})
- .shared();
-
- self.mention_set
- .insert_text_thread(path.clone(), task.clone());
-
- let editor = self.editor.clone();
- cx.spawn_in(window, async move |this, cx| {
- if task.await.notify_async_err(cx).is_some() {
- this.update(cx, |this, _| {
- this.mention_set.insert_uri(crease_id, uri);
- })
- .ok();
- } else {
- editor
- .update(cx, |editor, cx| {
- editor.display_map.update(cx, |display_map, cx| {
- display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
- });
- editor.remove_creases([crease_id], cx);
- })
- .ok();
- this.update(cx, |this, _| {
- this.mention_set.text_thread_summaries.remove(&path);
- })
- .ok();
- }
})
}
pub fn contents(
&self,
- window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
- let contents = self.mention_set.contents(
- &self.project,
- self.prompt_store.as_ref(),
- &self.prompt_capabilities.get(),
- window,
- cx,
- );
+ let contents = self
+ .mention_set
+ .contents(&self.prompt_capabilities.get(), cx);
let editor = self.editor.clone();
let prevent_slash_commands = self.prevent_slash_commands;
@@ -729,7 +689,7 @@ impl MessageEditor {
editor.display_map.update(cx, |map, cx| {
let snapshot = map.snapshot(cx);
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
- let Some(mention) = contents.get(&crease_id) else {
+ let Some((uri, mention)) = contents.get(&crease_id) else {
continue;
};
@@ -747,7 +707,6 @@ impl MessageEditor {
}
let chunk = match mention {
Mention::Text {
- uri,
content,
tracked_buffers,
} => {
@@ -764,17 +723,25 @@ impl MessageEditor {
})
}
Mention::Image(mention_image) => {
+ let uri = match uri {
+ MentionUri::File { .. } => Some(uri.to_uri().to_string()),
+ MentionUri::PastedImage => None,
+ other => {
+ debug_panic!(
+ "unexpected mention uri for image: {:?}",
+ other
+ );
+ None
+ }
+ };
acp::ContentBlock::Image(acp::ImageContent {
annotations: None,
data: mention_image.data.to_string(),
mime_type: mention_image.format.mime_type().into(),
- uri: mention_image
- .abs_path
- .as_ref()
- .map(|path| format!("file://{}", path.display())),
+ uri,
})
}
- Mention::UriOnly(uri) => {
+ Mention::UriOnly => {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
@@ -813,7 +780,13 @@ impl MessageEditor {
pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.clear(window, cx);
- editor.remove_creases(self.mention_set.drain(), cx)
+ editor.remove_creases(
+ self.mention_set
+ .mentions
+ .drain()
+ .map(|(crease_id, _)| crease_id),
+ cx,
+ )
});
}
@@ -853,7 +826,7 @@ impl MessageEditor {
}
cx.stop_propagation();
- let replacement_text = "image";
+ let replacement_text = MentionUri::PastedImage.as_link().to_string();
for image in images {
let (excerpt_id, text_anchor, multibuffer_anchor) =
self.editor.update(cx, |message_editor, cx| {
@@ -876,24 +849,62 @@ impl MessageEditor {
});
let content_len = replacement_text.len();
- let Some(anchor) = multibuffer_anchor else {
- return;
+ let Some(start_anchor) = multibuffer_anchor else {
+ continue;
};
- let task = Task::ready(Ok(Arc::new(image))).shared();
+ let end_anchor = self.editor.update(cx, |editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
+ });
+ let image = Arc::new(image);
let Some(crease_id) = insert_crease_for_image(
excerpt_id,
text_anchor,
content_len,
None.clone(),
- task.clone(),
+ Task::ready(Ok(image.clone())).shared(),
self.editor.clone(),
window,
cx,
) else {
- return;
+ continue;
};
- self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx)
- .detach();
+ let task = cx
+ .spawn_in(window, {
+ async move |_, cx| {
+ 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(Mention::Image(MentionImage {
+ data: image.source,
+ format,
+ }))
+ } else {
+ Err("Failed to convert image".into())
+ }
+ }
+ })
+ .shared();
+
+ self.mention_set
+ .mentions
+ .insert(crease_id, (MentionUri::PastedImage, task.clone()));
+
+ cx.spawn_in(window, async move |this, cx| {
+ if task.await.notify_async_err(cx).is_none() {
+ this.update(cx, |this, cx| {
+ this.editor.update(cx, |editor, cx| {
+ editor.edit([(start_anchor..end_anchor, "")], cx);
+ });
+ this.mention_set.mentions.remove(&crease_id);
+ })
+ .ok();
+ }
+ })
+ .detach();
}
}
@@ -995,67 +1006,6 @@ impl MessageEditor {
})
}
- fn confirm_mention_for_image(
- &mut self,
- crease_id: CreaseId,
- anchor: Anchor,
- abs_path: Option<PathBuf>,
- image: Shared<Task<Result<Arc<Image>, String>>>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Task<()> {
- let editor = self.editor.clone();
- let task = cx
- .spawn_in(window, {
- let abs_path = abs_path.clone();
- async move |_, cx| {
- let image = image.await?;
- 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,
- format,
- })
- } else {
- Err("Failed to convert image".into())
- }
- }
- })
- .shared();
-
- self.mention_set.insert_image(crease_id, task.clone());
-
- cx.spawn_in(window, async move |this, cx| {
- if task.await.notify_async_err(cx).is_some() {
- if let Some(abs_path) = abs_path.clone() {
- this.update(cx, |this, _cx| {
- this.mention_set
- .insert_uri(crease_id, MentionUri::File { abs_path });
- })
- .ok();
- }
- } else {
- editor
- .update(cx, |editor, cx| {
- editor.display_map.update(cx, |display_map, cx| {
- display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
- });
- editor.remove_creases([crease_id], cx);
- })
- .ok();
- this.update(cx, |this, _cx| {
- this.mention_set.images.remove(&crease_id);
- })
- .ok();
- }
- })
- }
-
pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.set_mode(mode);
@@ -1073,7 +1023,6 @@ impl MessageEditor {
let mut text = String::new();
let mut mentions = Vec::new();
- let mut images = Vec::new();
for chunk in message {
match chunk {
@@ -1084,26 +1033,58 @@ impl MessageEditor {
resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
..
}) => {
- if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
- let start = text.len();
- write!(&mut text, "{}", mention_uri.as_link()).ok();
- let end = text.len();
- mentions.push((start..end, mention_uri, resource.text));
- }
+ let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else {
+ continue;
+ };
+ let start = text.len();
+ write!(&mut text, "{}", mention_uri.as_link()).ok();
+ let end = text.len();
+ mentions.push((
+ start..end,
+ mention_uri,
+ Mention::Text {
+ content: resource.text,
+ tracked_buffers: Vec::new(),
+ },
+ ));
}
acp::ContentBlock::ResourceLink(resource) => {
if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
let start = text.len();
write!(&mut text, "{}", mention_uri.as_link()).ok();
let end = text.len();
- mentions.push((start..end, mention_uri, resource.uri));
+ mentions.push((start..end, mention_uri, Mention::UriOnly));
}
}
- acp::ContentBlock::Image(content) => {
+ acp::ContentBlock::Image(acp::ImageContent {
+ uri,
+ data,
+ mime_type,
+ annotations: _,
+ }) => {
+ let mention_uri = if let Some(uri) = uri {
+ MentionUri::parse(&uri)
+ } else {
+ Ok(MentionUri::PastedImage)
+ };
+ let Some(mention_uri) = mention_uri.log_err() else {
+ continue;
+ };
+ let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
+ log::error!("failed to parse MIME type for image: {mime_type:?}");
+ continue;
+ };
let start = text.len();
- text.push_str("image");
+ write!(&mut text, "{}", mention_uri.as_link()).ok();
let end = text.len();
- images.push((start..end, content));
+ mentions.push((
+ start..end,
+ mention_uri,
+ Mention::Image(MentionImage {
+ data: data.into(),
+ format,
+ }),
+ ));
}
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
}