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