Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aaaa3875aa | |||
| 2756b79f89 | |||
| 503342bff0 | |||
| f595455479 | |||
| ad9b214b8f |
@@ -26,6 +26,8 @@ Vamp starts in Normal mode. Press `i`, type text, then Esc to return to Normal.
|
|||||||
| Edit completion snippet arguments | Type to replace the highlighted argument; Tab / Shift+Tab moves next/previous |
|
| Edit completion snippet arguments | Type to replace the highlighted argument; Tab / Shift+Tab moves next/previous |
|
||||||
| Save in Normal | Space then `w`, or `:w` then Enter |
|
| Save in Normal | Space then `w`, or `:w` then Enter |
|
||||||
| Undo/redo in Normal | `u` / `U` (Shift+u) |
|
| Undo/redo in Normal | `u` / `U` (Shift+u) |
|
||||||
|
| Split panes in Normal | `_` above/below; `\|` left/right; Option/Alt+h/j/k/l changes focus |
|
||||||
|
| Close a pane | `:pane.close`; `:pane.force_close` explicitly discards exclusively displayed unsaved edits |
|
||||||
| Delete/change whole lines in Normal | `dd` / `cc`; leading counts work (`3dd`, `2cc`) |
|
| Delete/change whole lines in Normal | `dd` / `cc`; leading counts work (`3dd`, `2cc`) |
|
||||||
| Search in Normal | `/`, type a literal query, Enter; `n`/`N` for next/previous |
|
| Search in Normal | `/`, type a literal query, Enter; `n`/`N` for next/previous |
|
||||||
| Select/copy/cut | `v` characterwise or `V` whole rows, move to extend, then `y`/`d` in Visual |
|
| Select/copy/cut | `v` characterwise or `V` whole rows, move to extend, then `y`/`d` in Visual |
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
- [x] navigation control/actions: seek occurrence of character ('f' forwards, 'F' backwards by default; allow e.g. `2f` for seeking second occurrence instead of first).
|
- [x] navigation control/actions: seek occurrence of character ('f' forwards, 'F' backwards by default; allow e.g. `2f` for seeking second occurrence instead of first).
|
||||||
- [x] buffer ux improvement: enable mouse scrolling sideways so longer lines are easier to follow and read (e.g. in case they extend beyond the width of the buffer view).
|
- [x] buffer ux improvement: enable mouse scrolling sideways so longer lines are easier to follow and read (e.g. in case they extend beyond the width of the buffer view).
|
||||||
- [x] buffer ux improvement: add toggleable word wrap (no keybinding by default).
|
- [x] buffer ux improvement: add toggleable word wrap (no keybinding by default).
|
||||||
|
- [x] add panes/splits: split current pane above/below (`_`) or left/right (`|`) in Normal mode; navigate panes with Option/Alt+h/j/k/l.
|
||||||
|
- shared split layout and directional focus with diff views; independent pane views, shared buffers, and guarded close/force-close actions.
|
||||||
|
- [ ] remove diff viewer's file explorer's `A added M modified...` explanation - it's standard and the explanation doesn't fit in the pane, so it can go.
|
||||||
- [ ] add tabs: tab support, editor action to open a buffer in a new tab, default tab navigation is `ctrl+option+h/l`, close tab action.
|
- [ ] add tabs: tab support, editor action to open a buffer in a new tab, default tab navigation is `ctrl+option+h/l`, close tab action.
|
||||||
- [ ] diff view: add character-wise change highlight (enabled by default).
|
- [ ] diff view: add character-wise change highlight (enabled by default).
|
||||||
- [ ] diff view: lsp actions should act as they normally would. if an lsp action would open another file, then the file should be opened in another tab outside the diff view - if the referenced file/symbol is part of the changes in the diff, it should just be opened in the diff view.
|
- [ ] diff view: lsp actions should act as they normally would. if an lsp action would open another file, then the file should be opened in another tab outside the diff view - if the referenced file/symbol is part of the changes in the diff, it should just be opened in the diff view.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -40,8 +40,8 @@ Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the asc
|
|||||||
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
||||||
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
||||||
Frontends borrow `semantic_tokens()` for sorted, single-line grapheme spans from the active buffer. Immutable `Arc` slices are shared by native LSP and buffer storage; painting does not copy tokens. Text/path changes discard overlays, and native updates validate the whole applicable annotation batch before mutation. The TUI merges semantic styling into its existing glyph pass above syntax foregrounds but below diagnostic/search/selection/caret styling.
|
Frontends borrow `semantic_tokens()` for sorted, single-line grapheme spans from the active buffer. Immutable `Arc` slices are shared by native LSP and buffer storage; painting does not copy tokens. Text/path changes discard overlays, and native updates validate the whole applicable annotation batch before mutation. The TUI merges semantic styling into its existing glyph pass above syntax foregrounds but below diagnostic/search/selection/caret styling.
|
||||||
Frontends inspect `hover()` for optional read-only content and logical-line/grapheme scroll offsets. Popup request eligibility, dismissal, and scrolling stay editor/session-owned; the TUI clips the panel inside the document workspace without changing the primary cursor or viewport. Its hover-local syntax cache treats content as Markdown, including named fenced-code injections, and rebuilds only when content changes. The popup shares the document syntax palette and preserves raw Markdown text and logical scroll coordinates.
|
Frontends inspect `hover()` for optional read-only content and logical-line/grapheme scroll offsets. Popup request eligibility, dismissal, and scrolling stay editor/session-owned; the TUI clips the panel inside the full editor area without changing the primary cursor or viewport. Its hover-local syntax cache treats content as Markdown, including named fenced-code injections, and rebuilds only when content changes. The popup shares the document syntax palette and preserves raw Markdown text and logical scroll coordinates.
|
||||||
Frontends borrow `completion()` for the read-only item list and selected index: zero is Original text, and other indices select `items[selected - 1]`. The editor owns the immutable original document/carets, atomic candidate transactions, cycling/dismissal, pending Insert undo, and final-only repeat recording. It changes only the primary completion target and translates secondary carets through all edits. The TUI only places a windowed list near the current caret inside the document text workspace, sanitizes/clips labels and detail by grapheme, and preserves gutter, explorer, and hardware cursor ownership; wheel input over the list is consumed without dispatching edits.
|
Frontends borrow `completion()` for the read-only item list and selected index: zero is Original text, and other indices select `items[selected - 1]`. The editor owns the immutable original document/carets, atomic candidate transactions, cycling/dismissal, pending Insert undo, and final-only repeat recording. It changes only the primary completion target and translates secondary carets through all edits. The TUI places a windowed list near the current caret, bounded by the full editor rather than its pane, sanitizes/clips labels and detail by grapheme, and preserves hardware cursor ownership; wheel input over the list is consumed without dispatching edits.
|
||||||
Frontends borrow `snippet_selections()` for the active argument's end-exclusive grapheme ranges, including mirrors and empty tabstops. The editor caches those ranges after previews, argument edits, and navigation; rendering adds no snippet allocation. Template edits stay primary-only, translate other carets, and fold resolved replacement text and caret placement into the pending completion repeat step. Per-occurrence ancestry distinguishes nested arguments from adjacent empty stops, allowing enclosing replacements to retire only overwritten descendants.
|
Frontends borrow `snippet_selections()` for the active argument's end-exclusive grapheme ranges, including mirrors and empty tabstops. The editor caches those ranges after previews, argument edits, and navigation; rendering adds no snippet allocation. Template edits stay primary-only, translate other carets, and fold resolved replacement text and caret placement into the pending completion repeat step. Per-occurrence ancestry distinguishes nested arguments from adjacent empty stops, allowing enclosing replacements to retire only overwritten descendants.
|
||||||
|
|
||||||
## Terminal lifecycle and layout
|
## Terminal lifecycle and layout
|
||||||
|
|||||||
+25
-11
@@ -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.
|
||||||
|
|
||||||
@@ -57,16 +57,18 @@ For command-capable actions, canonical Ex names derive automatically from the ac
|
|||||||
Key notation follows [Herdr's readable style](https://herdr.dev/docs/configuration/#keybindings). Literal non-control Unicode characters are case-sensitive (`n`, `J`, `+`); named keys and modifiers are case-insensitive:
|
Key notation follows [Herdr's readable style](https://herdr.dev/docs/configuration/#keybindings). Literal non-control Unicode characters are case-sensitive (`n`, `J`, `+`); named keys and modifiers are case-insensitive:
|
||||||
|
|
||||||
- Named keys: `esc`, `enter`, `backspace`, `delete`, `left`, `right`, `up`, `down`, `home`, `end`, `tab`, `space`.
|
- Named keys: `esc`, `enter`, `backspace`, `delete`, `left`, `right`, `up`, `down`, `home`, `end`, `tab`, `space`.
|
||||||
- Modifiers: `ctrl+a` through `ctrl+z`, `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, Alt/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.
|
||||||
|
|
||||||
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.
|
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 `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.
|
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 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.
|
||||||
|
|
||||||
@@ -84,6 +86,7 @@ Normal `dd` and `cc` delete/change complete rows, with leading counts. Bind `con
|
|||||||
| Normal | `diff` |
|
| Normal | `diff` |
|
||||||
| Normal, Insert, Visual, Command, Search | `diff.close` |
|
| Normal, Insert, Visual, Command, Search | `diff.close` |
|
||||||
| Normal, Insert, Visual, Command, Search | `word_wrap.toggle` |
|
| Normal, Insert, Visual, Command, Search | `word_wrap.toggle` |
|
||||||
|
| Normal | `pane.split_horizontal`, `pane.split_vertical`, `pane.focus_left`, `pane.focus_down`, `pane.focus_up`, `pane.focus_right`, `pane.close`, `pane.force_close` |
|
||||||
| Normal | `jump_back`, `jump_forward` |
|
| Normal | `jump_back`, `jump_forward` |
|
||||||
| Normal | `lsp.definition`, `lsp.diagnostics`, `lsp.hover` |
|
| Normal | `lsp.definition`, `lsp.diagnostics`, `lsp.hover` |
|
||||||
| Insert | `lsp.completion`, `snippet.next`, `snippet.previous` |
|
| Insert | `lsp.completion`, `snippet.next`, `snippet.previous` |
|
||||||
@@ -108,7 +111,7 @@ Normal `dd` and `cc` delete/change complete rows, with leading counts. Bind `con
|
|||||||
|
|
||||||
`core.select` (`v`) and `core.select_linewise` (`V`) select characterwise and whole-row Visual shapes. Switching selectors retains endpoints; pressing the active shape's selector again cancels. Both use `config.modes.visual`; there is no separate linewise binding mode. `core.select_lines` (Visual Ctrl+l) still splits touched rows into Normal carets.
|
`core.select` (`v`) and `core.select_linewise` (`V`) select characterwise and whole-row Visual shapes. Switching selectors retains endpoints; pressing the active shape's selector again cancels. Both use `config.modes.visual`; there is no separate linewise binding mode. `core.select_lines` (Visual Ctrl+l) still splits touched rows into Normal carets.
|
||||||
|
|
||||||
`core.save` saves the current path; `core.save_and_quit` saves the active buffer and quits only if no buffer remains dirty. `core.quit` refuses unsaved changes in any buffer. `core.edit` and `core.buffer` open prefilled Ex prompts for their required path or buffer ID; deletion actions target the current buffer. Configured `core.force_quit` and `core.buffer_force_delete` keys act immediately, without confirmation.
|
`core.save` saves the current path; `core.save_and_quit` saves the active buffer and quits only if no buffer remains dirty. `core.quit` refuses unsaved changes in any buffer. `core.edit` and `core.buffer` open prefilled Ex prompts for their required path or buffer ID; deletion actions target the current buffer. Configured `core.force_quit`, `core.buffer_force_delete`, and `core.pane.force_close` keys act immediately, without confirmation.
|
||||||
|
|
||||||
## Binding overrides and failures
|
## Binding overrides and failures
|
||||||
|
|
||||||
@@ -123,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.
|
||||||
|
|
||||||
@@ -158,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
|
||||||
|
|
||||||
|
|||||||
+38
-7
@@ -15,6 +15,8 @@ Default controls and editing behavior. For custom bindings, see [configuration](
|
|||||||
| Normal | `i` insert before each caret; `a` insert after each caret; `o`/`O` open lines below/above |
|
| Normal | `i` insert before each caret; `a` insert after each caret; `o`/`O` open lines below/above |
|
||||||
| Normal, Visual | `I` (Shift+i) / `A` (Shift+a) insert at first nonblank / line end; Visual splits each touched row into a caret first |
|
| Normal, Visual | `I` (Shift+i) / `A` (Shift+a) insert at first nonblank / line end; Visual splits each touched row into a caret first |
|
||||||
| Normal | `h`/`j`/`k`/`l` or arrows move every caret; `0`/`$` or Home/End |
|
| Normal | `h`/`j`/`k`/`l` or arrows move every caret; `0`/`$` or Home/End |
|
||||||
|
| Normal | `_` splits above/below; `\|` splits left/right; the new pane inherits the current buffer and view |
|
||||||
|
| Normal | Option/Alt+h/j/k/l focuses the neighboring pane left/down/up/right |
|
||||||
| Normal | `w`/`b`/`e` move every caret to next word start, previous/current word start, word end; `W`/`B`/`E` for whitespace-delimited WORDs |
|
| Normal | `w`/`b`/`e` move every caret to next word start, previous/current word start, word end; `W`/`B`/`E` for whitespace-delimited WORDs |
|
||||||
| Normal, Visual | `f<char>` seeks forward on the current line; `F<char>` seeks backward; `2fx` finds the second `x` after each caret |
|
| Normal, Visual | `f<char>` seeks forward on the current line; `F<char>` seeks backward; `2fx` finds the second `x` after each caret |
|
||||||
| Normal | `gf` follows a Markdown file link or heading fragment at the primary caret |
|
| Normal | `gf` follows a Markdown file link or heading fragment at the primary caret |
|
||||||
@@ -168,15 +170,44 @@ Vertical wheel input over a hover popup or floating/docked file explorer scrolls
|
|||||||
|
|
||||||
## Word wrap
|
## Word wrap
|
||||||
|
|
||||||
Use `:word_wrap.toggle`, the **Toggle word wrap** palette action, or `config.core.word_wrap.toggle` in a custom binding. Wrapping defaults to **off** for new buffers and has **no default keybinding**. Each ordinary buffer retains its own setting; toggling does not edit text or enter undo history.
|
Use `:word_wrap.toggle`, the **Toggle word wrap** palette action, or `config.core.word_wrap.toggle` in a custom binding. Wrapping defaults to **off** for new buffers and has **no default keybinding**. Each ordinary pane retains its own setting for each buffer; toggling does not edit text or enter undo history.
|
||||||
|
|
||||||
Toggling preserves the source caret and anchors a visible caret to its previous screen row in ordinary and diff buffers. File boundaries clamp the anchor when there are not enough preceding rows. Horizontal position follows the reflowed text.
|
Toggling preserves the source caret and anchors a visible caret to its previous screen row in ordinary and diff buffers. File boundaries clamp the anchor when there are not enough preceding rows. Horizontal position follows the reflowed text.
|
||||||
|
|
||||||
Text wraps at word boundaries where possible, with grapheme-safe breaks for long words. Whitespace is preserved; tabs keep their source-column stops and wide/combining Unicode characters retain their geometry. Only the first display segment shows a line number. Syntax and semantic colors, diagnostics, search matches, selections, and cursors follow the wrapped text. Motions and counts still use source lines, so `j`/`k`, Home/End, and saved file contents are unchanged. Vertical wheel input scrolls continuation rows; switching wrapping off restores horizontal scrolling.
|
Text wraps at word boundaries where possible, with grapheme-safe breaks for long words. Whitespace is preserved; tabs keep their source-column stops and wide/combining Unicode characters retain their geometry. Only the first display segment shows a line number. Syntax and semantic colors, diagnostics, search matches, selections, and cursors follow the wrapped text. Motions and counts still use source lines, so `j`/`k`, Home/End, and saved file contents are unchanged. Vertical wheel input scrolls continuation rows; switching wrapping off restores horizontal scrolling.
|
||||||
|
|
||||||
|
## Panes and splits
|
||||||
|
|
||||||
|
Splits can be nested in either direction. The new pane receives focus. Panes showing the same buffer share text, saved state, diagnostics, and undo/redo history, but retain independent carets, selections, modes, searches, and vertical/horizontal viewports. Buffer navigation changes only the focused pane. A file is still loaded once, even when displayed in several panes.
|
||||||
|
|
||||||
|
| Canonical command | Typed action | Behavior |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `:pane.split_horizontal` | `config.core.pane.split_horizontal` | Split above/below (`_` in Normal) |
|
||||||
|
| `:pane.split_vertical` | `config.core.pane.split_vertical` | Split left/right (`\|` in Normal) |
|
||||||
|
| `:pane.focus_left` / `:pane.focus_down` / `:pane.focus_up` / `:pane.focus_right` | Corresponding `config.core.pane.focus_*` action | Directional focus (Option/Alt+h/j/k/l in Normal) |
|
||||||
|
| `:pane.close` | `config.core.pane.close` | Close the focused pane and its exclusively displayed buffer, subject to unsaved-change protection |
|
||||||
|
| `:pane.force_close` | `config.core.pane.force_close` | Explicitly discard an exclusively displayed buffer's changes and close its pane |
|
||||||
|
|
||||||
|
Closing a pane gives its space to its sibling:
|
||||||
|
|
||||||
|
| Situation | Close pane | Force close pane |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Clean buffer displayed only here | Close pane and buffer | Same |
|
||||||
|
| Dirty buffer displayed only here | Refuse; save or force-close explicitly | Discard edits and close pane and buffer |
|
||||||
|
| Buffer also displayed in another pane | Close only this pane; retain the buffer and edits | Same |
|
||||||
|
| Last workspace pane | Quit only if every open buffer is clean, including hidden buffers | Quit without saving |
|
||||||
|
|
||||||
|
There is no default close-pane binding. Option must send Alt/Meta in the terminal; on macOS, configure the terminal's Option-key behavior if it instead produces special characters. These bindings do not replace existing Ctrl+j or uppercase motion/edit bindings.
|
||||||
|
|
||||||
|
Pane-focus bindings also work while the ordinary file explorer or picker has focus. Moving to a neighboring pane returns keyboard focus to its document; a floating picker closes, while a docked explorer remains visible. Explicit `tree` or `selection` navigation overrides and disabled keys take precedence over pane-focus bindings.
|
||||||
|
|
||||||
|
Floating windows overlay the entire editor, above all workspace panes. File pickers, revision selectors, hover documentation, and completion lists are not clipped to the focused pane; completion remains anchored to its caret. Docked exploration reserves space at the editor edge before the remaining workspace is split into panes. The changed-file explorer follows the same layout, on the editor's right edge.
|
||||||
|
|
||||||
|
Diff comparisons reuse the split geometry and directional-focus engine. Option/Alt navigation moves between Before, After, and Files first, then to a neighboring workspace pane when there is no internal target in that direction. Splitting a diff text view creates an ordinary sibling showing the same buffer; historical snapshots remain read-only, including after the comparison closes.
|
||||||
|
|
||||||
## Buffers
|
## Buffers
|
||||||
|
|
||||||
One buffer is visible at a time. Each retains its text, path, saved state, directed selections and primary, mode, search state, and undo/redo history. The TUI retains separate vertical/horizontal scroll offsets by stable buffer ID. Caret-following adjusts them as needed; mouse-wheel scrolling can temporarily detach the view from the caret. Clipboard fragments, bindings, plugin hosts, and plugin retirement remain session-wide. Plugin context and effects refer to the active buffer.
|
Each buffer retains its shared text, path, saved state, and undo/redo history. View state is retained separately for each pane and buffer. Caret-following adjusts that view's offsets as needed; mouse-wheel scrolling can temporarily detach it from the caret. Clipboard fragments, bindings, plugin hosts, and plugin retirement remain session-wide. Plugin context and effects refer to the focused pane's active buffer.
|
||||||
|
|
||||||
| Command | Behavior |
|
| Command | Behavior |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -189,7 +220,7 @@ One buffer is visible at a time. Each retains its text, path, saved state, direc
|
|||||||
|
|
||||||
IDs start at 1 and are never renumbered or reused. Closing the last buffer creates a fresh unnamed clean buffer rather than quitting. Closing the active buffer selects the next remaining ID, with wraparound. Switching does not save, discard, or reload hidden edits. Existing-file aliases resolve to the same loaded buffer; the supplied path spelling remains the save/display path, so opening through a symlink does not bypass save rejection. Paths are relative to the process working directory; the entire `:edit` argument is the path, including spaces, with no shell expansion.
|
IDs start at 1 and are never renumbered or reused. Closing the last buffer creates a fresh unnamed clean buffer rather than quitting. Closing the active buffer selects the next remaining ID, with wraparound. Switching does not save, discard, or reload hidden edits. Existing-file aliases resolve to the same loaded buffer; the supplied path spelling remains the save/display path, so opening through a symlink does not bypass save rejection. Paths are relative to the process working directory; the entire `:edit` argument is the path, including spaces, with no shell expansion.
|
||||||
|
|
||||||
Switching from Insert commits its pending undo group and parks that buffer in Normal. Explicit buffer switching from Visual retains that buffer's directed ranges and Visual mode; switching back restores them. Command/search prompts, palette queries, and pending motion counts are transient, not restored per buffer. The new commands appear in the command palette and can be bound by exact command name in Normal mode. When several buffers are open, status shows the active ID and total count.
|
Switching from Insert commits its pending undo group and parks that view in Normal. Explicit buffer switching from Visual retains the pane's directed ranges and Visual mode for that buffer; switching back restores them. Command/search prompts, palette queries, and pending motion counts are transient, not restored per buffer. Buffer commands appear in the command palette and can be bound by their typed core actions in Normal mode. When several buffers are open, status shows the active ID and total count.
|
||||||
|
|
||||||
## File picker and explorer
|
## File picker and explorer
|
||||||
|
|
||||||
@@ -247,7 +278,7 @@ Comparison discovery builds the changed-path/status catalog first. File text, no
|
|||||||
|
|
||||||
The changed-file explorer is **docked on the right**. Modified files show Before on the left and After on the right. Added files show only After, using the full text workspace; deleted files show only Before. Rename-only files show one empty pane, with the old and new paths in the explorer; renames with text or final-newline changes retain the split view. Before and historical/remote After snapshots are read-only. Selecting **Working checkout** as After binds eligible regular UTF-8 files to normal editor buffers, reusing already-open dirty buffers and their undo history. Editable After supports native edits, completion, undo/redo, and `:w`. Removed files, rename-only files, binary files, symlinks, and submodules remain read-only. Closing the diff keeps its buffers and unsaved edits.
|
The changed-file explorer is **docked on the right**. Modified files show Before on the left and After on the right. Added files show only After, using the full text workspace; deleted files show only Before. Rename-only files show one empty pane, with the old and new paths in the explorer; renames with text or final-newline changes retain the split view. Before and historical/remote After snapshots are read-only. Selecting **Working checkout** as After binds eligible regular UTF-8 files to normal editor buffers, reusing already-open dirty buffers and their undo history. Editable After supports native edits, completion, undo/redo, and `:w`. Removed files, rename-only files, binary files, symlinks, and submodules remain read-only. Closing the diff keeps its buffers and unsaved edits.
|
||||||
|
|
||||||
Text panes use normal buffer interaction regardless of writability: motions and counts, search, Visual/Visual Line selections, multiple carets, yank, remapped bindings, and non-mutating actions work in Before and After. Read-only snapshots reject insert/delete/change/paste, repeat edits, undo/redo, mutating plugin effects, and saves, including saves to another path. Their text retains the revision's final newline. Snapshots use separate virtual LSP document identities and do not appear in ordinary buffer lists or dirty-buffer comparisons. Closing the diff disposes snapshots and restores the live or original buffer; explicit native navigation to an ordinary buffer leaves the comparison at that destination.
|
Text panes use normal buffer interaction regardless of writability: motions and counts, search, Visual/Visual Line selections, multiple carets, yank, remapped bindings, and non-mutating actions work in Before and After. Read-only snapshots reject insert/delete/change/paste, repeat edits, undo/redo, mutating plugin effects, and saves, including saves to another path. Their text retains the revision's final newline. Snapshots use separate virtual LSP document identities and do not appear in ordinary buffer lists or dirty-buffer comparisons. Closing the diff disposes unreferenced snapshots and restores the live or original buffer in its workspace pane; snapshots displayed by other panes remain read-only until closed. Explicit native navigation from the comparison pane to an ordinary buffer leaves the comparison at that destination.
|
||||||
|
|
||||||
Use the ordinary **file-picker/file-explorer actions and bindings**: Space `o` opens a floating picker, Space `e` opens/focuses the docked explorer, and Ctrl+w closes the focused picker/explorer and reclaims its width. Remapped bindings work too; Ctrl+e is no longer reserved by the diff. The changed-path catalog retains A/M/D/R markers and the normal compact tree, filtering, folder expansion, and configurable `tree`/`selection` navigation. It includes removed and historical paths without scanning the working directory. Enter toggles a folder or opens the selected comparison file and focuses its first visible text pane: After for added files, otherwise Before. While the explorer is hidden, Tab/Shift+Tab cycles between visible text panes only; a single-pane view keeps its focus.
|
Use the ordinary **file-picker/file-explorer actions and bindings**: Space `o` opens a floating picker, Space `e` opens/focuses the docked explorer, and Ctrl+w closes the focused picker/explorer and reclaims its width. Remapped bindings work too; Ctrl+e is no longer reserved by the diff. The changed-path catalog retains A/M/D/R markers and the normal compact tree, filtering, folder expansion, and configurable `tree`/`selection` navigation. It includes removed and historical paths without scanning the working directory. Enter toggles a folder or opens the selected comparison file and focuses its first visible text pane: After for added files, otherwise Before. While the explorer is hidden, Tab/Shift+Tab cycles between visible text panes only; a single-pane view keeps its focus.
|
||||||
|
|
||||||
@@ -281,13 +312,13 @@ Ctrl+p opens a searchable overlay without leaving the current mode. It lists eac
|
|||||||
|
|
||||||
Search is case-insensitive: every whitespace-separated query token must occur in the action/command name, canonical Ex name, a shorthand alias, human-readable label, or owner. Aliases find the same action rather than creating duplicate entries. Results have a stable label/name order. Ctrl+j/Down/Ctrl+n select the next result; Ctrl+k/Up select the previous, with wraparound. Backspace removes a whole grapheme. Enter with no match or an ineligible selection leaves the palette open. Bracketed paste is ignored while the palette is open.
|
Search is case-insensitive: every whitespace-separated query token must occur in the action/command name, canonical Ex name, a shorthand alias, human-readable label, or owner. Aliases find the same action rather than creating duplicate entries. Results have a stable label/name order. Ctrl+j/Down/Ctrl+n select the next result; Ctrl+k/Up select the previous, with wraparound. Backspace removes a whole grapheme. Enter with no match or an ineligible selection leaves the palette open. Bracketed paste is ignored while the palette is open.
|
||||||
|
|
||||||
Accepting a core action executes it in the underlying mode, including Visual edits and Insert-mode actions. `core.edit` and `core.buffer` instead open prefilled Ex prompts for their required arguments. The destructive `core.force_quit` and `core.buffer_force_delete` actions also open canonical Ex prompts, requiring another Enter before discarding changes. Accepting a plugin command is available only in Normal mode and opens a prefilled `:` prompt; add arguments if needed, then Enter executes through the usual session dispatch.
|
Accepting a core action executes it in the underlying mode, including Visual edits and Insert-mode actions. `core.edit` and `core.buffer` instead open prefilled Ex prompts for their required arguments. The destructive `core.force_quit`, `core.buffer_force_delete`, and `core.pane.force_close` actions also open canonical Ex prompts, requiring another Enter before discarding changes. Accepting a plugin command is available only in Normal mode and opens a prefilled `:` prompt; add arguments if needed, then Enter executes through the usual session dispatch.
|
||||||
|
|
||||||
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 keys before the file path, without a prefix label. 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
|
||||||
|
|
||||||
|
|||||||
+1941
-588
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ pub mod files;
|
|||||||
pub mod lsp;
|
pub mod lsp;
|
||||||
mod lsp_config;
|
mod lsp_config;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
|
pub mod panes;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod repository;
|
pub mod repository;
|
||||||
pub mod ruby;
|
pub mod ruby;
|
||||||
|
|||||||
+374
@@ -0,0 +1,374 @@
|
|||||||
|
//! Frontend-independent binary pane layout and spatial focus navigation.
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum SplitAxis {
|
||||||
|
/// Above/below.
|
||||||
|
Horizontal,
|
||||||
|
/// Left/right.
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum PaneDirection {
|
||||||
|
Left,
|
||||||
|
Down,
|
||||||
|
Up,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct PaneRect {
|
||||||
|
pub x: u16,
|
||||||
|
pub y: u16,
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaneRect {
|
||||||
|
fn is_empty(self) -> bool {
|
||||||
|
self.width == 0 || self.height == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Partition a rectangle into balanced halves, with the odd cell in the second.
|
||||||
|
pub fn split_rect(bounds: PaneRect, axis: SplitAxis) -> [PaneRect; 2] {
|
||||||
|
let mut first = bounds;
|
||||||
|
let mut second = bounds;
|
||||||
|
match axis {
|
||||||
|
SplitAxis::Horizontal => {
|
||||||
|
first.height = bounds.height / 2;
|
||||||
|
second.y = bounds.y.saturating_add(first.height);
|
||||||
|
second.height -= first.height;
|
||||||
|
}
|
||||||
|
SplitAxis::Vertical => {
|
||||||
|
first.width = bounds.width / 2;
|
||||||
|
second.x = bounds.x.saturating_add(first.width);
|
||||||
|
second.width -= first.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[first, second]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum Node {
|
||||||
|
Leaf(usize),
|
||||||
|
Split {
|
||||||
|
axis: SplitAxis,
|
||||||
|
children: Box<[Node; 2]>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
fn contains(&self, id: usize) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Leaf(leaf) => *leaf == id,
|
||||||
|
Self::Split { children, .. } => children.iter().any(|child| child.contains(id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_id(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Leaf(id) => *id,
|
||||||
|
Self::Split { children, .. } => children[0].first_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split(&mut self, id: usize, new_id: usize, axis: SplitAxis) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Leaf(leaf) if *leaf == id => {
|
||||||
|
*self = Self::Split {
|
||||||
|
axis,
|
||||||
|
children: Box::new([Self::Leaf(id), Self::Leaf(new_id)]),
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Self::Leaf(_) => false,
|
||||||
|
Self::Split { children, .. } => children
|
||||||
|
.iter_mut()
|
||||||
|
.any(|child| child.split(id, new_id, axis)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self, id: usize) -> Option<usize> {
|
||||||
|
let Self::Split { children, .. } = self else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
for index in 0..2 {
|
||||||
|
if matches!(children[index], Self::Leaf(leaf) if leaf == id) {
|
||||||
|
let sibling = 1 - index;
|
||||||
|
let focus = children[sibling].first_id();
|
||||||
|
*self = std::mem::replace(&mut children[sibling], Self::Leaf(id));
|
||||||
|
return Some(focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.iter_mut().find_map(|child| child.close(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_rects(&self, bounds: PaneRect, visitor: &mut impl FnMut(usize, PaneRect)) {
|
||||||
|
if bounds.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match self {
|
||||||
|
Self::Leaf(id) => visitor(*id, bounds),
|
||||||
|
Self::Split { axis, children } => {
|
||||||
|
let halves = split_rect(bounds, *axis);
|
||||||
|
children[0].visit_rects(halves[0], visitor);
|
||||||
|
children[1].visit_rects(halves[1], visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PaneTree {
|
||||||
|
root: Node,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaneTree {
|
||||||
|
pub fn new(id: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
root: Node::Leaf(id),
|
||||||
|
len: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep `id` first and insert `new_id` second. IDs must be unique.
|
||||||
|
pub fn split(&mut self, id: usize, new_id: usize, axis: SplitAxis) -> bool {
|
||||||
|
if self.contains(new_id) || !self.root.split(id, new_id, axis) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.len += 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close a leaf and expand its sibling; return the sibling's first leaf.
|
||||||
|
/// Unknown IDs and closing the last leaf leave the tree unchanged.
|
||||||
|
pub fn close(&mut self, id: usize) -> Option<usize> {
|
||||||
|
let focus = self.root.close(id)?;
|
||||||
|
self.len -= 1;
|
||||||
|
Some(focus)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, id: usize) -> bool {
|
||||||
|
self.root.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Visit nonempty leaves in tree order, without allocating.
|
||||||
|
pub fn visit_rects(&self, bounds: PaneRect, mut visitor: impl FnMut(usize, PaneRect)) {
|
||||||
|
self.root.visit_rects(bounds, &mut visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn neighbor(&self, id: usize, direction: PaneDirection, bounds: PaneRect) -> Option<usize> {
|
||||||
|
let mut current = None;
|
||||||
|
self.visit_rects(bounds, |leaf, rect| {
|
||||||
|
if leaf == id {
|
||||||
|
current = Some(rect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let current = current?;
|
||||||
|
let mut best = None;
|
||||||
|
self.visit_rects(bounds, |leaf, rect| {
|
||||||
|
if leaf != id {
|
||||||
|
consider_neighbor(current, direction, leaf, rect, &mut best);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
best.map(|(_, leaf)| leaf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score: prefer perpendicular overlap, then the nearest forward edge,
|
||||||
|
// perpendicular gap, center alignment, and finally the stable ID.
|
||||||
|
type NeighborScore = (bool, u32, u32, u32, usize);
|
||||||
|
|
||||||
|
fn neighbor_score(
|
||||||
|
current: PaneRect,
|
||||||
|
direction: PaneDirection,
|
||||||
|
id: usize,
|
||||||
|
candidate: PaneRect,
|
||||||
|
) -> Option<NeighborScore> {
|
||||||
|
if current.is_empty() || candidate.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let edges = |rect: PaneRect| {
|
||||||
|
let x = u32::from(rect.x);
|
||||||
|
let y = u32::from(rect.y);
|
||||||
|
(x, y, x + u32::from(rect.width), y + u32::from(rect.height))
|
||||||
|
};
|
||||||
|
let (x, y, right, bottom) = edges(current);
|
||||||
|
let (cx, cy, cright, cbottom) = edges(candidate);
|
||||||
|
let (distance, start, end, cstart, cend) = match direction {
|
||||||
|
PaneDirection::Left if cright <= x => (x - cright, y, bottom, cy, cbottom),
|
||||||
|
PaneDirection::Right if cx >= right => (cx - right, y, bottom, cy, cbottom),
|
||||||
|
PaneDirection::Up if cbottom <= y => (y - cbottom, x, right, cx, cright),
|
||||||
|
PaneDirection::Down if cy >= bottom => (cy - bottom, x, right, cx, cright),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let overlap = start.max(cstart) < end.min(cend);
|
||||||
|
let gap = start.max(cstart).saturating_sub(end.min(cend));
|
||||||
|
let alignment = (start + end).abs_diff(cstart + cend);
|
||||||
|
Some((!overlap, distance, gap, alignment, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consider_neighbor(
|
||||||
|
current: PaneRect,
|
||||||
|
direction: PaneDirection,
|
||||||
|
id: usize,
|
||||||
|
candidate: PaneRect,
|
||||||
|
best: &mut Option<(NeighborScore, usize)>,
|
||||||
|
) {
|
||||||
|
if let Some(score) = neighbor_score(current, direction, id, candidate)
|
||||||
|
&& best.as_ref().is_none_or(|(previous, _)| score < *previous)
|
||||||
|
{
|
||||||
|
*best = Some((score, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a forward rectangle, favoring overlap and the closest edge.
|
||||||
|
/// Empty rectangles are ignored; ties are independent of iterator order.
|
||||||
|
pub fn directional_neighbor(
|
||||||
|
current: PaneRect,
|
||||||
|
direction: PaneDirection,
|
||||||
|
candidates: impl Iterator<Item = (usize, PaneRect)>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut best = None;
|
||||||
|
for (id, candidate) in candidates {
|
||||||
|
consider_neighbor(current, direction, id, candidate, &mut best);
|
||||||
|
}
|
||||||
|
best.map(|(_, id)| id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn rect(x: u16, y: u16, width: u16, height: u16) -> PaneRect {
|
||||||
|
PaneRect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaves(tree: &PaneTree, bounds: PaneRect) -> Vec<(usize, PaneRect)> {
|
||||||
|
let mut leaves = Vec::new();
|
||||||
|
tree.visit_rects(bounds, |id, rect| leaves.push((id, rect)));
|
||||||
|
leaves
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_layout_partitions_bounds_and_preserves_ids() {
|
||||||
|
let bounds = rect(3, 5, 11, 9);
|
||||||
|
let mut tree = PaneTree::new(10);
|
||||||
|
assert!(tree.split(10, 20, SplitAxis::Vertical));
|
||||||
|
assert!(tree.split(20, 30, SplitAxis::Horizontal));
|
||||||
|
assert!(!tree.split(99, 40, SplitAxis::Horizontal));
|
||||||
|
assert!(!tree.split(10, 30, SplitAxis::Horizontal));
|
||||||
|
assert!(!tree.split(10, 10, SplitAxis::Horizontal));
|
||||||
|
assert_eq!(tree.len(), 3);
|
||||||
|
let panes = leaves(&tree, bounds);
|
||||||
|
assert_eq!(
|
||||||
|
panes,
|
||||||
|
vec![
|
||||||
|
(10, rect(3, 5, 5, 9)),
|
||||||
|
(20, rect(8, 5, 6, 4)),
|
||||||
|
(30, rect(8, 9, 6, 5))
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let mut cells = std::collections::HashSet::new();
|
||||||
|
for (_, pane) in panes {
|
||||||
|
for y in pane.y..pane.y + pane.height {
|
||||||
|
for x in pane.x..pane.x + pane.width {
|
||||||
|
assert!(cells.insert((x, y)), "overlapping pane cell");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
cells.len(),
|
||||||
|
usize::from(bounds.width) * usize::from(bounds.height)
|
||||||
|
);
|
||||||
|
assert!(leaves(&tree, rect(0, 0, 0, 10)).is_empty());
|
||||||
|
assert!(leaves(&tree, rect(0, 0, 10, 0)).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closing_expands_nested_sibling_and_refuses_last() {
|
||||||
|
let bounds = rect(0, 0, 20, 12);
|
||||||
|
let mut tree = PaneTree::new(1);
|
||||||
|
tree.split(1, 2, SplitAxis::Vertical);
|
||||||
|
tree.split(2, 3, SplitAxis::Horizontal);
|
||||||
|
assert_eq!(tree.close(1), Some(2));
|
||||||
|
assert_eq!(
|
||||||
|
leaves(&tree, bounds),
|
||||||
|
vec![(2, rect(0, 0, 20, 6)), (3, rect(0, 6, 20, 6))]
|
||||||
|
);
|
||||||
|
assert_eq!(tree.close(99), None);
|
||||||
|
assert_eq!(tree.close(2), Some(3));
|
||||||
|
assert_eq!(leaves(&tree, bounds), vec![(3, bounds)]);
|
||||||
|
assert_eq!(tree.close(3), None);
|
||||||
|
assert_eq!(tree.len(), 1);
|
||||||
|
assert!(!tree.contains(1));
|
||||||
|
assert!(tree.contains(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn directional_focus_handles_unequal_nested_regions() {
|
||||||
|
let bounds = rect(0, 0, 20, 12);
|
||||||
|
let mut tree = PaneTree::new(1);
|
||||||
|
tree.split(1, 2, SplitAxis::Vertical);
|
||||||
|
tree.split(2, 3, SplitAxis::Horizontal);
|
||||||
|
tree.split(3, 4, SplitAxis::Horizontal);
|
||||||
|
assert_eq!(tree.neighbor(1, PaneDirection::Right, bounds), Some(3));
|
||||||
|
assert_eq!(tree.neighbor(2, PaneDirection::Down, bounds), Some(3));
|
||||||
|
assert_eq!(tree.neighbor(3, PaneDirection::Down, bounds), Some(4));
|
||||||
|
assert_eq!(tree.neighbor(4, PaneDirection::Up, bounds), Some(3));
|
||||||
|
assert_eq!(tree.neighbor(4, PaneDirection::Left, bounds), Some(1));
|
||||||
|
assert_eq!(tree.neighbor(1, PaneDirection::Left, bounds), None);
|
||||||
|
assert_eq!(tree.neighbor(99, PaneDirection::Right, bounds), None);
|
||||||
|
assert_eq!(
|
||||||
|
tree.neighbor(1, PaneDirection::Right, rect(0, 0, 0, 0)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn navigation_prefers_overlap_then_edge_and_breaks_ties_by_id() {
|
||||||
|
let current = rect(10, 10, 10, 10);
|
||||||
|
let candidates = [
|
||||||
|
(1, rect(20, 0, 5, 5)),
|
||||||
|
(2, rect(25, 10, 5, 10)),
|
||||||
|
(4, rect(22, 10, 5, 10)),
|
||||||
|
(3, rect(22, 10, 5, 10)),
|
||||||
|
(0, rect(20, 10, 0, 10)),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
directional_neighbor(current, PaneDirection::Right, candidates.into_iter()),
|
||||||
|
Some(3)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
directional_neighbor(current, PaneDirection::Right, candidates.into_iter().rev()),
|
||||||
|
Some(3)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
directional_neighbor(
|
||||||
|
current,
|
||||||
|
PaneDirection::Up,
|
||||||
|
[(7, rect(10, 0, 10, 5))].into_iter()
|
||||||
|
),
|
||||||
|
Some(7)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
directional_neighbor(current, PaneDirection::Left, candidates.into_iter()),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
-21
@@ -145,6 +145,7 @@ struct TrackedDocument {
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct InvocationSource {
|
struct InvocationSource {
|
||||||
buffer_id: usize,
|
buffer_id: usize,
|
||||||
|
pane_id: usize,
|
||||||
revision: u64,
|
revision: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +271,11 @@ impl EditorSession {
|
|||||||
&self.editor
|
&self.editor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rendering-only mutable access; document synchronization stays in session dispatch.
|
||||||
|
pub(crate) fn editor_mut(&mut self) -> &mut Editor {
|
||||||
|
&mut self.editor
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_diff_viewport(
|
pub(crate) fn set_diff_viewport(
|
||||||
&mut self,
|
&mut self,
|
||||||
comparison_id: u64,
|
comparison_id: u64,
|
||||||
@@ -313,11 +319,14 @@ impl EditorSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_diff_request(&mut self) {
|
fn start_diff_request(&mut self) {
|
||||||
if self.editor.diff().is_none() {
|
if self.editor.workspace_diff().is_none() {
|
||||||
self.diff_worker = None;
|
self.diff_worker = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.editor.diff().is_some_and(|diff| !diff.loading)
|
if self
|
||||||
|
.editor
|
||||||
|
.workspace_diff()
|
||||||
|
.is_some_and(|diff| !diff.loading)
|
||||||
&& let Some(worker) = &mut self.diff_worker
|
&& let Some(worker) = &mut self.diff_worker
|
||||||
&& let Some(id) = worker.pending.take()
|
&& let Some(id) = worker.pending.take()
|
||||||
{
|
{
|
||||||
@@ -328,13 +337,7 @@ impl EditorSession {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if self.diff_worker.is_none() {
|
if self.diff_worker.is_none() {
|
||||||
let root = self
|
let root = self.editor.diff_root();
|
||||||
.editor
|
|
||||||
.path()
|
|
||||||
.and_then(Path::parent)
|
|
||||||
.filter(|path| !path.as_os_str().is_empty())
|
|
||||||
.unwrap_or_else(|| self.editor.project_root())
|
|
||||||
.to_path_buf();
|
|
||||||
match DiffWorker::start(root) {
|
match DiffWorker::start(root) {
|
||||||
Ok(worker) => self.diff_worker = Some(worker),
|
Ok(worker) => self.diff_worker = Some(worker),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -396,6 +399,7 @@ impl EditorSession {
|
|||||||
let context = self.editor.context();
|
let context = self.editor.context();
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id: context.buffer_id,
|
buffer_id: context.buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision: context.revision,
|
revision: context.revision,
|
||||||
};
|
};
|
||||||
self.requests.entry("core".into()).or_default().latest = Some((id, source));
|
self.requests.entry("core".into()).or_default().latest = Some((id, source));
|
||||||
@@ -431,6 +435,7 @@ impl EditorSession {
|
|||||||
.map(|id| {
|
.map(|id| {
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id: context.buffer_id,
|
buffer_id: context.buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision: context.revision,
|
revision: context.revision,
|
||||||
};
|
};
|
||||||
requests.pending.insert(id, source);
|
requests.pending.insert(id, source);
|
||||||
@@ -484,6 +489,7 @@ impl EditorSession {
|
|||||||
if let Some(requests) = self.requests.get_mut("core")
|
if let Some(requests) = self.requests.get_mut("core")
|
||||||
&& requests.latest.is_some_and(|(_, source)| {
|
&& requests.latest.is_some_and(|(_, source)| {
|
||||||
source.buffer_id != self.editor.buffer_id()
|
source.buffer_id != self.editor.buffer_id()
|
||||||
|
|| source.pane_id != self.editor.pane_id()
|
||||||
|| source.revision != self.editor.text_revision()
|
|| source.revision != self.editor.text_revision()
|
||||||
|| self.lsp_source_cursor != Some(self.editor.cursor())
|
|| self.lsp_source_cursor != Some(self.editor.cursor())
|
||||||
|| self.editor.mode() != self.lsp_source_mode
|
|| self.editor.mode() != self.lsp_source_mode
|
||||||
@@ -618,6 +624,7 @@ impl EditorSession {
|
|||||||
.is_some_and(|(id, latest)| {
|
.is_some_and(|(id, latest)| {
|
||||||
request_id == Some(id)
|
request_id == Some(id)
|
||||||
&& latest.buffer_id == source.buffer_id
|
&& latest.buffer_id == source.buffer_id
|
||||||
|
&& latest.pane_id == self.editor.pane_id()
|
||||||
&& latest.revision == source.revision
|
&& latest.revision == source.revision
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -674,6 +681,7 @@ impl EditorSession {
|
|||||||
}
|
}
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id: *buffer_id,
|
buffer_id: *buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision: *revision,
|
revision: *revision,
|
||||||
};
|
};
|
||||||
if self.completion_is_current(update.request_id, source)
|
if self.completion_is_current(update.request_id, source)
|
||||||
@@ -720,6 +728,7 @@ impl EditorSession {
|
|||||||
} => {
|
} => {
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id: *buffer_id,
|
buffer_id: *buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision: *revision,
|
revision: *revision,
|
||||||
};
|
};
|
||||||
if self.request_is_current(owner, update.request_id, source)
|
if self.request_is_current(owner, update.request_id, source)
|
||||||
@@ -738,6 +747,7 @@ impl EditorSession {
|
|||||||
} => {
|
} => {
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id: *buffer_id,
|
buffer_id: *buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision: *revision,
|
revision: *revision,
|
||||||
};
|
};
|
||||||
if self.hover_is_current(owner, update.request_id, source) {
|
if self.hover_is_current(owner, update.request_id, source) {
|
||||||
@@ -786,6 +796,7 @@ impl EditorSession {
|
|||||||
} => {
|
} => {
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id,
|
buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision,
|
revision,
|
||||||
};
|
};
|
||||||
if self.request_is_current(owner, update.request_id, source) {
|
if self.request_is_current(owner, update.request_id, source) {
|
||||||
@@ -805,6 +816,7 @@ impl EditorSession {
|
|||||||
} => {
|
} => {
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id,
|
buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision,
|
revision,
|
||||||
};
|
};
|
||||||
if self.hover_is_current(owner, update.request_id, source) {
|
if self.hover_is_current(owner, update.request_id, source) {
|
||||||
@@ -823,6 +835,7 @@ impl EditorSession {
|
|||||||
} => {
|
} => {
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id,
|
buffer_id,
|
||||||
|
pane_id: self.editor.pane_id(),
|
||||||
revision,
|
revision,
|
||||||
};
|
};
|
||||||
if self.completion_is_current(update.request_id, source) {
|
if self.completion_is_current(update.request_id, source) {
|
||||||
@@ -857,7 +870,10 @@ impl EditorSession {
|
|||||||
let Some(source) = source else {
|
let Some(source) = source else {
|
||||||
return Err("plugin response has an unknown invocation ID".into());
|
return Err("plugin response has an unknown invocation ID".into());
|
||||||
};
|
};
|
||||||
if !self.source_is_current(source) || self.editor.buffer_id() != source.buffer_id {
|
if !self.source_is_current(source)
|
||||||
|
|| self.editor.buffer_id() != source.buffer_id
|
||||||
|
|| self.editor.pane_id() != source.pane_id
|
||||||
|
{
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
if effects
|
if effects
|
||||||
@@ -948,6 +964,53 @@ mod tests {
|
|||||||
|
|
||||||
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
|
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_swaps_preserve_session_sources_and_pending_effects_do_not_cross_panes() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let path = fixture.file("buffer.txt", "source");
|
||||||
|
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
|
||||||
|
let source = InvocationSource {
|
||||||
|
pane_id: session.editor.pane_id(),
|
||||||
|
buffer_id: session.editor.buffer_id(),
|
||||||
|
revision: session.editor.text_revision(),
|
||||||
|
};
|
||||||
|
session
|
||||||
|
.requests
|
||||||
|
.entry("fixture".into())
|
||||||
|
.or_default()
|
||||||
|
.pending
|
||||||
|
.insert(1, source);
|
||||||
|
session.requests.entry("fixture".into()).or_default().latest = Some((1, source));
|
||||||
|
session.handle(Input::Char('|'));
|
||||||
|
assert_eq!(session.editor.buffer_id(), source.buffer_id);
|
||||||
|
assert_eq!(session.editor.cursor(), Cursor::default());
|
||||||
|
assert!(!session.request_is_current("fixture", Some(1), source));
|
||||||
|
assert!(
|
||||||
|
!session
|
||||||
|
.apply_response("fixture", 1, vec![Effect::Insert("wrong pane".into())])
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
session.requests.entry("core".into()).or_default().latest = Some((
|
||||||
|
2,
|
||||||
|
InvocationSource {
|
||||||
|
pane_id: session.editor.pane_id(),
|
||||||
|
..source
|
||||||
|
},
|
||||||
|
));
|
||||||
|
session.lsp_source_cursor = Some(session.editor.cursor());
|
||||||
|
session.lsp_source_mode = session.editor.mode();
|
||||||
|
session.editor_mut().with_render_pane(1, |pane| {
|
||||||
|
assert_eq!(pane.pane_id(), 1);
|
||||||
|
assert_eq!(pane.lines(), ["source"]);
|
||||||
|
});
|
||||||
|
session.synchronize_documents();
|
||||||
|
assert!(session.requests["core"].latest.is_some());
|
||||||
|
assert_eq!(session.editor.pane_id(), 2);
|
||||||
|
session.handle(Input::Alt('h'));
|
||||||
|
assert!(session.requests["core"].latest.is_none());
|
||||||
|
assert_eq!(session.editor.lines(), ["source"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_semantic_batches_are_atomic_and_malformed_updates_clear_hidden_overlays() {
|
fn native_semantic_batches_are_atomic_and_malformed_updates_clear_hidden_overlays() {
|
||||||
use crate::plugin::{SemanticToken, SemanticTokenKind};
|
use crate::plugin::{SemanticToken, SemanticTokenKind};
|
||||||
@@ -1529,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('!'));
|
||||||
@@ -1573,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));
|
||||||
}
|
}
|
||||||
@@ -1650,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'));
|
||||||
}
|
}
|
||||||
@@ -1659,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"]);
|
||||||
@@ -1671,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"]);
|
||||||
@@ -1704,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 "]);
|
||||||
@@ -2120,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));
|
||||||
}
|
}
|
||||||
@@ -2804,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}"
|
||||||
);
|
);
|
||||||
@@ -3412,6 +3475,7 @@ end
|
|||||||
session.editor.handle(Input::BackTab);
|
session.editor.handle(Input::BackTab);
|
||||||
let source = InvocationSource {
|
let source = InvocationSource {
|
||||||
buffer_id: session.editor.buffer_id(),
|
buffer_id: session.editor.buffer_id(),
|
||||||
|
pane_id: session.editor.pane_id(),
|
||||||
revision: session.editor.text_revision(),
|
revision: session.editor.text_revision(),
|
||||||
};
|
};
|
||||||
session
|
session
|
||||||
@@ -3447,6 +3511,7 @@ end
|
|||||||
session.editor.scroll_diff(Focus::Before, false);
|
session.editor.scroll_diff(Focus::Before, false);
|
||||||
let readonly = InvocationSource {
|
let readonly = InvocationSource {
|
||||||
buffer_id: session.editor.buffer_id(),
|
buffer_id: session.editor.buffer_id(),
|
||||||
|
pane_id: session.editor.pane_id(),
|
||||||
revision: session.editor.text_revision(),
|
revision: session.editor.text_revision(),
|
||||||
};
|
};
|
||||||
session
|
session
|
||||||
|
|||||||
+1178
-401
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user