Commit Graph

9 Commits

Author SHA1 Message Date
Levi Neuwirth 2a0884b377 feat(find-file): open a file by path with C-x C-f
Dired arc Stage 0 (docs/dired-framing.md section 10, Q#DR11). Until now
pmacs had no discoverable way to open a file by path: no find-file
command and no C-x C-f binding, so a file entered a session only from
the CLI, an LSP jump, a project-search visit, or C-x C-r, whose prompt
does pass free text through but completes only over the recent list.

The command prompts with completion rooted at the active buffer's
directory, or the process cwd when the buffer has no backing path, and
opens the result through pmacs.window.display_file. A path that does not
exist yet creates a buffer bound to it with the "[new file]" status,
which is Emacs parity and comes from resolve_target_buffer rather than
anything added here. Nothing is written to disk until the user saves.

Two substrate facts shape the design and are documented at the command
rather than left to be rediscovered.

Completion is flat: the files source lists one directory and yields bare
basenames, and a custom function source could not do better, because
sources are called with no arguments and run synchronously outside any
coroutine, so a callback can neither see the input to re-root on nor
await a directory listing. Hierarchical completion is a named Rust
change in the framing.

A selected candidate shadows typed text: recompute_candidates selects
index 0 whenever the candidate list is non-empty, and
resolve_accepted_value returns the candidate over the typed contents. So
typed text reaches the accept handler exactly when the input filters
every candidate away, which for basename candidates under a subsequence
filter means when it contains a separator. That makes the deeper-path
case work verbatim and leaves one hole: a new bare name that is a
subsequence of an existing entry opens the existing file. The acceptance
pins that as a decision rather than an accident; closing it needs a Rust
change to accept semantics that Stage 0 deliberately does not make.

A leading tilde is expanded before the path reaches the core, because
get_or_load_buffer normalizes the path it stores but loads from the raw
one -- so an unexpanded tilde path deduplicates against an already-open
buffer yet fails to load a file that is not open yet.

The prompt field starts empty and names its root in the prompt string
instead: any prefill would contain a separator and silently disable
completion.

Acceptance is dispatch-driven throughout -- a real C-x C-f, real typing,
a real RET -- so a dead binding cannot pass vacuously and the Lua
lifecycle accept(), which bypasses the path interactive input takes, is
not used.
2026-07-25 10:45:50 -04:00
Levi Neuwirth 7b5365cfbf feat(edit): auto-indent on newline (Arc 2)
RET now runs edit.newline-and-indent (builtin/runtime/indent.lua):
one insert/replace of "\n" plus the current line's leading whitespace,
copied verbatim and clipped at the split point (Q#AI3). Region RET
stays a single Replace (CUA type-over, one undo step, one CRDT op);
the selection clears after every successful edit (Q#AI4). Fix-up is
snapshot-guarded against context-switching intercepts and repairs the
cursor by right-gravity translation through the effective edit
(Q#AI5). buffer.newline remains the plain-newline escape hatch.

GPU (Q#AI1/Q#AI6): plain Enter is no longer optimistic-eligible --
its classifier arm's premise (byte-identical to a self-insert) died
with the new binding. Enter round-trips like the TUI, which also
makes global and buffer-local RET rebindings (buffer-list visit)
reachable from the GPU frontend.

Substrate fixes that RET would otherwise ship on top of:

- Q#AI8 search staleness: notify_buffer_edit now marks matches stale
  and right-gravity-translates the live session origin, matching
  apply_active_edit; SearchStore::step and search_match_summary fail
  closed while stale (a live search un-sticks on the next pattern
  keystroke, since set() clears staleness).
- Q#AI9 empty selections: insert_char reports success and the
  no-region arm of insert_char_over_region clears a lingering anchor
  only on Ok -- ordinary typing no longer type-overs its own previous
  keystroke after S-Left at BOF, and a rejected insert mutates no
  state.

Acceptance: tests/auto_indent_acceptance.rs (20 dispatch-driven
cases), tests/auto_indent_crdt_acceptance.rs (pending optimistic
input then round-tripped Enter converges on the source replica),
flipped GPU classifier test, and lib tests for the store, core, and
dispatch seams.

Framing: docs/auto-indent-framing.md (five review rounds).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATiKMwJ4864d82D39EvsU6
2026-07-10 12:11:05 -04:00
Levi Neuwirth 7e7b3f2dcd feat(edit): query-replace (M-% / C-M-%) — Arc 2
Emacs query-replace, built on isearch with zero protocol change
(framing: docs/query-replace-framing.md).

- search.rs: find_first_from (literal) + find_first_regex_from (cached
  engine, zero-width-skip) + compile_search_regex (shared smart-case
  compile). Q#QR2's forward-scan-past-replacement primitive.
- QueryReplaceSession + core methods (editor_core.rs): begin (invalid
  regex refuses, Q#QR2), replace/skip/all/replace-and-quit/finish;
  matches run forward from next_from on the LIVE buffer, so offset
  shifts and never-re-matching-replacements (a->aa) fall out for free;
  current match highlighted via a single-element search_store set
  (SearchMatchActive, both frontends free) + cursor reveal; quit keeps
  replacements, only nothing-matched restores origin (Q#QR10).
- Dispatcher shadow (editor.rs): QueryReplaceKey (y/SPC, n/DEL, !, .,
  q/RET/Esc/C-g) + dispatch_query_replace_key, the 5th modal shadow;
  added to dispatch_idle disjunction (GPU round-trips keys) and fires
  buffer.after-edit itself (Q#QR1 — a shadow returns before the normal
  post-command check; once per !-batch).
- Lua: ed.query_replace_start/query_replace_active; query-replace /
  query-replace-regexp commands (chained minibuffer.read, separate
  from/to history buckets, empty-from reject / empty-to deletion);
  M-% / C-M-% bindings.
- Per-match prompt via core.status → v15 StatusFacts.message band.

Tests: 7 core unit + 11 dispatch_key acceptance (replace/skip/!/./quit,
nothing-matched restore, empty-to deletion, a->aa non-loop, regex incl
invalid, after-edit fires, dispatch_idle gate, explicit M-% AND C-M-%
binding tests) + 5 search unit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:05:23 -04:00
Levi Neuwirth b934723dfd pmacs context menu: clipboard commands/keys + LSP context accessor (Q#CM5/Q#CM6)
The Lua glue that gives the menu real items to surface.

- `edit.copy/cut/paste/select-all` commands (Q#CM6) over the core
  clipboard, with the Emacs kill/yank bindings `M-w`/`C-w`/`C-y` and
  `C-x h` (the CUA trio's keys are already bound: `C-a` line-start,
  `C-v` page-down).
- `pmacs.lsp.active_attachment()` (Q#CM5): a pure, side-effect-free
  attachment lookup for the menu's `symbol`/`diagnostic` visibility
  checks. Unlike `attached_for_active`, it never triggers an attach just
  because the menu opened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 22:03:58 -04:00
Levi Neuwirth 7723f51f12 regex-search: TUI regex mode + multi-line wash (Q#RX3/RX4/RX5)
Wires regex matching into the search session and the terminal
frontend.

SearchSession gains `regex` and `invalid` flags. search_begin takes a
`regex` argument; recompute dispatches find_all_regex (regex) vs
find_all (literal), recording `invalid` when the pattern won't
compile (an invalid pattern clears the matches and shows [invalid]
rather than a stale count). search_toggle_regex flips the mode and
re-runs the current query.

Input: C-M-s / C-M-r start a regex search (search.forward-regex /
search.backward-regex commands → ed.search_start(forward, regex)).
M-r toggles literal <-> regex mid-search — a new SearchKey decoded in
dispatch_search_key, so it works the same in both frontends (the GUI
already round-trips every key while searching). The TUI prompt reads
"Regex I-search:" in regex mode and "[invalid]" when the pattern
won't compile.

Multi-line: SearchView now washes each row a match spans, mirroring
paint_local_selection's per-row clip (newline excluded so a spanning
match doesn't paint a phantom trailing cell). Single-line matches —
every literal match — touch exactly one row, unchanged. The GPU
already fans multi-line ranges per-line, so it needs no change here.

Tests: regex match / smart-case / invalid-flags-and-recovers /
toggle-reinterprets-query (core); C-M-s starts regex + M-r toggles
mid-search (dispatch); multi-line per-row wash (SearchView render).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:46:33 -04:00
Levi Neuwirth 58b68f6ac0 search: TUI incremental isearch input (Q#SR5)
Wires the live-typing half of in-buffer search for the terminal
frontend, on a frontend-agnostic core so the GPU (next commit) can
share it.

EditorCore gains a `search: Option<SearchSession>` (query + origin
cursor + direction) and the `search_*` methods that drive it:
begin records the origin, input_char/backspace re-run `find_all`
against the origin buffer and refocus the match nearest the origin
(failing searches anchor the cursor back at the origin), step walks
the store's active match (wrapping, also usable post-accept), and
finish either keeps the cursor + matches (accept) or restores the
origin and clears them (cancel). The matches live in the shared
`search_store`, so the decorations producer and the TUI SearchView
light up live as you type.

Input routing is intercepted in `EditorState::dispatch_key`: while
a search runs, every key flows through `dispatch_search_key`
(SearchKey::from_chord) instead of the global keymap — printable
chars extend the query, C-s/C-r (and Down/Up) step, RET accepts,
C-g/Esc cancel, BS shortens. This is the same dispatch path the
daemon's `FrontendEvent::Key` round-trip uses, so the daemon-side
search already works; the GPU just needs to route keys + show the
prompt (commit 4). The TUI paints an `I-search: <query> (n/m)`
prompt on the bottom row while keeping the terminal cursor in the
buffer at the active match.

C-s / C-r start the search (search.forward / search.backward Lua
commands → ed.search_start). Both keys were free in the default
map (save is C-x C-s, redo is C-x r), so isearch lands without
disturbing the CUA / Emacs editing keys — no cursor.right rebind
needed (the framing doc had flagged C-f for veto; C-s is cleaner
and Emacs-faithful).

Any edit now marks the buffer's matches stale in apply_active_edit
(M11.8), closing the headline "stale-after-edit linger" bet:
accepted highlights vanish the moment the text they described
changes, rather than painting at wrong offsets.

Tests: EditorCore-level (begin/type/step/wrap/focus-from-origin/
cancel/accept/backspace/smart-case/stale-on-edit) and dispatch-
level acceptance (C-s drives the whole loop; Esc restores; query
keys never self-insert).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:15:26 -04:00
Levi Neuwirth bd728c4705 CUA region semantics — shift selection, region-aware delete + type-over
- S-<arrows>/S-<home>/S-<end> (+C-S word/paragraph variants) extend a
  selection; the TUI grid paints it reverse-video; double-click
  selects the word at point.
- Backspace / Delete consume the active region (delete_region first,
  falling back to single-codepoint semantics).
- Typing replaces the region: buffer.self-insert / newline / tab
  delete_region before inserting. pmacs-gpu cooperates by
  round-tripping keys while an own-window selection is active, so
  the region-aware commands run instead of a raw optimistic op.
- tests/cua_region_acceptance.rs drives the real dispatch path:
  select -> BS/DEL/char/Enter, plus the no-region fallbacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:50:11 -04:00
Levi Neuwirth c8d0d67615 Fix PTY final-output drain race 2026-05-04 09:44:30 -04:00
Levi Neuwirth 4da4b09d5d Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00