diff --git a/docs/comment-toggle-framing.md b/docs/comment-toggle-framing.md new file mode 100644 index 0000000..5296158 --- /dev/null +++ b/docs/comment-toggle-framing.md @@ -0,0 +1,149 @@ +# Comment/uncomment — framing (Arc 2, editing table stakes) + +pmacs has no way to comment code out. This adds the table-stakes +toggle: `M-;` comments or uncomments the current line or the selected +lines, language-aware, as one undoable edit. + +Roadmap: `docs/roadmap-2026-07.md` Arc 2 ("comment/uncomment"). + +## Ground truth (as of `2dde4b8`) + +- **No comment-syntax knowledge exists anywhere** — not in the grammar + registry, not in LSP config, not in Lua. A language → prefix table is + new surface. +- **Language detection**: `active_buffer_language()` in + `builtin/runtime/lsp.lua` chains grammar detection + (`pmacs.parse.language_for_path`) with the user-extensible + `pmacs.lsp.filetypes` map — but it is a **local**. Known languages + today: rust/lua/c/cpp + js/ts via grammars; python, c, go, + tsx/jsx, lua, bash, toml, zig via filetypes. +- **Bindings**: `M-;` (Emacs `comment-dwim`'s home) is free. `C-/` is + taken by undo (terminal `Ctrl+/` ambiguity — three undo bindings + exist), so the VSCode-style toggle chord is unavailable. +- **Undo granularity**: every applied edit pushes one `UndoEntry` + (`src/buffer.rs:130`) — there is no grouping/transaction. N per-line + edits would need N undos. +- From Arc 2 (merged): the buffer mutators return the **effective + post-intercept edit** `(start, end, inserted_len)`; keybound commands + rotate the command boundary and get `buffer.after-edit` from + dispatch; `M-x` gets both via `invoke_interactive` + + `with_after_edit_check`. Comment-toggle inherits all of it for free. + +## Decisions + +### Q#CT1 — Pure Lua, zero Rust changes + +`builtin/runtime/comment.lua`. Everything needed exists: `buf:slice`, +the effective-edit-returning `buf:replace`, `ed.region/cursor/ +goto_byte/clear_selection`, and the language chain. The one lsp.lua +touch: **export** the existing local as +`pmacs.lsp.active_buffer_language()` (one line) rather than replicating +its grammar+filetypes chain and drifting. + +### Q#CT2 — Command + binding + +One command, `edit.toggle-comment`, bound **`M-;`**: + +- **Region active** → toggle the whole lines the region touches, then + clear the selection (CUA convention after a region op) and leave the + cursor at the span start. +- **No region** → toggle the current line, then **move to the next + line** — Emacs's `comment-line` behavior, which makes repeated `M-;` + walk down a block toggling as it goes. + +Named deviation from Emacs: `M-;` in Emacs is `comment-dwim`, whose +no-region case *appends* an empty comment at end of line. That mode is +rarely what modern muscle memory wants from the toggle key; pmacs's +`M-;` behaves like Emacs `C-x C-;` (`comment-line`). DWIM's +append-comment can come later under its own name. + +### Q#CT3 — The comment-string table + +`pmacs.comment.strings` — a public, user-extensible map (the +`pmacs.lsp.filetypes` pattern), language → line-comment prefix: + +`//`: rust, c, cpp, go, zig, javascript, typescript, +javascriptreact, typescriptreact · `--`: lua · `#`: python, bash, +toml, yaml, sh. + +Unknown language (or no language): status *"no comment syntax known +for ``"*, no edit. Users add entries from init.lua: +`pmacs.comment.strings.mylang = ";;"`. **Block comments are deferred** +— line comments cover the table-stakes use, and block toggling has +real edge cases (nesting, mid-line spans) that don't belong in v1. + +### Q#CT4 — Toggle semantics + +Over the span's lines: + +- **Uncomment** when every non-blank line starts (after its + indentation) with the prefix; removal strips the prefix plus one + following space if present. +- **Comment** otherwise: insert `prefix + " "` at the **minimum + indentation column** of the span's non-blank lines (Emacs + `comment-region` style — the comments line up instead of hugging + each line's own indent). **Blank lines are skipped** in both + directions and don't influence the min-indent computation. +- A span that is entirely blank is a no-op with a status. + +Mixed spans (some commented, some not) therefore **comment** — the +double-prefix on already-commented lines round-trips back out, which +is Emacs's behavior and preserves inner commented-out code. + +### Q#CT5 — One edit, one undo step, one CRDT op + +The whole toggle is a **single `buf:replace(span_start, span_end, +new_text)`**: Lua builds the rewritten span, one edit applies it. +Consequences, all deliberate: + +- **One `C-/` undoes the whole toggle** (there is no undo grouping to + lean on; N per-line edits would need N undos). +- One CRDT op for replica frontends. +- One effective-edit verification: the kill-ring discipline — if the + returned `(start, end, inserted_len)` deviates from the request (a + buffer intercept rewrote it), report *"comment toggle altered by + buffer intercept"* and skip the cursor fix-up; the interceptor's + result stands. `pcall`'d, so a rejecting intercept reports rather + than throws. + +### Q#CT6 — Chain/hook plumbing: nothing to build + +Keybound `M-;` rotates the command boundary (breaking kill chains — +correct) and fires `buffer.after-edit` from dispatch's revision check; +`M-x edit.toggle-comment` gets the same via `invoke_interactive` and +the accept-path hook wrapper. Both are the Arc 2 substrate working as +designed — the acceptance suite asserts the hook fires once anyway. + +## Bets + +1. **Single-replace is the right granularity** — no complaint about + whole-span replaces (vs. per-line edits) from CRDT replicas or LSP + didChange (full-text sync makes this moot today). +2. **Min-indent + skip-blank matches expectation** — no "why is my + comment at column 0" or "why did my blank line get a `//`". + +## Deferred (named) + +- Block comments (`/* */`) and mid-line spans. +- `comment-dwim`'s append-comment-at-EOL mode. +- Doc-comment continuation on newline (belongs to auto-indent). +- Per-language *padding* config (always one space in v1). + +## Acceptance (`tests/comment_toggle_acceptance.rs`, dispatch-driven) + +- Rust buffer: `M-;` comments the line (`// ` at indent), cursor moves + to the next line; `M-;` on a commented line uncomments (round-trip, + including the space). +- Region across mixed-indent lines → prefixes at min indent, aligned; + blank line inside the span untouched; selection cleared. +- All-commented span → uncomments; mixed span → comments (inner + prefix preserved, round-trips). +- Lua buffer gets `--`, Python `#`; unknown/no language → status, no + edit. +- **One undo step**: multi-line toggle then a single `buffer.undo` + restores the original text exactly. +- Intercept discipline: rejecting intercept → status, no throw; + transforming intercept → reported, no cursor fix-up. +- `after-edit` probe fires exactly once per toggle (keybound and + `M-x`). +- Kill-chain break: `C-k`, `M-;`, `C-k` → two ring entries.