path_range.rs

  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}