Skip to content

Commit 7bf6cc0

Browse files
cole-millerConradIrwin
authored andcommitted
acp: Eagerly load all kinds of mentions (#36741)
This PR makes it so that all kinds of @-mentions start loading their context as soon as they are confirmed. Previously, we were waiting to load the context for file, symbol, selection, and rule mentions until the user's message was sent. By kicking off loading immediately for these kinds of context, we can support adding selections from unsaved buffers, and we make the semantics of @-mentions more consistent. Loading all kinds of context eagerly also makes it possible to simplify the structure of the MentionSet and the code around it. Now MentionSet is just a single hash map, all the management of creases happens in a uniform way in `MessageEditor::confirm_completion`, and the helper methods for loading different kinds of context are much more focused and orthogonal. Release Notes: - N/A --------- Co-authored-by: Conrad <[email protected]>
1 parent e926e0b commit 7bf6cc0

File tree

7 files changed

+698
-836
lines changed

7 files changed

+698
-836
lines changed

crates/acp_thread/src/mention.rs

Lines changed: 109 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId};
55
use serde::{Deserialize, Serialize};
66
use std::{
77
fmt,
8-
ops::Range,
8+
ops::RangeInclusive,
99
path::{Path, PathBuf},
1010
str::FromStr,
1111
};
@@ -17,13 +17,14 @@ pub enum MentionUri {
1717
File {
1818
abs_path: PathBuf,
1919
},
20+
PastedImage,
2021
Directory {
2122
abs_path: PathBuf,
2223
},
2324
Symbol {
24-
path: PathBuf,
25+
abs_path: PathBuf,
2526
name: String,
26-
line_range: Range<u32>,
27+
line_range: RangeInclusive<u32>,
2728
},
2829
Thread {
2930
id: acp::SessionId,
@@ -38,8 +39,9 @@ pub enum MentionUri {
3839
name: String,
3940
},
4041
Selection {
41-
path: PathBuf,
42-
line_range: Range<u32>,
42+
#[serde(default, skip_serializing_if = "Option::is_none")]
43+
abs_path: Option<PathBuf>,
44+
line_range: RangeInclusive<u32>,
4345
},
4446
Fetch {
4547
url: Url,
@@ -48,36 +50,44 @@ pub enum MentionUri {
4850

4951
impl MentionUri {
5052
pub fn parse(input: &str) -> Result<Self> {
53+
fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
54+
let range = fragment
55+
.strip_prefix("L")
56+
.context("Line range must start with \"L\"")?;
57+
let (start, end) = range
58+
.split_once(":")
59+
.context("Line range must use colon as separator")?;
60+
let range = start
61+
.parse::<u32>()
62+
.context("Parsing line range start")?
63+
.checked_sub(1)
64+
.context("Line numbers should be 1-based")?
65+
..=end
66+
.parse::<u32>()
67+
.context("Parsing line range end")?
68+
.checked_sub(1)
69+
.context("Line numbers should be 1-based")?;
70+
Ok(range)
71+
}
72+
5173
let url = url::Url::parse(input)?;
5274
let path = url.path();
5375
match url.scheme() {
5476
"file" => {
5577
let path = url.to_file_path().ok().context("Extracting file path")?;
5678
if let Some(fragment) = url.fragment() {
57-
let range = fragment
58-
.strip_prefix("L")
59-
.context("Line range must start with \"L\"")?;
60-
let (start, end) = range
61-
.split_once(":")
62-
.context("Line range must use colon as separator")?;
63-
let line_range = start
64-
.parse::<u32>()
65-
.context("Parsing line range start")?
66-
.checked_sub(1)
67-
.context("Line numbers should be 1-based")?
68-
..end
69-
.parse::<u32>()
70-
.context("Parsing line range end")?
71-
.checked_sub(1)
72-
.context("Line numbers should be 1-based")?;
79+
let line_range = parse_line_range(fragment)?;
7380
if let Some(name) = single_query_param(&url, "symbol")? {
7481
Ok(Self::Symbol {
7582
name,
76-
path,
83+
abs_path: path,
7784
line_range,
7885
})
7986
} else {
80-
Ok(Self::Selection { path, line_range })
87+
Ok(Self::Selection {
88+
abs_path: Some(path),
89+
line_range,
90+
})
8191
}
8292
} else if input.ends_with("/") {
8393
Ok(Self::Directory { abs_path: path })
@@ -105,6 +115,17 @@ impl MentionUri {
105115
id: rule_id.into(),
106116
name,
107117
})
118+
} else if path.starts_with("/agent/pasted-image") {
119+
Ok(Self::PastedImage)
120+
} else if path.starts_with("/agent/untitled-buffer") {
121+
let fragment = url
122+
.fragment()
123+
.context("Missing fragment for untitled buffer selection")?;
124+
let line_range = parse_line_range(fragment)?;
125+
Ok(Self::Selection {
126+
abs_path: None,
127+
line_range,
128+
})
108129
} else {
109130
bail!("invalid zed url: {:?}", input);
110131
}
@@ -121,13 +142,16 @@ impl MentionUri {
121142
.unwrap_or_default()
122143
.to_string_lossy()
123144
.into_owned(),
145+
MentionUri::PastedImage => "Image".to_string(),
124146
MentionUri::Symbol { name, .. } => name.clone(),
125147
MentionUri::Thread { name, .. } => name.clone(),
126148
MentionUri::TextThread { name, .. } => name.clone(),
127149
MentionUri::Rule { name, .. } => name.clone(),
128150
MentionUri::Selection {
129-
path, line_range, ..
130-
} => selection_name(path, line_range),
151+
abs_path: path,
152+
line_range,
153+
..
154+
} => selection_name(path.as_deref(), line_range),
131155
MentionUri::Fetch { url } => url.to_string(),
132156
}
133157
}
@@ -137,6 +161,7 @@ impl MentionUri {
137161
MentionUri::File { abs_path } => {
138162
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
139163
}
164+
MentionUri::PastedImage => IconName::Image.path().into(),
140165
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
141166
.unwrap_or_else(|| IconName::Folder.path().into()),
142167
MentionUri::Symbol { .. } => IconName::Code.path().into(),
@@ -157,29 +182,40 @@ impl MentionUri {
157182
MentionUri::File { abs_path } => {
158183
Url::from_file_path(abs_path).expect("mention path should be absolute")
159184
}
185+
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
160186
MentionUri::Directory { abs_path } => {
161187
Url::from_directory_path(abs_path).expect("mention path should be absolute")
162188
}
163189
MentionUri::Symbol {
164-
path,
190+
abs_path,
165191
name,
166192
line_range,
167193
} => {
168-
let mut url = Url::from_file_path(path).expect("mention path should be absolute");
194+
let mut url =
195+
Url::from_file_path(abs_path).expect("mention path should be absolute");
169196
url.query_pairs_mut().append_pair("symbol", name);
170197
url.set_fragment(Some(&format!(
171198
"L{}:{}",
172-
line_range.start + 1,
173-
line_range.end + 1
199+
line_range.start() + 1,
200+
line_range.end() + 1
174201
)));
175202
url
176203
}
177-
MentionUri::Selection { path, line_range } => {
178-
let mut url = Url::from_file_path(path).expect("mention path should be absolute");
204+
MentionUri::Selection {
205+
abs_path: path,
206+
line_range,
207+
} => {
208+
let mut url = if let Some(path) = path {
209+
Url::from_file_path(path).expect("mention path should be absolute")
210+
} else {
211+
let mut url = Url::parse("zed:///").unwrap();
212+
url.set_path("/agent/untitled-buffer");
213+
url
214+
};
179215
url.set_fragment(Some(&format!(
180216
"L{}:{}",
181-
line_range.start + 1,
182-
line_range.end + 1
217+
line_range.start() + 1,
218+
line_range.end() + 1
183219
)));
184220
url
185221
}
@@ -191,7 +227,10 @@ impl MentionUri {
191227
}
192228
MentionUri::TextThread { path, name } => {
193229
let mut url = Url::parse("zed:///").unwrap();
194-
url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
230+
url.set_path(&format!(
231+
"/agent/text-thread/{}",
232+
path.to_string_lossy().trim_start_matches('/')
233+
));
195234
url.query_pairs_mut().append_pair("name", name);
196235
url
197236
}
@@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
237276
}
238277
}
239278

240-
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
279+
pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
241280
format!(
242281
"{} ({}:{})",
243-
path.file_name().unwrap_or_default().display(),
244-
line_range.start + 1,
245-
line_range.end + 1
282+
path.and_then(|path| path.file_name())
283+
.unwrap_or("Untitled".as_ref())
284+
.display(),
285+
*line_range.start() + 1,
286+
*line_range.end() + 1
246287
)
247288
}
248289

@@ -302,14 +343,14 @@ mod tests {
302343
let parsed = MentionUri::parse(symbol_uri).unwrap();
303344
match &parsed {
304345
MentionUri::Symbol {
305-
path,
346+
abs_path: path,
306347
name,
307348
line_range,
308349
} => {
309350
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
310351
assert_eq!(name, "MySymbol");
311-
assert_eq!(line_range.start, 9);
312-
assert_eq!(line_range.end, 19);
352+
assert_eq!(line_range.start(), &9);
353+
assert_eq!(line_range.end(), &19);
313354
}
314355
_ => panic!("Expected Symbol variant"),
315356
}
@@ -321,16 +362,39 @@ mod tests {
321362
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
322363
let parsed = MentionUri::parse(selection_uri).unwrap();
323364
match &parsed {
324-
MentionUri::Selection { path, line_range } => {
325-
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
326-
assert_eq!(line_range.start, 4);
327-
assert_eq!(line_range.end, 14);
365+
MentionUri::Selection {
366+
abs_path: path,
367+
line_range,
368+
} => {
369+
assert_eq!(
370+
path.as_ref().unwrap().to_str().unwrap(),
371+
path!("/path/to/file.rs")
372+
);
373+
assert_eq!(line_range.start(), &4);
374+
assert_eq!(line_range.end(), &14);
328375
}
329376
_ => panic!("Expected Selection variant"),
330377
}
331378
assert_eq!(parsed.to_uri().to_string(), selection_uri);
332379
}
333380

381+
#[test]
382+
fn test_parse_untitled_selection_uri() {
383+
let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
384+
let parsed = MentionUri::parse(selection_uri).unwrap();
385+
match &parsed {
386+
MentionUri::Selection {
387+
abs_path: None,
388+
line_range,
389+
} => {
390+
assert_eq!(line_range.start(), &0);
391+
assert_eq!(line_range.end(), &9);
392+
}
393+
_ => panic!("Expected Selection variant without path"),
394+
}
395+
assert_eq!(parsed.to_uri().to_string(), selection_uri);
396+
}
397+
334398
#[test]
335399
fn test_parse_thread_uri() {
336400
let thread_uri = "zed:///agent/thread/session123?name=Thread+name";

crates/agent/src/thread_store.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,8 +893,19 @@ impl ThreadsDatabase {
893893

894894
let needs_migration_from_heed = mdb_path.exists();
895895

896-
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
896+
let connection = if *ZED_STATELESS {
897897
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
898+
} else if cfg!(any(feature = "test-support", test)) {
899+
// rust stores the name of the test on the current thread.
900+
// We use this to automatically create a database that will
901+
// be shared within the test (for the test_retrieve_old_thread)
902+
// but not with concurrent tests.
903+
let thread = std::thread::current();
904+
let test_name = thread.name();
905+
Connection::open_memory(Some(&format!(
906+
"THREAD_FALLBACK_{}",
907+
test_name.unwrap_or_default()
908+
)))
898909
} else {
899910
Connection::open_file(&sqlite_path.to_string_lossy())
900911
};

crates/agent2/src/db.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,19 @@ impl ThreadsDatabase {
266266
}
267267

268268
pub fn new(executor: BackgroundExecutor) -> Result<Self> {
269-
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
269+
let connection = if *ZED_STATELESS {
270270
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
271+
} else if cfg!(any(feature = "test-support", test)) {
272+
// rust stores the name of the test on the current thread.
273+
// We use this to automatically create a database that will
274+
// be shared within the test (for the test_retrieve_old_thread)
275+
// but not with concurrent tests.
276+
let thread = std::thread::current();
277+
let test_name = thread.name();
278+
Connection::open_memory(Some(&format!(
279+
"THREAD_FALLBACK_{}",
280+
test_name.unwrap_or_default()
281+
)))
271282
} else {
272283
let threads_dir = paths::data_dir().join("threads");
273284
std::fs::create_dir_all(&threads_dir)?;

0 commit comments

Comments
 (0)