fix(layout): evaluate (when FORM . SPEC) display specs and measure buffer :align-to from the text area - #354
Conversation
|
Reviewed at
Validation: 39 existing scoped tests passed, including alignment and conditional-prefix integration tests. Four additional temporary regression probes reproduced the findings above; all source edits were restored after the review. The full suite and GUI pixel checks were not rerun for this review. |
…area `(space :align-to center)` (and `left`/`right`) in buffer text came out one left-fringe width too far left: the pixel calculator resolves the region symbol in text-area coordinates (`text_area_left == 0` for buffer rows) while `stretch_width` subtracted a pen measured from the row origin, the text area's left edge in window coordinates. Dashboard's centered banner and title sat 8px left of center on a default frame. GNU `produce_stretch_glyph` (src/xdisp.c:32853-32859, emacs-31.0.90) makes a resolved region coordinate text-area-relative for buffer text, `align_to - window_box_left_offset (it->w, TEXT_AREA)`, keeps window coordinates for mode/header/tab lines (`else if (align_to < 0) align_to = window_box_left_offset (...)`), and only then subtracts the pen. Move the target into the row's origin space for buffer rows; chrome rows have a zero origin and window-coordinate targets already, so they are unchanged. Test: a buffer row with `(space :align-to center)` in a 240px text area yields a 120px stretch with a 0px and with an 8px fringe (was 112px with the fringe). Verified against the rendered surface: neomacs placed a centered 156px image at x=162 where GNU places it at 170. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
A `(when FORM . SPEC)` display spec was applied whenever FORM was merely
non-nil ("resolved structurally"), and a list of specs kept its last
replacing element for strings as well as buffers. Dashboard centers its
banner with a `line-prefix` whose `display` value is
`((when (display-graphic-p) space :align-to (- center (0.5 . IMAGE)))
(when (not (display-graphic-p)) space :align-to (- center 15)))`, so a
graphic frame got the text-terminal clause: the logo sat 15 columns from
the center instead of half its width, 42px too far left with a 17px font.
GNU `handle_single_display_spec` (src/xdisp.c:6130-6160, emacs-31.0.90)
takes nil and t literally and otherwise evaluates FORM through
`dsafe_eval` with `object`, `position` and `buffer-position` bound
(`specbind`), an error counting as nil; `handle_display_spec` stops at
the first element that replaced text of a STRING and lets later elements
override for buffer text (`if (!it || STRINGP (object)) break;`,
src/xdisp.c:6034-6040 and 6055-6061).
The walk cannot run Lisp while it holds the buffer, so the forms of the
span the walk can reach -- the one fontification just covered -- are
evaluated once before it (`Context::display_when_form_holds`, the GNU
bindings and error rule; `evaluate_window_display_when_forms`, the scan of
`display`/`line-prefix`/`wrap-prefix` text and overlay properties, the
buffer-local prefix values, and the `display` properties inside prefix
strings). The results ride in the `LayoutBufferSnapshot` and reach every
reader through `LayoutBufferView::layout_display_when_conditions`: the
text cursor, the row route, the bridge predicate, and the Lisp-string
sources of buffer-scoped sessions (prefixes, overlay strings, display
replacements). `classify_display_property` takes the conditions and the
object kind, with the string-vs-buffer first/last rule.
Declared, not GNU: a FORM is evaluated once with its first occurrence's
bindings (GNU evaluates every occurrence); frame-local chrome strings and
readers without a snapshot keep the structural rule (non-nil holds); a
FORM the scan did not see falls back to it too.
Tests (red first): the evaluator binds the three variables, unbinds them,
and maps an error to nil; the scan evaluates forms from text properties,
overlays and the buffer-local prefix; the classifier honours a false
result and the first-replacing string rule; a realized GUI frame laying
out a prefix with the text-terminal clause listed FIRST selects the
graphic clause (column 10, not 20) -- red when the engine does not attach
the evaluated results.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
… line numbers Adversarial review of the two previous commits (four blocking findings, three confirmed against the mirror, the fourth confirmed on a different code path than it named): - `when` forms ran with the caller's buffer current. GNU's iterator runs after `set_buffer_internal_1 (XBUFFER (w->contents))` (src/xdisp.c: 20533-20535); `Context::with_display_buffer_current` now selects the window's buffer around the evaluation and restores the caller's, the same switch fontification uses. - `display_when_form_holds` was a bare `eval`. GNU `dsafe_eval` is `dsafe_calln (true, Qeval, sexpr, Qt)` (src/xdisp.c:3170-3173): it binds `inhibit-redisplay` and `inhibit-quit`, evaluates lexically, and maps an error to nil (3118-3131). Ported with the `try_specbind_or_unwind_to` / `unbind_to_with_result` pattern the other `dsafe_call` sites use. Declared: no `inhibit-eval-during-redisplay` variable here, and the catch-all condition frame that keeps the debugger out is not ported (as for the other `dsafe_call` ports). - `(disable-eval …)` pushed a nil FORM into the table, so the real FORM was never found and the spec applied. GNU takes FORM as nil there (src/xdisp.c:6139-6140); the site now records the FORM as failing. - Buffer-text `:align-to` under `display-line-numbers`. Buffer `display` stretches resolve through `DisplaySpaceGeometry`, whose pixel context had no line-number width, so `center` was half the full text width where GNU puts it at the field plus half of the remainder (vanilla 31.0.90 oracle: 28px field, 560px text area, `center` at 294px, `:align-to 10` at 98px). The context now carries the field and the width past it; a raw number is measured from the text-area edge with the field folded into it (`XFLOATINT (prop) * base_unit + lnum_pixel_width`, src/xdisp.c:30248; `align_to = 0`, 32853-32859). The row builder, which serves prefix and overlay strings, counted the field's glyphs into the pen a second time; it now takes them out as GNU does (`x -= it->lnum_pixel_width`, src/xdisp.c:32846-32848) through the field width the row geometry carries. Declared: the two resolvers remain separate implementations of one GNU rule; continuation-row and horizontal-scroll pen adjustments (src/xdisp.c:32841-32843, 32862-32874) are not ported. Also from the review: the scan covers overlay `before-string`/ `after-string`s and the default `line-prefix`/`wrap-prefix` values; the evaluation runs inside the freshness/topology guards; the conditions are a closed `Structural | Evaluated` type with an explicit `Unseen` verdict; the classifier's doc is back on the function; citations corrected. Tests: evaluation binds and unbinds `inhibit-redisplay`/`inhibit-quit`; the window's buffer is current and restored; a `disable-eval` form fails; a single false clause leaves the prefix out (red on the old structural rule regardless of the first/last rule); under `display-line-numbers` a numeric target moves by the whole field and `center` by half of it for buffer text and for prefix strings alike (red before on both paths). Verified on macOS; Linux and Windows not run (no platform-specific code). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
…ign-to` Adversarial review of the three previous commits (three blocking findings, all confirmed): - The `center` arithmetic ported was Emacs master's, not the 31.0.90 baseline: the "vanilla oracle" on this machine is a 32.0.50 build, whose `center` arm is `(LNUM_ONCE + window_box_width) / 2`, while 31.0.90 has `window_box_left_offset + lnum_pixel_width + window_box_width / 2` (src/xdisp.c:30438-30441) with no line-number term in `window_box_width` (src/xdisp.c:1279-1300). Shrinking the context's text width to reproduce master also double-subtracted the field in the `text` arm (src/xdisp.c:30418-30421). Both resolvers now hand the calculator GNU's numbers -- the whole box width plus the field -- and measure from the text-area edge: the geometry path keeps `params.text_bounds.width`; the row builder's context is the append width plus the field with `line_number_pixel_width` set, and buffer rows base both raw and region targets on `content_x - line_number_width`. Under line numbers a numeric target and `center` therefore both move by the whole field (31.0.90); master's half-field `center` is declared in the comments and tests. - The site values were held in Rust locals across Lisp evaluation. The collector is precise (tagged/CONCURRENT_GC.md); a FORM that drops the property carrying a later FORM would free it. The evaluation loop now lives in neovm-core (`Context::evaluate_display_when_sites`), roots every site's form and object through `save_specpdl_roots`/`push_specpdl_root` for the loop's span, and runs with the window's buffer current. - `display_when_form_holds` muted every `Flow`. GNU's `internal_condition_case_n` catches conditions only; the port's `safe_funcall` mutes `Flow::Signal` and propagates the rest. It now returns `Result<bool, Flow>`, mutes signals to `Ok(false)` with `inhibit-debugger` bound like `safe_funcall`, and a non-signal flow makes the engine fall back to the structural rule for that layout (declared: redisplay here cannot unwind a `throw`). Also: an overlay `after-string` binds `buffer-position` to the overlay's end (GNU `it->current.pos`, src/xdisp.c:5919-5923); the `XFLOATINT (prop) * base_unit + lnum_pixel_width` citation is src/xdisp.c:30493 (the previous commit said 30248); `dsafe_eval_handler`'s "Error during redisplay" `*Messages*` line (src/xdisp.c:3098-3104) is declared not ported; `with_display_buffer_current` documents the killed-caller case. Tests: the site loop evaluates each form once with the buffer current and survives a `garbage-collect` inside a form; the line-number engine test now expects the 31.0.90 whole-field shift for `center` on both paths. Verified on macOS; Linux and Windows not run (no platform-specific code). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
Adversarial review of the previous commit found its after-string claim
was not in the diff: an overlay `after-string`'s `buffer-position` was
still the overlay's start. GNU loads the after-string when the iterator
reaches the overlay's end (src/xdisp.c:7172-7173) and binds
`buffer-position` to that position (src/xdisp.c:5919-5923); the site now
uses the overlay end, the before-string and prefixes keep the start.
Also from the review: the scan skips overlays scoped to another window
before it records their strings, as GNU does ("Skip this overlay if it
doesn't apply to IT->w", src/xdisp.c:7153-7156; the window id now reaches
the scan); the once-per-form rule is stated as once per `equal` form (the
table is keyed structurally); a before-string of an overlay starting
before the span is declared bound to the span start; the `dsafe__call`
pieces not ported are listed (nested-redisplay abort, src/xdisp.c:
3127-3136; `backtrace-on-redisplay-error`, 3159-3160); the geometry path
declares the continuation/hscroll pen terms and GNU's one-pixel stretch
past the target (src/xdisp.c:32882-32883); a coordinate comment now says
frame-absolute; the engine's fallback log no longer calls a dead buffer a
non-error flow.
Tests: an after-string's form sees the overlay end and a before-string's
the start; an overlay with a `window` property is scanned only for that
window. Verified on macOS at 2x (numeric and image-based targets land
where GNU's arithmetic puts them for the rendered image); Linux and
Windows not run (no platform-specific code).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
…hen` scan Adversarial review of the previous commit: the scan computed for itself where an overlay's before- and after-strings are displayed, and drifted from the walk on two GNU rules -- an overlay whose text is invisible shows both strings at whichever end the walk reaches (src/xdisp.c: 7158-7175), and the strings of an overlay that starts or ends exactly at the iterator position are loaded there (src/xdisp.c:7141-7150), so an overlay ending at the window start still shows its after-string on the first row. The scan now collects the overlay boundaries inside the span and asks `RustTextPropAccess::overlay_strings_at`, the walk's own resolver, which strings are displayed at each, binding `buffer-position` to that position (`it->current.pos`, src/xdisp.c: 5919-5923); the `window` filter, the invisible-text rule and the load window have one owner. Strings displayed at a boundary outside the span are not evaluated (declared). The overlay `display`/prefix properties keep the overlay loop with the same `window` filter, which GNU applies to properties through `overlay_matches_window` as well as to strings. Also from the review: the geometry path no longer declares a phantom gap -- GNU's `zero_width_ok_p` (src/xdisp.c:32860, 32876) gives a past-target `:align-to` zero width, as here -- and the overlay-prefix comment declares that GNU reads a prefix at each row start (src/xdisp.c:25131-25132, 25180) where the scan binds the overlay start. Tests: a hidden overlay's after-string sees the overlay start; an overlay ending at the window start is scanned (its nil form fails) while one ending past the span stays unseen. Linux and Windows not run (no platform-specific code). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
Adversarial review of the previous commit: the scan kept only overlay boundaries strictly before the span end, so strings anchored at point-max -- a completion popup's empty overlay with its candidates in `before-string`, or an overlay ending at point-max with an after-string -- were never evaluated although the walk displays them through its end-of-buffer anchor path (GNU `reseat` -> `handle_stop` at ZV, src/xdisp.c:8046-8052). The span is closed at its end now. Also from the review: the overlay `display`/prefix membership is decided from the overlay's own start and end instead of a per-overlay range query compared structurally; an empty overlay contributes no property, as GNU `get_char_property_and_overlay` skips an overlay whose end is at or before the position (`node->end < pos + 1`, src/textprop.c:652-653); the accessor is built with `new_for_optional_window`. Tests: an empty overlay at point-max and an overlay ending there have their strings' forms evaluated; the beyond-span case ends past the span. Linux and Windows not run (no platform-specific code); clippy clean on the changed file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
…re scan bounds Upstream's architecture tests now require every `emacs_core/<domain>` entry to be a subsystem directory and out-of-line tests to live under a `tests/` directory; the two loose files this branch added under `emacs_core/display/` were the only remaining violators after the rebase. Move them to `display/display_when/mod.rs` and `display/display_when/tests/mod.rs`. From the sixth review of the scan: the span end is declared -- a `to` short of the buffer end is the fontification budget, so boundaries and rows past the last displayed one are evaluated where GNU would not reach them; the strings anchored at ZV are loaded by `next_element_from_buffer` (src/xdisp.c:9846-9860), which the comment now cites instead of `reseat`; and GNU's single-winner rule for overlay properties (src/textprop.c:656-675) is declared, the scan evaluating every covering overlay's form. Test: an empty overlay's `display` form stays unseen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
Rebuild window bodies with evaluated when forms instead of replaying glyphs whose arbitrary Lisp dependencies are absent from the retained key. Keep disable-eval local to each property so equal enabled forms remain independent. Preserve property clause order during evaluation and use the layout classifier to stop string evaluation at the first replacing clause. Root saved elements and freshly decoded forms/payloads across mutation and GC. Temporarily install nonselected window point using an unwind-protected marker. Add ten regression tests with red/green verification for the four findings, condition ordering, point restoration on error, and both detached-payload GC cases. Correct the documented GNU buffer replacement limitation. Validation: targeted layout/core tests and formatting pass; fresh review found no remaining issues. Clippy passes with the pre-existing never_loop lint allowed. Full-suite validation remains inconclusive: the unchanged PR head had 55 failures, and modified full runs aborted with a segmentation fault and stack overflow. Individually checked additional failures pass on both revisions.
Share imagep validation with the display classifier so an invalid or unsupported image does not suppress later conditional string clauses. Update image classification fixtures to use valid descriptors. Save the nonselected window buffer's numeric character position and resolve its byte position after evaluation, including errors and throws. GNU redisplay restores that numeric position rather than a marker that moves when Lisp inserts text before point. Reproduced both failures before implementation. Add six engine tests for invalid, unsupported and valid images, plus multibyte edits with normal, signal and throw outcomes. Conditional display tests (33), display property tests (53), and image tests (110) pass. Formatting and Clippy pass with the existing never_loop lint allowed. Earlier full-suite validation remains inconclusive as recorded in the preceding commit.
GNU handles one WHEN, then one optional margin prefix, before checking ordinary replacement content. Recursive classification could accept a nested wrapper and newly skip a later condition in a display string. Separate classification of resolved payloads from conditional unwrapping and use it directly after evaluation. Restrict margin contents to ordinary replacement kinds so nested margins or WHENs cannot reenter decoding. Reproduced three failing engine cases before changing production code: nested WHEN, WHEN inside a margin, and nested margins. All now evaluate the following condition and display its replacement. Conditional tests (36) and display-property tests (53) pass, as do formatting and Clippy with the pre-existing never_loop lint allowed.
Return an empty classification immediately for resolved WHEN wrappers and invalid images. Falling through after rejecting their replacement could otherwise reinterpret embedded :raise or :height keywords as text modifiers and alter the original text. Both regression tests failed with nonempty modifiers before the fix. Display-property tests (55) and conditional-display tests (36) pass; formatting and Clippy pass with the existing never_loop lint allowed. Linux, Windows, and manual GUI validation were not run. Earlier full-suite validation remains inconclusive as documented in the first fix commit.
7ac86d3 to
acd5feb
Compare
Dashboard's centered banner (and any buffer text using
(space :align-to REGION)or a(when FORM . SPEC)display spec) lands where GNU Emacs 31.0.90 puts it. Two defects, both visible on the dashboard logo::align-toregion targets were measured from the window edge, not the text area.center/left/rightin buffer text came out one left-fringe width too far left (8px on a default frame). GNUproduce_stretch_glyphmakes a resolved region coordinate text-area-relative for buffer text and only then subtracts the pen (src/xdisp.c:32853-32859); chrome rows keep window coordinates and are unchanged.(when FORM . SPEC)was applied whenever FORM was non-nil. Dashboard'sline-prefixcarries(when (display-graphic-p) …)and(when (not (display-graphic-p)) …); a graphic frame took the text-terminal clause (42px off with a 17px font). GNU takes nil/t literally and otherwise evaluates FORM throughdsafe_evalwithobject,positionandbuffer-positionbound, an error counting as nil (src/xdisp.c:6130-6160); a list of specs stops at the first replacing element for strings and lets later ones override for buffer text (6034-6040, 6055-6061).How the
whenforms are evaluatedThe layout walk cannot run Lisp while it holds the buffer, so the forms of the span the walk can reach (the one fontification just covered) are evaluated once before it:
Context::display_when_form_holds(neovm-core,display/display_when/) is thedsafe_evalport: literal nil/t,inhibit-redisplay/inhibit-quit/inhibit-debuggerbound likesafe_funcall, the three GNU bindings, signals muted to nil, other flows propagated.Context::evaluate_display_when_sitesroots every site's form and object (precise collector) and runs with the window's buffer current.evaluate_window_display_when_forms(layout engine,display_when.rs) collects the sites:display/line-prefix/wrap-prefixtext and overlay properties, the buffer-local and default prefix values, thedisplayproperties inside prefix strings, and overlay before/after-strings placed by the walk's own resolver (RustTextPropAccess::overlay_strings_at, so thewindowfilter, the invisible-text rule and the load position have one owner). The scan is closed at its end so strings anchored at point-max are reached (GNUnext_element_from_buffer, src/xdisp.c:9846-9860).LayoutBufferSnapshotand reach every reader throughLayoutBufferView::layout_display_when_conditions: the text cursor, the row route, the bridge predicate, and the Lisp-string sources of buffer-scoped sessions.classify_display_propertytakes the conditions and the object kind (string vs buffer first/last rule).Under line numbers both
:align-toresolvers hand the pixel calculator GNU's numbers (whole box width plus the field) and measure from the text-area edge, so a numeric target andcenterboth move by the whole field as in 31.0.90 (src/xdisp.c:30438-30441, 30493). Emacs master's half-fieldcenteris declared in comments and tests, not ported.Declared gaps (in code comments)
equalform with its first occurrence's bindings; GNU evaluates every occurrence.inhibit-eval-during-redisplay;dsafe_eval_handler's "Error during redisplay" line, the nested-redisplay abort andbacktrace-on-redisplay-errorare not ported.Verification
centerwith and without a fringe), and a realized GUI frame selecting the graphic clause when the text-terminal clause is listed first.Flowpropagation, the 31.0.90 vs mastercenterarithmetic, overlay-string placement through the walk's resolver, after-stringbuffer-positionat the overlay end, point-max anchors).Found while comparing with GNU but not in this PR: at 2x images auto-scale from the device column width and render smaller than GNU's, and the native macOS menu bar is not implemented (menus render in-frame on every platform).
🤖 Generated with Claude Code
https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC