Separate first nonblank line motion
This commit is contained in:
@@ -213,8 +213,8 @@ such as arrow keys, Home/End, PageUp/PageDown, Escape, or `%`.
|
||||
| `w` `b` `e` | word forward/back/end |
|
||||
| `gg` `G` | document start / bottom line |
|
||||
| `Ctrl-u` `Ctrl-d` | half-page up/down when the SSH client exposes Ctrl; `g u` / `g d` are mobile fallback aliases |
|
||||
| `H` `L` | mobile-reachable line start/end aliases |
|
||||
| `0` `^` `$` | attached/Vim-style line start/first-nonblank/end aliases; `Space g` `l` opens line targets |
|
||||
| `H` `L` | mobile-reachable absolute line start/end aliases |
|
||||
| `0` `^` `$` | attached/Vim-style line start/first-nonblank/end aliases; `^` jumps past leading spaces/tabs, not to column zero; `Space g` `l` opens line targets |
|
||||
| `.` | repeat the last repeatable local edit (insert/change/delete/put/open-line subset) |
|
||||
| `u` | undo |
|
||||
| `Space u` `r` | redo |
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Rows are redgate TSV requirements: `ring<TAB>id<TAB>summary [tag]`.
|
||||
2 005 Keyboard layout profiles SHALL be source-patched tables backed by recorded terminal traces, starting with iOS QWERTZ. [mobile]
|
||||
1 006 Physical keyboard input SHALL be a first-class terminal path with discoverable shortcuts and no loss of mobile no-required-modifier command access. [mobile]
|
||||
1 007 The v1 keymap SHALL keep modes, verbs, objects, counts/repetition, panels, and tool operations orthogonal so operations compose instead of multiplying bindings. [mobile]
|
||||
1 008 Line start/end motions SHALL have mobile-reachable aliases in addition to attached/Vim-style symbol or digit keys. [mobile]
|
||||
1 008 Line start/end motions SHALL have mobile-reachable aliases in addition to attached/Vim-style symbol or digit keys, and first-nonblank line motion SHALL remain distinct from absolute line start. [mobile]
|
||||
1 009 Insert-mode Tab SHALL expand to spaces by default. [mobile]
|
||||
1 010 Select mode SHALL extend the active range with motions and keep semantic object selection behind an explicit object rail. [mobile]
|
||||
1 011 Vertical line motions SHALL preserve a preferred cursor cell across ragged lines, clamped to each target line until horizontal/editing motion resets the preference. [mobile]
|
||||
|
||||
@@ -96,6 +96,7 @@ fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session,
|
||||
if (std.mem.eql(u8, command, "move_word_forward")) return dispatchAndRespond(allocator, session, .move_word_forward);
|
||||
if (std.mem.eql(u8, command, "move_word_back")) return dispatchAndRespond(allocator, session, .move_word_back);
|
||||
if (std.mem.eql(u8, command, "move_word_end")) return dispatchAndRespond(allocator, session, .move_word_end);
|
||||
if (std.mem.eql(u8, command, "move_line_first_nonblank")) return dispatchAndRespond(allocator, session, .move_line_first_nonblank);
|
||||
if (std.mem.eql(u8, command, "move_line_start")) return dispatchAndRespond(allocator, session, .move_line_start);
|
||||
if (std.mem.eql(u8, command, "move_line_end")) return dispatchAndRespond(allocator, session, .move_line_end);
|
||||
if (std.mem.eql(u8, command, "move_document_start")) return dispatchAndRespond(allocator, session, .move_document_start);
|
||||
@@ -489,3 +490,19 @@ test "adversarial: protocol list errors are explicit" {
|
||||
try std.testing.expectEqualStrings("err no panel open\n", response);
|
||||
}
|
||||
}
|
||||
|
||||
test "regular: protocol exposes first-nonblank line motion" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
|
||||
var response = try handleLine(std.testing.allocator, &session, "open top\n alpha\n");
|
||||
std.testing.allocator.free(response);
|
||||
response = try handleLine(std.testing.allocator, &session, "command move_down\n");
|
||||
std.testing.allocator.free(response);
|
||||
response = try handleLine(std.testing.allocator, &session, "command move_line_end\n");
|
||||
std.testing.allocator.free(response);
|
||||
response = try handleLine(std.testing.allocator, &session, "command move_line_first_nonblank\n");
|
||||
defer std.testing.allocator.free(response);
|
||||
|
||||
try std.testing.expectEqualStrings("ok state cursor_byte=6 cursor_cell=2 bytes_len=11 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ pub const Command = union(enum) {
|
||||
move_word_forward,
|
||||
move_word_back,
|
||||
move_word_end,
|
||||
move_line_first_nonblank,
|
||||
insert: []const u8,
|
||||
insert_pair: Pair,
|
||||
delete_backward,
|
||||
@@ -95,6 +96,7 @@ pub const Buffer = struct {
|
||||
.move_word_forward => self.moveWordForward(),
|
||||
.move_word_back => self.moveWordBack(),
|
||||
.move_word_end => self.moveWordEnd(),
|
||||
.move_line_first_nonblank => self.moveLineFirstNonblank(),
|
||||
.insert => |text| try self.insert(text),
|
||||
.insert_pair => |pair| try self.insertPair(pair),
|
||||
.delete_backward => self.deleteBackward(),
|
||||
@@ -267,6 +269,14 @@ pub const Buffer = struct {
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn moveLineFirstNonblank(self: *Buffer) void {
|
||||
const range = self.currentLineRange(false);
|
||||
var at = range.start;
|
||||
while (at < range.end and (self.bytes.items[at] == ' ' or self.bytes.items[at] == '\t')) : (at += 1) {}
|
||||
self.cursor.byte = at;
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn moveDocumentStart(self: *Buffer) void {
|
||||
self.cursor.byte = 0;
|
||||
self.refreshCellResetPreferred();
|
||||
@@ -795,3 +805,33 @@ test "regular: document motions jump to top and bottom line" {
|
||||
try std.testing.expectEqual(@as(usize, 0), snap.cursor_byte);
|
||||
try std.testing.expectEqual(@as(usize, 0), snap.cursor_cell);
|
||||
}
|
||||
|
||||
test "regular: line first-nonblank is distinct from absolute line start" {
|
||||
var buffer = try Buffer.openFromBytes(std.testing.allocator, "top\n \talpha\nend");
|
||||
defer buffer.deinit();
|
||||
|
||||
buffer.moveDown();
|
||||
buffer.moveLineEnd();
|
||||
try std.testing.expectEqual(@as(usize, 12), buffer.cursor.byte);
|
||||
|
||||
buffer.moveLineStart();
|
||||
try std.testing.expectEqual(@as(usize, 4), buffer.cursor.byte);
|
||||
|
||||
buffer.moveLineEnd();
|
||||
buffer.moveLineFirstNonblank();
|
||||
try std.testing.expectEqual(@as(usize, 7), buffer.cursor.byte);
|
||||
try std.testing.expectEqual(@as(usize, 2), buffer.cursor.cell);
|
||||
}
|
||||
|
||||
test "adversarial: line first-nonblank on blank or whitespace-only line goes to line end" {
|
||||
var buffer = try Buffer.openFromBytes(std.testing.allocator, "a\n \n\t\tb");
|
||||
defer buffer.deinit();
|
||||
|
||||
buffer.moveDown();
|
||||
buffer.moveLineFirstNonblank();
|
||||
try std.testing.expectEqual(@as(usize, 4), buffer.cursor.byte);
|
||||
|
||||
buffer.moveDown();
|
||||
buffer.moveLineFirstNonblank();
|
||||
try std.testing.expectEqual(@as(usize, 7), buffer.cursor.byte);
|
||||
}
|
||||
|
||||
+4
-2
@@ -1115,7 +1115,8 @@ pub const Client = struct {
|
||||
if (std.mem.eql(u8, text, ".")) return self.repeatLastEdit();
|
||||
if (std.mem.eql(u8, text, "u")) return self.undoEdit();
|
||||
if (std.mem.eql(u8, text, "U")) return self.redoEdit();
|
||||
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0") or std.mem.eql(u8, text, "^")) return self.applyProtocol("command move_line_start");
|
||||
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0")) return self.applyProtocol("command move_line_start");
|
||||
if (std.mem.eql(u8, text, "^")) return self.applyProtocol("command move_line_first_nonblank");
|
||||
if (std.mem.eql(u8, text, "L") or std.mem.eql(u8, text, "$")) return self.applyProtocol("command move_line_end");
|
||||
if (std.mem.eql(u8, text, "%")) return self.jumpToMatch(null);
|
||||
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
|
||||
@@ -1186,7 +1187,8 @@ pub const Client = struct {
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "%")) return self.extendSelectionWithJump(null);
|
||||
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0") or std.mem.eql(u8, text, "^")) return self.extendSelectionWithProtocol("command move_line_start");
|
||||
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0")) return self.extendSelectionWithProtocol("command move_line_start");
|
||||
if (std.mem.eql(u8, text, "^")) return self.extendSelectionWithProtocol("command move_line_first_nonblank");
|
||||
if (std.mem.eql(u8, text, "L") or std.mem.eql(u8, text, "$")) return self.extendSelectionWithProtocol("command move_line_end");
|
||||
if (std.mem.eql(u8, text, "j")) return self.repeatSelectionProtocol("command move_down", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "k")) return self.repeatSelectionProtocol("command move_up", self.takeRepeat());
|
||||
|
||||
@@ -552,6 +552,7 @@ SCENARIOS = [
|
||||
Scenario("counted-move-inserts-at-count", 56, 12, "short", "attached-keyboard", "counts", "truecolor", "file", setup_short, (b"3", b"l", b"i", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "abcXdef", ("1│", "☻")),
|
||||
Scenario("percent-match-jump", 56, 12, "brackets", "attached-keyboard", "match-jump", "truecolor", "file", setup_brackets, (b"%", b"i", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "(abX)", ("1│", "☻")),
|
||||
Scenario("select-mode-motions", 56, 12, "short", "attached-keyboard", "select", "truecolor", "file", setup_short, (b"L", b"s", b"H", b"L", b"n", b" ", b"q"), "abcdef\n", ("attr:selection", "░", "mode:normal")),
|
||||
Scenario("first-nonblank-line-motion", 56, 12, "indented", "attached-keyboard", "line-edges", "truecolor", "file", setup_indented, (b"L", b"^", b"i", b"X", b"\x1b", b" ", b"w", b" ", b"q"), " Xitem", ("1│", "☻", "mode:normal")),
|
||||
Scenario("horizontal-cursor-scroll", 24, 8, "wide-line", "attached-keyboard", "horizontal-scroll", "truecolor", "file", setup_long_line, (b"L", b" ", b"q"), "abcdefghijklmnopqrstuvwxyz0123456789\n", ("0123456789", "☻", "mode:normal")),
|
||||
Scenario("vertical-motion-preferred-column", 56, 12, "ragged", "attached-keyboard", "motion", "truecolor", "file", setup_ragged, (b"L", b"j", b"j", b"i", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "abcdef\nxy\n123456X", ("1│", "3│", "☻", "mode:normal")),
|
||||
Scenario("document-half-page-motions", 56, 10, "tall", "attached-keyboard", "motion", "truecolor", "file", setup_tall, (b"G", b"i", b"X", b"\x1b", b"g", b"g", b"\x04", b"i", b"Y", b"\x1b", b"G", b"g", b"u", b"i", b"Z", b"\x1b", b" ", b"w", b" ", b"q"), "l1\nl2\nl3\nl4\nl5\nZl6\nl7\nl8\nl9\nl10\nYl11\nl12\nl13\nl14\nl15\nXl16", ("11│", "16│", "☻", "mode:normal")),
|
||||
|
||||
Reference in New Issue
Block a user