feat(compile): make building discoverable — journey step 9
Implements `docs/journey-stage1b1-compile-defaults-framing.md` (approved at revision 2). Lua, tests and docs; no Rust change and no protocol change. `C-c c` now runs `compile.run`, and the first prompt is prefilled from the detected project kind through `pmacs.compile.defaults` — seeded `rust = "cargo build"` and extensible from `init.lua`. `_last` still wins, so a session that has compiled keeps its own command. The prompt CAPTURES its directory rather than re-resolving it. Sharing one resolver between the prompt and the run is necessary and not sufficient: `pmacs.minibuffer.read` is asynchronous and nothing freezes the active window while a prompt is open, so two calls to the same resolver at two different moments are still two different answers — the user could be offered `cargo build` for A and handed a run in B by clicking away mid-prompt. This is Journey Stage 1a's `commit_to` discipline on a smaller seam. `pmacs.compile.defaults` is public and assignable, so the lookup is guarded: a throwing `__index`, a non-string entry and a non-table replacement all degrade to the pre-stage empty prompt and never prevent compiling. Only `rust` ships seeded. Rust has one answer; npm/yarn/pnpm, make/cmake, and `go build` versus `go test` do not, and a wrong prefill costs more than an empty one. Adds eight step-9 rows to the journey ratchet and five module pins to the compile suite. Corrects `COHERENCE.md`, which named a `ProjectKind::Cargo` that does not exist — the variant is `Rust`, line 77 is its doc comment, and Lua only ever sees the tag string "rust". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
This commit is contained in:
parent
44c3be1ed4
commit
6bee82c1a5
45
COHERENCE.md
45
COHERENCE.md
|
|
@ -378,7 +378,7 @@ Full verdict table:
|
||||||
| 6 | Language intelligence | **Partial** | Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (`builtin/runtime/lsp.lua:44-52`) — but a missing binary fails silently (§1.2) and highlighting masks it. No LSP status command exists to diagnose |
|
| 6 | Language intelligence | **Partial** | Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (`builtin/runtime/lsp.lua:44-52`) — but a missing binary fails silently (§1.2) and highlighting masks it. No LSP status command exists to diagnose |
|
||||||
| 7 | Find symbol / file | **File: fixed (open by path merged #162; browsing #165). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit. Now `C-x C-f` opens a known path and `C-x d` / `C-x C-j` browse (flat listing, `dired` mode keymap); `M-.`/`M-?`/`C-c o` still bound but advertised nowhere and server-gated; no workspace-symbol command; `pmacs.index.*` has no UI |
|
| 7 | Find symbol / file | **File: fixed (open by path merged #162; browsing #165). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit. Now `C-x C-f` opens a known path and `C-x d` / `C-x C-j` browse (flat listing, `dired` mode keymap); `M-.`/`M-?`/`C-c o` still bound but advertised nowhere and server-gated; no workspace-symbol command; `pmacs.index.*` has no UI |
|
||||||
| 8 | Open terminal | **Works** | Full PTY with scrollback + modeline segment, bound to `C-c t` and configurable through three registered settings (`terminal.default-profile`, `terminal.scrollback-rows`, `terminal.escape-key`) plus named `pmacs.terminal.profiles` (PR #173), and searchable through `M-x terminal.copy-mode` / `C-c C-t`, which materializes the retained scrollback into an ordinary read-only buffer (Stage 2). Named limitations: `C-c t` is unreachable from *inside* a terminal window, where `C-c` is consumed as the escape — `M-x terminal` still works there; and there is still **no close/kill command**, which is the remaining half of this step's discoverability gap. *Was broken outright on the GPU frontend until the double terminal-layout sync was fixed: the child took a `SIGWINCH` storm at tick cadence, so typing into it was impossible while output still flowed.* |
|
| 8 | Open terminal | **Works** | Full PTY with scrollback + modeline segment, bound to `C-c t` and configurable through three registered settings (`terminal.default-profile`, `terminal.scrollback-rows`, `terminal.escape-key`) plus named `pmacs.terminal.profiles` (PR #173), and searchable through `M-x terminal.copy-mode` / `C-c C-t`, which materializes the retained scrollback into an ordinary read-only buffer (Stage 2). Named limitations: `C-c t` is unreachable from *inside* a terminal window, where `C-c` is consumed as the escape — `M-x terminal` still works there; and there is still **no close/kill command**, which is the remaining half of this step's discoverability gap. *Was broken outright on the GPU frontend until the double terminal-layout sync was fixed: the child took a `SIGWINCH` storm at tick cadence, so typing into it was impossible while output still flowed.* |
|
||||||
| 9 | Build / test | **Partial** | `M-x compile.run` works, defaults cwd to detected project root, parses Rust `-->` errors — but no keybinding, an **empty first prompt** (`initial = last and last.cmdline or ""`, `builtin/runtime/compile.lua:1134-1138`), and no `cargo build`/`cargo test` suggestion despite `ProjectKind::Cargo` existing (`src/project.rs:77`) |
|
| 9 | Build / test | **Works** | Journey Stage 1b-1: `C-c c` runs `compile.run`, and the first prompt is prefilled from the detected project kind (`pmacs.compile.defaults`, seeded `rust = "cargo build"`). The prompt **captures** its directory rather than re-resolving at accept time, so the command it offers and the directory it runs in cannot drift while the minibuffer waits. Still defaults cwd to the detected project root and parses Rust `-->` errors. Named limitation: after `pmacs <dir>` the active buffer is dired's and pathless, so the cwd falls back to the process cwd — §8's execution-location model owns that |
|
||||||
| 10 | Inspect error | **Partial (good once reached)** | `E:n W:n` modeline counts, underlines, `M-g n/p` + ``C-x ` `` walking a unified compile/grep/diag source, message echo, `RET` visits. Gated entirely on step 6 or 9 succeeding first |
|
| 10 | Inspect error | **Partial (good once reached)** | `E:n W:n` modeline counts, underlines, `M-g n/p` + ``C-x ` `` walking a unified compile/grep/diag source, message echo, `RET` visits. Gated entirely on step 6 or 9 succeeding first |
|
||||||
| 11 | See background work | **Works but undiscoverable** | `*workers*` view via `M-x editor.list-workers`; `C-c C-k` cancel-at-point. No keybinding, no statusline spinner/progress indicator anywhere (§9) |
|
| 11 | See background work | **Works but undiscoverable** | `*workers*` view via `M-x editor.list-workers`; `C-c C-k` cancel-at-point. No keybinding, no statusline spinner/progress indicator anywhere (§9) |
|
||||||
| 12 | Close + restore | **Partial** | Per-file cursor+scroll (saveplace), recent files, minibuffer history, autosave recovery all restore zero-config. Open-buffer set and window layout do **not**: desktop-save is opt-in (`pmacs.session.desktop_mode(true)`) *and* a documented no-op under a daemon (`src/desktop.rs:323-326`, `:353-356`, Q#DS9) |
|
| 12 | Close + restore | **Partial** | Per-file cursor+scroll (saveplace), recent files, minibuffer history, autosave recovery all restore zero-config. Open-buffer set and window layout do **not**: desktop-save is opt-in (`pmacs.session.desktop_mode(true)`) *and* a documented no-op under a daemon (`src/desktop.rs:323-326`, `:353-356`, Q#DS9) |
|
||||||
|
|
@ -388,12 +388,14 @@ A journey observation worth keeping verbatim from the audit:
|
||||||
C-M-s` opens all folds, while opening a file, opening a terminal, and
|
C-M-s` opens all folds, while opening a file, opening a terminal, and
|
||||||
running a build have no bindings at all.
|
running a build have no bindings at all.
|
||||||
|
|
||||||
Two of that observation's three examples have since been answered —
|
All three of that observation's examples have now been answered —
|
||||||
opening a file by `C-x C-f` (#162) and opening a terminal by `C-c t`
|
opening a file by `C-x C-f` (#162), opening a terminal by `C-c t`
|
||||||
(#173). **Running a build still has no binding**, and the underlying
|
(#173), and running a build by `C-c c` (Journey Stage 1b-1). **The quote
|
||||||
inversion is a standing bias in how new work gets bound, not three
|
stays as written**: it names a standing bias in how new work gets bound,
|
||||||
isolated omissions: the quote stays as written because it names the
|
not three isolated omissions, and three fixes do not retire a bias. What
|
||||||
pattern, and the pattern is not retired until step 9 is.
|
has changed is that the bias no longer has an uncontested example in the
|
||||||
|
golden journey — the next new surface that ships without a binding is
|
||||||
|
evidence the pattern is live again, and should be read that way.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1542,14 +1544,17 @@ blockers. **State: runs to step 5; thin from step 6 (§2). Mostly wiring,
|
||||||
and unusually cheap:** directory-argument handling (**done**: Journey
|
and unusually cheap:** directory-argument handling (**done**: Journey
|
||||||
Stage 1a); a find-file surface (**done**: #162 open-by-path, #165
|
Stage 1a); a find-file surface (**done**: #162 open-by-path, #165
|
||||||
browsing); surfacing the LSP spawn failure with guidance (§1.2); a
|
browsing); surfacing the LSP spawn failure with guidance (§1.2); a
|
||||||
compile keybinding + `cargo build`/`test` default from the existing
|
compile keybinding + `cargo build`/`test` default (**done**: Journey
|
||||||
`ProjectKind::Cargo`; a terminal keybinding (**done**: `C-c t`, #173); a
|
Stage 1b-1, from the existing `ProjectKind::Rust` — **not** `Cargo`, see
|
||||||
welcome buffer. The journey acceptance suite (§19) is the ratchet that
|
§24); a terminal keybinding (**done**: `C-c t`, #173); a welcome
|
||||||
|
buffer. The journey acceptance suite (§19) is the ratchet that
|
||||||
keeps it fixed — it **exists now** (`tests/journey_acceptance.rs`,
|
keeps it fixed — it **exists now** (`tests/journey_acceptance.rs`,
|
||||||
Stage 1a), seeded with steps 2, 3, and 5.
|
Stage 1a), seeded with steps 2, 3, and 5.
|
||||||
|
|
||||||
Journey Stage 1b is the named remainder: the compile binding + Cargo
|
Journey Stage 1b is the named remainder, and it splits: **1b-1 — the
|
||||||
defaults, LSP spawn guidance, and the welcome buffer.
|
compile binding + project-kind defaults — is done**; 1b-2 (LSP spawn
|
||||||
|
guidance, step 6) and 1b-3 (the welcome buffer, step 4) remain. The
|
||||||
|
ratchet is now seeded with steps 2, 3, 5 and 9.
|
||||||
|
|
||||||
### Priority 2: Make workspace and location explicit
|
### Priority 2: Make workspace and location explicit
|
||||||
|
|
||||||
|
|
@ -1620,8 +1625,10 @@ implementation — this list is direction, not commitment):
|
||||||
`resolve_target_buffer` unification, the destination-scope substrate,
|
`resolve_target_buffer` unification, the destination-scope substrate,
|
||||||
and the first journey acceptance suite. It routes `pmacs .` into
|
and the first journey acceptance suite. It routes `pmacs .` into
|
||||||
#165's dired buffer rather than growing a second directory surface.
|
#165's dired buffer rather than growing a second directory surface.
|
||||||
**Stage 1b — remaining**: compile defaults, LSP-failure surfacing,
|
**Stage 1b-1 — landed**: the compile binding and project-kind
|
||||||
bindings, welcome buffer.
|
defaults, with the prompt capturing its directory rather than
|
||||||
|
re-resolving it at accept time. **Stage 1b-2 / 1b-3 — remaining**:
|
||||||
|
LSP-failure surfacing, welcome buffer.
|
||||||
2. **Discovery surface** (P4): the describe/list/where-is command
|
2. **Discovery surface** (P4): the describe/list/where-is command
|
||||||
family, M-x rich rows, help unification, help prefix.
|
family, M-x rich rows, help unification, help prefix.
|
||||||
3. **Transient keymap layer** (§6): the overlay scope + lifetime
|
3. **Transient keymap layer** (§6): the overlay scope + lifetime
|
||||||
|
|
@ -1713,6 +1720,16 @@ available without being imposed.
|
||||||
Found during the audit; fix opportunistically, ideally before this
|
Found during the audit; fix opportunistically, ideally before this
|
||||||
document is wired into CLAUDE.md/AGENTS.md as required reading:
|
document is wired into CLAUDE.md/AGENTS.md as required reading:
|
||||||
|
|
||||||
|
- **This document named a `ProjectKind` variant that does not exist**,
|
||||||
|
in two places: §2's step-9 row and §20 Priority 1 both said
|
||||||
|
"`ProjectKind::Cargo` existing (`src/project.rs:77`)". Line 77 is the
|
||||||
|
*doc comment*; the variant on line 78 is **`ProjectKind::Rust`**,
|
||||||
|
produced by the `Cargo.toml` marker. The audit read the comment and
|
||||||
|
named the comment. Corrected by Journey Stage 1b-1 — which also
|
||||||
|
establishes that the Lua side never sees the variant at all:
|
||||||
|
`pmacs.project.detect` returns the **tag string** `"rust"`. Kept here
|
||||||
|
rather than silently fixed, because a wrong type name in the document
|
||||||
|
work is evaluated against costs a scout a real detour.
|
||||||
- `docs/keybindings.md` — every `src/editor.rs` line citation in §3 is
|
- `docs/keybindings.md` — every `src/editor.rs` line citation in §3 is
|
||||||
stale by ~250–1000 lines despite a "last verified @ `f8096ff`
|
stale by ~250–1000 lines despite a "last verified @ `f8096ff`
|
||||||
(2026-07-20)" stamp; its shadow list also omits the terminal `C-c`
|
(2026-07-20)" stamp; its shadow list also omits the terminal `C-c`
|
||||||
|
|
|
||||||
|
|
@ -620,6 +620,60 @@ local function daemon_working_directory()
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Where a run started right now would execute. Extracted from the
|
||||||
|
-- inline expression that used to live in `pmacs.compile.run`, because
|
||||||
|
-- the interactive command needs the SAME answer to build its prompt:
|
||||||
|
-- a suggestion computed from a different rule than the run obeys can
|
||||||
|
-- name a toolchain that is not at the directory the command executes
|
||||||
|
-- in (journey Stage 1b-1 framing §3.1).
|
||||||
|
local function resolve_cwd(explicit)
|
||||||
|
return explicit or project_root_of_active() or daemon_working_directory()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Default compile command per detected project kind, keyed by the tag
|
||||||
|
--- `pmacs.project.detect` returns. Assign into this table from
|
||||||
|
--- `init.lua` to add or override one:
|
||||||
|
---
|
||||||
|
--- pmacs.compile.defaults.go = "go build ./..."
|
||||||
|
---
|
||||||
|
--- Only `rust` ships seeded, and that is a decision rather than an
|
||||||
|
--- omission: Rust has one answer, while npm/yarn/pnpm, make/cmake, and
|
||||||
|
--- `go build` versus `go test` do not. A wrong prefill costs more than
|
||||||
|
--- an empty one — the user must delete it before typing.
|
||||||
|
pmacs.compile.defaults = { rust = "cargo build" }
|
||||||
|
|
||||||
|
-- Read `defaults[kind]` defensively. The table is public and
|
||||||
|
-- assignable, so a metatable with a throwing `__index`, a non-string
|
||||||
|
-- entry, or a non-table replacement all have to be survivable — the
|
||||||
|
-- same discipline `validated_rules` keeps for a hostile rule container
|
||||||
|
-- (round-2 finding 3). A broken `defaults` degrades to the pre-stage
|
||||||
|
-- empty prompt; it never prevents compiling.
|
||||||
|
local function default_command_for(kind)
|
||||||
|
if type(kind) ~= "string" then return nil end
|
||||||
|
local ok, value = pcall(function() return pmacs.compile.defaults[kind] end)
|
||||||
|
if ok and type(value) == "string" and #value > 0 then return value end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Where the next compile would run, and what kind of project is
|
||||||
|
--- detected *from that directory*. Public getter (per API
|
||||||
|
--- conventions): `{ cwd = string|nil, kind = string|nil }`.
|
||||||
|
---
|
||||||
|
--- `kind` is always `detect(cwd)`, one rule regardless of which branch
|
||||||
|
--- produced `cwd`. Note that is not the same as "`cwd` is a project
|
||||||
|
--- root": a Cargo workspace subdirectory contains no `Cargo.toml`, is
|
||||||
|
--- detected as `rust` by the ancestor walk, and is a perfectly good
|
||||||
|
--- place to run `cargo build`.
|
||||||
|
function pmacs.compile.context(explicit_cwd)
|
||||||
|
local cwd = resolve_cwd(explicit_cwd)
|
||||||
|
local kind = nil
|
||||||
|
if cwd then
|
||||||
|
local ok, proj = pcall(pmacs.project.detect, cwd)
|
||||||
|
if ok and proj and type(proj.kind) == "string" then kind = proj.kind end
|
||||||
|
end
|
||||||
|
return { cwd = cwd, kind = kind }
|
||||||
|
end
|
||||||
|
|
||||||
local function format_exit_marker(label, ev)
|
local function format_exit_marker(label, ev)
|
||||||
if ev.kind == "exited" then
|
if ev.kind == "exited" then
|
||||||
return string.format("\n[%s exited with code %d]\n", label, ev.code or 0)
|
return string.format("\n[%s exited with code %d]\n", label, ev.code or 0)
|
||||||
|
|
@ -762,7 +816,7 @@ local function start_run(slot, cmdline, opts)
|
||||||
if cur and not pmacs.compile.is_generated_buffer(cur) then
|
if cur and not pmacs.compile.is_generated_buffer(cur) then
|
||||||
slot.prev = cur
|
slot.prev = cur
|
||||||
end
|
end
|
||||||
local cwd = opts.cwd or project_root_of_active() or daemon_working_directory()
|
local cwd = resolve_cwd(opts.cwd)
|
||||||
|
|
||||||
-- Supersede (Q#CM9): terminate the old group and tombstone its
|
-- Supersede (Q#CM9): terminate the old group and tombstone its
|
||||||
-- pump entry; its terminal event still drives forget.
|
-- pump entry; its terminal event still drives forget.
|
||||||
|
|
@ -1132,13 +1186,27 @@ pmacs.command.define {
|
||||||
description = "Compile: run a command in a streaming *compilation* buffer (M-x compile).",
|
description = "Compile: run a command in a streaming *compilation* buffer (M-x compile).",
|
||||||
fn = function()
|
fn = function()
|
||||||
local last = pmacs.compile._last
|
local last = pmacs.compile._last
|
||||||
|
-- Captured ONCE, here. `pmacs.minibuffer.read` is asynchronous and
|
||||||
|
-- nothing freezes the active window while a prompt is open, so
|
||||||
|
-- re-resolving inside `on_accept` would let the prompt offer
|
||||||
|
-- `cargo build` for A and execute in B. Sharing the resolver is
|
||||||
|
-- necessary and not sufficient; the resolution has to be captured.
|
||||||
|
-- This is Journey Stage 1a's `commit_to` discipline on a smaller
|
||||||
|
-- seam — the mechanism differs, the failure prevented is the same.
|
||||||
|
local ctx = pmacs.compile.context()
|
||||||
pmacs.minibuffer.read {
|
pmacs.minibuffer.read {
|
||||||
prompt = "Compile command: ",
|
prompt = "Compile command: ",
|
||||||
history = "compile",
|
history = "compile",
|
||||||
initial = last and last.cmdline or "",
|
-- `_last` still wins: a user who ran `cargo test` once gets it
|
||||||
|
-- back rather than being reset to the project default.
|
||||||
|
initial = last and last.cmdline or default_command_for(ctx.kind) or "",
|
||||||
on_accept = function(cmdline)
|
on_accept = function(cmdline)
|
||||||
if cmdline == nil or cmdline == "" then return end
|
if cmdline == nil or cmdline == "" then return end
|
||||||
pmacs.compile.run(cmdline)
|
-- `ctx.cwd` passes through verbatim, INCLUDING nil: a nil cwd
|
||||||
|
-- means every resolution step failed, and the header renders
|
||||||
|
-- "(unknown)" exactly as before. Re-resolving here would
|
||||||
|
-- reintroduce the drift for the case least able to tolerate it.
|
||||||
|
pmacs.compile.run(cmdline, { cwd = ctx.cwd })
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
|
|
@ -1186,3 +1254,16 @@ pmacs.keymap.bind { scope = "global", sequence = "M-g n", command = "error.next"
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "M-g p", command = "error.previous" }
|
pmacs.keymap.bind { scope = "global", sequence = "M-g p", command = "error.previous" }
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "C-x `", command = "error.next" }
|
pmacs.keymap.bind { scope = "global", sequence = "C-x `", command = "error.next" }
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "M-!", command = "shell.command" }
|
pmacs.keymap.bind { scope = "global", sequence = "M-!", command = "shell.command" }
|
||||||
|
-- Journey step 9 (COHERENCE §2): running a build had no binding at all,
|
||||||
|
-- while `C-c @ C-M-s` opened all folds. `C-c c` is free, sits under the
|
||||||
|
-- established `C-c` prefix, and does not collide with CUA copy (that is
|
||||||
|
-- `M-w`). Bound here rather than in `default.lua` because a runtime
|
||||||
|
-- module owns its own global keys — `terminal.lua` binds `C-c t`,
|
||||||
|
-- `lsp.lua` binds `C-c o`.
|
||||||
|
--
|
||||||
|
-- Two inherited reachability limits, both pre-existing: inside a
|
||||||
|
-- terminal window `C-c` is consumed as the escape key, and the repl
|
||||||
|
-- package binds `C-c` at buffer scope. `M-x compile.run` still works in
|
||||||
|
-- both. `compile.recompile` gets no global chord — `g` in
|
||||||
|
-- `*compilation*` already covers rerun.
|
||||||
|
pmacs.keymap.bind { scope = "global", sequence = "C-c c", command = "compile.run" }
|
||||||
|
|
|
||||||
|
|
@ -253,13 +253,19 @@ If it does not, stop and repair the remote/fetch configuration.
|
||||||
never been enforced. Any CI job that compiles the `crdt` targets has to
|
never been enforced. Any CI job that compiles the `crdt` targets has to
|
||||||
fix them first or it will be red on arrival.
|
fix them first or it will be red on arrival.
|
||||||
|
|
||||||
## Journey lane (P1) — STAGE 1a MERGED; STAGE 1b-1 FRAMING OPEN, revision 2
|
## Journey lane (P1) — STAGE 1a MERGED; STAGE 1b-1 IMPLEMENTED, PR OPEN
|
||||||
|
|
||||||
- **Branch `journey-stage1b1-compile-defaults`**, worktree
|
- **Branch `journey-stage1b1-compile-defaults`**, worktree
|
||||||
`../pmacs-journey-1b1`, based on `githubsucks/main` @ `22df6ab`.
|
`../pmacs-journey-1b1`, based on `githubsucks/main` @ `22df6ab`.
|
||||||
**Framing only; no code, no PR yet.**
|
`docs/journey-stage1b1-compile-defaults-framing.md` revision 2,
|
||||||
`docs/journey-stage1b1-compile-defaults-framing.md` revision 2, one
|
approved after one review round (two blocking, two major, all
|
||||||
review round closed (two blocking, two major, all accepted).
|
accepted). **Implemented; PR open.**
|
||||||
|
- **Bites found two vacuous pins of my own.** The nested-project pin
|
||||||
|
passed with the keybinding removed, because `minibuffer.contents()` is
|
||||||
|
`""` both for an empty prefill and for no minibuffer at all — `""`
|
||||||
|
compared with `""`. And the hostile-`defaults` pin called
|
||||||
|
`compile.run` directly, so it never consulted `defaults` and passed
|
||||||
|
with the guard removed. Both now assert their precondition.
|
||||||
- **Round 1's blocking finding is the Stage 1a lesson repeating.**
|
- **Round 1's blocking finding is the Stage 1a lesson repeating.**
|
||||||
Sharing one cwd resolver between the prompt and the run is *not*
|
Sharing one cwd resolver between the prompt and the run is *not*
|
||||||
enough: `pmacs.minibuffer.read` is async, the active window can change
|
enough: `pmacs.minibuffer.read` is async, the active window can change
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,48 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
interaction islands added, config-registry adoption, background-work
|
interaction islands added, config-registry adoption, background-work
|
||||||
attribution. Its §2 grades the golden journey; **Journey Stage 1a
|
attribution. Its §2 grades the golden journey; **Journey Stage 1a
|
||||||
moved that grade off "broken at step 3"** — see the arc bullet below.
|
moved that grade off "broken at step 3"** — see the arc bullet below.
|
||||||
|
- **Journey arc (P1) — Stage 1b-1 LANDED** (`docs/journey-stage1b1-compile-defaults-framing.md`).
|
||||||
|
Journey step 9 moves **Partial → Works**: `C-c c` runs `compile.run`,
|
||||||
|
and the first prompt is prefilled from the detected project kind via
|
||||||
|
`pmacs.compile.defaults` (seeded `rust = "cargo build"`, extensible
|
||||||
|
from `init.lua`). Lua, tests and docs; no Rust change, no protocol
|
||||||
|
change. The ratchet now carries steps 2, 3, 5 and **9**.
|
||||||
|
- **Sharing a resolver is not capturing one.** `pmacs.minibuffer.read`
|
||||||
|
is asynchronous and nothing freezes the active window while a prompt
|
||||||
|
is open, so having the prompt and `compile.run` call the *same* cwd
|
||||||
|
resolver still let them disagree: the user could be offered
|
||||||
|
`cargo build` for A and given a run in B by clicking away
|
||||||
|
mid-prompt. The interactive command captures `pmacs.compile.context()`
|
||||||
|
once and passes its `cwd` through to the run. **This is Stage 1a's
|
||||||
|
`commit_to` lesson on a smaller seam** — capture at request time,
|
||||||
|
never re-derive when the async work lands. Review found it; the
|
||||||
|
first framing had the weaker design and said it was sufficient.
|
||||||
|
- **A pin that never crosses the accept boundary cannot see that
|
||||||
|
class of bug.** Every originally-proposed pin compared values the
|
||||||
|
prompt and the resolver already agreed on, so the defect above
|
||||||
|
passed all of them. Two pins now accept the prompt and observe a
|
||||||
|
real process.
|
||||||
|
- **`ProjectKind::Cargo` does not exist** — `COHERENCE.md` named it
|
||||||
|
twice, citing `src/project.rs:77`, which is the *doc comment*; the
|
||||||
|
variant on line 78 is `ProjectKind::Rust`. Lua never sees the
|
||||||
|
variant anyway: `pmacs.project.detect` returns the tag string
|
||||||
|
`"rust"`. Corrected in both COHERENCE sites and recorded in its §24.
|
||||||
|
- **The compile fallback cwd is the process cwd, which under `cargo
|
||||||
|
test` is the pmacs repo — itself a Cargo project.** Any pin
|
||||||
|
asserting the *absence* of a Cargo suggestion that reaches the
|
||||||
|
fallback reports pmacs's own `Cargo.toml`. Bite D confirmed it: with
|
||||||
|
the kind derived from the process cwd, the plain-fixture prefill pin
|
||||||
|
still passed and only the nested-project pin caught it.
|
||||||
|
`set_search_boundary` is no defence — it clamps only a walk starting
|
||||||
|
*below* the boundary.
|
||||||
|
- **Named limitation, deliberately unfixed:** after `pmacs <dir>` the
|
||||||
|
active buffer is dired's and **pathless** (`pmacs.buffer.create`
|
||||||
|
assigns no path; dired compensates through its own module-local
|
||||||
|
`handle_for_buffer`), so the cwd falls through to the process cwd.
|
||||||
|
Launched from elsewhere that is the wrong directory. The fix is
|
||||||
|
§8's execution-location model, not a reach into dired's private
|
||||||
|
table. What is guaranteed is that the failure stays *coherent*: the
|
||||||
|
suggestion always describes the directory the run will use.
|
||||||
- **Journey arc (P1) — Stage 1a LANDED**
|
- **Journey arc (P1) — Stage 1a LANDED**
|
||||||
(`docs/journey-stage1a-framing.md`). `pmacs .` opens a directory
|
(`docs/journey-stage1a-framing.md`). `pmacs .` opens a directory
|
||||||
instead of exiting 1, on **one** path: `resolve_target_buffer` gained a
|
instead of exiting 1, on **one** path: `resolve_target_buffer` gained a
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,12 @@ Not in 1a — Stage 1b: a compile keybinding and `cargo build`/`test`
|
||||||
defaults from the existing `ProjectKind::Cargo`, LSP spawn-failure
|
defaults from the existing `ProjectKind::Cargo`, LSP spawn-failure
|
||||||
guidance (§1.2), a welcome buffer.
|
guidance (§1.2), a welcome buffer.
|
||||||
|
|
||||||
|
*(Correction, added by Stage 1b-1: there is no `ProjectKind::Cargo`. The
|
||||||
|
variant is `ProjectKind::Rust`; this line inherited the error from
|
||||||
|
`COHERENCE.md` §20, now recorded in its §24. Left in place rather than
|
||||||
|
rewritten — this document records what was planned — but flagged so the
|
||||||
|
wrong type name does not survive by being quoted.)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Ground truth (scouted 2026-07-26, `main` @ `d400f30`; re-verified rev 4)
|
## 2. Ground truth (scouted 2026-07-26, `main` @ `d400f30`; re-verified rev 4)
|
||||||
|
|
|
||||||
|
|
@ -187,9 +187,13 @@ Source: `builtin/runtime/compile.lua`.
|
||||||
| `M-g n` / `M-g p` | `error.next` / `error.previous` — compile/grep errors when that source has claimed navigation, otherwise LSP diagnostics |
|
| `M-g n` / `M-g p` | `error.next` / `error.previous` — compile/grep errors when that source has claimed navigation, otherwise LSP diagnostics |
|
||||||
| `` C-x ` `` | `error.next` |
|
| `` C-x ` `` | `error.next` |
|
||||||
| `M-!` | `shell.command` — asynchronous output in `*shell-command*` |
|
| `M-!` | `shell.command` — asynchronous output in `*shell-command*` |
|
||||||
|
| `C-c c` | `compile.run` — prompts, prefilled from the detected project kind |
|
||||||
|
|
||||||
`compile.run` and `compile.recompile` are available through `M-x`; no
|
`compile.recompile` is available through `M-x`, and through `g` inside
|
||||||
global key is assigned to them.
|
`*compilation*`; no global key is assigned to it. `C-c c` is unreachable
|
||||||
|
from inside a terminal window (`C-c` is consumed as the escape key) and
|
||||||
|
inside a repl buffer (which binds `C-c` at buffer scope); `M-x
|
||||||
|
compile.run` still works in both.
|
||||||
|
|
||||||
## 2. Buffer-local panel keymaps
|
## 2. Buffer-local panel keymaps
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2750,3 +2750,106 @@ fn r5f3_tracked_line_start_matches_the_scan_across_transitions() {
|
||||||
"every rewind lands at the tracked line start; buffer:\n{text:?}"
|
"every rewind lands at the tracked line start; buffer:\n{text:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Journey Stage 1b-1 — `pmacs.compile.defaults` is public and assignable
|
||||||
|
//
|
||||||
|
// The table is user-writable, so a metatable with a throwing `__index`, a
|
||||||
|
// non-string entry, and a non-table replacement all have to be
|
||||||
|
// survivable — the same discipline `validated_rules` keeps for a hostile
|
||||||
|
// rule container. A broken `defaults` must degrade to the pre-stage empty
|
||||||
|
// prompt; it must never prevent compiling.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Open the compile prompt and return what it offered.
|
||||||
|
fn compile_prompt_initial(s: &EditorState) -> String {
|
||||||
|
exec(s, "pmacs.command.invoke('compile.run')");
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(s, "return pmacs.minibuffer.is_active()"),
|
||||||
|
"compile.run must open a prompt even with a hostile defaults table"
|
||||||
|
);
|
||||||
|
eval(s, "return pmacs.minibuffer.contents()")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_non_table_defaults_degrades_to_an_empty_prompt() {
|
||||||
|
let s = editor();
|
||||||
|
exec(&s, "pmacs.compile.defaults = 42");
|
||||||
|
assert_eq!(compile_prompt_initial(&s), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_raising_defaults_index_degrades_to_an_empty_prompt() {
|
||||||
|
let s = editor();
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
"pmacs.compile.defaults = setmetatable({}, {
|
||||||
|
__index = function() error('hostile') end,
|
||||||
|
})",
|
||||||
|
);
|
||||||
|
assert_eq!(compile_prompt_initial(&s), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_non_string_default_is_ignored() {
|
||||||
|
let s = editor();
|
||||||
|
exec(&s, "pmacs.compile.defaults = { rust = {}, }");
|
||||||
|
assert_eq!(compile_prompt_initial(&s), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_hostile_defaults_still_lets_a_typed_command_run() {
|
||||||
|
// The consequence that matters: degradation must not cost the user
|
||||||
|
// the ability to compile. Asserts output produced, not a property
|
||||||
|
// preserved.
|
||||||
|
let mut s = editor();
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
"pmacs.compile.defaults = setmetatable({}, {
|
||||||
|
__index = function() error('hostile') end,
|
||||||
|
})",
|
||||||
|
);
|
||||||
|
// Through the INTERACTIVE command, so the hostile lookup actually
|
||||||
|
// happens. Calling `pmacs.compile.run` directly never consults
|
||||||
|
// `defaults` at all, which made an earlier draft of this pin vacuous
|
||||||
|
// — it passed with the guard removed.
|
||||||
|
exec(&s, "pmacs.command.invoke('compile.run')");
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(&s, "return pmacs.minibuffer.is_active()"),
|
||||||
|
"the prompt must still open"
|
||||||
|
);
|
||||||
|
exec(&s, "pmacs.minibuffer.set_contents('echo still-compiles')");
|
||||||
|
exec(&s, "pmacs.minibuffer.accept()");
|
||||||
|
assert!(
|
||||||
|
pump_until(&mut s, 10_000, |s| {
|
||||||
|
compilation_text(s).contains("still-compiles")
|
||||||
|
}),
|
||||||
|
"a broken defaults table must not prevent compiling; got:\n{}",
|
||||||
|
compilation_text(&s)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_context_reports_the_cwd_a_run_would_use() {
|
||||||
|
// `pmacs.compile.context` and `pmacs.compile.run` must answer from
|
||||||
|
// one resolution — an explicit cwd is honoured by both, and the kind
|
||||||
|
// is detected from that same directory.
|
||||||
|
let s = editor();
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
std::fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"x\"\n").expect("write");
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
&format!(
|
||||||
|
"pmacs.project.set_search_boundary({:?})",
|
||||||
|
dir.path().display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let kind: String = eval(
|
||||||
|
&s,
|
||||||
|
&format!(
|
||||||
|
"return pmacs.compile.context({:?}).kind",
|
||||||
|
dir.path().display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(kind, "rust", "the kind is detected from the given cwd");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1297,3 +1297,374 @@ fn preservation_display_file_still_refuses_a_directory() {
|
||||||
"a refused display_file must not create a buffer"
|
"a refused display_file must not create a buffer"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Step 9 — build or test the project (Journey Stage 1b-1)
|
||||||
|
//
|
||||||
|
// `COHERENCE.md` §2 graded this **Partial**: `M-x compile.run` worked but
|
||||||
|
// had no keybinding, an empty first prompt, and no `cargo build`
|
||||||
|
// suggestion. These rows are the ratchet for the three.
|
||||||
|
//
|
||||||
|
// The trap these fixtures are built around: compile's last-resort cwd is
|
||||||
|
// `std::env::current_dir()` evaluated at call time, which under `cargo
|
||||||
|
// test` is the **pmacs repo root — itself a Cargo project**
|
||||||
|
// (`compile_mode_acceptance.rs` pins exactly that). So any pin asserting
|
||||||
|
// the *absence* of a Cargo suggestion must be shaped so the fallback is
|
||||||
|
// never consulted: each fixture carries its own marker, and
|
||||||
|
// `project_root_of_active()` therefore answers from inside the fixture.
|
||||||
|
// `set_search_boundary` is not the defence — it clamps only a walk that
|
||||||
|
// starts below the boundary, and the fallback's walk starts at the repo.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn ctrl(s: &mut EditorState, c: char) {
|
||||||
|
s.dispatch_key(
|
||||||
|
FrontendId::LOCAL,
|
||||||
|
key(KeyCode::Char(c), KeyModifiers::CONTROL),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the compile prompt through the real `C-c c` binding.
|
||||||
|
fn press_compile_chord(s: &mut EditorState) {
|
||||||
|
ctrl(s, 'c');
|
||||||
|
type_char(s, 'c');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Cargo project a journey can plausibly be run against.
|
||||||
|
fn cargo_project() -> TempDir {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
std::fs::write(
|
||||||
|
td.path().join("Cargo.toml"),
|
||||||
|
b"[package]\nname = \"journey-fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
|
||||||
|
)
|
||||||
|
.expect("write Cargo.toml");
|
||||||
|
std::fs::create_dir_all(td.path().join("src")).expect("mkdir src");
|
||||||
|
std::fs::write(td.path().join("src/main.rs"), b"fn main() {}\n").expect("write main.rs");
|
||||||
|
td
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp detection to the fixture so a stray marker above the tempdir
|
||||||
|
/// (a developer's `/tmp/.git`) cannot leak in.
|
||||||
|
fn bound_detection_to(s: &EditorState, dir: &Path) {
|
||||||
|
exec(
|
||||||
|
s,
|
||||||
|
&format!(
|
||||||
|
"pmacs.project.set_search_boundary({:?})",
|
||||||
|
dir.display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minibuffer_active(s: &EditorState) -> bool {
|
||||||
|
eval(s, "return pmacs.minibuffer.is_active()")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minibuffer_contents(s: &EditorState) -> String {
|
||||||
|
eval(s, "return pmacs.minibuffer.contents()")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_text(s: &EditorState, name: &str) -> String {
|
||||||
|
eval(
|
||||||
|
s,
|
||||||
|
&format!(
|
||||||
|
r#"
|
||||||
|
for _, id in ipairs(pmacs.buffer.list()) do
|
||||||
|
if pmacs.describe.buffer(id).name == {name:?} then
|
||||||
|
return id:slice(0, id:len())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drive frames until `pred` holds, pumping the process supervisor as
|
||||||
|
/// well as the async runtime — a compile run is a spawned child.
|
||||||
|
fn pump_processes_until(
|
||||||
|
s: &mut EditorState,
|
||||||
|
timeout_ms: u64,
|
||||||
|
mut pred: impl FnMut(&EditorState) -> bool,
|
||||||
|
) -> bool {
|
||||||
|
let stop = Instant::now() + Duration::from_millis(timeout_ms);
|
||||||
|
loop {
|
||||||
|
if pred(s) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if Instant::now() >= stop {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
s.tick_processes();
|
||||||
|
s.tick_async();
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the journey to the point where a build would be run: launch on
|
||||||
|
/// the directory, then `RET` on `name` to open a file.
|
||||||
|
fn walk_to_open_file(dir: &Path, name: &str) -> EditorState {
|
||||||
|
let mut s = launch(dir);
|
||||||
|
bound_detection_to(&s, dir);
|
||||||
|
let line = line_of(&s, name);
|
||||||
|
exec(&s, &format!("pmacs.editor.move_to_line({line})"));
|
||||||
|
press(&mut s, KeyCode::Enter);
|
||||||
|
pump(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
active_name(&s),
|
||||||
|
dir.join(name).display().to_string(),
|
||||||
|
"precondition: the walk must actually open the file, or every pin \
|
||||||
|
below is really testing the dired buffer"
|
||||||
|
);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — `C-c c` reaches `compile.run`. Separate from N2 because a
|
||||||
|
/// prefill assertion alone stays green if the binding is removed and the
|
||||||
|
/// prompt is opened another way — and the binding is the thing
|
||||||
|
/// `COHERENCE.md` says is missing.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_compile_chord_opens_the_prompt() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert!(
|
||||||
|
minibuffer_active(&s),
|
||||||
|
"C-c c must open the compile prompt; active buffer is {}",
|
||||||
|
active_name(&s)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the prompt is prefilled from the detected project kind.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_prompt_is_prefilled_for_a_cargo_project() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"cargo build",
|
||||||
|
"a Rust project's first compile prompt must offer its build command"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the directory the prompt captured survives a window switch.
|
||||||
|
///
|
||||||
|
/// The prompt is opened against A, the active buffer then moves to an
|
||||||
|
/// unrelated directory B, and only then is the command accepted. Both
|
||||||
|
/// readings — the header pmacs writes and the shell's own `pwd` — must
|
||||||
|
/// say A. Falsified by dropping `{ cwd = ctx.cwd }` from `on_accept`,
|
||||||
|
/// which is what re-resolving at accept time looks like.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_prompt_runs_in_the_directory_it_captured() {
|
||||||
|
let a = cargo_project();
|
||||||
|
let b = project();
|
||||||
|
let mut s = walk_to_open_file(a.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert!(minibuffer_active(&s), "prompt must be open against A");
|
||||||
|
|
||||||
|
// Move the active buffer to B while the prompt waits for input.
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
&format!(
|
||||||
|
"pmacs.window.display_file({:?})",
|
||||||
|
b.path().join("alpha.txt").display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
pump(&mut s);
|
||||||
|
|
||||||
|
// Accept a cheap command: the subject is the directory, so a fast
|
||||||
|
// one keeps the failure message about the directory. Only the
|
||||||
|
// editing of an already-open prompt is short-circuited; the prompt
|
||||||
|
// itself was opened through the real chord.
|
||||||
|
exec(&s, "pmacs.minibuffer.set_contents('pwd')");
|
||||||
|
press(&mut s, KeyCode::Enter);
|
||||||
|
let finished = pump_processes_until(&mut s, 10_000, |s| {
|
||||||
|
named_text(s, "*compilation*").contains("exited")
|
||||||
|
});
|
||||||
|
assert!(finished, "the accepted run must finish");
|
||||||
|
|
||||||
|
let text = named_text(&s, "*compilation*");
|
||||||
|
let a_path = canon(a.path());
|
||||||
|
assert!(
|
||||||
|
text.contains(&format!("Directory: {a_path}")),
|
||||||
|
"the header must name the directory the prompt captured, not the newly active one;\n{text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains(&a_path),
|
||||||
|
"and `pwd` must agree with the header;\n{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the offered command runs, in the offered directory.
|
||||||
|
///
|
||||||
|
/// Every other pin here compares values the prompt and the resolver
|
||||||
|
/// already agree on. A wrong directory inside `on_accept` passes all of
|
||||||
|
/// them. This one accepts what was offered, unedited, and observes a
|
||||||
|
/// real process.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_offered_command_builds_the_project() {
|
||||||
|
if !binary_available("cargo") {
|
||||||
|
if std::env::var_os("PMACS_REQUIRE_CARGO_BUILD").is_some() {
|
||||||
|
panic!("PMACS_REQUIRE_CARGO_BUILD is set but `cargo` is not on PATH");
|
||||||
|
}
|
||||||
|
eprintln!("skipping: `cargo` is not on PATH");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"cargo build",
|
||||||
|
"precondition: the prompt offers the build command"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Accept exactly what was offered — no editing.
|
||||||
|
press(&mut s, KeyCode::Enter);
|
||||||
|
let finished = pump_processes_until(&mut s, 120_000, |s| {
|
||||||
|
named_text(s, "*compilation*").contains("exited")
|
||||||
|
});
|
||||||
|
assert!(finished, "the offered build must finish");
|
||||||
|
|
||||||
|
let text = named_text(&s, "*compilation*");
|
||||||
|
assert!(
|
||||||
|
text.contains(&format!("Directory: {}", canon(td.path()))),
|
||||||
|
"the build runs in the detected project root;\n{text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains("[compile exited with code 0]"),
|
||||||
|
"and the command that was offered is one that actually runs;\n{text}"
|
||||||
|
);
|
||||||
|
// Positive control on the run itself. A clean exit alone does not
|
||||||
|
// prove cargo compiled anything — this names the fixture crate, so
|
||||||
|
// the pin fails if the offered command ever stops being a build.
|
||||||
|
assert!(
|
||||||
|
text.contains("journey-fixture"),
|
||||||
|
"cargo must actually have built the fixture crate;\n{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the suggestion follows the directory the run will use.
|
||||||
|
///
|
||||||
|
/// A Node project nested inside a Cargo one, opened at the *inner* file.
|
||||||
|
/// Falsified by deriving the kind from the launch directory (the Cargo
|
||||||
|
/// root here) or from the process cwd — both yield `cargo build`.
|
||||||
|
///
|
||||||
|
/// It does **not** catch re-detecting from `project_root_of_active()`'s
|
||||||
|
/// answer: that helper already returns the innermost root, so detecting
|
||||||
|
/// from it yields `node` again and this pin stays green.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one() {
|
||||||
|
let outer = cargo_project();
|
||||||
|
let sub = outer.path().join("sub");
|
||||||
|
std::fs::create_dir_all(&sub).expect("mkdir sub");
|
||||||
|
std::fs::write(sub.join("package.json"), b"{ \"name\": \"inner\" }\n").expect("write pkg");
|
||||||
|
std::fs::write(sub.join("index.js"), b"console.log(1)\n").expect("write index.js");
|
||||||
|
|
||||||
|
let mut s = walk_to_open_file(&sub, "index.js");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
// `minibuffer_contents` is "" both for an empty prefill and for no
|
||||||
|
// minibuffer at all, so the emptiness assertion below is vacuous
|
||||||
|
// without this. Bite A caught it: with the binding removed this pin
|
||||||
|
// stayed green while six others failed.
|
||||||
|
assert!(
|
||||||
|
minibuffer_active(&s),
|
||||||
|
"precondition: the prompt is actually open"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
eval::<String>(&s, "return pmacs.compile.context().kind"),
|
||||||
|
"node",
|
||||||
|
"the kind is detected from the resolved cwd, not the launch directory"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"",
|
||||||
|
"and `node` has no seeded default, so nothing is offered"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — `context()` is total in a launched session.
|
||||||
|
///
|
||||||
|
/// Asserted with the pathless dired buffer active, so the fallback
|
||||||
|
/// branch is the one under test. It pins a *property*, deliberately not
|
||||||
|
/// a value: which directory the fallback resolves to is the test
|
||||||
|
/// runner's cwd and pinning it would pin the environment.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_compile_context_is_total_even_with_no_file_open() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let s = launch(td.path());
|
||||||
|
assert!(
|
||||||
|
active_name(&s).starts_with("*dired:"),
|
||||||
|
"precondition: the pathless dired buffer is active"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(&s, "return pmacs.compile.context().cwd ~= nil"),
|
||||||
|
"a launched session always has somewhere to run a build"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(
|
||||||
|
&s,
|
||||||
|
"local c = pmacs.compile.context()
|
||||||
|
local ok, p = pcall(pmacs.project.detect, c.cwd)
|
||||||
|
local detected = (ok and p) and p.kind or nil
|
||||||
|
return c.kind == detected"
|
||||||
|
),
|
||||||
|
"and its kind is exactly what detection answers for that cwd"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P** — `_last` still outranks the kind default.
|
||||||
|
///
|
||||||
|
/// Green on the pre-image (there was no default). The targeted mutation
|
||||||
|
/// is reordering the precedence chain to put `defaults[kind]` first.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_preservation_the_last_command_outranks_the_default() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
exec(&s, "pmacs.compile.run('true')");
|
||||||
|
let finished = pump_processes_until(&mut s, 10_000, |s| {
|
||||||
|
named_text(s, "*compilation*").contains("exited")
|
||||||
|
});
|
||||||
|
assert!(finished, "precondition: a run completed and set _last");
|
||||||
|
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"true",
|
||||||
|
"a session that has compiled keeps its own command, not the project default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P** — the compile keys that already existed still dispatch.
|
||||||
|
///
|
||||||
|
/// Targeted mutation: writing the new binding as an unbind+bind pair
|
||||||
|
/// over one of these sequences.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_preservation_the_existing_compile_bindings_survive() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
for (sequence, command) in [
|
||||||
|
("M-g n", "error.next"),
|
||||||
|
("M-g p", "error.previous"),
|
||||||
|
("C-x `", "error.next"),
|
||||||
|
("M-!", "shell.command"),
|
||||||
|
("C-c c", "compile.run"),
|
||||||
|
] {
|
||||||
|
let bound: String = eval(
|
||||||
|
&s,
|
||||||
|
&format!("return pmacs.keymap.lookup({sequence:?}).command"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bound, command,
|
||||||
|
"{sequence} must still dispatch to {command}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when `name` resolves on PATH — the fixture-dependency gate the
|
||||||
|
/// process suite uses, so a missing binary skips rather than fails.
|
||||||
|
fn binary_available(name: &str) -> bool {
|
||||||
|
std::process::Command::new("which")
|
||||||
|
.arg(name)
|
||||||
|
.output()
|
||||||
|
.is_ok_and(|o| o.status.success())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue