1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub struct LineRange {
4 start: usize,
5 end: usize,
6}
7
8impl LineRange {
9 pub fn new(start: usize, end: usize) -> Self {
10 Self { start, end }
11 }
12
13 pub fn start_one_based(&self) -> usize {
14 self.start
15 }
16
17 pub fn end_one_based(&self) -> usize {
18 self.end
19 }
20
21 pub fn single(line: usize) -> Self {
22 Self {
23 start: line,
24 end: line,
25 }
26 }
27}
28
29#[derive(Debug, Clone, Copy)]
32enum Address {
33 Number(usize), Current,
35 Last,
36 Mark(char),
37}
38
39fn parse_address(s: &str) -> Option<(Address, &str)> {
41 let mut chars = s.char_indices();
42 let (_, first) = chars.next()?;
43 match first {
44 '.' => Some((Address::Current, &s[1..])),
45 '$' => Some((Address::Last, &s[1..])),
46 '\'' => {
47 let (_, mark) = chars.next()?;
48 Some((Address::Mark(mark), &s[2..]))
49 }
50 '0'..='9' => {
51 let mut end = 1;
52 for (i, c) in s.char_indices().skip(1) {
53 if c.is_ascii_digit() {
54 end = i + c.len_utf8();
55 } else {
56 break;
57 }
58 }
59 let n: usize = s[..end].parse().ok()?;
60 Some((Address::Number(n), &s[end..]))
61 }
62 _ => None,
63 }
64}
65
66fn resolve_address<H: hjkl_engine::Host>(
69 addr: Address,
70 editor: &hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
71) -> Result<usize, String> {
72 let line_count = editor.buffer().row_count();
73 let last = line_count.max(1);
75 match addr {
76 Address::Number(n) => Ok(n.clamp(1, last)),
77 Address::Current => Ok(editor.cursor().0 + 1), Address::Last => Ok(last),
79 Address::Mark(c) => editor
80 .mark(c)
81 .map(|(r, _)| (r + 1).min(last)) .ok_or_else(|| format!("mark `{c}` not set")),
83 }
84}
85
86pub fn parse_range<'a, H: hjkl_engine::Host>(
98 cmd: &'a str,
99 editor: &hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
100) -> Result<(Option<LineRange>, &'a str), String> {
101 if let Some(rest) = cmd.strip_prefix('%') {
103 let line_count = editor.buffer().row_count().max(1);
104 return Ok((Some(LineRange::new(1, line_count)), rest));
105 }
106
107 let Some((start_addr, after_start)) = parse_address(cmd) else {
108 return Ok((None, cmd));
109 };
110
111 let start = resolve_address(start_addr, editor)?;
112
113 if let Some(after_comma) = after_start.strip_prefix(',') {
114 if after_comma.is_empty() {
116 return Err("missing end address after ','".into());
117 }
118 let Some((end_addr, rest)) = parse_address(after_comma) else {
119 return Err(format!("invalid end address in range: `{after_comma}`"));
121 };
122 let end = resolve_address(end_addr, editor)?;
123 let (lo, hi) = if start <= end {
124 (start, end)
125 } else {
126 (end, start)
127 };
128 return Ok((Some(LineRange::new(lo, hi)), rest));
129 }
130
131 Ok((Some(LineRange::single(start)), after_start))
132}
133
134#[cfg(test)]
137mod tests {
138 use super::*;
139 use hjkl_engine::{DefaultHost, Editor, Options};
140
141 fn make_editor_with_lines(lines: &[&str]) -> Editor<hjkl_buffer::Buffer, DefaultHost> {
142 use hjkl_buffer::Buffer;
143 let content = lines.join("\n");
144 let buf = Buffer::from_str(&content);
145 let host = DefaultHost::new();
146 Editor::new(buf, host, Options::default())
147 }
148
149 fn make_editor() -> Editor<hjkl_buffer::Buffer, DefaultHost> {
150 make_editor_with_lines(&["line1", "line2", "line3", "line4", "line5"])
151 }
152
153 fn parse(cmd: &str) -> Result<(Option<(usize, usize)>, String), String> {
155 let e = make_editor();
156 parse_range(cmd, &e).map(|(r, rest)| (r.map(|lr| (lr.start, lr.end)), rest.to_owned()))
157 }
158
159 #[test]
160 fn bare_number() {
161 let (r, rest) = parse("5").unwrap();
162 assert_eq!(r, Some((5, 5)));
163 assert_eq!(rest, "");
164 }
165
166 #[test]
167 fn comma_separated() {
168 let (r, rest) = parse("5,10").unwrap();
169 assert_eq!(r, Some((5, 5)));
171 assert_eq!(rest, "");
172 }
173
174 #[test]
175 fn comma_separated_within_range() {
176 let (r, rest) = parse("2,4").unwrap();
177 assert_eq!(r, Some((2, 4)));
178 assert_eq!(rest, "");
179 }
180
181 #[test]
182 fn percent_whole_buffer() {
183 let (r, rest) = parse("%").unwrap();
184 assert_eq!(r, Some((1, 5)));
185 assert_eq!(rest, "");
186 }
187
188 #[test]
189 fn dot_dollar() {
190 let (r, rest) = parse(".,$").unwrap();
192 assert_eq!(r, Some((1, 5)));
193 assert_eq!(rest, "");
194 }
195
196 #[test]
197 fn mark_range() {
198 use hjkl_buffer::Buffer;
199 use hjkl_engine::{DefaultHost, Editor, Options};
200 let buf = Buffer::from_str("a\nb\nc\nd\ne");
201 let host = DefaultHost::new();
202 let mut editor = Editor::new(buf, host, Options::default());
203 editor.set_mark('a', (0, 0)); editor.set_mark('b', (2, 0)); let (r, rest) = parse_range("'a,'b", &editor).unwrap();
207 assert_eq!(r, Some(LineRange::new(1, 3)));
208 assert_eq!(rest, "");
209 }
210
211 #[test]
212 fn range_followed_by_command() {
213 let (r, rest) = parse("5,10w").unwrap();
214 assert_eq!(r, Some((5, 5)));
216 assert_eq!(rest, "w");
217 }
218
219 #[test]
220 fn range_2_4_followed_by_command() {
221 let (r, rest) = parse("2,4w").unwrap();
222 assert_eq!(r, Some((2, 4)));
223 assert_eq!(rest, "w");
224 }
225
226 #[test]
227 fn no_range() {
228 let (r, rest) = parse("w").unwrap();
229 assert_eq!(r, None);
230 assert_eq!(rest, "w");
231 }
232
233 #[test]
234 fn invalid_end_address() {
235 let result = parse("5,x");
236 assert!(result.is_err(), "expected error for invalid end address");
237 }
238
239 #[test]
240 fn mark_not_set_returns_error() {
241 let result = parse("'z");
242 assert!(result.is_err());
243 }
244
245 #[test]
246 fn line_range_single_start_equals_end() {
247 let r = LineRange::single(5);
248 assert_eq!(r.start_one_based(), 5);
249 assert_eq!(r.end_one_based(), 5);
250 }
251}