#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineRange {
start: usize,
end: usize,
}
impl LineRange {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn start_one_based(&self) -> usize {
self.start
}
pub fn end_one_based(&self) -> usize {
self.end
}
pub fn single(line: usize) -> Self {
Self {
start: line,
end: line,
}
}
}
#[derive(Debug, Clone, Copy)]
enum Address {
Number(usize), Current,
Last,
Mark(char),
}
fn parse_address(s: &str) -> Option<(Address, &str)> {
let mut chars = s.char_indices();
let (_, first) = chars.next()?;
match first {
'.' => Some((Address::Current, &s[1..])),
'$' => Some((Address::Last, &s[1..])),
'\'' => {
let (_, mark) = chars.next()?;
Some((Address::Mark(mark), &s[2..]))
}
'0'..='9' => {
let mut end = 1;
for (i, c) in s.char_indices().skip(1) {
if c.is_ascii_digit() {
end = i + c.len_utf8();
} else {
break;
}
}
let n: usize = s[..end].parse().ok()?;
Some((Address::Number(n), &s[end..]))
}
_ => None,
}
}
fn resolve_address<H: hjkl_engine::Host>(
addr: Address,
editor: &hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
) -> Result<usize, String> {
let line_count = editor.buffer().row_count();
let last = line_count.max(1);
match addr {
Address::Number(n) => Ok(n.clamp(1, last)),
Address::Current => Ok(editor.cursor().0 + 1), Address::Last => Ok(last),
Address::Mark(c) => editor
.mark(c)
.map(|(r, _)| (r + 1).min(last)) .ok_or_else(|| format!("mark `{c}` not set")),
}
}
pub fn parse_range<'a, H: hjkl_engine::Host>(
cmd: &'a str,
editor: &hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
) -> Result<(Option<LineRange>, &'a str), String> {
if let Some(rest) = cmd.strip_prefix('%') {
let line_count = editor.buffer().row_count().max(1);
return Ok((Some(LineRange::new(1, line_count)), rest));
}
let Some((start_addr, after_start)) = parse_address(cmd) else {
return Ok((None, cmd));
};
let start = resolve_address(start_addr, editor)?;
if let Some(after_comma) = after_start.strip_prefix(',') {
if after_comma.is_empty() {
return Err("missing end address after ','".into());
}
let Some((end_addr, rest)) = parse_address(after_comma) else {
return Err(format!("invalid end address in range: `{after_comma}`"));
};
let end = resolve_address(end_addr, editor)?;
let (lo, hi) = if start <= end {
(start, end)
} else {
(end, start)
};
return Ok((Some(LineRange::new(lo, hi)), rest));
}
Ok((Some(LineRange::single(start)), after_start))
}
#[cfg(test)]
mod tests {
use super::*;
use hjkl_engine::{DefaultHost, Editor, Options};
fn make_editor_with_lines(lines: &[&str]) -> Editor<hjkl_buffer::Buffer, DefaultHost> {
use hjkl_buffer::Buffer;
let content = lines.join("\n");
let buf = Buffer::from_str(&content);
let host = DefaultHost::new();
Editor::new(buf, host, Options::default())
}
fn make_editor() -> Editor<hjkl_buffer::Buffer, DefaultHost> {
make_editor_with_lines(&["line1", "line2", "line3", "line4", "line5"])
}
fn parse(cmd: &str) -> Result<(Option<(usize, usize)>, String), String> {
let e = make_editor();
parse_range(cmd, &e).map(|(r, rest)| (r.map(|lr| (lr.start, lr.end)), rest.to_owned()))
}
#[test]
fn bare_number() {
let (r, rest) = parse("5").unwrap();
assert_eq!(r, Some((5, 5)));
assert_eq!(rest, "");
}
#[test]
fn comma_separated() {
let (r, rest) = parse("5,10").unwrap();
assert_eq!(r, Some((5, 5)));
assert_eq!(rest, "");
}
#[test]
fn comma_separated_within_range() {
let (r, rest) = parse("2,4").unwrap();
assert_eq!(r, Some((2, 4)));
assert_eq!(rest, "");
}
#[test]
fn percent_whole_buffer() {
let (r, rest) = parse("%").unwrap();
assert_eq!(r, Some((1, 5)));
assert_eq!(rest, "");
}
#[test]
fn dot_dollar() {
let (r, rest) = parse(".,$").unwrap();
assert_eq!(r, Some((1, 5)));
assert_eq!(rest, "");
}
#[test]
fn mark_range() {
use hjkl_buffer::Buffer;
use hjkl_engine::{DefaultHost, Editor, Options};
let buf = Buffer::from_str("a\nb\nc\nd\ne");
let host = DefaultHost::new();
let mut editor = Editor::new(buf, host, Options::default());
editor.set_mark('a', (0, 0)); editor.set_mark('b', (2, 0)); let (r, rest) = parse_range("'a,'b", &editor).unwrap();
assert_eq!(r, Some(LineRange::new(1, 3)));
assert_eq!(rest, "");
}
#[test]
fn range_followed_by_command() {
let (r, rest) = parse("5,10w").unwrap();
assert_eq!(r, Some((5, 5)));
assert_eq!(rest, "w");
}
#[test]
fn range_2_4_followed_by_command() {
let (r, rest) = parse("2,4w").unwrap();
assert_eq!(r, Some((2, 4)));
assert_eq!(rest, "w");
}
#[test]
fn no_range() {
let (r, rest) = parse("w").unwrap();
assert_eq!(r, None);
assert_eq!(rest, "w");
}
#[test]
fn invalid_end_address() {
let result = parse("5,x");
assert!(result.is_err(), "expected error for invalid end address");
}
#[test]
fn mark_not_set_returns_error() {
let result = parse("'z");
assert!(result.is_err());
}
#[test]
fn line_range_single_start_equals_end() {
let r = LineRange::single(5);
assert_eq!(r.start_one_based(), 5);
assert_eq!(r.end_one_based(), 5);
}
}