ekp/tests/ekp-gui-verify.el
Kinneyzhang 83352c4571 feat!: add source-clean live buffer layout
Replace the physical ekp-region renderer with ekp-buffer text-property projection backed by shared semantic layout plans and stable live-edit transactions.

Optimize the C-backed resize hot path, and add deterministic performance, ERT, GUI, fuzz, release, and documentation coverage for task017 through task032.

BREAKING CHANGE: require ekp-buffer instead of ekp-region and rename module-owned ekp-region-* settings to ekp-buffer-*.
2026-07-30 01:07:25 +08:00

1202 lines
50 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; ekp-gui-verify.el --- GUI pixel-fit verification for ekp -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 Kinney Zhang
;; Author: Kinney Zhang <kinneyzhang666@gmail.com>
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Ground-truth verification that justified text really fits the
;; window, measured with `window-text-pixel-size' on the live display
;; — the one thing batch tests cannot check.
;;
;; Two ways to run:
;;
;; M-x ekp-gui-verify in ANY running GUI Emacs — including your
;; fully customized session. Use this when
;; justified text looks truncated: it tells
;; you whether measurement matches rendering
;; under your fonts/remappings.
;;
;; ${EMACS} -Q -L . -L tests -l tests/ekp-gui-verify.el \
;; -f ekp-gui-verify-matrix
;; runs the full matrix (plain, text-scale
;; up/down, face remap, narrow+scale) and
;; prints a PASS/FAIL table.
;;
;; Criterion: every justified line's rendered width equals the target
;; width (± `ekp-buffer-margin-pixel'). Verbatim paragraphs
;; (`ekp-verbatim') are exempt — code blocks pass through unwrapped by
;; design and may exceed a narrow window, like any code line.
;;; Code:
(require 'ekp)
(require 'ekp-buffer)
(require 'ekp-showcase)
(defun ekp-gui-verify--line-width (window base line)
"Measure planned LINE in WINDOW at paragraph BASE."
(car (window-text-pixel-size
window
(+ base (ekp-layout-line-source-start line))
(+ base (ekp-layout-line-source-end line))
t)))
(defun ekp-gui-verify--scan-span (window span margin)
"Return measurement counters for SPAN in WINDOW using MARGIN."
(let* ((plan (ekp-buffer--span-plan span))
(records (ekp-buffer--span-lines span))
(target (ekp-buffer--span-width span))
(widest 0) (over 0) (wrong 0))
(dolist (record records)
(let* ((line (ekp-buffer--projected-line-line record))
(base (marker-position
(ekp-buffer--projected-line-base record)))
(pixel (ekp-gui-verify--line-width window base line)))
(setq widest (max widest pixel))
(when (> pixel (+ target margin))
(setq over (1+ over)))
(when (and (> (length (ekp-layout-line-gaps line)) 0)
(ekp-layout-line-break-kind line)
(> (abs (- pixel target)) margin))
(setq wrong (1+ wrong)))))
(list :widest widest :over over :wrong wrong
:lines (length records)
:static (and plan t))))
(defun ekp-gui-verify--scan (buffer &optional skip-predicate)
"Measure every projected display line of BUFFER.
When SKIP-PREDICATE is non-nil, omit spans for which it returns non-nil."
(with-current-buffer buffer
(let* ((window (get-buffer-window buffer))
(body (window-body-width window t))
(target ekp-buffer--auto-width)
(margin (max 2 ekp-buffer-margin-pixel))
(widest 0) (over 0) (wrong 0) (lines 0))
(dolist (span ekp-buffer--spans)
(unless (and skip-predicate (funcall skip-predicate span))
(let ((result (ekp-gui-verify--scan-span window span margin)))
(setq widest (max widest (plist-get result :widest))
over (+ over (plist-get result :over))
wrong (+ wrong (plist-get result :wrong))
lines (+ lines (plist-get result :lines))))))
(list :body body :target target :widest widest :over over
:wrong wrong :lines lines :exempt 0
:source-clean
(and (not (text-property-not-all
(point-min) (point-max) 'ekp-soft-break nil))
(not (text-property-not-all
(point-min) (point-max) 'ekp-glue nil))
(null (overlays-in (point-min) (point-max))))
:pass (and (= over 0) (= wrong 0) (> lines 0))))))
;;;###autoload
(defun ekp-gui-verify ()
"Verify pixel-exact justification against this session's display.
Opens the ekp showcase, enables follow-window justification, and
checks with `window-text-pixel-size' that every justified line
renders at exactly the window's text width — under YOUR fonts,
themes, remappings and text-scale. Reports PASS or FAIL."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(ekp-showcase)
(redisplay t)
(with-current-buffer "*ekp-showcase*"
(ekp-auto-justify-mode 1)
(when (timerp ekp-buffer--resize-timer)
(cancel-timer ekp-buffer--resize-timer))
(ekp-buffer--reflow (current-buffer) (ekp-buffer--effective-width))
(redisplay t)
(let* ((r (ekp-gui-verify--scan (current-buffer)))
(msg (format
"ekp-gui-verify: %s — %d lines, widest %dpx vs target %dpx (window %dpx)%s"
(if (plist-get r :pass) "PASS" "FAIL")
(plist-get r :lines) (plist-get r :widest)
(plist-get r :target) (plist-get r :body)
(if (> (plist-get r :exempt) 0)
(format ", %d verbatim lines exempt"
(plist-get r :exempt))
""))))
(message "%s" msg)
r)))
(defun ekp-gui-verify--case (name setup)
"Run one matrix case NAME with buffer SETUP; return its result plist."
;; Leftover debounce timers from the previous case must not fire
;; into this case's fresh buffer.
(dolist (fn (list #'ekp-buffer--reflow
#'ekp-buffer--process-chunk))
(cancel-function-timers fn))
(when (get-buffer "*ekp-showcase*")
(kill-buffer "*ekp-showcase*"))
(ekp-showcase)
(redisplay t)
(with-current-buffer "*ekp-showcase*"
(funcall setup)
(redisplay t)
(ekp-auto-justify-mode 1)
(when (timerp ekp-buffer--resize-timer)
(cancel-timer ekp-buffer--resize-timer))
(ekp-buffer--reflow (current-buffer) (ekp-buffer--effective-width))
(redisplay t)
(let ((r (ekp-gui-verify--scan (current-buffer))))
(prog1 (append (list :name name) r)
(ekp-auto-justify-mode -1)))))
(defun ekp-gui-verify--format-result (result)
"Format one matrix RESULT plist as a report line."
(format "%-22s body=%4d target=%4d widest=%4d over=%d/%d %s"
(plist-get result :name)
(plist-get result :body)
(plist-get result :target)
(plist-get result :widest)
(plist-get result :over)
(plist-get result :lines)
(if (plist-get result :pass) "PASS" "FAIL")))
(defun ekp-gui-verify--report (results)
"Report matrix RESULTS and return their formatted table.
In batch mode, terminate with status 1 when any result fails."
(let ((table (mapconcat #'ekp-gui-verify--format-result results "\n"))
(passed t))
(dolist (result results)
(unless (plist-get result :pass)
(setq passed nil)))
(if noninteractive
(princ (concat table "\n"))
(with-current-buffer (get-buffer-create "*ekp-gui-verify*")
(erase-buffer)
(insert table "\n")
(display-buffer (current-buffer))))
(when (and noninteractive (not passed))
(kill-emacs 1))
table))
(defun ekp-gui-verify--pixel-width (window beg end)
"Return displayed width from BEG to END in WINDOW."
(car (window-text-pixel-size window beg end t)))
(defun ekp-gui-verify--exact-ascii-glue (window)
"Return failed 164px ASCII glue targets in WINDOW."
(let ((natural (ekp-gui-verify--pixel-width window 2 3))
failures)
(dotimes (index 64)
(let ((target (1+ index)))
(with-silent-modifications
(ekp-buffer--remove-properties 1 4)
(ekp-buffer--put-display
2 3 (ekp-buffer--space-display natural target)))
(redisplay t)
(let ((actual (- (ekp-gui-verify--pixel-width window 1 4)
(ekp-gui-verify--pixel-width window 1 2)
(ekp-gui-verify--pixel-width window 3 4))))
(unless (= actual target)
(push (cons target actual) failures)))))
(nreverse failures)))
(defun ekp-gui-verify--exact-cjk-glue (window)
"Return failed 164px zero-source CJK glue targets in WINDOW."
(let ((first (ekp-gui-verify--pixel-width window 1 2))
(second (ekp-gui-verify--pixel-width window 2 3))
failures)
(dotimes (index 64)
(let ((target (1+ index)))
(with-silent-modifications
(ekp-buffer--remove-properties 1 3)
(ekp-buffer--put-display
1 2 (ekp-buffer--min-width (+ first target))))
(redisplay t)
(let ((actual (- (ekp-gui-verify--pixel-width window 1 3)
first second)))
(unless (= actual target)
(push (cons target actual) failures)))))
(nreverse failures)))
(defun ekp-gui-verify--hyphen-width (text)
"Return a width that chooses a discretionary hyphen in TEXT."
(seq-find
(lambda (width)
(seq-some
(lambda (line) (ekp-layout-line-hyphen-p line))
(append (ekp-layout-plan-lines
(ekp-layout-plan text width))
nil)))
(number-sequence 60 220 4)))
(defun ekp-gui-verify--hyphen-owner ()
"Return the source position owning the displayed discretionary hyphen."
(seq-find
(lambda (position)
(let ((display (get-text-property position 'display)))
(and (stringp display)
(string-match-p "-\n"
(substring-no-properties display)))))
(number-sequence (point-min) (1- (point-max)))))
(defun ekp-gui-verify--editor-semantics (window owner text)
"Verify point, region, and mouse semantics in WINDOW at OWNER for TEXT."
(let ((point-visible
(cl-loop
for position from (point-min) to (point-max)
always
(progn
(goto-char position)
(redisplay t)
(pos-visible-in-window-p position window t)))))
(goto-char (point-min))
(let ((vertical (vertical-motion 1 window)))
(set-mark (point-min))
(goto-char (point-max))
(activate-mark)
(redisplay t)
(let* ((xy (pos-visible-in-window-p owner window t))
(posn (and xy
(posn-at-x-y (car xy) (cadr xy) window)))
(mouse (and posn (posn-point posn))))
(list
:point-visible point-visible
:vertical-motion vertical
:vertical-source-position (point)
:region-source
(and (use-region-p)
(equal (buffer-substring-no-properties
(region-beginning) (region-end))
text))
:mouse-source-position
(and (integer-or-marker-p mouse)
(<= (point-min) mouse (point-max))))))))
(defun ekp-gui-verify--hyphen-case (window text)
"Return verification data for display-only hyphenation of TEXT in WINDOW."
(let ((width (ekp-gui-verify--hyphen-width text)))
(ekp-justify-region (point-min) (point-max) width)
(redisplay t)
(let ((owner (ekp-gui-verify--hyphen-owner)))
(append
(list
:width width
:source-clean
(equal (substring-no-properties (buffer-string)) text)
:source-lines (cl-count ?\n (buffer-string))
:screen-lines (count-screen-lines (point-min) (point-max))
:hyphen-display (and owner t)
:overlays (length (overlays-in (point-min) (point-max)))
:scan (ekp-gui-verify--scan (current-buffer))
:window-width (window-body-width window t))
(ekp-gui-verify--editor-semantics window owner text)))))
(defun ekp-gui-verify--display-properties-pass-p (result)
"Return non-nil when display-property verification RESULT passes."
(let ((hyphen (plist-get result :hyphen)))
(and (null (plist-get result :ascii-failures))
(null (plist-get result :cjk-failures))
(plist-get hyphen :source-clean)
(= (plist-get hyphen :source-lines) 0)
(> (plist-get hyphen :screen-lines) 1)
(plist-get hyphen :hyphen-display)
(= (plist-get hyphen :overlays) 0)
(plist-get hyphen :point-visible)
(> (plist-get hyphen :vertical-motion) 0)
(plist-get hyphen :region-source)
(plist-get hyphen :mouse-source-position)
(plist-get (plist-get hyphen :scan) :pass))))
;;;###autoload
(defun ekp-gui-verify-display-properties ()
"Verify exact text-property glue and display-only hyphenation."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(let ((buffer (generate-new-buffer "*ekp-display-properties*"))
result)
(unwind-protect
(progn
(switch-to-buffer buffer)
(delete-other-windows)
(insert "a b")
(redisplay t)
(let ((ascii (ekp-gui-verify--exact-ascii-glue
(selected-window))))
(erase-buffer)
(insert "中文")
(redisplay t)
(let ((cjk (ekp-gui-verify--exact-cjk-glue
(selected-window))))
(erase-buffer)
(let ((text
"extraordinary hyphenation demonstration paragraph"))
(insert text)
(setq result
(list :ascii-failures ascii
:cjk-failures cjk
:hyphen
(ekp-gui-verify--hyphen-case
(selected-window) text)))
(setq result
(plist-put
result :pass
(ekp-gui-verify--display-properties-pass-p
result)))))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))
(when (called-interactively-p 'interactive)
(message "ekp display-property verification: %s"
(if (plist-get result :pass) "PASS" "FAIL")))
result))
(defvar-local ekp-gui-verify--live-original nil)
(defvar-local ekp-gui-verify--live-expected nil)
(defvar-local ekp-gui-verify--live-suffix " overflow")
(defvar-local ekp-gui-verify--live-yank " pasted 中文 semantic prefix")
(defvar-local ekp-gui-verify--live-original-signatures nil)
(defvar-local ekp-gui-verify--live-original-projection-hash nil)
(defvar-local ekp-gui-verify--live-original-active-index nil)
(defvar-local ekp-gui-verify--live-original-prefix-end nil)
(defvar-local ekp-gui-verify--live-original-width nil)
(defvar-local ekp-gui-verify--live-original-screen-lines nil)
(defvar-local ekp-gui-verify--live-post-crossing-state nil)
(defvar-local ekp-gui-verify--live-motion-generation nil)
(defvar-local ekp-gui-verify--live-motion-cache-size nil)
(defvar-local ekp-gui-verify--live-motion-state nil)
(defvar-local ekp-gui-verify--live-middle-position nil)
(defvar-local ekp-gui-verify--live-middle-later-beg nil)
(defvar-local ekp-gui-verify--live-middle-later-end nil)
(defvar-local ekp-gui-verify--live-middle-later-hash nil)
(defvar-local ekp-gui-verify--live-middle-plan nil)
(defvar-local ekp-gui-verify--live-middle-spans nil)
(defvar-local ekp-gui-verify--live-stage "setup")
(defun ekp-gui-verify--near-overflow-text (target)
"Return prose whose natural final line is nearly full at TARGET."
(let ((words ["alpha" "中文" "beta" "排版" "gamma" "编辑"])
(text "")
(index 0)
found)
(while (and (< index 300) (not found))
(setq text
(concat text
(if (string-empty-p text) "" " ")
(aref words (% index (length words)))))
(let* ((plan (ekp-layout-plan text target))
(lines (ekp-layout-plan-lines plan)))
(when (> (length lines) 2)
(let* ((line (aref lines (1- (length lines))))
(tail (substring
text
(ekp-layout-line-source-start line)
(ekp-layout-line-source-end line)))
(remaining (- target (ekp--measured-width tail))))
(setq found
(and (> remaining 0)
(< remaining
(ekp--measured-width
ekp-gui-verify--live-suffix)))))))
(setq index (1+ index)))
text))
(defun ekp-gui-verify--range-property-hash (beg end)
"Return an exact text-property hash for BEG through END."
(secure-hash
'sha256
(prin1-to-string (buffer-substring beg end))))
(defun ekp-gui-verify--configure-middle-edit ()
"Record one projected space and a later stable anchor range."
(let* ((spans (ekp-buffer--live-state-spans ekp-buffer--live-state))
(dirty (car spans))
(later (cadr spans))
(dirty-beg (marker-position (ekp-buffer--span-beg dirty)))
(dirty-end (marker-position (ekp-buffer--span-end dirty)))
(space
(save-excursion
(goto-char dirty-beg)
(search-forward " " dirty-end t))))
(unless (and space later)
(error "Live GUI setup needs one dirty row and one later anchor"))
(setq ekp-gui-verify--live-middle-position (copy-marker (1- space))
ekp-gui-verify--live-middle-later-beg
(copy-marker (marker-position (ekp-buffer--span-beg later)))
ekp-gui-verify--live-middle-later-end
(copy-marker
(marker-position (ekp-buffer--span-end (car (last spans)))) t)
ekp-gui-verify--live-middle-later-hash
(ekp-gui-verify--range-property-hash
ekp-gui-verify--live-middle-later-beg
ekp-gui-verify--live-middle-later-end)
ekp-gui-verify--live-middle-plan
(ekp-buffer--live-state-plan ekp-buffer--live-state)
ekp-gui-verify--live-middle-spans spans)))
;;;###autoload
(defun ekp-gui-verify-live-setup ()
"Create a deterministic GUI buffer for live typing verification."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(set-frame-parameter (selected-frame) 'fullscreen 'fullboth)
(delete-other-windows)
(let ((buffer (get-buffer-create "*EKP Live Layout*")))
(switch-to-buffer buffer)
(when ekp-auto-justify-mode
(ekp-auto-justify-mode -1))
(let ((inhibit-read-only t))
(erase-buffer)
(text-mode)
(setq-local truncate-lines nil)
(redisplay t)
(insert (ekp-gui-verify--near-overflow-text
(ekp-buffer--effective-width)))
(setq ekp-gui-verify--live-original
(substring-no-properties (buffer-string))
ekp-gui-verify--live-expected
ekp-gui-verify--live-original
ekp-gui-verify--live-post-crossing-state nil
ekp-gui-verify--live-motion-generation nil
ekp-gui-verify--live-motion-cache-size nil
ekp-gui-verify--live-motion-state nil
ekp-gui-verify--live-stage "setup")
(ekp-auto-justify-mode 1)
(goto-char (point-max))
(run-hooks 'post-command-hook)
(redisplay t)
(setq ekp-gui-verify--live-original-signatures
(copy-tree
(ekp-buffer--live-state-signatures ekp-buffer--live-state))
ekp-gui-verify--live-original-projection-hash
(ekp-gui-verify--projection-hash)
ekp-gui-verify--live-original-active-index
(ekp-buffer--live-state-active-index ekp-buffer--live-state)
ekp-gui-verify--live-original-prefix-end
(marker-position
(ekp-buffer--live-state-prefix-end ekp-buffer--live-state))
ekp-gui-verify--live-original-width
ekp-buffer--auto-width
ekp-gui-verify--live-original-screen-lines
(count-screen-lines (point-min) (point-max)))
(ekp-gui-verify--configure-middle-edit))
(ekp-gui-verify-live-adapter)))
(defun ekp-gui-verify--owned-break-positions (&optional beg end)
"Return EKP-owned replacing-display newline positions from BEG to END."
(let ((position (or beg (point-min)))
(limit (or end (point-max)))
positions)
(while (< position limit)
(let* ((display (get-text-property
position 'ekp-buffer--display))
(next (or (next-single-property-change
position 'ekp-buffer--display nil limit)
limit)))
(when (and (stringp display)
(string-match-p
"\n" (substring-no-properties display)))
(push position positions))
(setq position next)))
(nreverse positions)))
(defun ekp-gui-verify--owned-property-runs ()
"Return stable descriptions of all EKP-owned projection runs."
(let ((position (point-min))
runs)
(while (< position (point-max))
(let* ((next (or (next-property-change
position nil (point-max))
(point-max)))
(display (get-text-property
position 'ekp-buffer--display))
(prefix (get-text-property
position 'ekp-buffer--line-prefix))
(justified (get-text-property position 'ekp-justified)))
(when (or display prefix justified)
(push (list position next display prefix justified) runs))
(setq position next)))
(nreverse runs)))
(defun ekp-gui-verify--projection-hash ()
"Return a stable hash of the current EKP projection."
(secure-hash
'sha256 (prin1-to-string (ekp-gui-verify--owned-property-runs))))
(defun ekp-gui-verify--stable-live-state ()
"Return the committed live projection state relevant to stability."
(let ((state ekp-buffer--live-state))
(list
:active (ekp-buffer--live-state-active-index state)
:prefix-end
(marker-position (ekp-buffer--live-state-prefix-end state))
:signatures (ekp-buffer--live-state-signatures state)
:projection (ekp-gui-verify--projection-hash)
:plan (ekp-buffer--live-state-plan state)
:spans (ekp-buffer--live-state-spans state))))
(defun ekp-gui-verify--stable-live-state-matches-p (snapshot)
"Return non-nil when the committed projection matches SNAPSHOT exactly."
(let ((current (ekp-gui-verify--stable-live-state)))
(and (eql (plist-get current :active)
(plist-get snapshot :active))
(eql (plist-get current :prefix-end)
(plist-get snapshot :prefix-end))
(eq (plist-get current :signatures)
(plist-get snapshot :signatures))
(equal (plist-get current :projection)
(plist-get snapshot :projection))
(eq (plist-get current :plan)
(plist-get snapshot :plan))
(eq (plist-get current :spans)
(plist-get snapshot :spans)))))
(defun ekp-gui-verify--dirty-live-span-p (span)
"Return non-nil when SPAN belongs to the current natural dirty island."
(when-let* ((edit ekp-buffer--live-edit)
(beg (marker-position
(ekp-buffer--live-edit-dirty-beg edit)))
(end (marker-position
(ekp-buffer--live-edit-dirty-end edit))))
(ekp-buffer--span-overlaps-p span beg end)))
(defun ekp-gui-verify--live-active-bounds ()
"Return the active semantic source range, or nil."
(if ekp-buffer--live-edit
(let ((beg (marker-position
(ekp-buffer--live-edit-dirty-beg ekp-buffer--live-edit)))
(end (marker-position
(ekp-buffer--live-edit-dirty-end ekp-buffer--live-edit))))
(and beg end (<= beg end) (cons beg end)))
(when-let* ((state ekp-buffer--live-state)
(plan (ekp-buffer--live-state-plan state))
(active (ekp-buffer--live-state-active-index state))
(beg (marker-position (ekp-buffer--live-state-beg state)))
(end (marker-position (ekp-buffer--live-state-end state))))
(let ((lines (ekp-layout-plan-lines plan)))
(when (< active (length lines))
(let ((line (aref lines active)))
(cons (+ beg (ekp-layout-line-source-start line)) end)))))))
(defun ekp-gui-verify--owned-layout-p (beg end)
"Return non-nil when BEG through END carries EKP layout."
(or (text-property-not-all beg end 'ekp-buffer--display nil)
(text-property-not-all beg end 'ekp-buffer--line-prefix nil)
(text-property-not-all beg end 'ekp-justified nil)))
(defun ekp-gui-verify--active-line-natural-p ()
"Return non-nil when the active semantic line has no EKP layout."
(when-let* ((bounds (ekp-gui-verify--live-active-bounds)))
(not (ekp-gui-verify--owned-layout-p
(car bounds) (cdr bounds)))))
(defun ekp-gui-verify--current-live-signatures ()
"Return the current semantic prefix signatures."
(and ekp-buffer--live-state
(ekp-buffer--live-state-signatures ekp-buffer--live-state)))
(defun ekp-gui-verify--stage-check (stage predicate)
"Return PREDICATE for STAGE, and true for every other stage."
(if (equal ekp-gui-verify--live-stage stage)
(if predicate t :false)
t))
(defun ekp-gui-verify-live-adapter ()
"Return JSON-compatible state for the live verification buffer."
(let* ((live-beg
(and ekp-buffer--live-state
(marker-position
(ekp-buffer--live-state-beg ekp-buffer--live-state))))
(live-end
(and ekp-buffer--live-state
(marker-position
(ekp-buffer--live-state-end ekp-buffer--live-state))))
(scan (ekp-gui-verify--scan
(current-buffer) #'ekp-gui-verify--dirty-live-span-p))
(logical (substring-no-properties (buffer-string)))
(breaks (ekp-gui-verify--owned-break-positions))
(live-breaks
(and live-beg live-end
(ekp-gui-verify--owned-break-positions live-beg live-end)))
(active (and ekp-buffer--live-state
(ekp-buffer--live-state-active-index
ekp-buffer--live-state)))
(prefix-end
(and ekp-buffer--live-state
(marker-position
(ekp-buffer--live-state-prefix-end
ekp-buffer--live-state))))
(signatures (ekp-gui-verify--current-live-signatures))
(projection-hash (ekp-gui-verify--projection-hash))
(later-hash
(and (markerp ekp-gui-verify--live-middle-later-beg)
(marker-position ekp-gui-verify--live-middle-later-beg)
(ekp-gui-verify--range-property-hash
ekp-gui-verify--live-middle-later-beg
ekp-gui-verify--live-middle-later-end)))
(original-p
(equal ekp-gui-verify--live-expected
ekp-gui-verify--live-original)))
`((logical_length . ,(length logical))
(logical_sha256 . ,(secure-hash 'sha256 logical))
(expected_source . ,(if (equal logical
ekp-gui-verify--live-expected)
t :false))
(source_newlines . ,(cl-count ?\n logical))
(owned_breaks . ,(length breaks))
(live_owned_breaks . ,(length live-breaks))
(stage . ,ekp-gui-verify--live-stage)
(projection_sha256 . ,projection-hash)
(active_index . ,(or active -1))
(stable_boundary_position . ,(or prefix-end -1))
(prefix_lines . ,(length signatures))
(plan_lines
. ,(if-let* ((plan (and ekp-buffer--live-state
(ekp-buffer--live-state-plan
ekp-buffer--live-state))))
(length (ekp-layout-plan-lines plan))
0))
(middle_anchor_preserved
. ,(ekp-gui-verify--stage-check
"middle-dirty"
(and ekp-buffer--live-edit
(equal later-hash
ekp-gui-verify--live-middle-later-hash)
(eq (ekp-buffer--live-state-plan ekp-buffer--live-state)
ekp-gui-verify--live-middle-plan)
(eq (ekp-buffer--live-state-spans ekp-buffer--live-state)
ekp-gui-verify--live-middle-spans))))
(middle_exact_restored
. ,(ekp-gui-verify--stage-check
"middle-restored"
(and original-p
(not ekp-buffer--live-edit)
(equal projection-hash
ekp-gui-verify--live-original-projection-hash)
(equal later-hash
ekp-gui-verify--live-middle-later-hash)
(eq (ekp-buffer--live-state-plan ekp-buffer--live-state)
ekp-gui-verify--live-middle-plan)
(eq (ekp-buffer--live-state-spans ekp-buffer--live-state)
ekp-gui-verify--live-middle-spans))))
(prefix_replanned
. ,(ekp-gui-verify--stage-check
"typed"
(and ekp-gui-verify--live-post-crossing-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-post-crossing-state)
(not (equal signatures
ekp-gui-verify--live-original-signatures)))))
(delete_preserved_committed_projection
. ,(ekp-gui-verify--stage-check
"suffix-deleted"
(and original-p
ekp-gui-verify--live-post-crossing-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-post-crossing-state))))
(point_move_back_preserved_projection
. ,(ekp-gui-verify--stage-check
"moved-back"
(and ekp-gui-verify--live-motion-generation
(= ekp-buffer--generation
ekp-gui-verify--live-motion-generation)
(= (length ekp-buffer--live-plan-cache)
ekp-gui-verify--live-motion-cache-size)
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(point_move_forward_preserved_projection
. ,(ekp-gui-verify--stage-check
"moved-forward"
(and ekp-gui-verify--live-motion-generation
(= ekp-buffer--generation
ekp-gui-verify--live-motion-generation)
(= (length ekp-buffer--live-plan-cache)
ekp-gui-verify--live-motion-cache-size)
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(yank_kept_current_row_natural
. ,(ekp-gui-verify--stage-check
"yanked"
(and ekp-buffer--live-edit
ekp-gui-verify--live-motion-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(undo_preserved_committed_projection
. ,(ekp-gui-verify--stage-check
"undone"
(and original-p
ekp-buffer--live-edit
ekp-gui-verify--live-motion-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(resize_reflowed
. ,(ekp-gui-verify--stage-check
"resized"
(< ekp-buffer--auto-width
ekp-gui-verify--live-original-width)))
(resize_restored
. ,(ekp-gui-verify--stage-check
"resize-restored"
(= ekp-buffer--auto-width
ekp-gui-verify--live-original-width)))
(completed_paragraph
. ,(if (or (not (equal ekp-gui-verify--live-stage "completed"))
(and (= (cl-count ?\n logical) 1)
(get-text-property (point-min) 'ekp-justified)
live-beg live-end
(= live-beg live-end (point-max))))
t :false))
(screen_lines . ,(count-screen-lines (point-min) (point-max)))
(overlays . ,(length (overlays-in (point-min) (point-max))))
(hscroll . ,(window-hscroll))
(active_paragraph . ,(if ekp-buffer--live-state t :false))
(authoritative_width . ,(or ekp-buffer--auto-width 0))
(active_line_natural
. ,(if (or (member ekp-gui-verify--live-stage
'("completed" "resized" "resize-restored"))
(ekp-gui-verify--active-line-natural-p))
t :false))
(generation . ,ekp-buffer--generation)
(pending_edit . ,(if ekp-buffer--live-edit t :false))
(live_cache_size . ,(length ekp-buffer--live-plan-cache))
(pixel_scan_pass
. ,(if (or (= (or active 0) 0)
(plist-get scan :pass))
t :false))
(pixel_overflow_lines . ,(plist-get scan :over)))))
(defun ekp-gui-verify-live-assertions ()
"Return adapter assertions for the live verification buffer."
(let* ((adapter (ekp-gui-verify-live-adapter))
(value (lambda (key) (cdr (assq key adapter)))))
`(((name . "logical-source-exact")
(passed . ,(funcall value 'expected_source)))
((name . "zero-overlays")
(passed . ,(if (= (funcall value 'overlays) 0) t :false)))
((name . "zero-horizontal-scroll")
(passed . ,(if (= (funcall value 'hscroll) 0) t :false)))
((name . "middle-edit-preserves-later-anchor")
(passed . ,(funcall value 'middle_anchor_preserved)))
((name . "middle-reversal-restores-exact-projection")
(passed . ,(funcall value 'middle_exact_restored)))
((name . "visual-row-crossing-commits-prefix")
(passed . ,(funcall value 'prefix_replanned)))
((name . "delete-keeps-last-committed-projection")
(passed . ,(funcall value
'delete_preserved_committed_projection)))
((name . "point-back-preserves-projection")
(passed . ,(funcall value
'point_move_back_preserved_projection)))
((name . "point-forward-preserves-projection")
(passed . ,(funcall value
'point_move_forward_preserved_projection)))
((name . "same-row-yank-keeps-projection")
(passed . ,(funcall value 'yank_kept_current_row_natural)))
((name . "real-undo-keeps-committed-projection")
(passed . ,(funcall value
'undo_preserved_committed_projection)))
((name . "window-resize-reflows-prefix")
(passed . ,(funcall value 'resize_reflowed)))
((name . "window-width-restore-reflows-prefix")
(passed . ,(funcall value 'resize_restored)))
((name . "hard-newline-completes-previous-paragraph")
(passed . ,(funcall value 'completed_paragraph)))
((name . "active-semantic-line-natural")
(passed . ,(funcall value 'active_line_natural)))
((name . "live-cache-bounded")
(passed . ,(if (<= (funcall value 'live_cache_size) 16) t :false)))
((name . "no-pixel-overflow")
(passed . ,(if (= (funcall value 'pixel_overflow_lines) 0)
t :false))))))
;;;###autoload
(defun ekp-gui-verify-live-delete-middle-space ()
"Delete one projected middle-row space through the public command path."
(interactive)
(goto-char (1+ (marker-position
ekp-gui-verify--live-middle-position)))
(call-interactively #'delete-backward-char)
(let ((offset (- (marker-position
ekp-gui-verify--live-middle-position)
(point-min))))
(setq ekp-gui-verify--live-expected
(concat (substring ekp-gui-verify--live-original 0 offset)
(substring ekp-gui-verify--live-original (1+ offset)))
ekp-gui-verify--live-stage "middle-dirty"))
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-restore-middle-space ()
"Reinsert the deleted middle-row space and restore the exact baseline."
(interactive)
(goto-char ekp-gui-verify--live-middle-position)
(let ((last-command-event ?\s))
(call-interactively #'self-insert-command))
(setq ekp-gui-verify--live-expected ekp-gui-verify--live-original
ekp-gui-verify--live-stage "middle-restored")
(goto-char (point-max))
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-type-suffix ()
"Type the deterministic suffix through the command loop."
(interactive)
(setq ekp-gui-verify--live-stage "typed")
(setq ekp-gui-verify--live-expected
(concat ekp-gui-verify--live-original
ekp-gui-verify--live-suffix))
(execute-kbd-macro ekp-gui-verify--live-suffix)
(redisplay t)
(setq ekp-gui-verify--live-post-crossing-state
(ekp-gui-verify--stable-live-state))
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-delete-suffix ()
"Delete the suffix while retaining the last committed projection."
(interactive)
(execute-kbd-macro
(vconcat
(make-list (length ekp-gui-verify--live-suffix) 'backspace)))
(setq ekp-gui-verify--live-expected
ekp-gui-verify--live-original
ekp-gui-verify--live-stage "suffix-deleted")
(redisplay t)
(setq ekp-gui-verify--live-motion-generation ekp-buffer--generation
ekp-gui-verify--live-motion-cache-size
(length ekp-buffer--live-plan-cache)
ekp-gui-verify--live-motion-state
(ekp-gui-verify--stable-live-state))
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-move-back ()
"Move point into the first semantic line through the command loop."
(interactive)
(execute-kbd-macro (kbd "M-<"))
(setq ekp-gui-verify--live-stage "moved-back")
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-move-forward ()
"Move point back to the final semantic line through the command loop."
(interactive)
(execute-kbd-macro (kbd "M->"))
(setq ekp-gui-verify--live-stage "moved-forward")
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-yank ()
"Yank a multi-script suffix through the public command path."
(interactive)
(undo-boundary)
(kill-new ekp-gui-verify--live-yank)
(execute-kbd-macro (kbd "C-y"))
(setq ekp-gui-verify--live-expected
(concat ekp-gui-verify--live-original
ekp-gui-verify--live-yank)
ekp-gui-verify--live-stage "yanked")
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-undo-yank ()
"Undo the verification yank through the public command path."
(interactive)
(execute-kbd-macro (kbd "C-/"))
(setq ekp-gui-verify--live-expected
ekp-gui-verify--live-original
ekp-gui-verify--live-stage "undone")
(redisplay t)
(ekp-gui-verify-live-adapter))
(defun ekp-gui-verify--live-change-margin (columns stage)
"Set the right margin to COLUMNS and record verification STAGE."
(set-window-margins (selected-window) 0 columns)
(run-hooks 'window-configuration-change-hook)
(sit-for (+ ekp-auto-justify-resize-delay 0.2))
(setq ekp-gui-verify--live-stage stage)
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-resize-narrower ()
"Narrow the live text area and wait for the real resize debounce."
(interactive)
(ekp-gui-verify--live-change-margin 24 "resized"))
;;;###autoload
(defun ekp-gui-verify-live-resize-restore ()
"Restore the live text area and wait for the real resize debounce."
(interactive)
(ekp-gui-verify--live-change-margin nil "resize-restored"))
;;;###autoload
(defun ekp-gui-verify-live-complete-paragraph ()
"Insert a hard newline and complete the previous paragraph."
(interactive)
(execute-kbd-macro "\n")
(setq ekp-gui-verify--live-expected
(concat ekp-gui-verify--live-original "\n")
ekp-gui-verify--live-stage "completed")
(redisplay t)
(ekp-gui-verify-live-adapter))
(defconst ekp-gui-verify--split-text
"soft wrap 中文 mixed editing stays natural across narrow side by side windows"
"Text typed by the split-window soft-wrap verification.")
(defvar-local ekp-gui-verify--split-expected "")
(defvar-local ekp-gui-verify--split-stage "setup")
(defvar-local ekp-gui-verify--split-original-wrap nil)
;;;###autoload
(defun ekp-gui-verify-split-wrap-setup ()
"Create an intentional narrow split for native soft-wrap verification."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(set-frame-parameter (selected-frame) 'fullscreen 'fullboth)
(delete-other-windows)
(redisplay t)
(sit-for 0.2)
(let* ((target (get-buffer-create "*EKP Split Soft Wrap*"))
(control (get-buffer-create "*EKP Split Control*"))
(left (selected-window))
(right (split-window-right)))
(set-window-buffer left target)
(set-window-buffer right control)
(window-resize left (- 44 (window-total-width left)) t)
(select-window left)
(with-current-buffer control
(erase-buffer)
(insert "Intentional control window for partial-width verification."))
(with-current-buffer target
(when ekp-auto-justify-mode
(ekp-auto-justify-mode -1))
(erase-buffer)
(text-mode)
(kill-local-variable 'truncate-lines)
(kill-local-variable 'truncate-partial-width-windows)
(setq ekp-gui-verify--split-expected ""
ekp-gui-verify--split-stage "setup"
ekp-gui-verify--split-original-wrap
(list (local-variable-p 'truncate-lines)
truncate-lines
(local-variable-p 'truncate-partial-width-windows)
truncate-partial-width-windows))
(ekp-auto-justify-mode 1)
(goto-char (point-max)))
(redisplay t)
(ekp-gui-verify-split-wrap-adapter)))
(defun ekp-gui-verify--split-row-start ()
"Return point's native visual-row start in the selected window."
(save-excursion
(vertical-motion 0 (selected-window))
(point)))
(defun ekp-gui-verify--split-display-state-restored-p ()
"Return non-nil when split verification restored its display state."
(pcase-let ((`(,lines-local ,lines ,partial-local ,partial)
ekp-gui-verify--split-original-wrap))
(and (eq (local-variable-p 'truncate-lines) lines-local)
(equal truncate-lines lines)
(eq (local-variable-p 'truncate-partial-width-windows)
partial-local)
(equal truncate-partial-width-windows partial))))
(defun ekp-gui-verify-split-wrap-adapter ()
"Return JSON-compatible state for split-window soft-wrap verification."
(let* ((logical (substring-no-properties (buffer-string)))
(typed-p (equal ekp-gui-verify--split-stage "typed"))
(disabled-p (equal ekp-gui-verify--split-stage "disabled"))
(breaks (ekp-gui-verify--owned-break-positions)))
`((stage . ,ekp-gui-verify--split-stage)
(expected_source
. ,(if (equal logical ekp-gui-verify--split-expected) t :false))
(logical_sha256 . ,(secure-hash 'sha256 logical))
(window_count . ,(length (window-list nil 'no-minibuf)))
(target_columns . ,(window-total-width))
(auto_mode . ,(if ekp-auto-justify-mode t :false))
(truncate_lines . ,(if truncate-lines t :false))
(partial_truncation
. ,(if truncate-partial-width-windows t :false))
(wrap_bindings_local
. ,(if (and (local-variable-p 'truncate-lines)
(local-variable-p 'truncate-partial-width-windows))
t :false))
(screen_lines . ,(count-screen-lines (point-min) (point-max)))
(visual_row_advanced
. ,(if (or (not typed-p)
(> (ekp-gui-verify--split-row-start) (point-min)))
t :false))
(semantic_prefix_lines
. ,(if ekp-buffer--live-state
(length
(ekp-buffer--live-state-signatures
ekp-buffer--live-state))
0))
(active_line_natural
. ,(if (or (not typed-p)
disabled-p
(ekp-gui-verify--active-line-natural-p))
t :false))
(hscroll . ,(window-hscroll))
(live_owned_breaks . ,(length breaks))
(overlays . ,(length (overlays-in (point-min) (point-max))))
(pending_edit . ,(if ekp-buffer--live-edit t :false))
(display_state_restored
. ,(if (or (not disabled-p)
(ekp-gui-verify--split-display-state-restored-p))
t :false)))))
(defun ekp-gui-verify-split-wrap-assertions ()
"Return assertions for split-window soft-wrap verification."
(let* ((adapter (ekp-gui-verify-split-wrap-adapter))
(value (lambda (key) (cdr (assq key adapter))))
(disabled-p
(equal (funcall value 'stage) "disabled")))
`(((name . "logical-source-exact")
(passed . ,(funcall value 'expected_source)))
((name . "intentional-two-window-layout")
(passed . ,(if (= (funcall value 'window_count) 2) t :false)))
((name . "target-window-below-default-truncation-threshold")
(passed . ,(if (< (funcall value 'target_columns) 50) t :false)))
((name . "auto-mode-owns-soft-wrap")
(passed . ,(if (or disabled-p
(and (funcall value 'auto_mode)
(eq (funcall value 'truncate_lines) :false)
(eq (funcall value 'partial_truncation) :false)
(funcall value 'wrap_bindings_local)))
t :false)))
((name . "typing-crosses-visual-boundary")
(passed . ,(funcall value 'visual_row_advanced)))
((name . "no-horizontal-scroll")
(passed . ,(if (or disabled-p
(= (funcall value 'hscroll) 0))
t :false)))
((name . "semantic-prefix-publishes-complete-lines")
(passed . ,(if (or (not (equal (funcall value 'stage) "typed"))
(and (> (funcall value 'semantic_prefix_lines) 0)
(> (funcall value 'live_owned_breaks) 0)))
t :false)))
((name . "active-semantic-line-natural")
(passed . ,(funcall value 'active_line_natural)))
((name . "zero-overlays")
(passed . ,(if (= (funcall value 'overlays) 0) t :false)))
((name . "no-pending-live-transaction")
(passed . ,(if (eq (funcall value 'pending_edit) :false)
t :false)))
((name . "mode-disable-restores-display-state")
(passed . ,(funcall value 'display_state_restored))))))
(defun ekp-gui-verify-split-generic-assertions
(window buffer start end)
"Return generic capture assertions for an intentional split layout."
(list
`((name . "selected-target-window-live")
(passed . ,(if (and (window-live-p window)
(eq (window-buffer window) buffer))
t :false)))
`((name . "intentional-two-window-capture")
(passed . ,(if (= (length (window-list nil 'no-minibuf)) 2)
t :false)))
`((name . "visible-range-valid")
(passed . ,(if (and (integer-or-marker-p start)
(integer-or-marker-p end)
(<= start end)
(<= end (with-current-buffer buffer (point-max))))
t :false)))))
;;;###autoload
(defun ekp-gui-verify-split-wrap-type ()
"Type across the native wrap boundary in the narrow target window."
(interactive)
(mapc
(lambda (character)
(let ((last-command-event character))
(call-interactively #'self-insert-command))
(redisplay t)
(sit-for 0.02))
(string-to-list ekp-gui-verify--split-text))
(setq ekp-gui-verify--split-expected ekp-gui-verify--split-text
ekp-gui-verify--split-stage "typed")
(redisplay t)
(ekp-gui-verify-split-wrap-adapter))
;;;###autoload
(defun ekp-gui-verify-split-wrap-disable ()
"Disable auto mode and expose restored truncation ownership."
(interactive)
(ekp-auto-justify-mode -1)
(setq ekp-gui-verify--split-stage "disabled")
(redisplay t)
(ekp-gui-verify-split-wrap-adapter))
;;;###autoload
(defun ekp-gui-verify-matrix ()
"Run the display-context matrix and print a PASS/FAIL table.
Covers: plain, text-scale up/down, family+height face remap, and a
narrow frame with scaling. Intended for `emacs -Q'; in a customized
session prefer `ekp-gui-verify'."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(save-current-buffer
(ekp-gui-verify--matrix-1)))
(defun ekp-gui-verify--matrix-1 ()
"Run the matrix cases; caller guards the current buffer."
(let (results)
(set-frame-size (selected-frame) 190 40)
(push (ekp-gui-verify--case "base" #'ignore) results)
(push (ekp-gui-verify--case "text-scale +3"
(lambda () (text-scale-set 3)))
results)
(push (ekp-gui-verify--case "text-scale -2"
(lambda () (text-scale-set -2)))
results)
(push (ekp-gui-verify--case "remap family+height"
(lambda ()
(face-remap-add-relative
'default :height 1.15)))
results)
(push (ekp-gui-verify--case "no fringes"
(lambda ()
(set-window-fringes
(get-buffer-window (current-buffer))
0 0)))
results)
(push (ekp-gui-verify--case "no fringes + scale +2"
(lambda ()
(set-window-fringes
(get-buffer-window (current-buffer))
0 0)
(text-scale-set 2)))
results)
(set-frame-size (selected-frame) 70 40)
(push (ekp-gui-verify--case "narrow + scale +2"
(lambda () (text-scale-set 2)))
results)
(ekp-gui-verify--report (nreverse results))))
(provide 'ekp-gui-verify)
;;; ekp-gui-verify.el ends here