feat: support configurable modified-key binding sequences
This commit is contained in:
@@ -8,7 +8,7 @@ Contributor-facing module boundaries and implementation contracts. Project termi
|
|||||||
|
|
||||||
One crate; no workspace or terminal-coordinate types in the editing backend:
|
One crate; no workspace or terminal-coordinate types in the editing backend:
|
||||||
|
|
||||||
- `editor`: owns buffer lifecycle and per-buffer text, saved state, primary/selection set, modes, search, and undo/redo; session-wide clipboard policy, independent registers and owned-system fragment metadata, semantic native repeat, action metadata, command-palette and file-picker focus/dispatch, key notation, leader/count prefixes and binding resolution, modal paste policy, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
- `editor`: owns buffer lifecycle and per-buffer text, saved state, primary/selection set, modes, search, and undo/redo; session-wide clipboard policy, independent registers and owned-system fragment metadata, semantic native repeat, action metadata, command-palette and file-picker focus/dispatch, key notation, key-sequence/count prefixes and binding resolution, modal paste policy, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
||||||
- `files`: owns the gitignore-aware directory/file catalog, authoritative OS paths, compact tree projection, query filtering, browse/search focus, expansion/selection restoration, navigation, and scan/error state shared by floating and docked presentations.
|
- `files`: owns the gitignore-aware directory/file catalog, authoritative OS paths, compact tree projection, query filtering, browse/search focus, expansion/selection restoration, navigation, and scan/error state shared by floating and docked presentations.
|
||||||
Virtual file leaves retain their original catalog input index for caller-owned annotations; scanned entries and directories remain unannotated.
|
Virtual file leaves retain their original catalog input index for caller-owned annotations; scanned entries and directories remain unannotated.
|
||||||
- `diff`: owns searchable Before/After selection, category, focus and explorer visibility, request freshness, read-only comparison DTOs, and aligned rows/shared vertical and independent horizontal offsets. Changed paths form a virtual `files::FilePicker` catalog, reusing the normal tree projection, filtering, and navigation without scanning disk. It has no subprocess or terminal dependencies.
|
- `diff`: owns searchable Before/After selection, category, focus and explorer visibility, request freshness, read-only comparison DTOs, and aligned rows/shared vertical and independent horizontal offsets. Changed paths form a virtual `files::FilePicker` catalog, reusing the normal tree projection, filtering, and navigation without scanning disk. It has no subprocess or terminal dependencies.
|
||||||
@@ -32,7 +32,7 @@ The TUI also observes the editor's logical caret-reveal generation. Core motion/
|
|||||||
Frontends inspect `palette()` for the optional read-only query, filtered command entries, selected index, effective bindings, and eligibility. Palette input handling and acceptance stay in the editor; registered command execution stays in the session.
|
Frontends inspect `palette()` for the optional read-only query, filtered command entries, selected index, effective bindings, and eligibility. Palette input handling and acceptance stay in the editor; registered command execution stays in the session.
|
||||||
Frontends use cached `selection_navigation_hint()` for command lists and `file_navigation_hint()` for the active file view/context, without reconstructing input policy.
|
Frontends use cached `selection_navigation_hint()` for command lists and `file_navigation_hint()` for the active file view/context, without reconstructing input policy.
|
||||||
Frontends inspect `count()` for the optional pending numeric prefix; parsing, counted motion dispatch, and counted Normal line actions remain editor-owned.
|
Frontends inspect `count()` for the optional pending numeric prefix; parsing, counted motion dispatch, and counted Normal line actions remain editor-owned.
|
||||||
Frontends inspect `leader_pending()` for the cached display of a pending leader sequence; prefix recognition, cancellation, and dispatch remain editor-owned.
|
Frontends inspect `sequence_pending()` for the cached display of a pending key sequence; prefix recognition, cancellation, and dispatch remain editor-owned. Symbolic leader routes and explicit key prefixes share the same binding storage and dispatcher; explicit prefixes do not change when the leader is reconfigured.
|
||||||
Frontends inspect `g_pending()` for the built-in Normal/Visual `gt`/`gb` prefix and Normal-only `gf`. Document-edge motions share multi-target motion dispatch and viewport reveal, retaining Visual anchors and selection shape. Markdown following preflights links and headings before buffer/caret mutation; an unloaded target is validated/prepared once, while loaded targets use their current unsaved text. File identity and activation stay editor-owned.
|
Frontends inspect `g_pending()` for the built-in Normal/Visual `gt`/`gb` prefix and Normal-only `gf`. Document-edge motions share multi-target motion dispatch and viewport reveal, retaining Visual anchors and selection shape. Markdown following preflights links and headings before buffer/caret mutation; an unloaded target is validated/prepared once, while loaded targets use their current unsaved text. File identity and activation stay editor-owned.
|
||||||
Frontends inspect `line_operator_pending()` for the optional Normal `d`/`c` prefix. Dedicated `dd`/`cc` actions project counted caret rows through existing linewise selection normalization, clipboard publication, and atomic range editing, without exposing a temporary Visual mode. Clipboard failures restore the original targets before returning. Repeat recipes retain the requested row count rather than the originally clamped range; change recipes share the existing Insert undo group. There is no general operator–motion parser.
|
Frontends inspect `line_operator_pending()` for the optional Normal `d`/`c` prefix. Dedicated `dd`/`cc` actions project counted caret rows through existing linewise selection normalization, clipboard publication, and atomic range editing, without exposing a temporary Visual mode. Clipboard failures restore the original targets before returning. Repeat recipes retain the requested row count rather than the originally clamped range; change recipes share the existing Insert undo group. There is no general operator–motion parser.
|
||||||
Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and grapheme cursors plus a current index, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal uses unrecorded buffer activation and position placement, skips closed/clamped-duplicate locations, and truncates forward entries only on a distinct new jump. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and grapheme cursors plus a current index, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal uses unrecorded buffer activation and position placement, skips closed/clamped-duplicate locations, and truncates forward entries only on a distinct new jump. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
||||||
|
|||||||
+20
-9
@@ -38,7 +38,7 @@ end
|
|||||||
|
|
||||||
## Preferences
|
## Preferences
|
||||||
|
|
||||||
Plugin paths resolve relative to the config file, not the shell's working directory. `config.bind key, target, mode: config.modes.normal` overrides one key or leader-prefixed sequence; editing modes are `config.modes.normal`, `config.modes.insert`, `config.modes.command`, `config.modes.search`, and `config.modes.visual`. `config.modes.selection` configures shared list navigation; `config.modes.tree` overrides it in tree browsing. Omitting `mode` selects Normal. The leader defaults to `"space"` and is applied before configured bindings.
|
Plugin paths resolve relative to the config file, not the shell's working directory. `config.bind key, target, mode: config.modes.normal` overrides one key or a whitespace-separated key sequence; editing modes are `config.modes.normal`, `config.modes.insert`, `config.modes.command`, `config.modes.search`, and `config.modes.visual`. Sequences require Normal/Visual. `config.modes.selection` configures shared list navigation; `config.modes.tree` overrides it in tree browsing. Omitting `mode` selects Normal. The leader defaults to `"space"` and is applied before configured bindings.
|
||||||
`config.file_explorer_side` defaults to `"right"`; other choices are `"left"`, `"top"`, and `"bottom"`. Independent `file_explorer_view` / `file_picker_view` preferences both default to `"tree"` and accept `"list"` as an alternative. Invalid preference strings report notices and retain defaults while valid bindings still install. Non-string/empty values disable the whole configuration like other Ruby type errors.
|
`config.file_explorer_side` defaults to `"right"`; other choices are `"left"`, `"top"`, and `"bottom"`. Independent `file_explorer_view` / `file_picker_view` preferences both default to `"tree"` and accept `"list"` as an alternative. Invalid preference strings report notices and retain defaults while valid bindings still install. Non-string/empty values disable the whole configuration like other Ruby type errors.
|
||||||
`config.clipboard_mode` accepts `"system"` (default) or `"separate"`. An invalid policy string reports a notice and retains System while valid bindings still install; non-string/empty values reject the whole Ruby configuration.
|
`config.clipboard_mode` accepts `"system"` (default) or `"separate"`. An invalid policy string reports a notice and retains System while valid bindings still install; non-string/empty values reject the whole Ruby configuration.
|
||||||
|
|
||||||
@@ -60,15 +60,15 @@ Key notation follows [Herdr's readable style](https://herdr.dev/docs/configurati
|
|||||||
- Modifiers: `ctrl+a` through `ctrl+z`, `alt+a` through `alt+z` (`option+` is an alias for `alt+`), `shift+a` through `shift+z`, and `shift+tab`. `shift+j` and `J` address the same key.
|
- Modifiers: `ctrl+a` through `ctrl+z`, `alt+a` through `alt+z` (`option+` is an alias for `alt+`), `shift+a` through `shift+z`, and `shift+tab`. `shift+j` and `J` address the same key.
|
||||||
- Named punctuation: `minus`, `comma`, `ampersand`, `plus`, `backtick`. Each addresses the same binding as its literal character.
|
- Named punctuation: `minus`, `comma`, `ampersand`, `plus`, `backtick`. Each addresses the same binding as its literal character.
|
||||||
|
|
||||||
`ctrl+i` aliases `tab`, and `ctrl+m` aliases `enter`, including inside leader sequences. Ctrl+h and Ctrl+j are distinct control keys, not Backspace/Enter aliases. A single modifier on an ASCII letter is supported, plus Shift+Tab. Arbitrary non-leader key sequences, Cmd/Super, combined modifiers, other modified special keys, and Vim-style angle-bracket notation are rejected rather than silently treated as another key.
|
`ctrl+i` aliases `tab`, and `ctrl+m` aliases `enter`, including inside sequences. Ctrl+h and Ctrl+j are distinct control keys, not Backspace/Enter aliases. A single modifier on an ASCII letter is supported, plus Shift+Tab. Cmd/Super, combined modifiers, other modified special keys, and Vim-style angle-bracket notation are rejected rather than silently treated as another key.
|
||||||
|
|
||||||
Normal mode binds `ctrl+o` to `config.core.jump_back` and `ctrl+i`/`tab` to `config.core.jump_forward`. Insert-mode Tab inserts literal text outside snippets; inside one it moves to the next argument. Override or disable these keys with the ordinary per-mode binding interface; jump actions also appear in the command palette.
|
Normal mode binds `ctrl+o` to `config.core.jump_back` and `ctrl+i`/`tab` to `config.core.jump_forward`. Insert-mode Tab inserts literal text outside snippets; inside one it moves to the next argument. Override or disable these keys with the ordinary per-mode binding interface; jump actions also appear in the command palette.
|
||||||
|
|
||||||
Normal mode uses literal `_` and `|` for above/below and left/right splits, and `alt+h/j/k/l` for directional pane focus. Bind the produced punctuation rather than a physical Shift combination. Close actions have no default binding; for example, `config.bind "leader x", config.core.pane.close`. See [panes and splits](editing.md#panes-and-splits) for shared-buffer and unsaved-change policies.
|
Normal mode uses literal `_` and `|` for above/below and left/right splits, and `alt+h/j/k/l` for directional pane focus. Bind the produced punctuation rather than a physical Shift combination. Close actions have no default binding; for example, `config.bind "leader x", config.core.pane.close`. See [panes and splits](editing.md#panes-and-splits) for shared-buffer and unsaved-change policies.
|
||||||
|
|
||||||
The built-in `gt`/`gb` sequences move to the first/last line in Normal and Visual; `gf` follows Markdown file links in Normal only. This is not a configurable general sequence prefix: bind `config.core.document_start`, `config.core.document_end`, or `config.core.follow_link` to a single key or leader sequence to add another route. A configured or disabled `g` overrides the built-in prefix in that binding mode; choosing `g` as the leader overrides it in both modes.
|
The built-in `gt`/`gb` sequences move to the first/last line in Normal and Visual; `gf` follows Markdown file links in Normal only. Bind `config.core.document_start`, `config.core.document_end`, or `config.core.follow_link` to another key or sequence to add another route. A configured/disabled `g`, a configured sequence starting with `g`, or choosing `g` as the leader overrides the built-in prefix in that binding mode.
|
||||||
|
|
||||||
Normal `dd` and `cc` delete/change complete rows, with leading counts. Bind `config.core.delete_line` or `config.core.change_line` to a single key or leader sequence for another route. A configured/disabled `d` or `c`, or choosing that key as the leader, overrides its built-in doubled sequence. These are dedicated line actions, not general operator–motion grammar.
|
Normal `dd` and `cc` delete/change complete rows, with leading counts. Bind `config.core.delete_line` or `config.core.change_line` to another key or sequence for another route. A configured/disabled `d` or `c`, a configured sequence starting with that key, or choosing it as the leader overrides its built-in doubled sequence. These are dedicated line actions, not general operator–motion grammar.
|
||||||
|
|
||||||
`f<char>` / `F<char>` seeks forward/backward on the current line in Normal and Visual, with counts such as `2fx`. Rebind `config.core.seek_forward` / `config.core.seek_backward` to a single key or leader sequence; the action then waits for a literal target character. For example, `config.bind "s", config.core.seek_forward` makes `2sx` seek the second following `x`. Pass `mode: config.modes.visual` to install a Visual route. These native-only actions do not have Ex commands.
|
`f<char>` / `F<char>` seeks forward/backward on the current line in Normal and Visual, with counts such as `2fx`. Rebind `config.core.seek_forward` / `config.core.seek_backward` to a single key or leader sequence; the action then waits for a literal target character. For example, `config.bind "s", config.core.seek_forward` makes `2sx` seek the second following `x`. Pass `mode: config.modes.visual` to install a Visual route. These native-only actions do not have Ex commands.
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ Vamp.configure do |config|
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The last valid declaration for a key/mode wins. Invalid keys, incompatible action/mode pairs, ambiguous leader prefixes, or unknown command names leave that binding unchanged and appear in the status message; other valid bindings still load. An invalid leader key reports a notice and retains Space, while valid bindings still install. Ruby syntax errors, unknown methods, and type errors—including non-string or empty leader values—disable the entire configuration and fall back to defaults. One plugin's load failure does not prevent other plugins loading. Disabling a plugin removes its single-key and leader bindings, without restoring any default keys they replaced.
|
The last valid declaration for a key/mode wins. Invalid keys, incompatible action/mode pairs, ambiguous sequence prefixes, or unknown command names leave that binding unchanged and appear in the status message; other valid bindings still load. An invalid leader key reports a notice and retains Space, while valid bindings still install. Ruby syntax errors, unknown methods, and type errors—including non-string or empty leader values—disable the entire configuration and fall back to defaults. One plugin's load failure does not prevent other plugins loading. Disabling a plugin removes its single-key and sequence bindings, without restoring any default keys they replaced.
|
||||||
|
|
||||||
Bindings are resolved inside the editor module, not the terminal adapter. Bracketed paste remains literal and bypasses keybindings. If configuration disables your keys for leaving Insert/Command/Search/Visual mode or entering commands, restart with `--no-config` to restore defaults.
|
Bindings are resolved inside the editor module, not the terminal adapter. Bracketed paste remains literal and bypasses keybindings. If configuration disables your keys for leaving Insert/Command/Search/Visual mode or entering commands, restart with `--no-config` to restore defaults.
|
||||||
|
|
||||||
@@ -161,15 +161,26 @@ Ctrl+h is **not** a Backspace alias or default deletion key in any mode. Raw Ctr
|
|||||||
|
|
||||||
Ctrl+j is distinct from Enter: in the TUI's raw mode, LF is Ctrl+j and CR is Enter; CSI-u control events also work. No enhanced-terminal negotiation is required. Ctrl+m remains an Enter alias. Outside completion and selection lists, Ctrl+k still clones above in Normal/Visual, and Ctrl+j defaults to newline in Insert or submit in Command/Search. While completion is open, Ctrl+j/k cycles candidates; Enter always closes completion and inserts a newline. Enter and Ctrl+j can be rebound independently outside the completion cycle.
|
Ctrl+j is distinct from Enter: in the TUI's raw mode, LF is Ctrl+j and CR is Enter; CSI-u control events also work. No enhanced-terminal negotiation is required. Ctrl+m remains an Enter alias. Outside completion and selection lists, Ctrl+k still clones above in Normal/Visual, and Ctrl+j defaults to newline in Insert or submit in Command/Search. While completion is open, Ctrl+j/k cycles candidates; Enter always closes completion and inserts a newline. Enter and Ctrl+j can be rebound independently outside the completion cycle.
|
||||||
|
|
||||||
## Leader key
|
## Key sequences and leader
|
||||||
|
|
||||||
Space starts a leader sequence in Normal/Visual when it has no direct binding in that mode. Leader bindings use `leader` followed by whitespace-separated single-key names, for example `config.bind "leader b n", config.core.buffer_next`. Nested prefixes wait for the next key without a timeout. The status bar shows the actual pending sequence before the file path; the palette advertises effective sequences alongside ordinary bindings.
|
Space starts a leader sequence in Normal/Visual when it has no direct binding in that mode. Leader bindings use `leader` followed by whitespace-separated single-key names, for example `config.bind "leader b n", config.core.buffer_next`. Nested prefixes wait for the next key without a timeout. The status bar shows the actual pending sequence before the file path; the palette advertises effective sequences alongside ordinary bindings.
|
||||||
|
|
||||||
Set `config.leader = ","` to change the trigger for every default and configured leader route. Suffix keys stay unchanged: the default palette binding becomes comma then Space, save becomes comma then `w`, and buffer-next becomes comma then `b` then `n`. Choose an unused single key in the existing notation; Escape, ASCII digits, malformed keys, and a new trigger already directly bound in Normal/Visual are rejected without replacing the current choice. A direct binding on the current trigger—including `nil`—takes precedence per mode and hides unreachable leader routes from the palette.
|
Explicit prefixes use the same notation and do not change the leader:
|
||||||
|
|
||||||
Esc cancels a pending sequence and count without leaving Visual or collapsing carets. Unknown suffixes consume the key and cancel safely, never falling through to an edit. Paste, external command execution, buffer switching, and binding/leader changes clear pending state. A count before a leader motion applies normally; digits inside a sequence are literal suffix keys. Plugins execute once. Outside Normal/Visual, the leader has no prefix behavior, so Space still types normally in Insert and prompts.
|
```ruby
|
||||||
|
config.bind "ctrl+w h", config.core.pane.focus_left
|
||||||
|
config.bind "ctrl+w j", config.core.pane.focus_down
|
||||||
|
config.bind "ctrl+w k", config.core.pane.focus_up
|
||||||
|
config.bind "ctrl+w l", config.core.pane.focus_right
|
||||||
|
```
|
||||||
|
|
||||||
Leader bindings require Normal/Visual and the target's existing mode eligibility; Ex/plugin targets remain Normal-only. A sequence cannot be both a complete binding and another binding's prefix: ambiguous declarations are refused atomically. Disabled leaves remain reserved. Only leader-prefixed sequences are supported—not arbitrary multi-key mappings, timers, or macros.
|
Press Ctrl+w, release it, then press the direction key. Modified suffixes and deeper nesting also work, for example `"ctrl+w ctrl+j"` or `"ctrl+w p j"`. Explicit prefixes must be unbound in that mode and cannot be Escape, an ASCII digit, or the current leader; use `leader` for symbolic leader routes. The prefix remains reserved while any of its leaves exists, including disabled leaves. Tree/selection navigation overrides retain priority while exploring files; configured control/special-key prefixes can start there, and pending suffixes then belong to the sequence rather than tree navigation.
|
||||||
|
|
||||||
|
Set `config.leader = ","` to change the trigger for every default and configured leader route. Explicit prefixes do not move. Suffix keys stay unchanged: the default palette binding becomes comma then Space, save becomes comma then `w`, and buffer-next becomes comma then `b` then `n`. Choose an unused single key in the existing notation; Escape, ASCII digits, malformed keys, and a new trigger already bound or reserved as a sequence prefix in Normal/Visual are rejected without replacing the current choice. A direct binding on the current trigger—including `nil`—takes precedence per mode and hides unreachable leader routes from the palette.
|
||||||
|
|
||||||
|
Esc cancels a pending sequence and count without leaving Visual or collapsing carets. Unknown suffixes consume the key and cancel safely, never falling through to an edit. Paste, external command execution, buffer switching, and binding/leader changes clear pending state. A count before a sequence motion applies normally; digits inside a sequence are literal suffix keys. Plugins execute once. Outside Normal/Visual, sequences have no prefix behavior, so ordinary text still types normally in Insert and prompts.
|
||||||
|
|
||||||
|
Sequences require Normal/Visual and the target's existing mode eligibility; Ex/plugin targets remain Normal-only. A sequence cannot be both a complete binding and another binding's prefix: ambiguous declarations are refused atomically, including collisions between a single-key binding and an explicit prefix. Disabled leaves remain reserved. Sequences wait without a timeout; timers and macros are not supported.
|
||||||
|
|
||||||
## Native LSP
|
## Native LSP
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -316,9 +316,9 @@ Accepting a core action executes it in the underlying mode, including Visual edi
|
|||||||
|
|
||||||
Esc or Ctrl+p cancels without changing text, selections, the previous command/search prompt, or pending Insert history. The opening key can be remapped with `config.core.palette`; list navigation uses the shared configurable `selection` context. Other popup controls keep their defaults unless explicitly intercepted by a selection binding. The TUI uses a constraint-based modal panel, keeps the selected result visible, and prioritizes the query on tiny terminals.
|
Esc or Ctrl+p cancels without changing text, selections, the previous command/search prompt, or pending Insert history. The opening key can be remapped with `config.core.palette`; list navigation uses the shared configurable `selection` context. Other popup controls keep their defaults unless explicitly intercepted by a selection binding. The TUI uses a constraint-based modal panel, keeps the selected result visible, and prioritizes the query on tiny terminals.
|
||||||
|
|
||||||
## Leader sequences
|
## Key sequences
|
||||||
|
|
||||||
Space begins a leader sequence in Normal/Visual by default. Press the suffix keys in order; nested prefixes wait without a timeout. The status bar shows the pending sequence. Esc cancels the sequence and pending count without leaving Visual or collapsing carets. See [leader configuration](configuration.md#leader-key) for remapping and declaration rules.
|
Space begins a leader sequence in Normal/Visual by default. Explicit sequences such as `ctrl+w j` can coexist without changing that leader. Press the keys in order; nested prefixes wait without a timeout. The status bar shows the pending sequence. Esc cancels the sequence and pending count without leaving Visual or collapsing carets. See [sequence configuration](configuration.md#key-sequences-and-leader) for remapping and declaration rules.
|
||||||
|
|
||||||
## Jump history
|
## Jump history
|
||||||
|
|
||||||
|
|||||||
+337
-152
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -1592,10 +1592,10 @@ end
|
|||||||
let mut session =
|
let mut session =
|
||||||
EditorSession::open(Some(buffer.clone()), vec!["text".into()], None).unwrap();
|
EditorSession::open(Some(buffer.clone()), vec!["text".into()], None).unwrap();
|
||||||
session.handle(Input::Char(' '));
|
session.handle(Input::Char(' '));
|
||||||
assert_eq!(session.editor().leader_pending(), Some("Space"));
|
assert_eq!(session.editor().sequence_pending(), Some("Space"));
|
||||||
session.handle(Input::Char(' '));
|
session.handle(Input::Char(' '));
|
||||||
assert!(session.editor().palette().is_some());
|
assert!(session.editor().palette().is_some());
|
||||||
assert!(session.editor().leader_pending().is_none());
|
assert!(session.editor().sequence_pending().is_none());
|
||||||
session.handle(Input::Escape);
|
session.handle(Input::Escape);
|
||||||
session.handle(Input::Char('i'));
|
session.handle(Input::Char('i'));
|
||||||
session.handle(Input::Char('!'));
|
session.handle(Input::Char('!'));
|
||||||
@@ -1636,15 +1636,15 @@ end
|
|||||||
);
|
);
|
||||||
session.handle(Input::Char(','));
|
session.handle(Input::Char(','));
|
||||||
session.handle(Input::Char('z'));
|
session.handle(Input::Char('z'));
|
||||||
assert_eq!(session.editor().leader_pending(), Some(", z"));
|
assert_eq!(session.editor().sequence_pending(), Some(", z"));
|
||||||
assert_eq!(session.editor().lines(), &["alpha"]);
|
assert_eq!(session.editor().lines(), &["alpha"]);
|
||||||
session.handle(Input::Char('n'));
|
session.handle(Input::Char('n'));
|
||||||
assert_eq!(session.editor().lines(), &["1:buffer.txt:alpha"]);
|
assert_eq!(session.editor().lines(), &["1:buffer.txt:alpha"]);
|
||||||
assert!(session.editor().leader_pending().is_none());
|
assert!(session.editor().sequence_pending().is_none());
|
||||||
session.handle(Input::Char(','));
|
session.handle(Input::Char(','));
|
||||||
session.handle(Input::Char('z'));
|
session.handle(Input::Char('z'));
|
||||||
session.execute(&format!("edit {}", second.display()));
|
session.execute(&format!("edit {}", second.display()));
|
||||||
assert!(session.editor().leader_pending().is_none());
|
assert!(session.editor().sequence_pending().is_none());
|
||||||
for key in [',', 'z', 'n'] {
|
for key in [',', 'z', 'n'] {
|
||||||
session.handle(Input::Char(key));
|
session.handle(Input::Char(key));
|
||||||
}
|
}
|
||||||
@@ -1713,7 +1713,7 @@ end
|
|||||||
for key in ['3', ',', 'z', last] {
|
for key in ['3', ',', 'z', last] {
|
||||||
session.handle(Input::Char(key));
|
session.handle(Input::Char(key));
|
||||||
}
|
}
|
||||||
assert!(session.editor().leader_pending().is_none());
|
assert!(session.editor().sequence_pending().is_none());
|
||||||
assert_eq!(session.editor().lines(), &["abcdef"]);
|
assert_eq!(session.editor().lines(), &["abcdef"]);
|
||||||
session.handle(Input::Char('l'));
|
session.handle(Input::Char('l'));
|
||||||
}
|
}
|
||||||
@@ -1722,7 +1722,7 @@ end
|
|||||||
session.handle(Input::Char(key));
|
session.handle(Input::Char(key));
|
||||||
}
|
}
|
||||||
session.handle(Input::Paste("n:q!\n".into()));
|
session.handle(Input::Paste("n:q!\n".into()));
|
||||||
assert!(session.editor().leader_pending().is_none());
|
assert!(session.editor().sequence_pending().is_none());
|
||||||
session.handle(Input::Char('l'));
|
session.handle(Input::Char('l'));
|
||||||
assert_eq!(session.editor().cursor().column, 3);
|
assert_eq!(session.editor().cursor().column, 3);
|
||||||
assert_eq!(session.editor().lines(), &["abcdef"]);
|
assert_eq!(session.editor().lines(), &["abcdef"]);
|
||||||
@@ -1734,7 +1734,7 @@ end
|
|||||||
session.handle(Input::Escape);
|
session.handle(Input::Escape);
|
||||||
assert_eq!(session.editor().mode(), crate::editor::Mode::Visual);
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Visual);
|
||||||
assert_eq!(session.editor().selections(), selected);
|
assert_eq!(session.editor().selections(), selected);
|
||||||
assert!(session.editor().leader_pending().is_none());
|
assert!(session.editor().sequence_pending().is_none());
|
||||||
session.handle(Input::Char('h'));
|
session.handle(Input::Char('h'));
|
||||||
assert_eq!(session.editor().cursor().column, 2);
|
assert_eq!(session.editor().cursor().column, 2);
|
||||||
assert_eq!(session.editor().lines(), &["abcdef"]);
|
assert_eq!(session.editor().lines(), &["abcdef"]);
|
||||||
@@ -1767,7 +1767,7 @@ end
|
|||||||
session.handle(Input::Ctrl('f'));
|
session.handle(Input::Ctrl('f'));
|
||||||
assert_eq!(session.editor().cursor().column, 1);
|
assert_eq!(session.editor().cursor().column, 1);
|
||||||
session.handle(Input::Char(' '));
|
session.handle(Input::Char(' '));
|
||||||
assert_eq!(session.editor().leader_pending(), Some("Space"));
|
assert_eq!(session.editor().sequence_pending(), Some("Space"));
|
||||||
session.handle(Input::Char('z'));
|
session.handle(Input::Char('z'));
|
||||||
session.handle(Input::Char('n'));
|
session.handle(Input::Char('n'));
|
||||||
assert_eq!(session.editor().lines(), &["healthy "]);
|
assert_eq!(session.editor().lines(), &["healthy "]);
|
||||||
@@ -2183,7 +2183,7 @@ end
|
|||||||
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
session.handle(Input::Char(' '));
|
session.handle(Input::Char(' '));
|
||||||
assert_eq!(session.editor().mode(), crate::editor::Mode::Command);
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Command);
|
||||||
assert!(session.editor().leader_pending().is_none());
|
assert!(session.editor().sequence_pending().is_none());
|
||||||
for ch in "w界".chars() {
|
for ch in "w界".chars() {
|
||||||
session.handle(Input::Char(ch));
|
session.handle(Input::Char(ch));
|
||||||
}
|
}
|
||||||
@@ -2867,7 +2867,7 @@ end
|
|||||||
assert_eq!(session.editor().lines(), &[String::new()], "{expression}");
|
assert_eq!(session.editor().lines(), &[String::new()], "{expression}");
|
||||||
session.handle(Input::Char(' '));
|
session.handle(Input::Char(' '));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
session.editor().leader_pending(),
|
session.editor().sequence_pending(),
|
||||||
Some("Space"),
|
Some("Space"),
|
||||||
"{expression}"
|
"{expression}"
|
||||||
);
|
);
|
||||||
|
|||||||
+32
-1
@@ -1432,7 +1432,7 @@ fn draw_status(frame: &mut Frame, editor: &Editor, syntax: &SyntaxCache, status_
|
|||||||
} else {
|
} else {
|
||||||
format!(" {}", editor.mode())
|
format!(" {}", editor.mode())
|
||||||
};
|
};
|
||||||
if let Some(sequence) = editor.leader_pending() {
|
if let Some(sequence) = editor.sequence_pending() {
|
||||||
write!(status, " leader: {sequence}").unwrap();
|
write!(status, " leader: {sequence}").unwrap();
|
||||||
}
|
}
|
||||||
if editor.g_pending() {
|
if editor.g_pending() {
|
||||||
@@ -5270,6 +5270,37 @@ mod tests {
|
|||||||
terminal.get_cursor_position().unwrap()
|
terminal.get_cursor_position().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modified_sequences_navigate_diff_files_and_readonly_panes() {
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor.handle(Input::Char('|'));
|
||||||
|
install_readonly_diff(
|
||||||
|
&mut editor,
|
||||||
|
syntax_diff_file("history.txt", "before", "after"),
|
||||||
|
);
|
||||||
|
editor
|
||||||
|
.bind("normal", "ctrl+w h", Some("core.pane.focus_left".into()))
|
||||||
|
.unwrap();
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
|
assert_eq!(editor.diff().unwrap().focus, Focus::Files);
|
||||||
|
editor.handle(Input::Ctrl('w'));
|
||||||
|
assert_eq!(editor.sequence_pending(), Some("Ctrl+w"));
|
||||||
|
assert!(editor.diff().unwrap().files_visible);
|
||||||
|
editor.handle(Input::Char('h'));
|
||||||
|
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||||
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
|
editor.handle(Input::Ctrl('w'));
|
||||||
|
editor.handle(Input::Char('h'));
|
||||||
|
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||||
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
|
editor.handle(Input::Ctrl('w'));
|
||||||
|
editor.handle(Input::Char('h'));
|
||||||
|
assert_eq!(editor.pane_id(), 1);
|
||||||
|
assert_eq!(editor.sequence_pending(), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wrap_toggle_preserves_caret_screen_row_in_normal_buffers() {
|
fn wrap_toggle_preserves_caret_screen_row_in_normal_buffers() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user