feat: default editing to shared system clipboard
This commit is contained in:
@@ -30,7 +30,7 @@ Without a file, vamp opens an unnamed buffer. A nonexistent file opens an empty
|
||||
| Normal | `x`/Delete delete; `u` undo; `U` (Shift+u) redo; `:` enter command mode |
|
||||
| Normal, Visual | `.` repeats the last successful native edit against the current targets; one undo group |
|
||||
| Normal | `/` opens search; `n` finds the next match; `N` (Shift+n) finds the previous match |
|
||||
| Normal | `v` starts Visual selections; `p`/`P` paste internal clipboard after/before each caret; Esc keeps only the primary caret |
|
||||
| Normal | `v` starts Visual selections; `p`/`P` paste the configured clipboard after/before each caret (system by default); Esc keeps only the primary caret |
|
||||
| Normal, Visual | Ctrl+k clones above; Ctrl+e clones below |
|
||||
| Visual | Cursor/word motions and `n`/`N` extend every selection; `y` copies; `d`/`x`/Delete cuts; `c` changes selections; `v`/Esc cancels highlighting |
|
||||
| Visual | Ctrl+l splits touched rows into Normal carets at first nonblank; Ctrl+n adds the next occurrence; Ctrl+s skips the primary occurrence; Ctrl+a selects all occurrences |
|
||||
@@ -58,11 +58,29 @@ Ctrl+k/Ctrl+e clone each target to the adjacent row, preserving preferred termin
|
||||
|
||||
Copy and cut return to Normal at each selection start; Visual cancellation keeps each active endpoint and removes highlighting. Normal Esc collapses to the primary. `p`/`P` paste after/before each current grapheme, including multiline text. Each caret lands on its last inserted grapheme, or the new row's start when its fragment ends in a newline. Every multi-target edit is planned against the original buffer and committed as one undo action, not one per target. Copy, movement, and cancellation do not add history or clear redo. Undo/redo restore text, the full caret set, and primary identity in Normal mode, not Visual highlighting or the clipboard.
|
||||
|
||||
Visual `c` removes all selected text into the internal clipboard and enters Insert at each selection start. Deletion and replacement typing form one undo action when Insert mode is exited, including through explicit command execution. Esc without typing commits the deletion alone. An identical replacement creates no history entry and preserves redo. Changing only empty final-row selections enters Insert without overwriting the clipboard.
|
||||
Visual `c` removes all selected text into the configured clipboard and enters Insert at each selection start. In the default System policy, clipboard publication must succeed before any deletion; failures leave text and selections intact. Deletion and replacement typing form one undo action when Insert mode is exited, including through explicit command execution. Esc without typing commits the deletion alone. An identical replacement creates no history entry and preserves redo. Changing only empty final-row selections enters Insert without overwriting either clipboard.
|
||||
|
||||
Bracketed paste broadcasts the same literal text to all Insert carets in a separate undo group, distinct from typing before/after it. A rejected paste leaves the pending change intact. Normal-mode operator grammar such as `cw` is not supported; `c` types literal text in Insert mode.
|
||||
|
||||
The clipboard is internal to the editor session and independent of the system clipboard. Copy/cut store fragments in document order: one fragment broadcasts to every caret; matching fragment/caret counts pair in document order; any other count refuses paste without changing state. Empty clipboard paste does not edit text; copying/cutting only empty final-row selections does not overwrite the clipboard. Open a new search or enter Ex commands after leaving Visual mode. Plugin commands execute once, not once per target: successful text effects collapse to the primary and apply once; rejected or message-only effects preserve the selection set. There is no selection-aware plugin API yet.
|
||||
Clipboard policy is session-wide and defaults to the system clipboard. Copy/cut store fragments in document order: one fragment broadcasts to every caret; matching fragment/caret counts pair in document order; any other count refuses paste without changing editing state. Empty clipboard paste does not edit text; copying/cutting only empty final-row selections does not overwrite either register. Open a new search or enter Ex commands after leaving Visual mode. Plugin commands execute once, not once per target: successful text effects collapse to the primary and apply once; rejected or message-only effects preserve the selection set. There is no selection-aware plugin API yet.
|
||||
|
||||
### Clipboard policy
|
||||
|
||||
`config.clipboard_mode = "system"` is the default: ordinary Visual `y`/`d`/`x`/`c` and Normal `p`/`P` use the OS clipboard. External clipboard changes are read on each paste. Set `"separate"` to keep ordinary operations on vamp's internal fragment register instead. Both registers remain independent across buffers; changing the policy does not overwrite either.
|
||||
|
||||
Explicit `config.core.copy_system` (Visual) and `config.core.paste_system_before` / `paste_system_after` (Normal) always use the system clipboard in either policy. They appear in the command palette and can be bound without replacing ordinary internal operations:
|
||||
|
||||
```ruby
|
||||
config.clipboard_mode = "separate"
|
||||
config.bind "ctrl+s", config.core.copy_system, mode: config.modes.visual
|
||||
config.bind "ctrl+g", config.core.paste_system_before
|
||||
```
|
||||
|
||||
For other applications, multi-selection exports join fragments with LF. Vamp retains their original boundaries privately while the OS text exactly matches its export, so its own copies still paste pairwise. Different external text invalidates that metadata and becomes one literal fragment—even when it contains newlines—broadcasting to all targets. Identical external replacement text is inherently indistinguishable from an unchanged export. No empty-system fallback to stale internal text occurs.
|
||||
|
||||
System paste validates UTF-8, unsupported controls, newlines, and a 4 MiB limit before editing; CRLF/CR normalize to LF. System exports also have a 4 MiB limit. Provider failures report an error without deleting/changing buffer text, caret/selection state, history, or repeat intent. No silent internal fallback occurs in System mode; choose Separate if clipboard access is unavailable. A native provider that fails after publishing may already have changed OS clipboard formats; vamp cannot roll that external side effect back.
|
||||
|
||||
macOS uses fixed JXA/AppKit `NSPasteboardTypeString` access through `osascript`, preserving literal RTF/EPS text rather than treating it as rich clipboard data. Linux supports Wayland with `wl-paste`/`wl-copy`, or X11 with `xclip`, on PATH with the corresponding display environment. Other platforms report unsupported access. Commands have a two-second deadline and bounded output; failures terminate/reap their process group. Tests and executable smoke use isolated providers and never read or overwrite the user's desktop clipboard.
|
||||
|
||||
### Repeat last edit
|
||||
|
||||
@@ -70,7 +88,7 @@ The clipboard is internal to the editor session and independent of the system cl
|
||||
|
||||
In Normal, Visual change/cut targeting replays its resolved motions and counts at each current caret: `vecitem` followed later by `.` changes a fresh whole word, even if its length differs. If targeting was rebuilt by cloning, occurrence gathering, search, or overlap merging, repeat uses relative whole-grapheme spans instead. Equal spans broadcast; unequal spans require matching target counts. In Visual, the current explicit ranges replace the stored targeting. Target-building commands themselves do not recreate old cursors.
|
||||
|
||||
Clipboard paste captures the fragments actually pasted, so a later yank does not change repeat's payload. One fragment broadcasts; equal fragment/target counts pair; mismatches refuse atomically. Other per-target edit data, such as partially effective Insert deletion, obeys the same conservative distribution policy. A refusal or cardinality change during replay leaves text, caret/primary state, clipboard, and history unchanged.
|
||||
Clipboard paste captures the fragments actually pasted, so a later yank or external clipboard change does not change repeat's payload; dot paste never rereads the OS. One fragment broadcasts; equal fragment/target counts pair; mismatches refuse atomically. Other per-target edit data, such as partially effective Insert deletion, obeys the same conservative distribution policy. Dot cut/change follows the current clipboard policy and stages publication until replay validation finishes. A refusal or provider error preserves editor text, caret/primary state, register caches, history, and recipe; already-published OS data cannot be transactionally restored.
|
||||
|
||||
Each repeat returns to Normal and creates one undo entry restoring the full caret set and primary. Original typing/paste/typing boundaries remain separate; dot replays the selected committed group as one transaction. Typed text retains scalar insertion boundaries, preserving combining/ZWJ interactions with surrounding graphemes; bracketed paste remains one bulk literal operation. Trailing Insert movement is not repeated.
|
||||
|
||||
@@ -182,10 +200,11 @@ Leader bindings require Normal/Visual and the target's existing mode eligibility
|
||||
|
||||
## Module seams
|
||||
|
||||
One crate, five modules, no workspace or terminal-coordinate types in the editing backend:
|
||||
One crate, six modules, 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 fragments, 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, 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.
|
||||
- `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.
|
||||
- `clipboard`: owns native plain-text provider selection and bounded subprocess I/O; it never computes selections or editing offsets.
|
||||
- `session`: `EditorSession` owns the editor and its Ruby hosts together. It loads configuration once, registers eligible plugins before installing shared bindings, executes selected commands against the active buffer, and retires both host and command ownership on invocation or effect-validation failure.
|
||||
- `tui`: translates terminal events to session input, renders read-only active-buffer state, keeps frontend-owned viewports by stable buffer ID, and advertises `text` and `styled_text` capabilities. It has no plugin lifecycle callback, host storage, or paste acceptance policy.
|
||||
- `ruby`: evaluates configuration and runs isolated plugin hosts. Protocol, deadlines, and process cleanup stay inside this module.
|
||||
@@ -217,6 +236,7 @@ By default, vamp loads `$XDG_CONFIG_HOME/vamp/config.rb`, or `~/.config/vamp/con
|
||||
```ruby
|
||||
Vamp.configure do |config|
|
||||
config.leader = "space"
|
||||
config.clipboard_mode = "system"
|
||||
config.file_explorer_side = "right"
|
||||
config.file_explorer_view = "tree"
|
||||
config.file_picker_view = "tree"
|
||||
@@ -239,6 +259,7 @@ end
|
||||
|
||||
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.
|
||||
`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.core` exposes the built-in actions below as immutable `Action` values; `config.modes` exposes immutable `Mode` values. Misspelled methods fail during configuration evaluation, and `bind` rejects the wrong value type. Built-in action strings and mode strings are not accepted. Registered Ex/plugin commands still use exact string names (`"w"`, `"greet"`, etc., without `:` or arguments) and can only be bound in Normal mode. Ruby provides runtime checks, not compile-time checking; the editor still validates action/mode compatibility.
|
||||
|
||||
@@ -256,6 +277,7 @@ Key notation follows [Herdr's readable style](https://herdr.dev/docs/configurati
|
||||
| Normal, Visual | `insert_first_nonblank`, `append_line_end`, `clone_above`, `clone_below` |
|
||||
| Normal | `join_lines`, `undo`, `redo`, `command` |
|
||||
| Normal | `search`, `paste_before`, `paste_after` |
|
||||
| Normal | `paste_system_before`, `paste_system_after` |
|
||||
| Normal, Visual | `select`, `search_next`, `search_previous` |
|
||||
| Normal, Visual | `repeat_last_edit` |
|
||||
| Normal, Visual | `file_picker`, `file_explorer` |
|
||||
@@ -265,6 +287,7 @@ Key notation follows [Herdr's readable style](https://herdr.dev/docs/configurati
|
||||
| Normal, Insert, Visual | `word_next`, `word_previous`, `word_end`, `big_word_next`, `big_word_previous`, `big_word_end` |
|
||||
| Normal, Insert | `delete` |
|
||||
| Visual | `copy`, `cut`, `change`, `select_lines`, `select_next`, `skip_occurrence`, `select_all` |
|
||||
| Visual | `copy_system` |
|
||||
| Normal, Insert, Command, Search, Visual | `normal`, `palette` |
|
||||
| Insert, Command, Search | `backspace` |
|
||||
| Insert | `newline` |
|
||||
@@ -313,7 +336,7 @@ JSON uses Ruby's standard library. Protocol Buffers is not installed or required
|
||||
|
||||
## Current limits
|
||||
|
||||
- One visible buffer at a time; Normal, Insert, Command, Search, and characterwise Visual modes. No system clipboard integration, named registers, line/block selection, Vim operators, splits/tabs, regex/multiline search, LSP, or graphical frontend.
|
||||
- One visible buffer at a time; Normal, Insert, Command, Search, and characterwise Visual modes. No named registers, line/block selection, Vim operators, splits/tabs, regex/multiline search, LSP, or graphical frontend. Native system clipboard access currently supports macOS and Linux; Separate mode retains internal editing on unsupported/headless environments.
|
||||
- Undo uses full-buffer snapshots and movement scans the current line. This favors straightforward correctness over large-file performance.
|
||||
- Plugin invocation is synchronous and may pause input for up to 500 ms before a host timeout. Config/plugin startup has a 2-second deadline per host. Buffer context must fit within a 4 MiB JSON request; a too-large request disables that plugin, not editing.
|
||||
- Disabling a plugin removes its commands and bindings until the next launch. There is no hot reload.
|
||||
|
||||
Reference in New Issue
Block a user