diff --git a/KEYMAP.md b/KEYMAP.md index 2df2571..457f674 100644 --- a/KEYMAP.md +++ b/KEYMAP.md @@ -134,12 +134,12 @@ Selection-building mode. Escape or Ctrl-[ returns to Normal. Movement extends th active selection by default. Vertical movement preserves a preferred cursor column across ragged lines, clamping only to the current line length until horizontal/editing motion resets -that preference. 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. Line object -selection includes the line's newline boundary when present, matching Vim-like -linewise edit semantics. Select mode can apply operations such as delete, -replace, copy, format, code action, or explain to the selection. +that preference. Semantic object selections live behind the select-object rail +(`s` then `o w`, `o l`, `o i`, `o p`, etc.) so bare `h/j/k/l`, word motions, +arrows, and mobile line aliases keep behaving as selection-extending motions. +Line object selection includes the line's newline boundary when present, matching +Vim-like linewise edit semantics. Select mode can apply operations such as +delete, replace, copy, format, code action, or explain to the selection. ### Panel mode @@ -254,12 +254,13 @@ Objects are shared by Normal and Select mode. | `m` `{` | jump/select `{}` pair, depending on mode | | `m` `[` | jump/select `[]` pair, depending on mode | | `m` `"` / `m` `'` / `m` `` ` `` | jump/select quoted string where parseable | -| `s` `w` | select word | -| `s` `l` | select line | -| `s` `i` | select current indent block | -| `s` `p` | select current parameter/argument | -| `s` `f` | select current function or syntactic enclosing form where available | -| `s` `d` | select current diagnostic range | +| `s` then motion | enter Select mode and extend the selection with that motion | +| `s` `o` `w` | select word | +| `s` `o` `l` | select line | +| `s` `o` `i` | select current indent block | +| `s` `o` `p` | select current parameter/argument | +| `s` `o` `f` | select current function or syntactic enclosing form where available | +| `s` `o` `d` | select current diagnostic range | Physical keyboards may expose `%` as an alias for `m m`. The mobile canonical path stays `m m` because `%` is awkward on software keyboards and QWERTZ. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 5101c6d..3b7730a 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -28,6 +28,7 @@ Rows are redgate TSV requirements: `ringidsummary [tag]`. 1 015 The keymap SHALL classify bindings into core mobile, mobile SSH accelerator, and attached-keyboard tiers so Esc/Ctrl/arrow-capable clients can be used without making those keys mandatory for core commands. [mobile] 1 016 Every attached-keyboard or Ctrl/Esc accelerator that performs a core editing, navigation, recovery, or panel action SHALL have a documented visible mobile fallback or rail path. [mobile] 1 017 Insert and Select mode SHALL return to Normal via Escape and Ctrl-[ as equivalent terminal events while preserving literal Insert-mode Space. [mobile] +1 018 Select mode SHALL keep bare movement keys as selection-extending motions and provide a non-conflicting mobile object-selection rail for word, line, indent, parameter, enclosing form, and diagnostic ranges. [mobile] ## ui diff --git a/src/tui.zig b/src/tui.zig index c14f0dc..7d9b15a 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -33,6 +33,7 @@ const PrefixRail = enum { none, insert_space, match, + select_object, go, repeat, delete, @@ -568,6 +569,7 @@ pub const Client = struct { .none => if (self.pending_count != 0) "count pending" else "", .insert_space => "insert-space: n normal Space literal text commits space+text", .match => "match: m jump s inside a around ( { [ quotes", + .select_object => "select-object: w word l line i indent p param f form d diagnostic", .go => "go: d definition r references e diagnostic a parameter", .repeat => "repeat: digits count . repeat-last", .delete => "delete: d line h previous-char l char", @@ -1198,6 +1200,7 @@ pub const Client = struct { 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, "o")) return self.openPrefix(.select_object); if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go); if (text.len == 1) { if (self.objectRangeForKey(text[0])) |range| { @@ -1256,6 +1259,9 @@ pub const Client = struct { .match => self.applyMatchRail(event) catch |err| { self.message = @errorName(err); }, + .select_object => self.applySelectObjectRail(event) catch |err| { + self.message = @errorName(err); + }, .go => try self.applyGoRail(event), .repeat => self.applyKnownRailOrMessage(event, "repeat rail ready"), .delete => try self.applyDeleteRail(event), @@ -1878,6 +1884,18 @@ pub const Client = struct { self.applyKnownRailOrMessage(event, "go rail ready"); } + fn applySelectObjectRail(self: *Client, event: input.Event) !void { + const text = eventText(event) orelse return self.unknownPrefixOrInput("select object"); + if (text.len == 1) { + const range = try self.objectRangeForKey(text[0]); + try self.session.selectRange(range.start, range.end); + self.mode = .select; + self.message = "selected object"; + return; + } + self.unknownPrefixOrInput("select object"); + } + const ParameterDirection = enum { next, previous }; fn moveParameter(self: *Client, direction: ParameterDirection) !void { diff --git a/tools/terminal_e2e.py b/tools/terminal_e2e.py index 70a026f..50153c0 100755 --- a/tools/terminal_e2e.py +++ b/tools/terminal_e2e.py @@ -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("select-object-word-rail", 56, 12, "short", "attached-keyboard", "select-object", "truecolor", "file", setup_short, (b"s", b"o", b"w", 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")),