diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 122cd9a5cabb9676b0a7afdf76c0de5862f979cb..bbd13da5fa4124546d5457755f2bd2f5d737ccac 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -126,6 +126,39 @@ impl MentionUri { abs_path: None, line_range, }) + } else if let Some(name) = path.strip_prefix("/agent/symbol/") { + let fragment = url + .fragment() + .context("Missing fragment for untitled buffer selection")?; + let line_range = parse_line_range(fragment)?; + let path = + single_query_param(&url, "path")?.context("Missing path for symbol")?; + Ok(Self::Symbol { + name: name.to_string(), + abs_path: path.into(), + line_range, + }) + } else if path.starts_with("/agent/file") { + let path = + single_query_param(&url, "path")?.context("Missing path for file")?; + Ok(Self::File { + abs_path: path.into(), + }) + } else if path.starts_with("/agent/directory") { + let path = + single_query_param(&url, "path")?.context("Missing path for directory")?; + Ok(Self::Directory { + abs_path: path.into(), + }) + } else if path.starts_with("/agent/selection") { + let fragment = url.fragment().context("Missing fragment for selection")?; + let line_range = parse_line_range(fragment)?; + let path = + single_query_param(&url, "path")?.context("Missing path for selection")?; + Ok(Self::Selection { + abs_path: Some(path.into()), + line_range, + }) } else { bail!("invalid zed url: {:?}", input); } @@ -180,20 +213,29 @@ impl MentionUri { pub fn to_uri(&self) -> Url { match self { MentionUri::File { abs_path } => { - Url::from_file_path(abs_path).expect("mention path should be absolute") + let mut url = Url::parse("zed:///").unwrap(); + url.set_path("/agent/file"); + url.query_pairs_mut() + .append_pair("path", &abs_path.to_string_lossy()); + url } 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") + let mut url = Url::parse("zed:///").unwrap(); + url.set_path("/agent/directory"); + url.query_pairs_mut() + .append_pair("path", &abs_path.to_string_lossy()); + url } MentionUri::Symbol { abs_path, name, line_range, } => { - let mut url = - Url::from_file_path(abs_path).expect("mention path should be absolute"); - url.query_pairs_mut().append_pair("symbol", name); + let mut url = Url::parse("zed:///").unwrap(); + url.set_path(&format!("/agent/symbol/{name}")); + url.query_pairs_mut() + .append_pair("path", &abs_path.to_string_lossy()); url.set_fragment(Some(&format!( "L{}:{}", line_range.start() + 1, @@ -202,15 +244,16 @@ impl MentionUri { url } MentionUri::Selection { - abs_path: path, + abs_path, line_range, } => { - let mut url = if let Some(path) = path { - Url::from_file_path(path).expect("mention path should be absolute") + let mut url = Url::parse("zed:///").unwrap(); + if let Some(abs_path) = abs_path { + url.set_path("/agent/selection"); + url.query_pairs_mut() + .append_pair("path", &abs_path.to_string_lossy()); } else { - let mut url = Url::parse("zed:///").unwrap(); url.set_path("/agent/untitled-buffer"); - url }; url.set_fragment(Some(&format!( "L{}:{}", @@ -295,37 +338,32 @@ mod tests { #[test] fn test_parse_file_uri() { - let file_uri = uri!("file:///path/to/file.rs"); - let parsed = MentionUri::parse(file_uri).unwrap(); + let old_uri = uri!("file:///path/to/file.rs"); + let parsed = MentionUri::parse(old_uri).unwrap(); match &parsed { MentionUri::File { abs_path } => { assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs")); } _ => panic!("Expected File variant"), } - assert_eq!(parsed.to_uri().to_string(), file_uri); + let new_uri = parsed.to_uri().to_string(); + assert!(new_uri.starts_with("zed:///agent/file")); + assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed); } #[test] fn test_parse_directory_uri() { - let file_uri = uri!("file:///path/to/dir/"); - let parsed = MentionUri::parse(file_uri).unwrap(); + let old_uri = uri!("file:///path/to/dir/"); + let parsed = MentionUri::parse(old_uri).unwrap(); match &parsed { MentionUri::Directory { abs_path } => { assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/")); } _ => panic!("Expected Directory variant"), } - assert_eq!(parsed.to_uri().to_string(), file_uri); - } - - #[test] - fn test_to_directory_uri_with_slash() { - let uri = MentionUri::Directory { - abs_path: PathBuf::from(path!("/path/to/dir/")), - }; - let expected = uri!("file:///path/to/dir/"); - assert_eq!(uri.to_uri().to_string(), expected); + let new_uri = parsed.to_uri().to_string(); + assert!(new_uri.starts_with("zed:///agent/directory")); + assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed); } #[test] @@ -333,14 +371,15 @@ mod tests { let uri = MentionUri::Directory { abs_path: PathBuf::from(path!("/path/to/dir")), }; - let expected = uri!("file:///path/to/dir/"); - assert_eq!(uri.to_uri().to_string(), expected); + let uri_string = uri.to_uri().to_string(); + assert!(uri_string.starts_with("zed:///agent/directory")); + assert_eq!(MentionUri::parse(&uri_string).unwrap(), uri); } #[test] fn test_parse_symbol_uri() { - let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20"); - let parsed = MentionUri::parse(symbol_uri).unwrap(); + let old_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20"); + let parsed = MentionUri::parse(old_uri).unwrap(); match &parsed { MentionUri::Symbol { abs_path: path, @@ -354,13 +393,15 @@ mod tests { } _ => panic!("Expected Symbol variant"), } - assert_eq!(parsed.to_uri().to_string(), symbol_uri); + let new_uri = parsed.to_uri().to_string(); + assert!(new_uri.starts_with("zed:///agent/symbol/MySymbol")); + assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed); } #[test] fn test_parse_selection_uri() { - let selection_uri = uri!("file:///path/to/file.rs#L5:15"); - let parsed = MentionUri::parse(selection_uri).unwrap(); + let old_uri = uri!("file:///path/to/file.rs#L5:15"); + let parsed = MentionUri::parse(old_uri).unwrap(); match &parsed { MentionUri::Selection { abs_path: path, @@ -375,7 +416,9 @@ mod tests { } _ => panic!("Expected Selection variant"), } - assert_eq!(parsed.to_uri().to_string(), selection_uri); + let new_uri = parsed.to_uri().to_string(); + assert!(new_uri.starts_with("zed:///agent/selection")); + assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed); } #[test] diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index f3324da448b40f6197b529f8915117df4ae6030e..7fb7a114cfaa28bfa678a2b9bfe79545218a9d5b 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1205,7 +1205,7 @@ mod tests { use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri}; use fs::FakeFs; use gpui::TestAppContext; - use indoc::indoc; + use indoc::formatdoc; use language_model::fake_provider::FakeLanguageModel; use serde_json::json; use settings::SettingsStore; @@ -1502,13 +1502,17 @@ mod tests { summary_model.end_last_completion_stream(); send.await.unwrap(); + let uri = MentionUri::File { + abs_path: path!("/a/b.md").into(), + } + .to_uri(); acp_thread.read_with(cx, |thread, cx| { assert_eq!( thread.to_markdown(cx), - indoc! {" + formatdoc! {" ## User - What does [@b.md](file:///a/b.md) mean? + What does [@b.md]({uri}) mean? ## Assistant @@ -1544,10 +1548,10 @@ mod tests { acp_thread.read_with(cx, |thread, cx| { assert_eq!( thread.to_markdown(cx), - indoc! {" + formatdoc! {" ## User - What does [@b.md](file:///a/b.md) mean? + What does [@b.md]({uri}) mean? ## Assistant diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 689d3e25eb239e2bd09952f887f1969fb5b510b5..b37e073f8ac627a79f495a72804aa75d169571ef 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -48,7 +48,7 @@ use std::{ use text::OffsetRangeExt; use theme::ThemeSettings; use ui::{ButtonLike, TintColor, Toggleable, prelude::*}; -use util::{ResultExt, debug_panic, paths::PathStyle, rel_path::RelPath}; +use util::{ResultExt, debug_panic, rel_path::RelPath}; use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; @@ -108,11 +108,6 @@ impl MessageEditor { available_commands.clone(), )); let mention_set = MentionSet::default(); - // TODO: fix mentions when remoting with mixed path styles. - let host_and_guest_paths_differ = project - .read(cx) - .remote_client() - .is_some_and(|client| client.read(cx).path_style() != PathStyle::local()); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); @@ -122,9 +117,7 @@ impl MessageEditor { editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); editor.set_use_modal_editing(true); - if !host_and_guest_paths_differ { - editor.set_completion_provider(Some(completion_provider.clone())); - } + editor.set_completion_provider(Some(completion_provider.clone())); editor.set_context_menu_options(ContextMenuOptions { min_entries_visible: 12, max_entries_visible: 12, @@ -1600,7 +1593,7 @@ mod tests { use serde_json::json; use text::Point; use ui::{App, Context, IntoElement, Render, SharedString, Window}; - use util::{path, paths::PathStyle, rel_path::rel_path, uri}; + use util::{path, paths::PathStyle, rel_path::rel_path}; use workspace::{AppState, Item, Workspace}; use crate::acp::{ @@ -2266,7 +2259,11 @@ mod tests { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); }); - let url_one = uri!("file:///dir/a/one.txt"); + let url_one = MentionUri::File { + abs_path: path!("/dir/a/one.txt").into(), + } + .to_uri() + .to_string(); editor.update(&mut cx, |editor, cx| { let text = editor.text(cx); assert_eq!(text, format!("Lorem [@one.txt]({url_one}) ")); @@ -2371,7 +2368,11 @@ mod tests { .into_values() .collect::>(); - let url_eight = uri!("file:///dir/b/eight.txt"); + let url_eight = MentionUri::File { + abs_path: path!("/dir/b/eight.txt").into(), + } + .to_uri() + .to_string(); { let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { @@ -2470,6 +2471,12 @@ mod tests { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); }); + let symbol = MentionUri::Symbol { + abs_path: path!("/dir/a/one.txt").into(), + name: "MySymbol".into(), + line_range: 0..=0, + }; + let contents = message_editor .update(&mut cx, |message_editor, cx| { message_editor.mention_set().contents( @@ -2489,12 +2496,7 @@ mod tests { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); - pretty_assertions::assert_eq!( - uri, - &format!("{url_one}?symbol=MySymbol#L1:1") - .parse::() - .unwrap() - ); + pretty_assertions::assert_eq!(uri, &symbol); } cx.run_until_parked(); @@ -2502,7 +2504,10 @@ mod tests { editor.read_with(&cx, |editor, cx| { assert_eq!( editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + format!( + "Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ", + symbol.to_uri(), + ) ); }); @@ -2512,7 +2517,7 @@ mod tests { editor.update(&mut cx, |editor, cx| { assert_eq!( editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri()) ); assert!(editor.has_visible_completions_menu()); assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]); @@ -2541,7 +2546,10 @@ mod tests { editor.read_with(&cx, |editor, cx| { assert_eq!( editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + format!( + "Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ", + symbol.to_uri() + ) ); }); @@ -2551,7 +2559,7 @@ mod tests { editor.update(&mut cx, |editor, cx| { assert_eq!( editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri()) ); assert!(editor.has_visible_completions_menu()); assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]); @@ -2566,11 +2574,14 @@ mod tests { // Mention was removed editor.read_with(&cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") - ); - }); + assert_eq!( + editor.text(cx), + format!( + "Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ", + symbol.to_uri() + ) + ); + }); // Now getting the contents succeeds, because the invalid mention was removed let contents = message_editor