Skip to content

Commit 3e20615

Browse files
barkurelhecker
andauthored
Support negative line numbers for goto (#841)
Co-authored-by: Leonard Hecker <leonard@hecker.io>
1 parent 729d6ae commit 3e20615

3 files changed

Lines changed: 62 additions & 40 deletions

File tree

crates/edit/src/bin/edit/documents.rs

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ impl Document {
105105

106106
None
107107
}
108+
109+
/// Moves the cursor to a 1-based line/char position.
110+
/// A negative line counts backwards from the end
111+
/// of the document, e.g. -1 is the last line.
112+
pub fn cursor_move_to_goto(&self, goto: Point) {
113+
let mut tb = self.buffer.borrow_mut();
114+
let x = goto.x.saturating_sub(1);
115+
let y = if goto.y < 0 {
116+
tb.logical_line_count().saturating_add(goto.y)
117+
} else {
118+
goto.y.saturating_sub(1)
119+
};
120+
tb.cursor_move_to_logical(Point { x, y });
121+
}
108122
}
109123

110124
#[derive(Default)]
@@ -281,22 +295,26 @@ impl DocumentManager {
281295
}
282296

283297
/// Parse a filename in the form of "filename:line:char".
284-
/// Returns the position of the first colon and the line/char coordinates.
298+
/// Returns the filename and the [`Document::cursor_move_to_goto`] coordinates.
285299
pub fn parse_filename_goto(path: &Path) -> (&Path, Option<Point>) {
286300
fn parse(s: &[u8]) -> Option<CoordType> {
287-
if s.is_empty() {
301+
let (negative, digits) = match s {
302+
[b'-', rest @ ..] => (true, rest),
303+
_ => (false, s),
304+
};
305+
if digits.is_empty() {
288306
return None;
289307
}
290308

291309
let mut num: CoordType = 0;
292-
for &b in s {
310+
for &b in digits {
293311
if !b.is_ascii_digit() {
294312
return None;
295313
}
296314
let digit = (b - b'0') as CoordType;
297315
num = num.checked_mul(10)?.checked_add(digit)?;
298316
}
299-
Some(num)
317+
Some(if negative { -num } else { num })
300318
}
301319

302320
fn find_colon_rev(bytes: &[u8], offset: usize) -> Option<usize> {
@@ -315,19 +333,19 @@ pub fn parse_filename_goto(path: &Path) -> (&Path, Option<Point>) {
315333
Some(last) => last,
316334
None => return (path, None),
317335
};
318-
let last = (last - 1).max(0);
319336
let mut len = colend;
320-
let mut goto = Point { x: 0, y: last };
337+
let mut goto = Point { x: 1, y: last };
321338

322-
if let Some(colbeg) = find_colon_rev(bytes, colend) {
339+
// Counting backwards is only supported for lines,
340+
// so a negative `last` rules out a char position.
341+
if last >= 0
342+
&& let Some(colbeg) = find_colon_rev(bytes, colend)
323343
// Same here: Don't allow empty filenames.
324-
if colbeg != 0
325-
&& let Some(first) = parse(&bytes[colbeg + 1..colend])
326-
{
327-
let first = (first - 1).max(0);
328-
len = colbeg;
329-
goto = Point { x: last, y: first };
330-
}
344+
&& colbeg != 0
345+
&& let Some(first) = parse(&bytes[colbeg + 1..colend])
346+
{
347+
len = colbeg;
348+
goto = Point { x: last, y: first };
331349
}
332350

333351
// Strip off the :line:char suffix.
@@ -351,20 +369,24 @@ mod tests {
351369
assert_eq!(parse("123"), ("123", None));
352370
assert_eq!(parse("abc"), ("abc", None));
353371
assert_eq!(parse(":123"), (":123", None));
354-
assert_eq!(parse("abc:123"), ("abc", Some(Point { x: 0, y: 122 })));
355-
assert_eq!(parse("45:123"), ("45", Some(Point { x: 0, y: 122 })));
356-
assert_eq!(parse(":45:123"), (":45", Some(Point { x: 0, y: 122 })));
357-
assert_eq!(parse("abc:45:123"), ("abc", Some(Point { x: 122, y: 44 })));
358-
assert_eq!(parse("abc:def:123"), ("abc:def", Some(Point { x: 0, y: 122 })));
359-
assert_eq!(parse("1:2:3"), ("1", Some(Point { x: 2, y: 1 })));
360-
assert_eq!(parse("::3"), (":", Some(Point { x: 0, y: 2 })));
361-
assert_eq!(parse("1::3"), ("1:", Some(Point { x: 0, y: 2 })));
372+
assert_eq!(parse("abc:123"), ("abc", Some(Point { x: 1, y: 123 })));
373+
assert_eq!(parse("45:123"), ("45", Some(Point { x: 1, y: 123 })));
374+
assert_eq!(parse(":45:123"), (":45", Some(Point { x: 1, y: 123 })));
375+
assert_eq!(parse("abc:45:123"), ("abc", Some(Point { x: 123, y: 45 })));
376+
assert_eq!(parse("abc:def:123"), ("abc:def", Some(Point { x: 1, y: 123 })));
377+
assert_eq!(parse("1:2:3"), ("1", Some(Point { x: 3, y: 2 })));
378+
assert_eq!(parse("::3"), (":", Some(Point { x: 1, y: 3 })));
379+
assert_eq!(parse("1::3"), ("1:", Some(Point { x: 1, y: 3 })));
362380
assert_eq!(parse(""), ("", None));
363381
assert_eq!(parse(":"), (":", None));
364382
assert_eq!(parse("::"), ("::", None));
365-
assert_eq!(parse("a:1"), ("a", Some(Point { x: 0, y: 0 })));
383+
assert_eq!(parse("a:1"), ("a", Some(Point { x: 1, y: 1 })));
366384
assert_eq!(parse("1:a"), ("1:a", None));
367-
assert_eq!(parse("file.txt:10"), ("file.txt", Some(Point { x: 0, y: 9 })));
368-
assert_eq!(parse("file.txt:10:5"), ("file.txt", Some(Point { x: 4, y: 9 })));
385+
assert_eq!(parse("file.txt:10"), ("file.txt", Some(Point { x: 1, y: 10 })));
386+
assert_eq!(parse("file.txt:10:5"), ("file.txt", Some(Point { x: 5, y: 10 })));
387+
assert_eq!(parse("file.txt:-1"), ("file.txt", Some(Point { x: 1, y: -1 })));
388+
assert_eq!(parse("file.txt:-10:5"), ("file.txt", Some(Point { x: 5, y: -10 })));
389+
assert_eq!(parse("file.txt:10:-5"), ("file.txt:10", Some(Point { x: 1, y: -5 })));
390+
assert_eq!(parse("file.txt:-"), ("file.txt:-", None));
369391
}
370392
}

crates/edit/src/bin/edit/draw_editor.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use std::num::ParseIntError;
5-
64
use edit::framebuffer::IndexedColor;
75
use edit::helpers::*;
86
use edit::icu;
@@ -319,14 +317,12 @@ pub fn draw_goto_menu(ctx: &mut Context, state: &mut State) {
319317
ctx.steal_focus();
320318

321319
if ctx.consume_shortcut(vk::RETURN) {
322-
match validate_goto_point(&state.goto_target) {
323-
Ok(point) => {
324-
let mut buf = doc.buffer.borrow_mut();
325-
buf.cursor_move_to_logical(point);
326-
buf.make_cursor_visible();
327-
done = true;
328-
}
329-
Err(_) => state.goto_invalid = true,
320+
if let Some(goto) = validate_goto_point(&state.goto_target) {
321+
doc.cursor_move_to_goto(goto);
322+
doc.buffer.borrow_mut().make_cursor_visible();
323+
done = true;
324+
} else {
325+
state.goto_invalid = true;
330326
}
331327
ctx.needs_rerender();
332328
}
@@ -344,13 +340,17 @@ pub fn draw_goto_menu(ctx: &mut Context, state: &mut State) {
344340
}
345341
}
346342

347-
fn validate_goto_point(line: &str) -> Result<Point, ParseIntError> {
343+
fn validate_goto_point(line: &str) -> Option<Point> {
348344
let mut coords = [0; 2];
349-
let (y, x) = line.split_once(':').unwrap_or((line, "0"));
345+
let (y, x) = line.split_once(':').unwrap_or((line, "1"));
350346
// Using a loop here avoids 2 copies of the str->int code.
351347
// This makes the binary more compact.
352348
for (i, s) in [x, y].iter().enumerate() {
353-
coords[i] = s.parse::<CoordType>()?.saturating_sub(1);
349+
coords[i] = s.parse::<CoordType>().ok()?;
350+
}
351+
// Counting backwards is only supported for lines.
352+
if coords[0] < 1 {
353+
return None;
354354
}
355-
Ok(Point { x: coords[0], y: coords[1] })
355+
Some(Point { x: coords[0], y: coords[1] })
356356
}

crates/edit/src/bin/edit/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ fn handle_args(state: &mut State) -> apperr::Result<bool> {
291291
for (p, goto) in &paths {
292292
let doc = state.documents.add_file_path(p)?;
293293
if let Some(goto) = goto {
294-
doc.buffer.borrow_mut().cursor_move_to_logical(*goto);
294+
doc.cursor_move_to_goto(*goto);
295295
}
296296
}
297297

0 commit comments

Comments
 (0)