Fix select motions and mobile line aliases
This commit is contained in:
@@ -97,17 +97,19 @@ selection entry, leader rails, tool commands, save/quit, and recoverability.
|
||||
|
||||
Text-entry mode for ordinary typing. `mim` starts in Normal mode; Insert mode is
|
||||
entered explicitly with `i`, `a`, `o`, or another visible command. In Insert mode,
|
||||
`Space` commits a literal space immediately. Physical keyboards may use
|
||||
`Space` commits a literal space immediately and `Tab` inserts spaces by default
|
||||
(expandtab, matching the dotfiles Neovim baseline). Physical keyboards may use
|
||||
conventional direct keys such as Escape as aliases back to Normal, but those
|
||||
aliases are optional accelerators, not required controls.
|
||||
|
||||
### Select mode
|
||||
|
||||
Selection-building mode. Movement extends the active selection by default.
|
||||
Selection commands are semantic: select word, line, current indent block,
|
||||
matching delimiter pair, enclosing text object, parameter, diagnostic range, and
|
||||
visible panel item. Select mode can apply operations such as delete, replace,
|
||||
copy, format, code action, or explain to the selection.
|
||||
Semantic object selections live behind the match/object rail (`m w`, `m l`,
|
||||
`m i`, `m p`, etc.) so bare `h/j/k/l`, word motions, arrows, and mobile line
|
||||
aliases keep behaving as selection-extending motions. Select mode can apply
|
||||
operations such as delete, replace, copy, format, code action, or explain to the
|
||||
selection.
|
||||
|
||||
### Panel mode
|
||||
|
||||
@@ -179,7 +181,8 @@ such as arrow keys, Home/End, PageUp/PageDown, Escape, or `%`.
|
||||
| `O` or `Space o` `a` | open line above and enter Insert |
|
||||
| `h` `j` `k` `l` | left/down/up/right where comfortable; arrows are aliases |
|
||||
| `w` `b` `e` | word forward/back/end |
|
||||
| `0` `$` | line start/end when available; `Space g` `l` opens line targets |
|
||||
| `H` `L` | mobile-reachable line start/end aliases |
|
||||
| `0` `^` `$` | attached/Vim-style line start/first-nonblank/end aliases; `Space g` `l` opens line targets |
|
||||
| `u` | undo |
|
||||
| `Space u` `r` | redo |
|
||||
| `Space w` | write/save current buffer |
|
||||
|
||||
@@ -18,11 +18,15 @@ 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 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]
|
||||
|
||||
## ui
|
||||
|
||||
0 001 Narrow terminals SHALL use transient panels instead of permanent desktop splits. [mobile]
|
||||
1 002 Files, git, diagnostics, LSP, build, terminal, and Pi surfaces SHALL share one panel navigation model. [panels]
|
||||
0 003 When the cursor moves beyond the visible horizontal viewport, the editor SHALL scroll the line horizontally enough to keep the cursor cell visible. [mobile]
|
||||
|
||||
## coding
|
||||
|
||||
|
||||
+67
-26
@@ -1055,6 +1055,8 @@ pub const Client = struct {
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "s")) {
|
||||
const snap = try self.session.snapshot();
|
||||
try self.session.selectRange(snap.cursor_byte, snap.cursor_byte);
|
||||
self.mode = .select;
|
||||
self.message = "select";
|
||||
self.pending_count = 0;
|
||||
@@ -1070,8 +1072,8 @@ pub const Client = struct {
|
||||
if (std.mem.eql(u8, text, "p")) return self.pasteRegister();
|
||||
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, "0")) return self.applyProtocol("command move_line_start");
|
||||
if (std.mem.eql(u8, text, "$")) return self.applyProtocol("command move_line_end");
|
||||
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, "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);
|
||||
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
|
||||
@@ -1110,7 +1112,7 @@ pub const Client = struct {
|
||||
.key => |key| switch (key) {
|
||||
.space => try self.insertText(" "),
|
||||
.enter => try self.insertNewlineWithIndent(),
|
||||
.tab => try self.insertText(try self.smartTabText()),
|
||||
.tab => try self.insertText(" "),
|
||||
.escape => {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
@@ -1136,7 +1138,16 @@ pub const Client = struct {
|
||||
self.message = "normal";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "%")) return self.jumpToMatch(null);
|
||||
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, "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());
|
||||
if (std.mem.eql(u8, text, "h")) return self.repeatSelectionProtocol("command move_left", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "l")) return self.repeatSelectionProtocol("command move_right", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "w")) return self.repeatSelectionProtocol("command move_word_forward", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "b")) return self.repeatSelectionProtocol("command move_word_back", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "e")) return self.repeatSelectionProtocol("command move_word_end", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
|
||||
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
|
||||
if (text.len == 1) {
|
||||
@@ -1153,12 +1164,12 @@ pub const Client = struct {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
},
|
||||
.arrow_left => try self.applyProtocol("command move_left"),
|
||||
.arrow_right => try self.applyProtocol("command move_right"),
|
||||
.arrow_up => try self.applyProtocol("command move_up"),
|
||||
.arrow_down => try self.applyProtocol("command move_down"),
|
||||
.home => try self.applyProtocol("command move_line_start"),
|
||||
.end => try self.applyProtocol("command move_line_end"),
|
||||
.arrow_left => try self.extendSelectionWithProtocol("command move_left"),
|
||||
.arrow_right => try self.extendSelectionWithProtocol("command move_right"),
|
||||
.arrow_up => try self.extendSelectionWithProtocol("command move_up"),
|
||||
.arrow_down => try self.extendSelectionWithProtocol("command move_down"),
|
||||
.home => try self.extendSelectionWithProtocol("command move_line_start"),
|
||||
.end => try self.extendSelectionWithProtocol("command move_line_end"),
|
||||
else => {},
|
||||
},
|
||||
.unknown => self.unknownPrefixOrInput("select"),
|
||||
@@ -1868,6 +1879,13 @@ pub const Client = struct {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (self.effectiveMode() == .select and text.len == 1) {
|
||||
if (self.objectRangeForKey(text[0])) |range| {
|
||||
try self.session.selectRange(range.start, range.end);
|
||||
self.message = "select";
|
||||
return;
|
||||
} else |_| {}
|
||||
}
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
@@ -2157,20 +2175,6 @@ pub const Client = struct {
|
||||
try self.insertText(text.items);
|
||||
}
|
||||
|
||||
fn smartTabText(self: *Client) ![]const u8 {
|
||||
const snap = try self.session.snapshot();
|
||||
const line_start = currentLineStart(snap.bytes, snap.cursor_byte);
|
||||
if (line_start < snap.bytes.len and snap.bytes[line_start] == '\t') return "\t";
|
||||
var i: usize = 0;
|
||||
while (i < snap.bytes.len) {
|
||||
if (snap.bytes[i] == '\t') return "\t";
|
||||
while (i < snap.bytes.len and snap.bytes[i] != '\n') : (i += 1) {}
|
||||
if (i < snap.bytes.len) i += 1;
|
||||
while (i < snap.bytes.len and snap.bytes[i] == ' ') : (i += 1) {}
|
||||
}
|
||||
return " ";
|
||||
}
|
||||
|
||||
fn currentLineStart(bytes: []const u8, cursor_byte: usize) usize {
|
||||
var start = @min(cursor_byte, bytes.len);
|
||||
while (start > 0 and bytes[start - 1] != '\n') start -= 1;
|
||||
@@ -2202,6 +2206,29 @@ pub const Client = struct {
|
||||
while (i < repeat) : (i += 1) try self.applyProtocol(line);
|
||||
}
|
||||
|
||||
fn extendSelectionWithProtocol(self: *Client, line: []const u8) !void {
|
||||
const before = try self.session.snapshot();
|
||||
const anchor = if (before.selection) |selection| selection.anchor else before.cursor_byte;
|
||||
try self.applyProtocol(line);
|
||||
const after = try self.session.snapshot();
|
||||
try self.session.selectRange(@min(anchor, after.cursor_byte), @max(anchor, after.cursor_byte));
|
||||
self.message = "select";
|
||||
}
|
||||
|
||||
fn repeatSelectionProtocol(self: *Client, line: []const u8, repeat: usize) !void {
|
||||
var i: usize = 0;
|
||||
while (i < repeat) : (i += 1) try self.extendSelectionWithProtocol(line);
|
||||
}
|
||||
|
||||
fn extendSelectionWithJump(self: *Client, delimiter: ?u8) !void {
|
||||
const before = try self.session.snapshot();
|
||||
const anchor = if (before.selection) |selection| selection.anchor else before.cursor_byte;
|
||||
try self.jumpToMatch(delimiter);
|
||||
const after = try self.session.snapshot();
|
||||
try self.session.selectRange(@min(anchor, after.cursor_byte), @max(anchor, after.cursor_byte));
|
||||
self.message = "select";
|
||||
}
|
||||
|
||||
fn repeatMutatingProtocol(self: *Client, line: []const u8, repeat: usize) !void {
|
||||
var i: usize = 0;
|
||||
while (i < repeat) : (i += 1) try self.applyMutatingProtocol(line);
|
||||
@@ -2417,9 +2444,13 @@ fn appendEditorLine(
|
||||
try out.append(allocator, '\n');
|
||||
return;
|
||||
}
|
||||
const horizontal_scroll = if (is_cursor_line and cursor_col >= content_width)
|
||||
cursor_col - content_width + 1
|
||||
else
|
||||
0;
|
||||
if (is_cursor_line) try out.appendSlice(allocator, ansi_current_line);
|
||||
try out.appendSlice(allocator, ansi_text);
|
||||
try appendEditorCells(allocator, out, line, line_start_byte, content_width, is_cursor_line, cursor_col, cursor_byte, selection);
|
||||
try appendEditorCells(allocator, out, line, line_start_byte, content_width, is_cursor_line, cursor_col, cursor_byte, selection, horizontal_scroll);
|
||||
try out.appendSlice(allocator, ansi_reset);
|
||||
try out.append(allocator, '\n');
|
||||
}
|
||||
@@ -2458,11 +2489,19 @@ fn appendEditorCells(
|
||||
cursor_col: usize,
|
||||
cursor_byte: usize,
|
||||
selection: ?session_mod.Selection,
|
||||
horizontal_scroll: usize,
|
||||
) !void {
|
||||
var i: usize = 0;
|
||||
var source_col: usize = 0;
|
||||
var visual_col: usize = 0;
|
||||
var drew_cursor = false;
|
||||
while (i < bytes.len and source_col < horizontal_scroll) {
|
||||
const len = std.unicode.utf8ByteSequenceLength(bytes[i]) catch 1;
|
||||
const end = @min(bytes.len, i + len);
|
||||
const width = @max(@as(usize, 1), session_mod.cellWidth(bytes[i..end]));
|
||||
source_col += width;
|
||||
i = end;
|
||||
}
|
||||
while (i < bytes.len and visual_col < max_cells) {
|
||||
const len = std.unicode.utf8ByteSequenceLength(bytes[i]) catch 1;
|
||||
const end = @min(bytes.len, i + len);
|
||||
@@ -4178,12 +4217,14 @@ test "regular: select mode object grammar selects word line indent parameter and
|
||||
try client.handleInput("j");
|
||||
try client.handleInput("w");
|
||||
try client.handleInput("s");
|
||||
try client.handleInput("m");
|
||||
try client.handleInput("w");
|
||||
var snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("alpha", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
|
||||
try client.handleInput("n");
|
||||
try client.handleInput("s");
|
||||
try client.handleInput("m");
|
||||
try client.handleInput("l");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings(" alpha(beta, gamma);\n", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
@@ -5100,5 +5141,5 @@ test "regular: insert Enter copies current line indent and Tab follows tab-inden
|
||||
try tabs.handleTraceLine("key tab");
|
||||
try tabs.handleInput("x");
|
||||
snap = try tabs.session.snapshot();
|
||||
try std.testing.expectEqualStrings("\titem\tx", snap.bytes);
|
||||
try std.testing.expectEqualStrings("\titem x", snap.bytes);
|
||||
}
|
||||
|
||||
@@ -448,6 +448,12 @@ def setup_long(tmp: Path) -> tuple[Path, str | None]:
|
||||
return target, "l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\n"
|
||||
|
||||
|
||||
def setup_long_line(tmp: Path) -> tuple[Path, str | None]:
|
||||
target = tmp / "wide.txt"
|
||||
target.write_text("abcdefghijklmnopqrstuvwxyz0123456789\n", encoding="utf-8")
|
||||
return target, "abcdefghijklmnopqrstuvwxyz0123456789\n"
|
||||
|
||||
|
||||
def setup_multifile(tmp: Path) -> tuple[Path, str | None]:
|
||||
root = tmp / "repo"
|
||||
root.mkdir()
|
||||
@@ -495,11 +501,12 @@ SCENARIOS = [
|
||||
Scenario("attached-cursor-left-insert", 56, 14, "empty", "attached-keyboard", "insert-cursor", "truecolor", "file", setup_empty, (b"i", b"a", b"b", b"\x1b[D", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "aXb", ("1│", "☻", "aX", "mode:normal")),
|
||||
Scenario("normal-A-append", 56, 14, "existing", "attached-keyboard", "append-eol", "truecolor", "file", setup_existing, (b"A", b"!", b"\x1b", b" ", b"w", b" ", b"q"), "alpha!\nbeta", ("1│", "☻", "alpha")),
|
||||
Scenario("insert-enter-indent", 56, 14, "indented", "attached-keyboard", "newline-indent", "truecolor", "file", setup_indented, (b"A", b"\r", b"x", b"\x1b", b" ", b"w", b" ", b"q"), " item\n x", ("1│", "2│", "☻")),
|
||||
Scenario("insert-tab-detects-tabs", 56, 14, "tabbed", "attached-keyboard", "tab-indent", "truecolor", "file", setup_tabbed, (b"A", b"\t", b"x", b"\x1b", b" ", b"w", b" ", b"q"), "\titem\tx", ("1│", "☻")),
|
||||
Scenario("insert-tab-expands-spaces", 56, 14, "tabbed", "attached-keyboard", "tab-indent", "truecolor", "file", setup_tabbed, (b"A", b"\t", b"x", b"\x1b", b" ", b"w", b" ", b"q"), "\titem x", ("1│", "☻")),
|
||||
Scenario("page-keys-move-cursor", 56, 10, "long", "attached-keyboard", "page-keys", "truecolor", "file", setup_long, (b"\x1b[6~", b"\x1b[5~", b"i", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "l1X\nl2\nl3\nl4\nl5\nl6\nl7\nl8", ("1│", "☻")),
|
||||
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-colors", 56, 12, "short", "attached-keyboard", "select", "truecolor", "file", setup_short, (b"s", b"w", b"n", b" ", b"q"), "abcdef\n", ("attr:selection", "░", "mode:normal")),
|
||||
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("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("multi-file-coding-loop", 72, 16, "repo-multifile", "attached-keyboard", "multi-file", "truecolor", "file", setup_multifile, (b"A", b"?", b"\x1b", b" ", b"w", b" ", b"o", b"r", b"e", b"p", b"o", b"/", b"a", b"l", b"p", b"h", b"a", b".", b"z", b"i", b"g", b"\r", b"A", b"!", b"\x1b", b" ", b"w", b" ", b"q"), None, ("alpha.zig", "beta", "☻", "mode:normal"), max_key_events=30, expect_files=(("alpha.zig", "alpha!\n"), ("beta.zig", "beta?"))),
|
||||
Scenario("lsp-assisted-coding", 60, 12, "lsp-fixture", "attached-keyboard", "lsp", "truecolor", "file", setup_lsp_assist, (b" ", b"l", b"h", b" ", b"l", b"s", b" ", b"d", b"q", b" ", b"d", b"n", b" ", b"l", b"f", b" ", b"w", b" ", b"q"), "ZLS", ("[zls] add(lhs, rhs)", "[zls] add(lhs: i32, rhs: i32) active=rhs", "diag:fresh:zls", "format:zls:applied", "ZLS", "mode:normal"), max_key_events=19, prelude=LSP_ASSIST_PRELUDE),
|
||||
Scenario("project-search-panel", 60, 12, "repo-search", "attached-keyboard", "project-search", "truecolor", "file", setup_project_search, (b" ", b"s", b"p", b"f", b"i", b"n", b"d", b"m", b"e", b"\r", b"q", b" ", b"q"), "main\n", ("panel [search]", "target.zig", "findme", "mode:normal"), max_key_events=13),
|
||||
|
||||
Reference in New Issue
Block a user