feat: reload clean buffers and guard external-change writes

This commit is contained in:
2026-10-12 22:27:08 +02:00
parent bb5c247ab2
commit 298d586c3e
7 changed files with 556 additions and 41 deletions
+10 -4
View File
@@ -154,20 +154,26 @@ The latest reported operation result appears in a non-focusable popup at the wor
Command-capable core actions derive canonical Ex names by removing `core.`: `core.quit` gives `:quit`, `core.save` gives `:save`, and `core.diff.close` gives `:diff.close`. Native editing and Insert-only completion/snippet actions remain action bindings, not additional Ex commands.
Commands: `:save [path]`, `:quit`, `:force_quit`, `:save_and_quit [path]`, `:plugins`, and `:plugin-command [arguments]`. Short names remain aliases of these canonical commands, with identical argument and safety rules:
Commands: `:save [path]`, `:force_save [path]`, `:quit`, `:force_quit`, `:save_and_quit [path]`, `:force_save_and_quit [path]`, `:plugins`, and `:plugin-command [arguments]`. Short names remain aliases of these canonical commands, with identical argument and safety rules:
| Alias | Canonical command |
| --- | --- |
| `:w` | `:save` |
| `:w!` | `:force_save` |
| `:q` | `:quit` |
| `:q!` | `:force_quit` |
| `:wq` | `:save_and_quit` |
| `:wq!` | `:force_save_and_quit` |
| `:bnext` | `:buffer_next` |
| `:bprev` | `:buffer_previous` |
| `:bdelete` | `:buffer_delete` |
| `:bdelete!` | `:buffer_force_delete` |
`:q` refuses unsaved changes in any open buffer; `:q!` exits without saving them. `:wq` saves the active buffer and quits only if every buffer is then clean. Saves use a same-directory temporary file and rename, preserve existing permissions, and reject target symlinks. Writing to a path owned by another loaded buffer refuses before modifying the file. UTF-8 text is edited by extended grapheme; tabs render at four-cell stops. LF and trailing-newline structure are preserved; CRLF and CR are normalized to LF. Binary/terminal-control-containing text is rejected. Bracketed paste inserts only in Insert mode and never executes pasted commands in Normal mode.
`:q` refuses unsaved changes in any open buffer; `:q!` exits without saving them. `:wq` saves the active buffer and quits only if every buffer is then clean; `:wq!` applies the same quit rule after a force save. The typed actions are `core.save`, `core.force_save`, `core.save_and_quit`, and `core.force_save_and_quit`. Normal saves re-read actual disk content and compare it with the saved baseline after CRLF/CR-to-LF normalization, independently of polling or timestamps. External changes or deletion refuse the write with a warning; a new buffer opened at a nonexistent path may save normally. Use `:w!` / `:force_save [path]` or `:wq!` / `:force_save_and_quit [path]` to explicitly overwrite external changes. Force bypasses only the external-change check: historical snapshots remain read-only, target symlinks are rejected, and another loaded buffer's path cannot be overwritten.
All saves use a same-directory temporary file and rename and preserve existing permissions. UTF-8 text is edited by extended grapheme; tabs render at four-cell stops. LF and trailing-newline structure are preserved; CRLF and CR are normalized to LF. Binary/terminal-control-containing text is rejected. Bracketed paste inserts only in Insert mode and never executes pasted commands in Normal mode.
Live file buffers automatically check disk metadata every 250 ms, including buffers in inactive tabs/panes and parked views; historical snapshots are excluded. Content is read only when metadata changes. A clean buffer reloads validated changed text, clears stale undo/redo, preserves caret/selection and view positions where possible, and clamps them to the new text. Reloads invalidate annotations and stale asynchronous requests and refresh live diffs. Dirty buffers retain their edits. Missing, unreadable, or invalid changed files preserve buffer text and report a warning rather than replacing it.
Paste is backend input: it is ignored in Normal, Command, Search, and Visual modes, and inserts literal text in Insert mode, with CRLF/CR normalization and a 4 MiB limit. Pasted text is one undo action, separate from typing before and after it. Rejected paste reports an error without changing text, cursor, mode, or pending insert history. Internal clipboard actions are separate from `Input::Paste` and use already-validated buffer text.
@@ -254,7 +260,7 @@ Each buffer retains its shared text, path, saved state, and undo/redo history. V
| `:buffer_delete [N]` | Close the active or named buffer; refuse unsaved changes |
| `:buffer_force_delete [N]` | Explicitly discard changes and close the active or named buffer |
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 or discard hidden edits; automatic disk checks also cover hidden clean buffers. 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 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.
@@ -364,7 +370,7 @@ 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; Ctrl+u clears the query without editing the underlying buffer. 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`, `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.
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`, `core.pane.force_close`, `core.force_save`, and `core.force_save_and_quit` actions also open canonical Ex prompts, requiring another Enter before discarding or overwriting 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.