diff --git a/crates/edit/src/bin/edit/documents.rs b/crates/edit/src/bin/edit/documents.rs index 6c0d4cf16a7..1ac332a2c2f 100644 --- a/crates/edit/src/bin/edit/documents.rs +++ b/crates/edit/src/bin/edit/documents.rs @@ -105,6 +105,20 @@ impl Document { None } + + /// Moves the cursor to a 1-based line/char position. + /// A negative line counts backwards from the end + /// of the document, e.g. -1 is the last line. + pub fn cursor_move_to_goto(&self, goto: Point) { + let mut tb = self.buffer.borrow_mut(); + let x = goto.x.saturating_sub(1); + let y = if goto.y < 0 { + tb.logical_line_count().saturating_add(goto.y) + } else { + goto.y.saturating_sub(1) + }; + tb.cursor_move_to_logical(Point { x, y }); + } } #[derive(Default)] @@ -281,22 +295,26 @@ impl DocumentManager { } /// Parse a filename in the form of "filename:line:char". -/// Returns the position of the first colon and the line/char coordinates. +/// Returns the filename and the [`Document::cursor_move_to_goto`] coordinates. pub fn parse_filename_goto(path: &Path) -> (&Path, Option) { fn parse(s: &[u8]) -> Option { - if s.is_empty() { + let (negative, digits) = match s { + [b'-', rest @ ..] => (true, rest), + _ => (false, s), + }; + if digits.is_empty() { return None; } let mut num: CoordType = 0; - for &b in s { + for &b in digits { if !b.is_ascii_digit() { return None; } let digit = (b - b'0') as CoordType; num = num.checked_mul(10)?.checked_add(digit)?; } - Some(num) + Some(if negative { -num } else { num }) } fn find_colon_rev(bytes: &[u8], offset: usize) -> Option { @@ -315,19 +333,19 @@ pub fn parse_filename_goto(path: &Path) -> (&Path, Option) { Some(last) => last, None => return (path, None), }; - let last = (last - 1).max(0); let mut len = colend; - let mut goto = Point { x: 0, y: last }; + let mut goto = Point { x: 1, y: last }; - if let Some(colbeg) = find_colon_rev(bytes, colend) { + // Counting backwards is only supported for lines, + // so a negative `last` rules out a char position. + if last >= 0 + && let Some(colbeg) = find_colon_rev(bytes, colend) // Same here: Don't allow empty filenames. - if colbeg != 0 - && let Some(first) = parse(&bytes[colbeg + 1..colend]) - { - let first = (first - 1).max(0); - len = colbeg; - goto = Point { x: last, y: first }; - } + && colbeg != 0 + && let Some(first) = parse(&bytes[colbeg + 1..colend]) + { + len = colbeg; + goto = Point { x: last, y: first }; } // Strip off the :line:char suffix. @@ -351,20 +369,24 @@ mod tests { assert_eq!(parse("123"), ("123", None)); assert_eq!(parse("abc"), ("abc", None)); assert_eq!(parse(":123"), (":123", None)); - assert_eq!(parse("abc:123"), ("abc", Some(Point { x: 0, y: 122 }))); - assert_eq!(parse("45:123"), ("45", Some(Point { x: 0, y: 122 }))); - assert_eq!(parse(":45:123"), (":45", Some(Point { x: 0, y: 122 }))); - assert_eq!(parse("abc:45:123"), ("abc", Some(Point { x: 122, y: 44 }))); - assert_eq!(parse("abc:def:123"), ("abc:def", Some(Point { x: 0, y: 122 }))); - assert_eq!(parse("1:2:3"), ("1", Some(Point { x: 2, y: 1 }))); - assert_eq!(parse("::3"), (":", Some(Point { x: 0, y: 2 }))); - assert_eq!(parse("1::3"), ("1:", Some(Point { x: 0, y: 2 }))); + assert_eq!(parse("abc:123"), ("abc", Some(Point { x: 1, y: 123 }))); + assert_eq!(parse("45:123"), ("45", Some(Point { x: 1, y: 123 }))); + assert_eq!(parse(":45:123"), (":45", Some(Point { x: 1, y: 123 }))); + assert_eq!(parse("abc:45:123"), ("abc", Some(Point { x: 123, y: 45 }))); + assert_eq!(parse("abc:def:123"), ("abc:def", Some(Point { x: 1, y: 123 }))); + assert_eq!(parse("1:2:3"), ("1", Some(Point { x: 3, y: 2 }))); + assert_eq!(parse("::3"), (":", Some(Point { x: 1, y: 3 }))); + assert_eq!(parse("1::3"), ("1:", Some(Point { x: 1, y: 3 }))); assert_eq!(parse(""), ("", None)); assert_eq!(parse(":"), (":", None)); assert_eq!(parse("::"), ("::", None)); - assert_eq!(parse("a:1"), ("a", Some(Point { x: 0, y: 0 }))); + assert_eq!(parse("a:1"), ("a", Some(Point { x: 1, y: 1 }))); assert_eq!(parse("1:a"), ("1:a", None)); - assert_eq!(parse("file.txt:10"), ("file.txt", Some(Point { x: 0, y: 9 }))); - assert_eq!(parse("file.txt:10:5"), ("file.txt", Some(Point { x: 4, y: 9 }))); + assert_eq!(parse("file.txt:10"), ("file.txt", Some(Point { x: 1, y: 10 }))); + assert_eq!(parse("file.txt:10:5"), ("file.txt", Some(Point { x: 5, y: 10 }))); + assert_eq!(parse("file.txt:-1"), ("file.txt", Some(Point { x: 1, y: -1 }))); + assert_eq!(parse("file.txt:-10:5"), ("file.txt", Some(Point { x: 5, y: -10 }))); + assert_eq!(parse("file.txt:10:-5"), ("file.txt:10", Some(Point { x: 1, y: -5 }))); + assert_eq!(parse("file.txt:-"), ("file.txt:-", None)); } } diff --git a/crates/edit/src/bin/edit/draw_editor.rs b/crates/edit/src/bin/edit/draw_editor.rs index d1d90e02cc1..ce03bbeb4cb 100644 --- a/crates/edit/src/bin/edit/draw_editor.rs +++ b/crates/edit/src/bin/edit/draw_editor.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::num::ParseIntError; - use edit::framebuffer::IndexedColor; use edit::helpers::*; use edit::icu; @@ -319,14 +317,12 @@ pub fn draw_goto_menu(ctx: &mut Context, state: &mut State) { ctx.steal_focus(); if ctx.consume_shortcut(vk::RETURN) { - match validate_goto_point(&state.goto_target) { - Ok(point) => { - let mut buf = doc.buffer.borrow_mut(); - buf.cursor_move_to_logical(point); - buf.make_cursor_visible(); - done = true; - } - Err(_) => state.goto_invalid = true, + if let Some(goto) = validate_goto_point(&state.goto_target) { + doc.cursor_move_to_goto(goto); + doc.buffer.borrow_mut().make_cursor_visible(); + done = true; + } else { + state.goto_invalid = true; } ctx.needs_rerender(); } @@ -344,13 +340,17 @@ pub fn draw_goto_menu(ctx: &mut Context, state: &mut State) { } } -fn validate_goto_point(line: &str) -> Result { +fn validate_goto_point(line: &str) -> Option { let mut coords = [0; 2]; - let (y, x) = line.split_once(':').unwrap_or((line, "0")); + let (y, x) = line.split_once(':').unwrap_or((line, "1")); // Using a loop here avoids 2 copies of the str->int code. // This makes the binary more compact. for (i, s) in [x, y].iter().enumerate() { - coords[i] = s.parse::()?.saturating_sub(1); + coords[i] = s.parse::().ok()?; + } + // Counting backwards is only supported for lines. + if coords[0] < 1 { + return None; } - Ok(Point { x: coords[0], y: coords[1] }) + Some(Point { x: coords[0], y: coords[1] }) } diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index d53e864ee4d..67c2fc6961d 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -291,7 +291,7 @@ fn handle_args(state: &mut State) -> apperr::Result { for (p, goto) in &paths { let doc = state.documents.add_file_path(p)?; if let Some(goto) = goto { - doc.buffer.borrow_mut().cursor_move_to_logical(*goto); + doc.cursor_move_to_goto(*goto); } }