1use std::{ops::Range, path::Path, sync::Arc};
2
3#[derive(Debug, Clone, PartialEq)]
4pub struct PathWithRange {
5 pub path: Arc<Path>,
6 pub range: Option<Range<LineCol>>,
7}
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct LineCol {
11 pub line: u32,
12 pub col: Option<u32>,
13}
14
15impl LineCol {
16 pub fn new(str: impl AsRef<str>) -> Option<Self> {
17 let str = str.as_ref();
18 match str.split_once(':') {
19 Some((line, col)) => match (line.parse::<u32>(), col.parse::<u32>()) {
20 (Ok(line), Ok(col)) => Some(Self {
21 line,
22 col: Some(col),
23 }),
24 _ => None,
25 },
26 None => match str.parse::<u32>() {
27 Ok(line) => Some(Self { line, col: None }),
28 Err(_) => None,
29 },
30 }
31 }
32}
33
34impl PathWithRange {
35 pub fn new(str: impl AsRef<str>) -> Self {
36 let str = str.as_ref();
37 // Sometimes the model will include a language at the start,
38 // e.g. "```rust zed/crates/markdown/src/markdown.rs#L1"
39 // We just discard that.
40 let str = match str.trim_end().rfind(' ') {
41 Some(space) => &str[space + 1..],
42 None => str.trim_start(),
43 };
44
45 match str.rsplit_once('#') {
46 Some((path, after_hash)) => {
47 // Be tolerant to the model omitting the "L" prefix, lowercasing it,
48 // or including it more than once.
49 let after_hash = after_hash.replace(['L', 'l'], "");
50
51 let range = {
52 let mut iter = after_hash.split('-').flat_map(LineCol::new);
53 iter.next()
54 .map(|start| iter.next().map(|end| start..end).unwrap_or(start..start))
55 };
56
57 Self {
58 path: Path::new(path).into(),
59 range,
60 }
61 }
62 None => Self {
63 path: Path::new(str).into(),
64 range: None,
65 },
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn test_linecol_parsing() {
76 let line_col = LineCol::new("10:5");
77 assert_eq!(
78 line_col,
79 Some(LineCol {
80 line: 10,
81 col: Some(5)
82 })
83 );
84
85 let line_only = LineCol::new("42");
86 assert_eq!(
87 line_only,
88 Some(LineCol {
89 line: 42,
90 col: None
91 })
92 );
93
94 assert_eq!(LineCol::new(""), None);
95 assert_eq!(LineCol::new("not a number"), None);
96 assert_eq!(LineCol::new("10:not a number"), None);
97 assert_eq!(LineCol::new("not:5"), None);
98 }
99
100 #[test]
101 fn test_pathrange_parsing() {
102 let path_range = PathWithRange::new("file.rs#L10-L20");
103 assert_eq!(path_range.path.as_ref(), Path::new("file.rs"));
104 assert!(path_range.range.is_some());
105 if let Some(range) = path_range.range {
106 assert_eq!(range.start.line, 10);
107 assert_eq!(range.start.col, None);
108 assert_eq!(range.end.line, 20);
109 assert_eq!(range.end.col, None);
110 }
111
112 let single_line = PathWithRange::new("file.rs#L15");
113 assert_eq!(single_line.path.as_ref(), Path::new("file.rs"));
114 assert!(single_line.range.is_some());
115 if let Some(range) = single_line.range {
116 assert_eq!(range.start.line, 15);
117 assert_eq!(range.end.line, 15);
118 }
119
120 let no_range = PathWithRange::new("file.rs");
121 assert_eq!(no_range.path.as_ref(), Path::new("file.rs"));
122 assert!(no_range.range.is_none());
123
124 let lowercase = PathWithRange::new("file.rs#l5-l10");
125 assert_eq!(lowercase.path.as_ref(), Path::new("file.rs"));
126 assert!(lowercase.range.is_some());
127 if let Some(range) = lowercase.range {
128 assert_eq!(range.start.line, 5);
129 assert_eq!(range.end.line, 10);
130 }
131
132 let complex = PathWithRange::new("src/path/to/file.rs#L100");
133 assert_eq!(complex.path.as_ref(), Path::new("src/path/to/file.rs"));
134 assert!(complex.range.is_some());
135 }
136
137 #[test]
138 fn test_pathrange_from_str() {
139 let with_range = PathWithRange::new("file.rs#L10-L20");
140 assert!(with_range.range.is_some());
141 assert_eq!(with_range.path.as_ref(), Path::new("file.rs"));
142
143 let without_range = PathWithRange::new("file.rs");
144 assert!(without_range.range.is_none());
145
146 let single_line = PathWithRange::new("file.rs#L15");
147 assert!(single_line.range.is_some());
148 }
149
150 #[test]
151 fn test_pathrange_leading_text_trimming() {
152 let with_language = PathWithRange::new("```rust file.rs#L10");
153 assert_eq!(with_language.path.as_ref(), Path::new("file.rs"));
154 assert!(with_language.range.is_some());
155 if let Some(range) = with_language.range {
156 assert_eq!(range.start.line, 10);
157 }
158
159 let with_spaces = PathWithRange::new("``` file.rs#L10-L20");
160 assert_eq!(with_spaces.path.as_ref(), Path::new("file.rs"));
161 assert!(with_spaces.range.is_some());
162
163 let with_words = PathWithRange::new("```rust code example file.rs#L15:10");
164 assert_eq!(with_words.path.as_ref(), Path::new("file.rs"));
165 assert!(with_words.range.is_some());
166 if let Some(range) = with_words.range {
167 assert_eq!(range.start.line, 15);
168 assert_eq!(range.start.col, Some(10));
169 }
170
171 let with_whitespace = PathWithRange::new(" file.rs#L5");
172 assert_eq!(with_whitespace.path.as_ref(), Path::new("file.rs"));
173 assert!(with_whitespace.range.is_some());
174
175 let no_leading = PathWithRange::new("file.rs#L10");
176 assert_eq!(no_leading.path.as_ref(), Path::new("file.rs"));
177 assert!(no_leading.range.is_some());
178 }
179
180 #[test]
181 fn test_pathrange_with_line_and_column() {
182 let line_and_col = PathWithRange::new("file.rs#L10:5");
183 assert_eq!(line_and_col.path.as_ref(), Path::new("file.rs"));
184 assert!(line_and_col.range.is_some());
185 if let Some(range) = line_and_col.range {
186 assert_eq!(range.start.line, 10);
187 assert_eq!(range.start.col, Some(5));
188 assert_eq!(range.end.line, 10);
189 assert_eq!(range.end.col, Some(5));
190 }
191
192 let full_range = PathWithRange::new("file.rs#L10:5-L20:15");
193 assert_eq!(full_range.path.as_ref(), Path::new("file.rs"));
194 assert!(full_range.range.is_some());
195 if let Some(range) = full_range.range {
196 assert_eq!(range.start.line, 10);
197 assert_eq!(range.start.col, Some(5));
198 assert_eq!(range.end.line, 20);
199 assert_eq!(range.end.col, Some(15));
200 }
201
202 let mixed_range1 = PathWithRange::new("file.rs#L10:5-L20");
203 assert_eq!(mixed_range1.path.as_ref(), Path::new("file.rs"));
204 assert!(mixed_range1.range.is_some());
205 if let Some(range) = mixed_range1.range {
206 assert_eq!(range.start.line, 10);
207 assert_eq!(range.start.col, Some(5));
208 assert_eq!(range.end.line, 20);
209 assert_eq!(range.end.col, None);
210 }
211
212 let mixed_range2 = PathWithRange::new("file.rs#L10-L20:15");
213 assert_eq!(mixed_range2.path.as_ref(), Path::new("file.rs"));
214 assert!(mixed_range2.range.is_some());
215 if let Some(range) = mixed_range2.range {
216 assert_eq!(range.start.line, 10);
217 assert_eq!(range.start.col, None);
218 assert_eq!(range.end.line, 20);
219 assert_eq!(range.end.col, Some(15));
220 }
221 }
222}