1use agent_client_protocol as acp;
2use anyhow::{Context as _, Result, bail};
3use file_icons::FileIcons;
4use prompt_store::{PromptId, UserPromptId};
5use serde::{Deserialize, Serialize};
6use std::{
7 borrow::Cow,
8 fmt,
9 ops::RangeInclusive,
10 path::{Path, PathBuf},
11};
12use ui::{App, IconName, SharedString};
13use url::Url;
14use urlencoding::decode;
15use util::{ResultExt, paths::PathStyle};
16
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
18pub enum MentionUri {
19 File {
20 abs_path: PathBuf,
21 },
22 PastedImage,
23 Directory {
24 abs_path: PathBuf,
25 },
26 Symbol {
27 abs_path: PathBuf,
28 name: String,
29 line_range: RangeInclusive<u32>,
30 },
31 Thread {
32 id: acp::SessionId,
33 name: String,
34 },
35 Rule {
36 id: PromptId,
37 name: String,
38 },
39 Diagnostics {
40 #[serde(default = "default_include_errors")]
41 include_errors: bool,
42 #[serde(default)]
43 include_warnings: bool,
44 },
45 Selection {
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 abs_path: Option<PathBuf>,
48 line_range: RangeInclusive<u32>,
49 },
50 Fetch {
51 url: Url,
52 },
53 TerminalSelection {
54 line_count: u32,
55 },
56 GitDiff {
57 base_ref: String,
58 },
59 MergeConflict {
60 file_path: String,
61 },
62}
63
64impl MentionUri {
65 pub fn parse(input: &str, path_style: PathStyle) -> Result<Self> {
66 fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
67 let range = fragment.strip_prefix("L").unwrap_or(fragment);
68
69 let (start, end) = if let Some((start, end)) = range.split_once(":") {
70 (start, end)
71 } else if let Some((start, end)) = range.split_once("-") {
72 // Also handle L10-20 or L10-L20 format
73 (start, end.strip_prefix("L").unwrap_or(end))
74 } else {
75 // Single line number like L1872 - treat as a range of one line
76 (range, range)
77 };
78
79 let start_line = start
80 .parse::<u32>()
81 .context("Parsing line range start")?
82 .checked_sub(1)
83 .context("Line numbers should be 1-based")?;
84 let end_line = end
85 .parse::<u32>()
86 .context("Parsing line range end")?
87 .checked_sub(1)
88 .context("Line numbers should be 1-based")?;
89
90 Ok(start_line..=end_line)
91 }
92
93 let url = url::Url::parse(input)?;
94 let path = url.path();
95 match url.scheme() {
96 "file" => {
97 let normalized = if path_style.is_windows() {
98 path.trim_start_matches("/")
99 } else {
100 path
101 };
102 let decoded = decode(normalized).unwrap_or(Cow::Borrowed(normalized));
103 let path = decoded.as_ref();
104
105 if let Some(fragment) = url.fragment() {
106 let line_range = parse_line_range(fragment).log_err().unwrap_or(1..=1);
107 if let Some(name) = single_query_param(&url, "symbol")? {
108 Ok(Self::Symbol {
109 name,
110 abs_path: path.into(),
111 line_range,
112 })
113 } else {
114 Ok(Self::Selection {
115 abs_path: Some(path.into()),
116 line_range,
117 })
118 }
119 } else if input.ends_with("/") {
120 Ok(Self::Directory {
121 abs_path: path.into(),
122 })
123 } else {
124 Ok(Self::File {
125 abs_path: path.into(),
126 })
127 }
128 }
129 "zed" => {
130 if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
131 let name = single_query_param(&url, "name")?.context("Missing thread name")?;
132 Ok(Self::Thread {
133 id: acp::SessionId::new(thread_id),
134 name,
135 })
136 } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
137 let name = single_query_param(&url, "name")?.context("Missing rule name")?;
138 let rule_id = UserPromptId(rule_id.parse()?);
139 Ok(Self::Rule {
140 id: rule_id.into(),
141 name,
142 })
143 } else if path == "/agent/diagnostics" {
144 let mut include_errors = default_include_errors();
145 let mut include_warnings = false;
146 for (key, value) in url.query_pairs() {
147 match key.as_ref() {
148 "include_warnings" => include_warnings = value == "true",
149 "include_errors" => include_errors = value == "true",
150 _ => bail!("invalid query parameter"),
151 }
152 }
153 Ok(Self::Diagnostics {
154 include_errors,
155 include_warnings,
156 })
157 } else if path.starts_with("/agent/pasted-image") {
158 Ok(Self::PastedImage)
159 } else if path.starts_with("/agent/untitled-buffer") {
160 let fragment = url
161 .fragment()
162 .context("Missing fragment for untitled buffer selection")?;
163 let line_range = parse_line_range(fragment)?;
164 Ok(Self::Selection {
165 abs_path: None,
166 line_range,
167 })
168 } else if let Some(name) = path.strip_prefix("/agent/symbol/") {
169 let fragment = url
170 .fragment()
171 .context("Missing fragment for untitled buffer selection")?;
172 let line_range = parse_line_range(fragment)?;
173 let path =
174 single_query_param(&url, "path")?.context("Missing path for symbol")?;
175 Ok(Self::Symbol {
176 name: name.to_string(),
177 abs_path: path.into(),
178 line_range,
179 })
180 } else if path.starts_with("/agent/file") {
181 let path =
182 single_query_param(&url, "path")?.context("Missing path for file")?;
183 Ok(Self::File {
184 abs_path: path.into(),
185 })
186 } else if path.starts_with("/agent/directory") {
187 let path =
188 single_query_param(&url, "path")?.context("Missing path for directory")?;
189 Ok(Self::Directory {
190 abs_path: path.into(),
191 })
192 } else if path.starts_with("/agent/selection") {
193 let fragment = url.fragment().context("Missing fragment for selection")?;
194 let line_range = parse_line_range(fragment)?;
195 let path =
196 single_query_param(&url, "path")?.context("Missing path for selection")?;
197 Ok(Self::Selection {
198 abs_path: Some(path.into()),
199 line_range,
200 })
201 } else if path.starts_with("/agent/terminal-selection") {
202 let line_count = single_query_param(&url, "lines")?
203 .unwrap_or_else(|| "0".to_string())
204 .parse::<u32>()
205 .unwrap_or(0);
206 Ok(Self::TerminalSelection { line_count })
207 } else if path.starts_with("/agent/git-diff") {
208 let base_ref =
209 single_query_param(&url, "base")?.unwrap_or_else(|| "main".to_string());
210 Ok(Self::GitDiff { base_ref })
211 } else if path.starts_with("/agent/merge-conflict") {
212 let file_path = single_query_param(&url, "path")?.unwrap_or_default();
213 Ok(Self::MergeConflict { file_path })
214 } else {
215 bail!("invalid zed url: {:?}", input);
216 }
217 }
218 "http" | "https" => Ok(MentionUri::Fetch { url }),
219 other => bail!("unrecognized scheme {:?}", other),
220 }
221 }
222
223 pub fn name(&self) -> String {
224 match self {
225 MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path
226 .file_name()
227 .unwrap_or_default()
228 .to_string_lossy()
229 .into_owned(),
230 MentionUri::PastedImage => "Image".to_string(),
231 MentionUri::Symbol { name, .. } => name.clone(),
232 MentionUri::Thread { name, .. } => name.clone(),
233 MentionUri::Rule { name, .. } => name.clone(),
234 MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
235 MentionUri::TerminalSelection { line_count } => {
236 if *line_count == 1 {
237 "Terminal (1 line)".to_string()
238 } else {
239 format!("Terminal ({} lines)", line_count)
240 }
241 }
242 MentionUri::GitDiff { base_ref } => format!("Branch Diff ({})", base_ref),
243 MentionUri::MergeConflict { file_path } => {
244 let name = Path::new(file_path)
245 .file_name()
246 .unwrap_or_default()
247 .to_string_lossy();
248 format!("Merge Conflict ({name})")
249 }
250 MentionUri::Selection {
251 abs_path: path,
252 line_range,
253 ..
254 } => selection_name(path.as_deref(), line_range),
255 MentionUri::Fetch { url } => url.to_string(),
256 }
257 }
258
259 pub fn tooltip_text(&self) -> Option<SharedString> {
260 match self {
261 MentionUri::File { abs_path } | MentionUri::Directory { abs_path } => {
262 Some(abs_path.to_string_lossy().into_owned().into())
263 }
264 MentionUri::Symbol {
265 abs_path,
266 line_range,
267 ..
268 } => Some(
269 format!(
270 "{}:{}-{}",
271 abs_path.display(),
272 line_range.start(),
273 line_range.end()
274 )
275 .into(),
276 ),
277 MentionUri::Selection {
278 abs_path: Some(path),
279 line_range,
280 ..
281 } => Some(
282 format!(
283 "{}:{}-{}",
284 path.display(),
285 line_range.start(),
286 line_range.end()
287 )
288 .into(),
289 ),
290 _ => None,
291 }
292 }
293
294 pub fn icon_path(&self, cx: &mut App) -> SharedString {
295 match self {
296 MentionUri::File { abs_path } => {
297 FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
298 }
299 MentionUri::PastedImage => IconName::Image.path().into(),
300 MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx)
301 .unwrap_or_else(|| IconName::Folder.path().into()),
302 MentionUri::Symbol { .. } => IconName::Code.path().into(),
303 MentionUri::Thread { .. } => IconName::Thread.path().into(),
304 MentionUri::Rule { .. } => IconName::Reader.path().into(),
305 MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
306 MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
307 MentionUri::Selection { .. } => IconName::Reader.path().into(),
308 MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
309 MentionUri::GitDiff { .. } => IconName::GitBranch.path().into(),
310 MentionUri::MergeConflict { .. } => IconName::GitMergeConflict.path().into(),
311 }
312 }
313
314 pub fn as_link<'a>(&'a self) -> MentionLink<'a> {
315 MentionLink(self)
316 }
317
318 pub fn to_uri(&self) -> Url {
319 match self {
320 MentionUri::File { abs_path } => {
321 let mut url = Url::parse("file:///").unwrap();
322 url.set_path(&abs_path.to_string_lossy());
323 url
324 }
325 MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
326 MentionUri::Directory { abs_path } => {
327 let mut url = Url::parse("file:///").unwrap();
328 url.set_path(&abs_path.to_string_lossy());
329 url
330 }
331 MentionUri::Symbol {
332 abs_path,
333 name,
334 line_range,
335 } => {
336 let mut url = Url::parse("file:///").unwrap();
337 url.set_path(&abs_path.to_string_lossy());
338 url.query_pairs_mut().append_pair("symbol", name);
339 url.set_fragment(Some(&format!(
340 "L{}:{}",
341 line_range.start() + 1,
342 line_range.end() + 1
343 )));
344 url
345 }
346 MentionUri::Selection {
347 abs_path,
348 line_range,
349 } => {
350 let mut url = if let Some(path) = abs_path {
351 let mut url = Url::parse("file:///").unwrap();
352 url.set_path(&path.to_string_lossy());
353 url
354 } else {
355 let mut url = Url::parse("zed:///").unwrap();
356 url.set_path("/agent/untitled-buffer");
357 url
358 };
359 url.set_fragment(Some(&format!(
360 "L{}:{}",
361 line_range.start() + 1,
362 line_range.end() + 1
363 )));
364 url
365 }
366 MentionUri::Thread { name, id } => {
367 let mut url = Url::parse("zed:///").unwrap();
368 url.set_path(&format!("/agent/thread/{id}"));
369 url.query_pairs_mut().append_pair("name", name);
370 url
371 }
372 MentionUri::Rule { name, id } => {
373 let mut url = Url::parse("zed:///").unwrap();
374 url.set_path(&format!("/agent/rule/{id}"));
375 url.query_pairs_mut().append_pair("name", name);
376 url
377 }
378 MentionUri::Diagnostics {
379 include_errors,
380 include_warnings,
381 } => {
382 let mut url = Url::parse("zed:///").unwrap();
383 url.set_path("/agent/diagnostics");
384 if *include_warnings {
385 url.query_pairs_mut()
386 .append_pair("include_warnings", "true");
387 }
388 if !include_errors {
389 url.query_pairs_mut().append_pair("include_errors", "false");
390 }
391 url
392 }
393 MentionUri::Fetch { url } => url.clone(),
394 MentionUri::TerminalSelection { line_count } => {
395 let mut url = Url::parse("zed:///agent/terminal-selection").unwrap();
396 url.query_pairs_mut()
397 .append_pair("lines", &line_count.to_string());
398 url
399 }
400 MentionUri::GitDiff { base_ref } => {
401 let mut url = Url::parse("zed:///agent/git-diff").unwrap();
402 url.query_pairs_mut().append_pair("base", base_ref);
403 url
404 }
405 MentionUri::MergeConflict { file_path } => {
406 let mut url = Url::parse("zed:///agent/merge-conflict").unwrap();
407 url.query_pairs_mut().append_pair("path", file_path);
408 url
409 }
410 }
411 }
412}
413
414pub struct MentionLink<'a>(&'a MentionUri);
415
416impl fmt::Display for MentionLink<'_> {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 write!(f, "[@{}]({})", self.0.name(), self.0.to_uri())
419 }
420}
421
422fn default_include_errors() -> bool {
423 true
424}
425
426fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
427 let pairs = url.query_pairs().collect::<Vec<_>>();
428 match pairs.as_slice() {
429 [] => Ok(None),
430 [(k, v)] => {
431 if k != name {
432 bail!("invalid query parameter")
433 }
434
435 Ok(Some(v.to_string()))
436 }
437 _ => bail!("too many query pairs"),
438 }
439}
440
441pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
442 format!(
443 "{} ({}:{})",
444 path.and_then(|path| path.file_name())
445 .unwrap_or("Untitled".as_ref())
446 .display(),
447 *line_range.start() + 1,
448 *line_range.end() + 1
449 )
450}
451
452#[cfg(test)]
453mod tests {
454 use util::{path, uri};
455
456 use super::*;
457
458 #[test]
459 fn test_parse_file_uri() {
460 let file_uri = uri!("file:///path/to/file.rs");
461 let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap();
462 match &parsed {
463 MentionUri::File { abs_path } => {
464 assert_eq!(abs_path, Path::new(path!("/path/to/file.rs")));
465 }
466 _ => panic!("Expected File variant"),
467 }
468 assert_eq!(parsed.to_uri().to_string(), file_uri);
469 }
470
471 #[test]
472 fn test_parse_directory_uri() {
473 let file_uri = uri!("file:///path/to/dir/");
474 let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap();
475 match &parsed {
476 MentionUri::Directory { abs_path } => {
477 assert_eq!(abs_path, Path::new(path!("/path/to/dir/")));
478 }
479 _ => panic!("Expected Directory variant"),
480 }
481 assert_eq!(parsed.to_uri().to_string(), file_uri);
482 }
483
484 #[test]
485 fn test_to_directory_uri_without_slash() {
486 let uri = MentionUri::Directory {
487 abs_path: PathBuf::from(path!("/path/to/dir/")),
488 };
489 let expected = uri!("file:///path/to/dir/");
490 assert_eq!(uri.to_uri().to_string(), expected);
491 }
492
493 #[test]
494 fn test_parse_symbol_uri() {
495 let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
496 let parsed = MentionUri::parse(symbol_uri, PathStyle::local()).unwrap();
497 match &parsed {
498 MentionUri::Symbol {
499 abs_path: path,
500 name,
501 line_range,
502 } => {
503 assert_eq!(path, Path::new(path!("/path/to/file.rs")));
504 assert_eq!(name, "MySymbol");
505 assert_eq!(line_range.start(), &9);
506 assert_eq!(line_range.end(), &19);
507 }
508 _ => panic!("Expected Symbol variant"),
509 }
510 assert_eq!(parsed.to_uri().to_string(), symbol_uri);
511 }
512
513 #[test]
514 fn test_parse_selection_uri() {
515 let selection_uri = uri!("file:///path/to/file.rs#L5:15");
516 let parsed = MentionUri::parse(selection_uri, PathStyle::local()).unwrap();
517 match &parsed {
518 MentionUri::Selection {
519 abs_path: path,
520 line_range,
521 } => {
522 assert_eq!(path.as_ref().unwrap(), Path::new(path!("/path/to/file.rs")));
523 assert_eq!(line_range.start(), &4);
524 assert_eq!(line_range.end(), &14);
525 }
526 _ => panic!("Expected Selection variant"),
527 }
528 assert_eq!(parsed.to_uri().to_string(), selection_uri);
529 }
530
531 #[test]
532 fn test_parse_file_uri_with_non_ascii() {
533 let file_uri = uri!("file:///path/to/%E6%97%A5%E6%9C%AC%E8%AA%9E.txt");
534 let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap();
535 match &parsed {
536 MentionUri::File { abs_path } => {
537 assert_eq!(abs_path, Path::new(path!("/path/to/日本語.txt")));
538 }
539 _ => panic!("Expected File variant"),
540 }
541 assert_eq!(parsed.to_uri().to_string(), file_uri);
542 }
543
544 #[test]
545 fn test_parse_untitled_selection_uri() {
546 let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
547 let parsed = MentionUri::parse(selection_uri, PathStyle::local()).unwrap();
548 match &parsed {
549 MentionUri::Selection {
550 abs_path: None,
551 line_range,
552 } => {
553 assert_eq!(line_range.start(), &0);
554 assert_eq!(line_range.end(), &9);
555 }
556 _ => panic!("Expected Selection variant without path"),
557 }
558 assert_eq!(parsed.to_uri().to_string(), selection_uri);
559 }
560
561 #[test]
562 fn test_parse_thread_uri() {
563 let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
564 let parsed = MentionUri::parse(thread_uri, PathStyle::local()).unwrap();
565 match &parsed {
566 MentionUri::Thread {
567 id: thread_id,
568 name,
569 } => {
570 assert_eq!(thread_id.to_string(), "session123");
571 assert_eq!(name, "Thread name");
572 }
573 _ => panic!("Expected Thread variant"),
574 }
575 assert_eq!(parsed.to_uri().to_string(), thread_uri);
576 }
577
578 #[test]
579 fn test_parse_rule_uri() {
580 let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule";
581 let parsed = MentionUri::parse(rule_uri, PathStyle::local()).unwrap();
582 match &parsed {
583 MentionUri::Rule { id, name } => {
584 assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
585 assert_eq!(name, "Some rule");
586 }
587 _ => panic!("Expected Rule variant"),
588 }
589 assert_eq!(parsed.to_uri().to_string(), rule_uri);
590 }
591
592 #[test]
593 fn test_parse_fetch_http_uri() {
594 let http_uri = "http://example.com/path?query=value#fragment";
595 let parsed = MentionUri::parse(http_uri, PathStyle::local()).unwrap();
596 match &parsed {
597 MentionUri::Fetch { url } => {
598 assert_eq!(url.to_string(), http_uri);
599 }
600 _ => panic!("Expected Fetch variant"),
601 }
602 assert_eq!(parsed.to_uri().to_string(), http_uri);
603 }
604
605 #[test]
606 fn test_parse_fetch_https_uri() {
607 let https_uri = "https://example.com/api/endpoint";
608 let parsed = MentionUri::parse(https_uri, PathStyle::local()).unwrap();
609 match &parsed {
610 MentionUri::Fetch { url } => {
611 assert_eq!(url.to_string(), https_uri);
612 }
613 _ => panic!("Expected Fetch variant"),
614 }
615 assert_eq!(parsed.to_uri().to_string(), https_uri);
616 }
617
618 #[test]
619 fn test_parse_diagnostics_uri() {
620 let uri = "zed:///agent/diagnostics?include_warnings=true";
621 let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
622 match &parsed {
623 MentionUri::Diagnostics {
624 include_errors,
625 include_warnings,
626 } => {
627 assert!(include_errors);
628 assert!(include_warnings);
629 }
630 _ => panic!("Expected Diagnostics variant"),
631 }
632 assert_eq!(parsed.to_uri().to_string(), uri);
633 }
634
635 #[test]
636 fn test_parse_diagnostics_uri_warnings_only() {
637 let uri = "zed:///agent/diagnostics?include_warnings=true&include_errors=false";
638 let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
639 match &parsed {
640 MentionUri::Diagnostics {
641 include_errors,
642 include_warnings,
643 } => {
644 assert!(!include_errors);
645 assert!(include_warnings);
646 }
647 _ => panic!("Expected Diagnostics variant"),
648 }
649 assert_eq!(parsed.to_uri().to_string(), uri);
650 }
651
652 #[test]
653 fn test_invalid_scheme() {
654 assert!(MentionUri::parse("ftp://example.com", PathStyle::local()).is_err());
655 assert!(MentionUri::parse("ssh://example.com", PathStyle::local()).is_err());
656 assert!(MentionUri::parse("unknown://example.com", PathStyle::local()).is_err());
657 }
658
659 #[test]
660 fn test_invalid_zed_path() {
661 assert!(MentionUri::parse("zed:///invalid/path", PathStyle::local()).is_err());
662 assert!(MentionUri::parse("zed:///agent/unknown/test", PathStyle::local()).is_err());
663 }
664
665 #[test]
666 fn test_single_line_number() {
667 // https://github.com/zed-industries/zed/issues/46114
668 let uri = uri!("file:///path/to/file.rs#L1872");
669 let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
670 match &parsed {
671 MentionUri::Selection {
672 abs_path: path,
673 line_range,
674 } => {
675 assert_eq!(path.as_ref().unwrap(), Path::new(path!("/path/to/file.rs")));
676 assert_eq!(line_range.start(), &1871);
677 assert_eq!(line_range.end(), &1871);
678 }
679 _ => panic!("Expected Selection variant"),
680 }
681 }
682
683 #[test]
684 fn test_dash_separated_line_range() {
685 let uri = uri!("file:///path/to/file.rs#L10-20");
686 let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
687 match &parsed {
688 MentionUri::Selection {
689 abs_path: path,
690 line_range,
691 } => {
692 assert_eq!(path.as_ref().unwrap(), Path::new(path!("/path/to/file.rs")));
693 assert_eq!(line_range.start(), &9);
694 assert_eq!(line_range.end(), &19);
695 }
696 _ => panic!("Expected Selection variant"),
697 }
698
699 // Also test L10-L20 format
700 let uri = uri!("file:///path/to/file.rs#L10-L20");
701 let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
702 match &parsed {
703 MentionUri::Selection {
704 abs_path: path,
705 line_range,
706 } => {
707 assert_eq!(path.as_ref().unwrap(), Path::new(path!("/path/to/file.rs")));
708 assert_eq!(line_range.start(), &9);
709 assert_eq!(line_range.end(), &19);
710 }
711 _ => panic!("Expected Selection variant"),
712 }
713 }
714
715 #[test]
716 fn test_parse_terminal_selection_uri() {
717 let terminal_uri = "zed:///agent/terminal-selection?lines=42";
718 let parsed = MentionUri::parse(terminal_uri, PathStyle::local()).unwrap();
719 match &parsed {
720 MentionUri::TerminalSelection { line_count } => {
721 assert_eq!(*line_count, 42);
722 }
723 _ => panic!("Expected TerminalSelection variant"),
724 }
725 assert_eq!(parsed.to_uri().to_string(), terminal_uri);
726 assert_eq!(parsed.name(), "Terminal (42 lines)");
727
728 // Test single line
729 let single_line_uri = "zed:///agent/terminal-selection?lines=1";
730 let parsed_single = MentionUri::parse(single_line_uri, PathStyle::local()).unwrap();
731 assert_eq!(parsed_single.name(), "Terminal (1 line)");
732 }
733}