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-*.
This commit is contained in:
parent
fd1210e557
commit
83352c4571
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
run: |
|
||||
emacs -Q --batch -L . \
|
||||
--eval '(setq byte-compile-error-on-warn t)' \
|
||||
-f batch-byte-compile ekp.el ekp-utils.el ekp-hyphen.el ekp-region.el
|
||||
-f batch-byte-compile ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
|
||||
- name: Run ERT suite (C-module tests auto-skip)
|
||||
run: tests/run-tests.sh emacs
|
||||
- name: Run ERT suite in permuted order
|
||||
@ -62,12 +62,12 @@ jobs:
|
||||
--eval "(require 'package-lint)" \
|
||||
--eval "(setq package-lint-main-file \"ekp.el\")" \
|
||||
-f package-lint-batch-and-exit \
|
||||
ekp.el ekp-utils.el ekp-hyphen.el ekp-region.el
|
||||
ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
|
||||
- name: checkdoc
|
||||
run: |
|
||||
emacs -Q --batch \
|
||||
--eval "(setq sentence-end-double-space t)" \
|
||||
--eval "(dolist (f '(\"ekp.el\" \"ekp-utils.el\" \"ekp-hyphen.el\" \"ekp-region.el\"))
|
||||
--eval "(dolist (f '(\"ekp.el\" \"ekp-utils.el\" \"ekp-hyphen.el\" \"ekp-buffer.el\"))
|
||||
(checkdoc-file f))" \
|
||||
2>&1 | tee checkdoc.log
|
||||
test ! -s checkdoc.log
|
||||
@ -162,13 +162,13 @@ jobs:
|
||||
emacs -Q --batch -L .
|
||||
--eval "(setq byte-compile-error-on-warn t)"
|
||||
-f batch-byte-compile
|
||||
ekp.el ekp-utils.el ekp-hyphen.el ekp-region.el
|
||||
ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
|
||||
- name: Run ERT suite (C-module tests auto-skip)
|
||||
shell: pwsh
|
||||
run: >
|
||||
emacs -Q --batch -L . -L tests
|
||||
-l tests/ekp-tests.el
|
||||
-l tests/ekp-region-tests.el
|
||||
-l tests/ekp-buffer-tests.el
|
||||
-l tests/ekp-gui-tests.el
|
||||
-l tests/ekp-c-tests.el
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
@ -37,6 +37,47 @@
|
||||
- 2026-07-28 make the same-string paragraph fast path property-sensitive
|
||||
(`task016`):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_property_sensitive_fast_path_20260728.md`
|
||||
- 2026-07-28 rename editor integration around its buffer owner (`task017`):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_buffer_module_naming_20260728.md`
|
||||
- 2026-07-28 record the logical-text API boundary (`issue011`, no
|
||||
implementation):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_logical_text_api_issue_20260728.md`
|
||||
- 2026-07-29 record the text-property-only layout and non-polluting hyphen
|
||||
feasibility (`task018`, `issue011` remains open):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_research_20260729.md`
|
||||
- 2026-07-29 implement and close the shared KP layout plan, source-clean
|
||||
static projection, seamless live flow, and repository quality gate
|
||||
(`task019`–`task022`; `issue011` awaits user-visible acceptance):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
|
||||
- 2026-07-29 fix and verify inactive-mark selection and same-turn
|
||||
edge-whitespace visibility (`task023`–`task024`; `issue012` and
|
||||
`issue013` await user-visible acceptance):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
|
||||
- 2026-07-29 replace partial live KP with the approved native progressive
|
||||
editing model (`task025`–`task026` complete; `issue014` awaits
|
||||
user-visible confirmation):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
|
||||
- 2026-07-29 implement and verify the native soft-wrap lifecycle for narrow
|
||||
split windows (`task027` complete; `issue015` awaits user-visible
|
||||
confirmation):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
|
||||
- 2026-07-29 implement and developer-verify semantic hard-line prefix live
|
||||
editing (`task028`; `issue016` awaits user-visible confirmation):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_semantic_live_prefix_20260729.md`
|
||||
- 2026-07-29 make point-only motion projection-stable and move the live
|
||||
boundary to a source-edit-owned frontier (`task029` complete;
|
||||
`issue016` and `issue017` await user-visible confirmation; separate
|
||||
narrow-width append debt is `issue018`/`task030`):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_editing_frontier_stability_20260729.md`
|
||||
- 2026-07-29 implement and developer-verify the stable live-layout
|
||||
transaction that supersedes per-keystroke frontier replanning
|
||||
(`task031` complete; `issue019` awaits user confirmation; `task030`
|
||||
is unblocked for structural-commit profiling):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_stable_live_transaction_20260729.md`
|
||||
- 2026-07-30 reduce core and complete C-backed resize p95 to
|
||||
27.687/27.487 ms with exact layout parity (`task032` complete;
|
||||
`issue020` awaits user confirmation):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_c_resize_latency_20260729.md`
|
||||
|
||||
## phase-doc-improvement-20260125
|
||||
- See `.phrase/phases/phase-doc-improvement-20260125/change_log.md`
|
||||
|
||||
@ -10,3 +10,13 @@
|
||||
- issue008 [x] [Hot-loop allocation and nil caching need benchmark gates.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
|
||||
- issue009 [x] [Release and documentation governance are not closed.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
|
||||
- issue010 [x] [Same-string property mutation bypasses paragraph cache identity.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
|
||||
- issue011 [ ] [Direct Elisp buffer APIs observe physical layout text.](../phases/phase-kp-overhaul-20260726/issue_logical_text_api_20260728.md)
|
||||
- issue012 [ ] [Reprojection activates an inactive mark and selects text.](../phases/phase-kp-overhaul-20260726/issue_live_editing_interaction_20260729.md#issue012--reprojection-activates-an-inactive-mark)
|
||||
- issue013 [ ] [Active-line edge whitespace is hidden until another glyph arrives.](../phases/phase-kp-overhaul-20260726/issue_live_editing_interaction_20260729.md#issue013--active-line-edge-whitespace-is-hidden)
|
||||
- issue014 [ ] [Near-edge live editing publishes KP breaks before the paragraph is complete.](../phases/phase-kp-overhaul-20260726/issue_natural_live_editing_20260729.md)
|
||||
- issue015 [ ] [Narrow side-by-side windows truncate instead of soft-wrapping live text.](../phases/phase-kp-overhaul-20260726/issue_native_soft_wrap_20260729.md)
|
||||
- issue016 [ ] [Live editing freezes native rows instead of projecting a semantic hard-line prefix.](../phases/phase-kp-overhaul-20260726/issue_semantic_live_prefix_20260729.md)
|
||||
- issue017 [ ] [Point-only motion changes an already published live projection.](../phases/phase-kp-overhaul-20260726/issue_point_motion_projection_stability_20260729.md)
|
||||
- issue018 [ ] [Unique live appends miss the frame budget at very narrow widths.](../phases/phase-kp-overhaul-20260726/issue_narrow_live_append_latency_20260729.md)
|
||||
- issue019 [ ] [Per-edit frontier replanning destabilizes live projection and cannot restore reversible edits exactly.](../phases/phase-kp-overhaul-20260726/issue_stable_live_edit_transaction_20260729.md)
|
||||
- issue020 [ ] [C-backed resize reflow still takes roughly 60–70 ms.](../phases/phase-kp-overhaul-20260726/issue_c_resize_latency_20260729.md)
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
# Change Log: Buffer Module Naming 2026-07-28
|
||||
|
||||
## task017
|
||||
|
||||
- **Delete/Add** — `ekp-region.el` → `ekp-buffer.el`
|
||||
- Renamed the editor integration file and provided feature around the
|
||||
buffer lifecycle owner.
|
||||
- Renamed module configuration and private symbols from `ekp-region*` to
|
||||
`ekp-buffer*`.
|
||||
- Attached the buffer customization subgroup to the top-level `ekp`
|
||||
group and made the protrusion-reserve helper explicitly private.
|
||||
- Kept public commands whose names correctly describe region, buffer, or
|
||||
mode behavior.
|
||||
|
||||
- **Delete/Add** — `tests/ekp-region-tests.el` →
|
||||
`tests/ekp-buffer-tests.el`
|
||||
- Renamed the test feature, fixtures, and ERT names.
|
||||
- Updated default, random-order, isolated, GUI, and showcase loaders.
|
||||
|
||||
- **Modify** — CI, contribution, public/developer/audit documentation
|
||||
- Updated compilation, lint, checkdoc, test, and Windows CI paths.
|
||||
- Replaced the documented require/configuration surface and file maps.
|
||||
- Added the breaking migration to `CHANGELOG.md`.
|
||||
- Recorded the ownership decision in
|
||||
`postmortem/20260728-buffer-module-naming.md`.
|
||||
|
||||
## Cleanup Review
|
||||
|
||||
- Naming mismatch: resolved at the owning module/feature/configuration/test
|
||||
boundary.
|
||||
- Dead code, duplication, error handling, dependency, and UI: unchanged and
|
||||
outside this pass.
|
||||
- Fallback findings: only the existing selected-window width fallback,
|
||||
classified as a grounded undisplayed-buffer boundary.
|
||||
- No masking fallback, swallowed error, compatibility shim, new dependency,
|
||||
new abstraction, or unrelated refactor was introduced.
|
||||
|
||||
## Validation
|
||||
|
||||
- Pre-change behavior lock: ERT 130/130.
|
||||
- Focused buffer ERT after rename: 44/44.
|
||||
- Default and seeded-permuted ERT: 130/130 each.
|
||||
- Fresh-process isolation: every one of 130 selected ERT tests passed.
|
||||
- C/Elisp property fuzz: 300/300.
|
||||
- Warning-as-error production byte compilation: pass.
|
||||
- Checkdoc and pinned package-lint: pass.
|
||||
- Release invariants, shell syntax, diff check, and active-surface stale-name
|
||||
scan: pass.
|
||||
- Mechanical runtime/test equivalence against the approved rename table:
|
||||
pass.
|
||||
|
||||
## Behavior and Risk
|
||||
|
||||
- Layout, serialization, search, copy, undo, and automatic reflow behavior
|
||||
are unchanged.
|
||||
- This is an intentional source-level breaking rename: users must require
|
||||
`ekp-buffer` and rename any `ekp-region-*` settings.
|
||||
- No saved-file format, dependency, Emacs baseline, C ABI, or release
|
||||
artifact changed.
|
||||
- Generated `.elc` verification artifacts were removed after compilation;
|
||||
they can be regenerated from the source.
|
||||
@ -0,0 +1,39 @@
|
||||
# Change: C Resize Latency 2026-07-29
|
||||
|
||||
## 2026-07-30 — Complete task032 core and resize optimization
|
||||
|
||||
- **Modify** `ekp.el`: resolve a paragraph once per layout plan, reuse its
|
||||
prepared DP data, memoize width-independent natural gap geometry, and omit
|
||||
true zero-source/zero-target projection gaps.
|
||||
- **Modify** `ekp-buffer.el`: publish owned properties in one mutation,
|
||||
consume prepared gap geometry without projection-time measurement, and
|
||||
keep the active paragraph out of the static resize pass before installing
|
||||
its live prefix.
|
||||
- **Add** focused RED/GREEN regressions in `tests/ekp-tests.el` and
|
||||
`tests/ekp-buffer-tests.el`, plus the frozen dual-path evaluator in
|
||||
`tests/ekp-c-resize-evaluator.el` and
|
||||
`tests/run-c-resize-evaluator.sh`.
|
||||
- **Performance:** four interleaved rounds reduce core p95 from 42.006 ms to
|
||||
27.687 ms (34.09%) and complete resize p95 from 46.611 ms to 27.487 ms
|
||||
(41.03%); both p50 gains also exceed 20%.
|
||||
- **Correctness:** frozen-C, candidate-C, and Elisp projection hashes match.
|
||||
Normal, random-order, and isolated ERT gates pass; 300 fuzz cases, static
|
||||
checks, release checks, and reviewed temporal GUI evidence pass.
|
||||
- **Risk:** external window-system allocation can trigger an Emacs GC pause
|
||||
at the resize callback boundary. Total, GC, and EKP mutator time remain
|
||||
separately observable; no debounce or global GC behavior changed.
|
||||
|
||||
## 2026-07-29 — Plan issue020 and task032
|
||||
|
||||
- **Add** `issue020` and `task032` for the reported 60–70 ms C-backed
|
||||
resize/reflow latency.
|
||||
- **Add** the `c-resize-latency` performance-goal contract before
|
||||
optimization: paired p50/p95 improvement of at least 20%, candidate p95
|
||||
at or below 50 ms, exact frozen-C and Elisp parity, and complete
|
||||
regression gates.
|
||||
- **Decision:** measure complete reflow, plan construction, Emacs/C
|
||||
marshalling, C DP, and projection publication separately before choosing
|
||||
the implementation layer.
|
||||
- **Risk:** a whole-reflow stopwatch can misattribute Elisp measurement or
|
||||
property publication to the C algorithm. No runtime change is included in
|
||||
this planning entry.
|
||||
@ -0,0 +1,74 @@
|
||||
# Change: Editing-Frontier Stability 2026-07-29
|
||||
|
||||
## 2026-07-29 — Implement and developer-verify task029
|
||||
|
||||
- **Modify** `ekp-buffer.el`.
|
||||
- Adds a source-relative `frontier` marker to live state.
|
||||
- Derives the active semantic line from the latest real edit, relocates
|
||||
it in `after-change`, and preserves it through width/font/context
|
||||
reflow.
|
||||
- Deletes point-driven live-boundary publication. Point motion inside
|
||||
the active hard line is display read-only; leaving the hard line keeps
|
||||
the existing static completion transition.
|
||||
- Keeps core DP semantics, `ekp.el`, C ABI, schema, plan contract, source
|
||||
characters, and no-overlay ownership unchanged.
|
||||
- **Modify** `tests/ekp-buffer-tests.el`,
|
||||
`tests/ekp-gui-verify.el`, and
|
||||
`tests/ekp-buffer-live-bench.el`.
|
||||
- Locks exact property/state/source/undo identity and zero
|
||||
plan/cache/property writes across backward/forward point motion.
|
||||
- Proves reflow and deferred IME completion preserve the source-edit
|
||||
frontier even when point moves elsewhere.
|
||||
- Updates GUI and benchmark adapters to assert projection, plan,
|
||||
generation, cache, active-index, and frontier stability.
|
||||
- **Modify** bilingual user/developer docs, spec, plan, technical
|
||||
reference, issues, changelog, and superseding postmortem to use the same
|
||||
source-edit-frontier vocabulary.
|
||||
- **Verification:** focused regressions RED before implementation and
|
||||
GREEN afterward; independent focused ERT 10/10; buffer ERT 93/93;
|
||||
default, seed-`20260729`, and isolated ERT 182/182; C/Elisp fuzz 300/300;
|
||||
warning-as-error compilation, checkdoc, pinned package-lint, release,
|
||||
dictionary, pinned-source, no-overlay, no-stale-symbol, and diff gates
|
||||
pass. Point-motion p99 is 0.033 ms on C and 0.037 ms in the independent
|
||||
Elisp review, both with zero planner/cache calls.
|
||||
- **GUI evidence:** reviewed run
|
||||
`/private/tmp/ekp-frontier-live-v3-66WYRW` contains 39 manifest lines and
|
||||
a 26.6-second recording. All checkpoints and assertions pass, no black
|
||||
segment is detected, and the final evidence verdict is PASS.
|
||||
- **Review:** independent architecture review is CLEAR. Independent code
|
||||
review reports zero blockers; the one residual performance concern is
|
||||
separately owned by `issue018`/`task030`.
|
||||
- **Status:** task029 developer gate complete. `issue016` and `issue017`
|
||||
remain open until the user personally confirms the visible editing
|
||||
experience.
|
||||
|
||||
## 2026-07-29 — Record the independent append-performance debt
|
||||
|
||||
- **Add** `issue018` and `task030` after the current checked-in 80-pixel
|
||||
benchmark failed to reproduce task028's 6.399 ms append p99.
|
||||
- **Add** `postmortem/20260729-narrow-live-append-replanning.md` to keep
|
||||
unique-source-state planning cost separate from task029's point-motion
|
||||
correctness fix.
|
||||
- **Evidence:** repeated GC-excluded C-backend runs record roughly
|
||||
33–85 ms append p99, 291 plan calls, and zero cache hits. The latest run
|
||||
recorded 84.526 ms. In the same run, point motion recorded 0.033 ms p99
|
||||
with zero plan/cache calls.
|
||||
- **Behavior/Risk:** documentation only. No performance shortcut or core
|
||||
DP change is included in task029.
|
||||
|
||||
## 2026-07-29 — Plan task029
|
||||
|
||||
- **Add** `issue017`: point-only motion currently mutates an already
|
||||
published semantic live prefix.
|
||||
- **Add** `task029`: move live-boundary ownership from transient point to
|
||||
the latest real source edit, while preserving whole-hard-line planning.
|
||||
- **Modify** the current spec, plan, technical reference, and `issue016`
|
||||
expectation so point is not a plan or projection invalidation input.
|
||||
- **Add** `postmortem/20260729-editing-frontier-not-point.md` as the
|
||||
superseding decision for the point-driven portion of `task028`.
|
||||
- **Validation planned:** focused RED/GREEN public-hook ERT, complete
|
||||
buffer/default/random/isolated suites, fuzz/static gates, live
|
||||
benchmark, GUI dynamic verification, full diff review, and independent
|
||||
code/architecture review.
|
||||
- **Status:** Planning complete; superseded by the implementation record
|
||||
above.
|
||||
@ -0,0 +1,28 @@
|
||||
# Change Log: Logical Text API Issue 2026-07-28
|
||||
|
||||
## issue011
|
||||
|
||||
- **Add** —
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/issue_logical_text_api_20260728.md`
|
||||
- Recorded that direct Elisp buffer APIs observe EKP's physical layout
|
||||
representation.
|
||||
- Captured the verified physical-vs-logical extraction evidence, root
|
||||
cause, required outcome, and unresolved architecture questions.
|
||||
- Explicitly deferred implementation and avoided selecting a design or
|
||||
creating an execution task.
|
||||
|
||||
- **Modify** — `.phrase/docs/ISSUES.md`, `.phrase/docs/CHANGE.md`
|
||||
- Added the open issue and linked this documentation-only change.
|
||||
|
||||
## Behavior and Risk
|
||||
|
||||
- Runtime code and behavior are unchanged.
|
||||
- No implementation plan, compatibility promise, or solution architecture
|
||||
was approved.
|
||||
- The issue remains open until the user chooses to resume design work.
|
||||
|
||||
## Validation
|
||||
|
||||
- Confirmed `issue011` is the next unused global issue ID.
|
||||
- Confirmed the issue index link resolves to the phase detail.
|
||||
- `git diff --check` passes.
|
||||
@ -0,0 +1,76 @@
|
||||
# Change Log: Semantic Live Prefix 2026-07-29
|
||||
|
||||
## 2026-07-29 — Implement and verify task028
|
||||
|
||||
- **Modify** `ekp-buffer.el`.
|
||||
- Replaces native-row commitment with full-hard-line
|
||||
`ekp-layout-plan` consumption and point-line semantic prefix
|
||||
projection.
|
||||
- Keeps the point-containing line and suffix natural; previous semantic
|
||||
lines own their break whitespace and may reflow together.
|
||||
- Adds a 16-entry buffer-local plan LRU, same-line point-motion fast
|
||||
path, common-prefix differential publication, transactional rollback,
|
||||
theme-enable/theme-disable/frame-font context invalidation, and
|
||||
fail-closed oversized/conflict handling.
|
||||
- Leaves `ekp.el`, core DP semantics, C ABI, DP schema, and plan records
|
||||
unchanged for this task.
|
||||
- **Modify** `tests/ekp-buffer-tests.el`,
|
||||
`tests/ekp-gui-verify.el`, and **Add**
|
||||
`tests/ekp-buffer-live-bench.el`.
|
||||
- Covers public editing commands, point boundaries, earlier-break
|
||||
revision, cache/history reuse, zero-write stable plans, whitespace and
|
||||
hyphen ownership, IME, failure rollback, resize, narrowing, lifecycle,
|
||||
exact source, and zero-overlay invariants.
|
||||
- Default and seed-`20260729` ERT pass 181/181; all 181 tests pass in
|
||||
independent Emacs processes; C/Elisp fuzz passes 300/300.
|
||||
- Main dynamic GUI evidence is retained at
|
||||
`/private/tmp/ekp-semantic-live-v4-vFZTkr`; split-window evidence is at
|
||||
`/private/tmp/ekp-semantic-split-v3-uPwuOi`. Both reviewed temporal
|
||||
reports return PASS with exact source, zero overlays, and `hscroll=0`.
|
||||
- C-backend append p99 is 6.399 ms with GC excluded. Default raw p99 is
|
||||
50.212 ms when samples include approximately 45 ms GC pauses; this
|
||||
residual risk is recorded instead of changing global GC behavior.
|
||||
- **Subsequent audit:** the 6.399 ms result is not reproducible with the
|
||||
current checked-in 80-pixel benchmark. Repeated GC-excluded task029
|
||||
runs measure roughly 33–85 ms p99 with 291 unique plan misses.
|
||||
`issue018`/`task030` now track that separate performance debt; this
|
||||
historical entry is retained to show what the task028 run reported.
|
||||
- **Modify** bilingual user/developer docs, `CHANGELOG.md`, `issue016`,
|
||||
plan/technical records, and the superseding postmortem to match the
|
||||
implemented semantic-prefix model.
|
||||
- **Behavior/Risk:** The active edit suffix is now native while all earlier
|
||||
semantic lines are one jointly revisable KP prefix. Automated and GUI
|
||||
developer gates pass. Independent code review returns APPROVE and
|
||||
independent architecture review returns CLEAR. `task028` is complete;
|
||||
`issue016` remains open until the user personally confirms the visible
|
||||
editing experience.
|
||||
|
||||
## 2026-07-29 — Plan task028
|
||||
|
||||
- **Add** `issue_semantic_live_prefix_20260729.md`.
|
||||
- Records `issue016`: live editing currently freezes completed native
|
||||
rows instead of repeatedly consuming the full hard-line KP plan and
|
||||
projecting only the semantic prefix before point.
|
||||
- **Modify** `task_repository_audit_20260728.md`.
|
||||
- Adds `task028` as the next atomic implementation task with red tests,
|
||||
performance checks, GUI dynamic verification, and user-confirmation
|
||||
closure rules.
|
||||
- **Modify** `spec_text_property_layout_engine_20260729.md`.
|
||||
- Replaces the native-row live contract with the semantic hard-line
|
||||
prefix contract: full hard-line plan input, point-line boundary,
|
||||
prefix-only projection, natural point line and suffix, and no core
|
||||
DP/C ABI changes.
|
||||
- **Modify** `plan_text_property_layout_engine_20260729.md`.
|
||||
- Adds milestone M7 for replacing native-row commitment with semantic
|
||||
prefix projection while preserving the completed-paragraph DP owner.
|
||||
- **Modify** `tech-refer_text_property_layout_20260729.md`.
|
||||
- Supersedes the native progressive row model for live editing and
|
||||
documents the buffer-local plan cache, line-signature diffing, and
|
||||
fail-closed ownership rules.
|
||||
- **Add** `postmortem/20260729-whole-hard-line-live-prefix.md`.
|
||||
- Records why the native-row model was wrong for global KP alignment and
|
||||
why the correction belongs in `ekp-buffer`, not in core DP.
|
||||
- **Behavior/Risk:** Documentation and planning only. Runtime behavior is
|
||||
unchanged. `issue015` remains open exactly as before pending user-visible
|
||||
confirmation; `issue016` remains open until implementation and user
|
||||
confirmation.
|
||||
@ -0,0 +1,56 @@
|
||||
# Change: Stable Live Edit Transaction 2026-07-29
|
||||
|
||||
## 2026-07-29 — Implement and verify task031
|
||||
|
||||
- **Modify** `ekp-buffer.el`: replace frontier-owned immediate publication
|
||||
with committed live state plus one baseline-snapshot edit transaction;
|
||||
naturalize only the owning dirty span range; restore reversible edits
|
||||
exactly; and publish only at native-row or structural commit events.
|
||||
- **Modify** `ekp-buffer.el`: keep active live spans out of static lazy-reflow
|
||||
chunks and recognize both forward and backward native-row crossings.
|
||||
- **Modify** `tests/ekp-buffer-tests.el` and
|
||||
`tests/ekp-gui-verify.el`: add RED/GREEN public-path coverage for zero-plan
|
||||
same-row edits, local middle-row anchors, object-identical reversal,
|
||||
forward/backward row crossing, zero-work point motion, lazy ownership,
|
||||
stable yank/undo, resize, and hard completion.
|
||||
- **Modify** bilingual user/developer documentation, changelog, spec, plan,
|
||||
technical reference, task/issue records, and the decision postmortem to
|
||||
make transaction triggers and signature-diff ownership explicit.
|
||||
- **Verification:** buffer 99/99; default, seed-`20260729`, and isolated ERT
|
||||
188/188 each; C/Elisp fuzz 300/300; warnings-as-errors production/test
|
||||
compile; empty checkdoc; pinned package-lint exit 0; release and 49-entry
|
||||
dictionary manifest pass. The earlier exact pinned-source run remains
|
||||
applicable because dictionary bytes did not change; two redundant final
|
||||
fetches failed with GitHub `early EOF`, not a byte mismatch.
|
||||
- **Benchmark:** with GC excluded, cache-revisit and point-motion scenarios
|
||||
perform zero plans; 291 appends contain 15 structural plans. Remaining
|
||||
C/Elisp structural p99 spikes move to `issue018`/`task030`.
|
||||
- **GUI evidence:** reviewed PASS at
|
||||
`/private/tmp/ekp-stable-transaction-final5-2BFryc`: 48.95 seconds,
|
||||
55 checkpoints, 17 assertions per checkpoint, completed run-end, one
|
||||
fullscreen target window, no black segment, split, stale buffer, client
|
||||
instruction, or transient blank frame.
|
||||
- **Behavior/Risk:** ordinary editing is intentionally stability-first;
|
||||
the current dirty row may be temporarily non-optimal until a structural
|
||||
commit. Completed paragraphs still use the unchanged global KP core.
|
||||
|
||||
## 2026-07-29 — Plan issue019 and task031
|
||||
|
||||
- **Add** `issue019`: per-edit whole-hard-line replanning and suffix
|
||||
invalidation share one overloaded frontier and cannot express stable local
|
||||
editing or exact reversible restoration.
|
||||
- **Add** `task031`: replace that model with committed projection, a dirty
|
||||
edit transaction, preserved unaffected anchors, and atomic structural
|
||||
commits.
|
||||
- **Modify** `task030` to depend on `task031`; performance work must measure
|
||||
the surviving path instead of optimizing the rejected control flow.
|
||||
- **Modify** the current spec, plan, technical reference, issue index, and
|
||||
decision history so trigger ownership is explicit: transactions decide
|
||||
when layout may change, signature diffing decides what properties change,
|
||||
and the unchanged core DP decides final layout.
|
||||
- **Validation planned:** focused public-path RED/GREEN ERT; complete
|
||||
automated/static gates; re-profiled live benchmark; temporal GUI evidence
|
||||
for stable input, local middle-line editing, exact reversible restoration,
|
||||
structural commits, and zero-work point motion.
|
||||
- **Behavior/Risk:** this planning entry is superseded by the completed
|
||||
implementation entry above.
|
||||
@ -0,0 +1,286 @@
|
||||
# Change Log: Text-Property Layout Implementation 2026-07-29
|
||||
|
||||
## 2026-07-29 — Complete task027
|
||||
|
||||
- **Modify** `ekp-buffer.el`.
|
||||
- Makes native soft wrapping a mode-owned precondition by temporarily
|
||||
setting both `truncate-lines` and
|
||||
`truncate-partial-width-windows` buffer-locally to nil before the
|
||||
first reflow.
|
||||
- Restores exact prior values and local/global ownership on normal
|
||||
disable, major-mode teardown, and failed activation. An activation
|
||||
error rolls back the full lifecycle and then remains visible.
|
||||
- Centralizes installation and removal of the auto-mode hook set; no
|
||||
source character, overlay, synthetic live break, or wrapping algorithm
|
||||
was added.
|
||||
- **Modify** buffer/GUI tests and bilingual user/developer documentation.
|
||||
- Adds public lifecycle regressions for normal disable, forced reflow
|
||||
failure, and major-mode teardown.
|
||||
- Adds a deterministic 44-column split-window scenario and records the
|
||||
native-wrap requirement in the spec, plan, issue, changelog, and
|
||||
decision history.
|
||||
- **Validation:** focused lifecycle ERT 3/3; buffer ERT 77/77; default,
|
||||
seed-`20260729`, and isolated full ERT 166/166; C/Elisp fuzz 300/300;
|
||||
warning-as-error compilation; checkdoc; pinned package-lint; release,
|
||||
dictionary, pinned-source, no-overlay, stale-name, conflict, artifact,
|
||||
and diff gates.
|
||||
- **Dynamic evidence:** `/private/tmp/ekp-soft-wrap-final-pass-PIUigY`
|
||||
retains a reviewed per-character Emacs 30.2 recording in an intentional
|
||||
two-window frame. The 44-column selected window advances from one to two
|
||||
visual rows with `hscroll=0`, exact source text, zero overlays, zero live
|
||||
replacing breaks, and no pending edit. Manifest/report/recording/contact
|
||||
SHA-256:
|
||||
`53cd23f8e1be1f29545edb6daa80184d438ca87d4d7068a182faf691a8437ad8`,
|
||||
`a97a74cc5a6255c2eb5992c266c727e3ed5d5556f37672ca319373a65a110e4d`,
|
||||
`2b3f7c155d1f0e2feeca2bb79a86aafe612cc1489c486121491ec6268bc1de0a`,
|
||||
`21a78ae849e3a1d5cda1d80ee375cd655e49b221b5192a219ac6ab11d9122d53`.
|
||||
- **Behavior/Risk:** Narrow split windows now follow normal Emacs soft
|
||||
wrapping while auto mode is active. A buffer that deliberately requested
|
||||
truncation gets that exact setting back when the mode ends. `issue015`
|
||||
stays open pending user-visible confirmation.
|
||||
|
||||
## 2026-07-29 — Plan task027
|
||||
|
||||
- **Add** `issue_native_soft_wrap_20260729.md`.
|
||||
- Records `issue015`: Emacs's default narrow partial-window policy
|
||||
horizontally truncates the active paragraph, so the native progressive
|
||||
engine never observes a completed visual row.
|
||||
- **Modify** phase spec, task list, and global issue/change indexes.
|
||||
- Makes native soft wrapping an explicit auto-mode lifecycle
|
||||
precondition with exact restoration on teardown.
|
||||
- **Behavior/Risk:** Planning and red-test preparation only. Runtime
|
||||
behavior is unchanged at this entry.
|
||||
|
||||
## 2026-07-29 — Complete task025 and task026
|
||||
|
||||
- **Modify** `ekp-buffer.el`.
|
||||
- Deletes partial-KP live lookahead, stable anchors, line signatures,
|
||||
convergence, and push/pull repair.
|
||||
- Makes Emacs native redisplay own the active tail. EKP projects only
|
||||
internal gap widths on completed native visual rows and reserves full
|
||||
KP breaks, prefixes, and discretionary hyphens for completed hard
|
||||
paragraphs.
|
||||
- Invalidates a committed prefix before edits cross back into it, defers
|
||||
IME composition, rejects stale generations, respects narrowing, and
|
||||
keeps foreign display ownership isolated.
|
||||
- **Modify** `ekp.el`, buffer tests, and GUI verification.
|
||||
- Removes the now-unused partial live-flow API and replaces its
|
||||
implementation-shaped tests with the approved three-state behavioral
|
||||
contract.
|
||||
- Adds dynamic actions for native wrap, deletion restoration, and hard
|
||||
paragraph completion.
|
||||
- **Modify** bilingual user/developer docs, phase spec/plan/tasks/issue,
|
||||
changelog, and the live-layout decision history.
|
||||
- Records the native progressive model as the current workflow without
|
||||
rewriting the earlier partial-flow decision as if it never existed.
|
||||
- **Validation:** focused live contract ERT 4/4; buffer ERT 75/75; default,
|
||||
seed-`20260729`, and isolated ERT 164/164; fuzz 300/300;
|
||||
warning-as-error compilation; checkdoc; pinned package-lint; release,
|
||||
dictionary, pinned-source, ownership, no-overlay, stale-name, and diff
|
||||
gates. Exact 1–64px ASCII/CJK probes and all seven GUI variants pass.
|
||||
- **Dynamic evidence:** `/private/tmp/ekp-native-live-2dYu2U` retains a
|
||||
14.82-second fullscreen single-window Emacs 30.2 recording. It proves
|
||||
native mixed-text wrap with zero live owned breaks, exact deletion back
|
||||
to the original three native rows, and full KP only after hard newline.
|
||||
Every immediate/redisplay assertion passes; the recording contains no
|
||||
black segment or non-Emacs frame. Manifest/report/recording SHA-256:
|
||||
`cc3a626c7f8fb68f1ef1ea4ba0dd1e276281afee7a56fb88835f49bd86f239e1`,
|
||||
`0be9337a95469c6fe710518dbfa27432b4abcd511f623182021776f911887ab0`,
|
||||
`f87c3adbf16e5d17860471c1a85c89dc688b24c8e08ad82172291fc87483562e`.
|
||||
- **Behavior/Risk:** Underfilled editing performs no layout planning and
|
||||
looks native. Completed native rows receive gap-only alignment. A hard
|
||||
paragraph receives full KP only after completion or exit. Native visual
|
||||
row discovery still depends on one authoritative graphical window;
|
||||
alternate-width simultaneous plans remain intentionally unsupported.
|
||||
`issue014` stays open pending user-visible confirmation.
|
||||
|
||||
## 2026-07-29 — Plan task025 and task026
|
||||
|
||||
- **Add** `issue_natural_live_editing_20260729.md`.
|
||||
- Records `issue014`: partial KP breaks and hyphens appear while typing
|
||||
near the right edge.
|
||||
- **Modify** the phase spec, plan, task list, and global issue/change
|
||||
indexes.
|
||||
- Replaces the live product contract with the approved three-state model:
|
||||
native active tail, gap-only completed native rows, and full KP after
|
||||
hard-paragraph completion.
|
||||
- **Behavior/Risk:** Planning and red-test preparation only. Runtime
|
||||
behavior is unchanged at this entry.
|
||||
|
||||
## 2026-07-29 — Complete task023 and task024
|
||||
|
||||
- **Modify** `ekp-buffer.el`.
|
||||
- Restores the mark marker without activating it, then restores
|
||||
`mark-active` as independent editor state.
|
||||
- Keeps static paragraph-edge whitespace hiding off the active live line
|
||||
and clears from the source span beginning when its first line becomes
|
||||
active.
|
||||
- **Modify** buffer tests and GUI verification evidence.
|
||||
- Adds inactive/active mark, same-turn leading/trailing space and tab,
|
||||
CJK space, deletion-exposed whitespace, consecutive space/backspace,
|
||||
yank, newline, and undo regressions through public command paths.
|
||||
- Retains the existing overflow/pullback, paragraph transition, resize,
|
||||
IME, foreign-owner, teardown, and bounded-work matrix.
|
||||
- **Modify** bilingual user/developer docs, phase spec/plan/tasks/issues,
|
||||
changelog, and `postmortem/20260729-active-line-edge-state.md`.
|
||||
- Records the editor-state and active-line ownership rules at their
|
||||
durable documentation boundaries.
|
||||
- **Validation:** focused red 0/3 → green 3/3; buffer ERT 79/79; full
|
||||
default and seed-`20260729` ERT 170/170; isolated fresh-process ERT
|
||||
170/170; C/Elisp fuzz 300/300; warning-as-error compilation; checkdoc;
|
||||
pinned package-lint; release, dictionary, shell, artifact, stale-name,
|
||||
no-overlay, and diff gates.
|
||||
- **Dynamic evidence:**
|
||||
`/private/tmp/ekp-interaction-clean-rdZhdD` records three actions:
|
||||
showcase width change, one-space insertion, and deletion exposing that
|
||||
space. Every immediate and redisplay assertion passes, with no region,
|
||||
pending transaction, replacing display, overlay, black segment, or
|
||||
non-Emacs window. Manifest/report/recording SHA-256:
|
||||
`0eaeebd1697d69304cfd70189d7381a026b761333ab46ab1e2c5923a6e2db0c9`,
|
||||
`940efdb923a831dcd4b098cafee33b9a67c472f227be1897bd54cab0693f8b68`,
|
||||
`519a4ba2757b453e54a2518c83784bfed26317eae4c3e246a984f38a2d30b2f5`.
|
||||
- **Behavior/Risk:** The two regressions are fixed at shared projection
|
||||
owners rather than showcase keys or self-insert advice. Static committed
|
||||
lines retain edge cleanup. `issue012` and `issue013` remain open only for
|
||||
required user-visible acceptance.
|
||||
|
||||
## 2026-07-29 — Plan task023 and task024
|
||||
|
||||
- **Add** `issue_live_editing_interaction_20260729.md`.
|
||||
- Records `issue012` for inactive-mark activation during reprojection and
|
||||
`issue013` for hidden active-line edge whitespace.
|
||||
- **Modify** the phase spec, plan, task list, and global issue/change
|
||||
indexes.
|
||||
- Makes same-turn whitespace visibility and independent `mark-active`
|
||||
preservation explicit product contracts.
|
||||
- Adds a bounded adjacent-interaction audit instead of treating the two
|
||||
reported symptoms as isolated key-handler patches.
|
||||
- **Behavior/Risk:** Planning and red-test records only; runtime behavior is
|
||||
unchanged at this entry.
|
||||
|
||||
## 2026-07-29 — Complete task022
|
||||
|
||||
- **Add** `spec_text_property_layout_engine_20260729.md`.
|
||||
- Records the current user-visible contract, command and live-edit flows,
|
||||
display-property mapping, failure/rollback behavior, edge cases, and
|
||||
acceptance criteria.
|
||||
- Corrects a completion-audit gap: the objective required a phase spec,
|
||||
but the implementation phase had only plan/technical records.
|
||||
- **Modify** implementation, tests, automation, bilingual documentation,
|
||||
phase records, changelog, and the live-layout postmortem.
|
||||
- Links the spec from the plan, tasks, issue, and change trace.
|
||||
- Closes the repository-wide text-property architecture gate after a full
|
||||
diff and ownership review.
|
||||
- Removes the dead `ekp-buffer--justified-spans` and
|
||||
`ekp--para-glue-shrink` functions; adds no wrapper, compatibility shim,
|
||||
dependency, overlay path, or source-character fallback.
|
||||
- **Validation:** buffer ERT 71/71; full default and seeded-permuted ERT
|
||||
162/162; isolated fresh-process ERT 162/162; C/Elisp fuzz 300/300;
|
||||
warning-as-error byte compilation; checkdoc; pinned package-lint;
|
||||
release, dictionary, pinned-upstream dictionary, shell, diff, stale-name,
|
||||
no-overlay, and dead-private-function scans; portable/native/debug/
|
||||
sanitizer C builds; exact 1–64px GUI glue; seven-case GUI matrix; and
|
||||
dynamic overflow/type/delete temporal verification.
|
||||
- **Retained GUI/dynamic evidence:**
|
||||
- `/tmp/ekp-live-final-029c-fOG8xp/manifest.jsonl`
|
||||
(`sha256:727305251edf07e4cee43bc6b8b25610a384e7d4f071656de388ec6e23913fd9`)
|
||||
records one live Emacs window, source/projection hashes, zero overlays,
|
||||
exact overflow/pullback assertions, immediate/redisplay equality, and a
|
||||
completed 31,707ms run.
|
||||
- The clean first and final screenshots are
|
||||
`frames/000001-setup-setup.png`
|
||||
(`sha256:5e324e25f278867b57c31b825c7d56c87d15b18f7fd1e99e276857127c0ddab8`)
|
||||
and `frames/000008-completion-final.png`
|
||||
(`sha256:f5e28d98190b8a494d7f010b423c217b104e76f8c79cc792a8359a328ae1797d`).
|
||||
- `report.md`
|
||||
(`sha256:fec75f10a851f220a937456fa17900c178f9466874340d1e178ca6e333693904`)
|
||||
returns `PASS` with no incomplete or black segments; `recording.mov`
|
||||
(`sha256:2e31a13b70c2387a0e9a08a1b87947180a46310e3f1becb8c445c768998ac347`)
|
||||
is the 4,875,735-byte temporal source.
|
||||
- **Independent review:** code reviewer `APPROVE`, zero findings;
|
||||
architecture reviewer `CLEAR`, no unresolved architectural blocker.
|
||||
- **Evidence correction:** replaces the stale duration in phase records
|
||||
with 31.71 seconds, matching the retained dynamic
|
||||
manifest's 31,707ms `run-end`.
|
||||
- **Strengthen** `tests/ekp-buffer-tests.el`.
|
||||
- Replaces presence-only display checks with exact structural assertions
|
||||
for ASCII `((space-width FACTOR) (min-width ((PIXELS))))`, zero-source
|
||||
CJK `(min-width ((PIXELS)))`, and the `line-prefix` pixel-space value.
|
||||
- Focused exact-property ERT passed 3/3; the current full suite passed
|
||||
162/162. Warning-as-error compilation of the strengthened test file,
|
||||
production checkdoc, release checks, artifact cleanliness, and
|
||||
`git diff --check` passed.
|
||||
- **Behavior/Risk:** The runtime and repository gates are complete.
|
||||
`issue011` intentionally remains open until the user accepts the visible
|
||||
editing behavior, as required by the issue lifecycle.
|
||||
|
||||
## 2026-07-29 — Complete task021
|
||||
|
||||
- **Modify** `ekp-buffer.el`.
|
||||
- Adds synchronous bounded live flow with a natural unfinished line,
|
||||
minimal overflow push, deletion pullback, stable-line convergence,
|
||||
generation-checked IME/resize work, and automatic paragraph limits.
|
||||
- Keeps one source-marker anchor for a continuous editing flow so
|
||||
repeated deletion cannot move the recomputation boundary into the
|
||||
stable prefix.
|
||||
- Releases the active anchor on paragraph exit, explicit refill,
|
||||
hard-boundary reflow, conflict abandonment, and mode teardown.
|
||||
- **Modify** `tests/ekp-buffer-tests.el`, `tests/ekp-gui-verify.el`.
|
||||
- Adds public-hook live-edit regressions, bounded-work tests, exact
|
||||
type/delete break restoration, anchor lifecycle checks, and
|
||||
before/immediate/settled source/projection hash assertions.
|
||||
- **Modify** bilingual user/developer docs, current plan/test specification,
|
||||
changelog, and `postmortem/20260729-text-property-live-layout.md`.
|
||||
- **Behavior/Risk:** Ordinary edits never run a delayed whole-paragraph
|
||||
formatter. The unfinished point line remains natural; overflow and
|
||||
pullback publish synchronously. Buffer ERT passed 71/71. The final
|
||||
31.71s dynamic artifact passed every checkpoint with zero overlays,
|
||||
no immediate/settled hash drift, and exact deletion restoration.
|
||||
|
||||
## 2026-07-29 — Complete task020
|
||||
|
||||
- **Delete** `ekp-region.el`, `tests/ekp-region-tests.el`.
|
||||
- **Add** `ekp-buffer.el`, `tests/ekp-buffer-tests.el`.
|
||||
- Replaces delete/insert physical layout with owned text properties on
|
||||
existing source characters only.
|
||||
- Maps ASCII glue to combined `space-width`/absolute-pixel `min-width`,
|
||||
zero-source glue to `min-width`, indentation to `line-prefix`, and
|
||||
breaks/hyphens to replacing display strings.
|
||||
- Preserves foreign property owners, direct logical text APIs, copy/save/
|
||||
search semantics, point, undo, modified state, and external hooks.
|
||||
- **Modify** GUI verification and bilingual user/developer documentation.
|
||||
- **Behavior/Risk:** Buffer ERT passed 71/71. The clean Emacs 30.2 static
|
||||
probe passed exact 1–64px ASCII/CJK targets, display-only break/hyphen,
|
||||
point/vertical motion/region/mouse, unchanged source, and zero overlays;
|
||||
the 7-case scale/remap/fringe/width matrix passed.
|
||||
|
||||
## 2026-07-29 — Complete task019
|
||||
|
||||
- **Modify** `ekp.el`.
|
||||
- Adds semantic `ekp-layout-plan`, `ekp-layout-line`, and
|
||||
`ekp-layout-gap` records with source offsets, exact glue targets,
|
||||
break kinds, and discretionary-hyphen decisions.
|
||||
- Makes the existing reversible string renderer consume the shared plan.
|
||||
- **Modify** `tests/ekp-tests.el`.
|
||||
- Adds source mapping, hyphen decision, and public-dispatch regressions.
|
||||
- **Modify** `DEVELOPER.md`, `DEVELOPER_ZH.md`.
|
||||
- Documents the layout/rendering representation boundary.
|
||||
- **Behavior/Risk:** Public string formatting remains compatible. The new
|
||||
plan is the single source for the upcoming buffer display projection.
|
||||
Validation: focused red 0/3 → green 3/3; full ERT 133/133; fuzz 300/300;
|
||||
warning-as-error compilation, checkdoc, and diff checks clean.
|
||||
|
||||
## 2026-07-29 — Plan task019 through task022
|
||||
|
||||
- **Add**
|
||||
`plan_text_property_layout_engine_20260729.md`.
|
||||
- Locks the shared core-plan architecture, text-property-only buffer
|
||||
mapping, live active-line state machine, narrowest-window policy,
|
||||
display ownership, verification gates, and rollback boundary.
|
||||
- **Modify**
|
||||
`task_repository_audit_20260728.md`.
|
||||
- Adds atomic `task019` through `task022` for core extraction, static
|
||||
projection, live editing, and final closure.
|
||||
- **Behavior/Risk:** Planning records only. Runtime behavior is unchanged.
|
||||
The main risk is Emacs display-property interaction; each milestone has a
|
||||
stop gate before downstream implementation.
|
||||
@ -0,0 +1,56 @@
|
||||
# Change Log: Text-Property Layout Research 2026-07-29
|
||||
|
||||
## task018 / issue011
|
||||
|
||||
- **Add** —
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/tech-refer_text_property_layout_20260729.md`
|
||||
- Recorded the user-mandated text-property-only direction.
|
||||
- Mapped KP boxes, Latin/CJK glue, indentation, chosen breaks, and
|
||||
discretionary hyphens to Emacs display properties.
|
||||
- Recorded official documentation/source evidence, exact boundaries, and
|
||||
a minimum GUI verification gate.
|
||||
- Proposed a non-polluting Latin hyphen representation in which the
|
||||
preceding source grapheme displays as
|
||||
`GRAPHEME + HYPHEN + NEWLINE`.
|
||||
|
||||
- **Modify** —
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/issue_logical_text_api_20260728.md`
|
||||
- Marked design research as resumed while keeping the issue open.
|
||||
- Added the confirmed primitives, Emacs 30.2 GUI probe results, proposed
|
||||
hyphen owner, and unresolved architecture constraints.
|
||||
|
||||
- **Modify** —
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/task_repository_audit_20260728.md`
|
||||
- Added and completed documentation-only `task018`.
|
||||
|
||||
- **Modify** — `.omx/plans/prd-ekp-seamless-live-layout.md`,
|
||||
`.omx/plans/test-spec-ekp-seamless-live-layout.md`
|
||||
- Added a supersession notice so the prior overlay-oriented prototypes
|
||||
are not executed after the user's text-property-only decision.
|
||||
|
||||
- **Modify** — `.phrase/docs/CHANGE.md`
|
||||
- Indexed this phase change.
|
||||
|
||||
## Behavior and Risk
|
||||
|
||||
- No runtime source, test implementation, package API, or persisted file
|
||||
format changed.
|
||||
- The recorded GUI probes used temporary buffers in clean Emacs 30.2
|
||||
daemons and left no repository or user-buffer state.
|
||||
- Static LTR feasibility is evidence, not proof of live editing,
|
||||
multi-window independence, bidi correctness, or foreign display-property
|
||||
composition.
|
||||
- `issue011` remains open.
|
||||
|
||||
## Validation
|
||||
|
||||
- Official GNU Emacs display/text-property documentation was cross-checked
|
||||
against the Emacs 30.2 display source.
|
||||
- GUI glue probe: a natural 7px source space rendered at exact 3px and 20px
|
||||
targets through combined `space-width` and `min-width`; source characters
|
||||
were unchanged.
|
||||
- GUI hyphen probe: `abcdefgh` remained eight source characters while a
|
||||
property on `d` displayed `d-\n`, produced two screen lines, moved one
|
||||
visual line to source position 5, and honored a 20px `line-prefix`.
|
||||
- Documentation links, task/change traceability, Markdown structure, and
|
||||
`git diff --check` were inspected.
|
||||
@ -0,0 +1,55 @@
|
||||
# Issue: C Resize Latency 2026-07-29
|
||||
|
||||
## issue020 [ ] C-backed resize reflow still takes roughly 60–70 ms
|
||||
|
||||
- **Status:** Developer verification complete; user confirmation pending.
|
||||
- **Summary:** Resizing with the C dynamic module enabled was observed at
|
||||
roughly 60–70 ms. That number covered more than the C algorithm: the
|
||||
buffer path also rebuilt layout input, crossed the Emacs-module boundary,
|
||||
reconstructed layout data, and published text properties.
|
||||
- **Environment:** Graphical Emacs 30.2 on Apple Silicon; C module 1.6;
|
||||
portable production profile unless a benchmark explicitly labels another
|
||||
profile.
|
||||
- **Repro:**
|
||||
1. Load the C module and a representative mixed Latin/CJK buffer.
|
||||
2. Reflow the same source through a deterministic sequence of uncached
|
||||
widths.
|
||||
3. Measure complete reflow and its plan, module, DP, and publication
|
||||
layers with GC pauses reported separately.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: candidate p95 is at most 50 ms and both p50 and p95 improve by
|
||||
at least 20% against the frozen same-machine baseline.
|
||||
- Actual: the final four-round evaluator records core p50/p95 of
|
||||
15.318/27.687 ms and complete resize p50/p95 of 15.900/27.487 ms.
|
||||
Against the frozen baseline this is a 33.25%/34.09% core improvement
|
||||
and a 43.51%/41.03% complete-resize improvement.
|
||||
- **Investigation:** The portable C call averaged roughly 1.8 ms and was
|
||||
not the dominant layer. Repeated paragraph resolution, projection-time
|
||||
gap measurement, no-op gap records, duplicate property publication, and
|
||||
publishing the active paragraph once statically and again as the live
|
||||
prefix owned most avoidable work. A real GUI resize can still include an
|
||||
Emacs GC pause after `set-window-margins`; that pause is reported
|
||||
separately instead of being hidden by a global threshold change.
|
||||
- **Required Outcome:** Reduce the real owning layer while preserving exact
|
||||
frozen-C and Elisp layout parity, exact cache identity, stable live
|
||||
transactions, and source-clean projection.
|
||||
- **Forbidden Shortcuts:** Longer debounce, skipped intermediate widths,
|
||||
stale or approximate plan reuse, asynchronous stale publication, and
|
||||
global GC changes.
|
||||
- **Fix:** Resolve each paragraph once per plan, carry prepared DP/gap
|
||||
geometry through projection, omit true zero-source/zero-target gaps,
|
||||
publish owned properties atomically, and exclude the active paragraph
|
||||
from the static resize pass before installing its live prefix.
|
||||
- **Verification:** Four interleaved frozen-baseline/candidate rounds pass
|
||||
the core and complete-resize gates with exact frozen-C/Elisp layout
|
||||
parity. The normal and permuted 193-test suites pass, 300 fuzz cases pass,
|
||||
warning-as-error byte compilation, package-lint, checkdoc, release checks,
|
||||
and the isolated-test run pass. Reviewed GUI evidence at
|
||||
`/private/tmp/ekp-c-resize-gui-final6-FMFLxx` records four real width
|
||||
changes, 14 checkpoints, no failed assertion or pixel overflow, and a
|
||||
maximum EKP mutator time of 22.989 ms; one 74.099 ms total sample contains
|
||||
58.792 ms of Emacs GC and 15.307 ms of EKP work.
|
||||
- **User Confirmation:** Required after implementation evidence is delivered.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Pending.
|
||||
- **Commit:** Pending.
|
||||
@ -0,0 +1,119 @@
|
||||
# Issue: Live Editing Interaction Regressions 2026-07-29
|
||||
|
||||
## issue012 [ ] Reprojection activates an inactive mark
|
||||
|
||||
- **Status:** Runtime fix and repository verification are complete. The
|
||||
issue remains open until the user confirms the visible behavior.
|
||||
- **Summary:** Changing width in `ekp-showcase` can highlight text even
|
||||
though the user did not activate a region.
|
||||
- **Environment:** Text-property renderer after `task022`; Emacs 30.2.
|
||||
- **Repro:**
|
||||
1. Open `ekp-showcase`.
|
||||
2. Move point so the buffer has an existing but inactive mark.
|
||||
3. Press `-`, `+`, Left, or Right to change the projection width.
|
||||
4. Observe a highlighted region between point and the old mark.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: width changes preserve point, mark position, and whether the
|
||||
region is active.
|
||||
- Actual: the old mark becomes active during reprojection.
|
||||
- **Investigation:** `ekp-justify-region` restores every existing mark with
|
||||
`set-mark`. That command activates the mark even when it was inactive
|
||||
before layout. The showcase exposes the central command defect because
|
||||
every width key calls the same reprojection path.
|
||||
- **Root Cause:** The projection command restores the mark's position but
|
||||
does not preserve the independent `mark-active` editor state.
|
||||
- **Required Outcome:** Restore the mark marker without activating it, and
|
||||
prove both the central command and showcase width flow leave an inactive
|
||||
mark inactive.
|
||||
- **Fix:** `task023` now restores the existing mark marker directly and
|
||||
restores `mark-active` independently. It never calls `set-mark` during
|
||||
reprojection.
|
||||
- **Verification:** The focused regression failed before the fix because
|
||||
`mark-active` became non-nil, then passed after the owner-layer change.
|
||||
A clean full-screen GUI width-key run preserved point 120, mark 20,
|
||||
`mark_active=false`, `region_active=false`, and zero overlays before,
|
||||
immediately after, and after redisplay.
|
||||
- **User Confirmation:** Pending after the visible fix is delivered.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Pending.
|
||||
- **Commit:** Pending.
|
||||
|
||||
## issue013 [ ] Active-line edge whitespace is hidden
|
||||
|
||||
- **Status:** Runtime fix and repository verification are complete. The
|
||||
issue remains open until the user confirms the visible behavior.
|
||||
- **Summary:** With `ekp-auto-justify-mode` enabled, a newly typed trailing
|
||||
space or tab can appear to do nothing. It becomes visible only after a
|
||||
following non-whitespace glyph is typed. The same defect affects newly
|
||||
typed leading whitespace and whitespace exposed by deletion.
|
||||
- **Environment:** Text-property renderer after `task022`; Emacs 30.2.
|
||||
- **Repro:**
|
||||
1. Enable `ekp-auto-justify-mode` in a non-empty paragraph.
|
||||
2. Move to the paragraph end and type one space.
|
||||
3. Inspect the inserted source character or type one ordinary glyph.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: every command-loop insertion is visible immediately on the
|
||||
natural active line.
|
||||
- Actual: the source space exists immediately, but EKP owns
|
||||
`display ""` on it until another glyph makes it an interior character.
|
||||
- **Investigation:** The live renderer correctly asks `ekp--project-line`
|
||||
to leave the active line's glue natural, then separately applies static
|
||||
paragraph-edge cleanup. Because the KP plan strips leading and trailing
|
||||
whitespace from its content bounds, that cleanup hides active-line edge
|
||||
whitespace after every edit.
|
||||
- **Root Cause:** Static completed-paragraph edge policy leaks into the
|
||||
unfinished active-line projection. The active line is only partly
|
||||
natural.
|
||||
- **Required Outcome:** Keep all source characters on the point-containing
|
||||
active line natural, including leading/trailing spaces and tabs. Preserve
|
||||
static edge cleanup on committed lines.
|
||||
- **Fix:** `task023` prevents static paragraph-edge hiding from running on
|
||||
the active line. When the first planned line is active, live cleanup also
|
||||
starts at the source span beginning so an old hidden leading edge cannot
|
||||
survive.
|
||||
- **Verification:** Focused mark/edge regressions failed 0/3 before the
|
||||
fix and passed 3/3 after it. The adjacent public-command matrix covers
|
||||
consecutive whitespace/backspace, yank, newline, undo, active region,
|
||||
overflow/pullback, paragraph exit, resize, IME, and teardown. In the
|
||||
clean GUI run, one space advanced point 67→68 and cursor x 462→469 in
|
||||
the same action; the source space had neither EKP-owned nor public
|
||||
replacing display immediately or after redisplay. Typing and deleting a
|
||||
following glyph left that state unchanged.
|
||||
- **User Confirmation:** Pending after the visible fix is delivered.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Pending.
|
||||
- **Commit:** Pending.
|
||||
|
||||
## Related Audit Scope
|
||||
|
||||
`task024` checks the same ownership boundary across character insertion,
|
||||
consecutive whitespace, deletion/backspace, newline, yank, undo, point,
|
||||
mark/region, visual-break boundaries, paragraph entry/exit, resize, and
|
||||
mode teardown. New concrete defects receive new issue IDs rather than being
|
||||
silently folded into these two symptoms.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
- Buffer ERT: 79/79.
|
||||
- Full ERT: 170/170 in default order and 170/170 with seed `20260729`;
|
||||
every test also passed in a fresh Emacs process.
|
||||
- C/Elisp property fuzz: 300/300.
|
||||
- Warning-as-error compilation: production files plus
|
||||
`tests/ekp-buffer-tests.el`.
|
||||
- checkdoc, pinned package-lint, release, dictionary, shell syntax,
|
||||
artifact, stale-name, no-overlay, and diff checks pass.
|
||||
- Dynamic artifact:
|
||||
`/private/tmp/ekp-interaction-clean-rdZhdD`.
|
||||
`report.md` returns `PASS`, `manifest.jsonl` records all assertions true,
|
||||
and the temporal contact sheet contains one clean full-screen Emacs
|
||||
window with no black segment.
|
||||
- Artifact hashes:
|
||||
- manifest:
|
||||
`0eaeebd1697d69304cfd70189d7381a026b761333ab46ab1e2c5923a6e2db0c9`
|
||||
- report:
|
||||
`940efdb923a831dcd4b098cafee33b9a67c472f227be1897bd54cab0693f8b68`
|
||||
- recording:
|
||||
`519a4ba2757b453e54a2518c83784bfed26317eae4c3e246a984f38a2d30b2f5`
|
||||
- first/final screenshots:
|
||||
`8cd697130fd54b65bde6d1c06410f88c205779abce563b1e83661849dfc163e2`,
|
||||
`11996136c3924aea9d539f77bd9db13b55310c089967c43091d2f61133a0d4db`
|
||||
@ -0,0 +1,132 @@
|
||||
# Issue: Logical Text API Boundary 2026-07-28
|
||||
|
||||
## issue011 [ ] Direct Elisp buffer APIs observe physical layout text
|
||||
|
||||
- **Status:** Runtime fix and complete repository gate implemented by
|
||||
`task019`–`task022`. The issue remains open only until the user accepts
|
||||
the visible behavior.
|
||||
- **Summary:** The former buffer renderer wrote synthesized glue spaces,
|
||||
soft newlines, discretionary hyphens, and hidden payload text into the
|
||||
real buffer. Boundary adapters made save/copy/search appear logical, but
|
||||
ordinary Elisp character APIs still observed the physical layout.
|
||||
- **Environment:** Text-property renderer and synchronous live flow after
|
||||
`task021`; Emacs 30.2.
|
||||
- **Historical Repro:**
|
||||
1. Insert text into a temporary buffer.
|
||||
2. Run the pre-`task020` `ekp-justify-region` with a narrow width.
|
||||
3. Compare the original text with `buffer-string` or
|
||||
`buffer-substring-no-properties`.
|
||||
4. Compare the same region through `filter-buffer-substring`.
|
||||
- **Historical Evidence:** The pre-fix batch probe produced a 39-character
|
||||
physical representation from a 30-character logical string.
|
||||
`buffer-string` did not equal the original, while the boundary-filtered
|
||||
substring did.
|
||||
- **Expected vs Current:**
|
||||
- **Expected:** Ordinary Elisp buffer-text APIs observe the original
|
||||
character sequence while EKP's justified presentation remains visible
|
||||
and editable.
|
||||
- **Current implementation:** `buffer-string`,
|
||||
`buffer-substring-no-properties`, direct character access, ordinary
|
||||
search, syntax, markers, and saving operate on the unchanged source
|
||||
characters. APIs that preserve or inspect text properties may still
|
||||
observe EKP's display metadata; copy/kill strips that owned metadata.
|
||||
- **Investigation:**
|
||||
- The former `ekp-justify-region` deleted source text and inserted the
|
||||
physical rendered string.
|
||||
- Reversible `ekp-glue`, `ekp-soft-break`, `ekp-soft-hyphen`, and
|
||||
`ekp-hidden` markers made restoration possible but did not keep direct
|
||||
buffer reads logical.
|
||||
- Save/copy/isearch adapters compensated at individual boundaries and
|
||||
could not cover arbitrary Elisp consumers.
|
||||
- The correct owner is the buffer representation: source characters must
|
||||
remain authoritative, and layout must be presentational state only.
|
||||
- **2026-07-29 Design Evidence:**
|
||||
- The user ruled out overlays and selected text properties on existing
|
||||
source characters as the only projection mechanism.
|
||||
- `(min-width ((PIXELS)))` can add an exact absolute-pixel minimum width
|
||||
without adding buffer characters.
|
||||
- `(space-width FACTOR)` can shrink or stretch existing ASCII spaces.
|
||||
Combined with `min-width`, it rendered a natural 7px space at exact 3px
|
||||
and 20px targets in a clean GUI Emacs 30.2 probe while source text
|
||||
remained `"a b"`.
|
||||
- `line-prefix` can provide display-only indentation for
|
||||
non-continuation lines. Native continuation lines still require
|
||||
`wrap-prefix`.
|
||||
- A replacing `display` string on an existing grapheme can show the
|
||||
grapheme followed by a discretionary hyphen and visual newline. A GUI
|
||||
probe displayed `d-\n` from the source `d` in `abcdefgh`; the source
|
||||
stayed eight characters, two screen lines were produced, one
|
||||
`vertical-motion` landed on source position 5, and a 20px
|
||||
`line-prefix` applied to both rows.
|
||||
- The proposed Latin hyphen owner is the complete grapheme immediately
|
||||
before the selected break. Its display string reproduces that grapheme,
|
||||
appends the hyphen and newline, and uses the `cursor` property to keep
|
||||
point on the source glyph.
|
||||
- Full reasoning, source links, limitations, and verification gates are
|
||||
recorded in `tech-refer_text_property_layout_20260729.md`.
|
||||
- **Root Cause:** Logical document state and rendered layout shared one
|
||||
mutable character sequence. Reversible markers made the physical
|
||||
representation recoverable, not logical.
|
||||
- **Required Outcome:** Remove the leak at the representation owner rather
|
||||
than adding per-command adapters. Ordinary Elisp character consumers must
|
||||
see logical content without losing justified display, exact round-trip,
|
||||
or seamless editing.
|
||||
- **Implemented Design:**
|
||||
- Logical source characters stay in the real buffer.
|
||||
- The buffer renderer creates no overlay and inserts no layout character.
|
||||
- Existing ASCII spaces use combined `space-width` and absolute-pixel
|
||||
`min-width`; zero-source CJK/mixed glue uses `min-width` on the
|
||||
preceding complete grapheme; indentation uses `line-prefix`.
|
||||
- CJK breaks and Latin discretionary hyphens use replacing display
|
||||
strings on an existing complete grapheme.
|
||||
- Projection values have exact EKP owner properties, are nonsticky, and
|
||||
update under `with-silent-modifications`.
|
||||
- Foreign `display`, `line-prefix`, `wrap-prefix`, `composition`, and
|
||||
`invisible` owners make only the affected hard paragraph natural and
|
||||
diagnostic.
|
||||
- One buffer uses the narrowest live window as its authoritative width;
|
||||
simultaneous independent per-window plans are not claimed.
|
||||
- Live editing uses a natural unfinished line, bounded overflow/pullback,
|
||||
a stable active-flow anchor, line-signature convergence, IME deferral,
|
||||
stale-generation rejection, and bounded automatic paragraph work.
|
||||
- **Fix:** `ekp.el` now exposes a reusable semantic layout plan while
|
||||
retaining the compatible returned-string API. `ekp-buffer.el` consumes
|
||||
that plan as a source-clean text-property projection and runs a distinct
|
||||
synchronous live-flow state machine.
|
||||
- **Verification to Date:**
|
||||
- Buffer ERT passes 79/79, including unchanged source/tick/undo/modified
|
||||
state, direct logical reads, save/copy/isearch, foreign ownership,
|
||||
no-overlay checks, exact property forms, overflow, deletion pullback,
|
||||
exact type/delete break restoration, immediate active-line edge
|
||||
whitespace, active/inactive mark preservation, anchor lifecycle, IME,
|
||||
undo, resize, multiwindow width, and overload behavior.
|
||||
- A clean Emacs 30.2 static probe passes exact 1–64px ASCII and
|
||||
zero-source CJK glue, display-only breaks/hyphens, `line-prefix`, point,
|
||||
vertical motion, region, mouse, unchanged source, and zero overlays.
|
||||
The seven-case scale/remap/fringe/width matrix passes.
|
||||
- A 31.71s dynamic recording passes before/immediate/settled overflow and
|
||||
deletion checkpoints. Immediate and settled source/projection hashes
|
||||
match; deletion restores the original source/projection hashes and
|
||||
break count; every assertion is true and temporal review passes.
|
||||
- Final-source default and seeded-permuted ERT pass 170/170; every one of
|
||||
the 170 tests passes in a fresh Emacs process; C/Elisp fuzz passes
|
||||
300/300.
|
||||
- Warning-as-error Elisp compilation, checkdoc, pinned package-lint,
|
||||
release/dictionary/static gates, all four C build profiles, full diff
|
||||
review, and anti-slop cleanup pass.
|
||||
- Independent final code review returns `APPROVE` with no findings;
|
||||
independent architecture review returns `CLEAR`.
|
||||
- **User Confirmation:** On 2026-07-28, the user explicitly defined the
|
||||
target as clean text through Elisp APIs and requested recording only,
|
||||
with implementation deferred. On 2026-07-29, the user resumed design,
|
||||
excluded overlays, selected text properties, identified `min-width`,
|
||||
`space-width`, and `line-prefix`, requested a non-polluting hyphen
|
||||
implementation, then explicitly requested complete implementation under
|
||||
the persistent goal model. Final visible-behavior acceptance is pending.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Pending.
|
||||
- **Commit:** Pending.
|
||||
- **Related:** `ekp.el`, `ekp-buffer.el`, `task017`–`task022`,
|
||||
`spec_text_property_layout_engine_20260729.md`,
|
||||
`tech-refer_text_property_layout_20260729.md`,
|
||||
`postmortem/20260729-text-property-live-layout.md`.
|
||||
@ -0,0 +1,59 @@
|
||||
# Issue: Narrow Live-Append Latency 2026-07-29
|
||||
|
||||
## issue018 [ ] Unique live appends miss the frame budget at very narrow widths
|
||||
|
||||
- **Status:** Re-profiled after `task031`; `task030` is unblocked.
|
||||
- **Summary:** Stable transactions removed per-key whole-hard-line planning.
|
||||
The synthetic 80-pixel workload now performs zero planning on ordinary
|
||||
same-row edits, but structural row-crossing commits still make append p99
|
||||
exceed the 16 ms frame budget.
|
||||
- **Environment:** Emacs 30.2 on macOS, C backend 1.6, benchmark width
|
||||
fixed at 80 px, `gc-cons-threshold` bound to
|
||||
`most-positive-fixnum` so mutator work is measured without GC pauses.
|
||||
- **Repro:**
|
||||
1. Load the C backend.
|
||||
2. Run `tests/ekp-buffer-live-bench.el` with GC excluded.
|
||||
3. Inspect the `append` row.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: live append p99 remains below the 16 ms interaction budget,
|
||||
including narrow windows.
|
||||
- Actual: the final task031 GC-excluded runs record only 15 permitted
|
||||
structural plans across 291 appends. C measures median 2.177 ms and p99
|
||||
51.170 ms; Elisp measures median 2.176 ms and p99 187.499 ms. Same-row
|
||||
cache-revisit work records zero plans (C p99 1.627 ms; Elisp p99
|
||||
1.502 ms), while point motion records zero plan/cache calls (C p99
|
||||
0.017 ms; Elisp p99 0.015 ms).
|
||||
- **Investigation:**
|
||||
- The task029 point-motion change is not the cause. Navigation is now a
|
||||
zero-work path and does not enter planning or projection publication.
|
||||
- The old workload produced 291 distinct hard-line strings and 291 plan
|
||||
calls. `task031` removes that obsolete control flow: only 15 actual
|
||||
visual-row crossings invoke `ekp-layout-plan`.
|
||||
- The remaining high percentiles coincide with those allowed structural
|
||||
commits and must be profiled as commit latency, not average key latency.
|
||||
- Wider measurements are materially faster; the pathological result is
|
||||
width-sensitive rather than a general point-motion regression.
|
||||
- The previously recorded 6.399 ms task028 append p99 is not
|
||||
reproducible with the current checked-in benchmark and must not remain
|
||||
the current performance claim.
|
||||
- Both backends still miss p99 at structural boundaries, so task030 must
|
||||
evaluate both rather than optimize only the C wrapper.
|
||||
- **Required Outcome:**
|
||||
- Diagnose and reduce unique-state narrow-width append planning cost
|
||||
without changing KP output semantics, the core DP contract, C ABI, or
|
||||
source-clean display model.
|
||||
- Keep cache identity exact; do not introduce approximate/stale plan
|
||||
reuse, timers, skipped edits, global GC changes, or point-based
|
||||
invalidation.
|
||||
- Preserve the task029 invariant that point-only motion performs zero
|
||||
plan, cache, and text-property work.
|
||||
- **Fix:** Deferred to `task030`; the prerequisite correctness replacement
|
||||
is complete. Optimize only the surviving structural-commit path.
|
||||
- **Verification:** Future work must use a reproducible width matrix,
|
||||
profiler evidence, result-equivalence tests, default-GC and GC-excluded
|
||||
runs, and GUI input evidence.
|
||||
- **User Confirmation:** Not applicable until a performance change is
|
||||
implemented.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Pending.
|
||||
- **Commit:** Pending.
|
||||
@ -0,0 +1,51 @@
|
||||
# Issue: Native Soft Wrap 2026-07-29
|
||||
|
||||
## issue015 [ ] Narrow split windows truncate live text
|
||||
|
||||
- **Status:** Implemented and independently verified; user confirmation
|
||||
remains required.
|
||||
- **Summary:** With `ekp-auto-justify-mode` enabled in a narrow
|
||||
side-by-side window, typing past the right edge horizontally scrolls the
|
||||
line and displays a `$` truncation indicator instead of naturally
|
||||
soft-wrapping.
|
||||
- **Environment:** Graphical Emacs 30.2; side-by-side windows; native
|
||||
progressive live layout.
|
||||
- **Repro:**
|
||||
1. Split a graphical frame side by side until the editing window is
|
||||
narrower than 50 columns.
|
||||
2. Enable `ekp-auto-justify-mode`.
|
||||
3. Type a hard line beyond the right edge.
|
||||
4. Observe horizontal scrolling and the left `$` indicator.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: the active hard line follows native soft wrapping, point
|
||||
moves onto a new visual row, and EKP may align only the row just left.
|
||||
- Actual: no visual row is created, so native-row publication cannot
|
||||
begin.
|
||||
- **Root Cause:** Emacs 30.2 defaults
|
||||
`truncate-partial-width-windows` to `50`. In narrower side-by-side
|
||||
windows that setting overrides `truncate-lines=nil` and enables
|
||||
truncation. The auto mode declared native soft wrap as an algorithmic
|
||||
precondition but did not own the corresponding display variables.
|
||||
- **Required Outcome:** While the mode is enabled, force native soft
|
||||
wrapping in full-width and partial-width windows. On disable or
|
||||
major-mode change, restore both the prior values and whether each value
|
||||
was buffer-local.
|
||||
- **Fix:** `task027` snapshots both truncation variables and their
|
||||
buffer-local ownership before enabling native wrapping. Normal disable,
|
||||
major-mode teardown, and failed activation restore that state exactly;
|
||||
activation errors remain visible after cleanup instead of leaving a
|
||||
half-enabled mode.
|
||||
- **Verification:** The lifecycle regressions pass 3/3; buffer ERT passes
|
||||
77/77; default, seed-`20260729`, and isolated ERT pass 166/166; C/Elisp
|
||||
fuzz passes 300/300. Warning-as-error compilation, checkdoc, pinned
|
||||
package-lint, release, dictionary, pinned-source, and static gates pass.
|
||||
Retained dynamic evidence in
|
||||
`/private/tmp/ekp-soft-wrap-final-pass-PIUigY` shows per-character typing
|
||||
naturally advancing from one to two visual rows in a 44-column split,
|
||||
with `hscroll=0`, exact source text, zero overlays, and zero live
|
||||
replacing breaks.
|
||||
- **User Confirmation:** Pending after delivery.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Implementation complete; issue stays open until
|
||||
user-visible confirmation.
|
||||
- **Commit:** Pending.
|
||||
@ -0,0 +1,54 @@
|
||||
# Issue: Natural Live Editing 2026-07-29
|
||||
|
||||
## issue014 [ ] Near-edge editing publishes premature KP layout
|
||||
|
||||
- **Status:** Implemented and independently verified; user-visible
|
||||
confirmation is pending.
|
||||
- **Summary:** With `ekp-auto-justify-mode` enabled, typing near the right
|
||||
edge can publish several visual KP breaks and a discretionary hyphen
|
||||
while the user is still composing the current hard paragraph. The result
|
||||
does not feel like ordinary Emacs editing.
|
||||
- **Environment:** Text-property live-flow renderer after `task024`;
|
||||
graphical Emacs 30.2; mixed Latin/CJK prose.
|
||||
- **Repro:**
|
||||
1. Enable `ekp-auto-justify-mode` in a graphical window.
|
||||
2. Type mixed Latin/CJK prose continuously toward the right edge.
|
||||
3. Observe the transition one character at a time as Emacs naturally
|
||||
wraps.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: the source tail containing point is displayed exactly as
|
||||
ordinary Emacs would display it. Before natural wrap, EKP publishes no
|
||||
layout property there. After natural wrap, EKP may align only the row
|
||||
the user has left. A complete KP pass runs only after the hard
|
||||
paragraph ends or point leaves it.
|
||||
- Actual: bounded KP flow selects and publishes display breaks during
|
||||
typing. `natural-p` suppresses glue projection only; it still permits
|
||||
prefix and break/hyphen projection.
|
||||
- **Root Cause:** The live model treats an unfinished paragraph as a
|
||||
continuously maintained partial KP plan. “Natural active line” is only
|
||||
a glue exception inside that plan, not native Emacs display ownership.
|
||||
- **Required Outcome:** Replace partial live KP with three explicit states:
|
||||
a natural active tail, locally aligned rows already left by natural
|
||||
wrapping, and a complete KP paragraph after completion. No live row may
|
||||
add a discretionary hyphen or replacing visual break.
|
||||
- **Fix:** `task025` deletes the partial-KP
|
||||
anchor/lookahead/convergence/push-pull state machine. The active hard
|
||||
paragraph is restored to native display before each source edit. After
|
||||
the edit, EKP derives completed native visual rows from Emacs redisplay
|
||||
and projects only their internal gap widths. The point-containing tail
|
||||
stays native, and only hard-paragraph completion or paragraph exit runs
|
||||
the complete KP planner. Live projection cannot install replacing
|
||||
breaks, prefixes, or discretionary hyphens.
|
||||
- **Verification:** Focused live contract ERT passes 4/4; buffer ERT passes
|
||||
75/75; default, seeded-permuted, and isolated full ERT pass 164/164; fuzz
|
||||
passes 300/300. Warning-as-error compilation, checkdoc, pinned
|
||||
package-lint, release, dictionary, pinned-source, static ownership,
|
||||
no-overlay, and diff gates pass. The retained clean Emacs 30.2 dynamic
|
||||
artifact at `/private/tmp/ekp-native-live-2dYu2U` proves native
|
||||
mixed-text wrap, exact deletion restoration, and the sole full-KP
|
||||
transition after hard newline; every immediate and redisplay checkpoint
|
||||
passes, with zero live owned breaks and no black recording segment.
|
||||
- **User Confirmation:** Pending after the visible fix is delivered.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Implementation complete; awaiting user confirmation.
|
||||
- **Commit:** Pending.
|
||||
@ -0,0 +1,82 @@
|
||||
# Issue: Point-Motion Projection Stability 2026-07-29
|
||||
|
||||
## issue017 [ ] Point-only motion changes an already published live projection
|
||||
|
||||
- **Status:** The zero-work point-motion invariant remains current.
|
||||
`issue019`/`task031` supersede frontier-owned immediate publication and
|
||||
extend the invariant across hard-line/paragraph navigation.
|
||||
- **Summary:** `task028` correctly made one whole hard line the planning
|
||||
unit, but it also made transient point position the persistent live
|
||||
projection boundary. Moving point across semantic plan lines therefore
|
||||
removes and reinstalls display properties even though source text,
|
||||
authoritative width, font metrics, and layout options are unchanged.
|
||||
- **Environment:** Graphical Emacs 30.2; `ekp-auto-justify-mode`; one hard
|
||||
line with a multi-line semantic KP plan.
|
||||
- **Repro:**
|
||||
1. Edit the end of a long hard line so EKP publishes a semantic prefix.
|
||||
2. Move point backward into an already projected semantic line without
|
||||
changing source text.
|
||||
3. Observe the prefix lose projection; move point forward and observe it
|
||||
return.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: point-only motion inside the active hard line is display
|
||||
read-only. The existing plan, editing frontier, active semantic index,
|
||||
line signatures, projection properties, source text, and cache state
|
||||
remain unchanged.
|
||||
- Actual: `post-command-hook` calls
|
||||
`ekp-buffer--publish-live-boundary`, derives a new active index from
|
||||
point, and mutates the projected prefix.
|
||||
- **Root Cause:** The live state stores a plan-relative `active-index` but
|
||||
no source-relative editing frontier. `ekp-buffer--project-live-plan`
|
||||
therefore reads `(point)` both after source edits and during unrelated
|
||||
reprojection. This gives cursor navigation ownership of layout state.
|
||||
- **Required Outcome:**
|
||||
- The latest real source edit owns a source-relative editing frontier.
|
||||
- `before-change-functions` naturalizes the semantic line actually being
|
||||
edited and its suffix before source mutation.
|
||||
- `after-change-functions` moves the frontier to the changed region's
|
||||
new end and publishes exactly once.
|
||||
- Point-only motion inside the same hard line performs no plan, cache, or
|
||||
text-property work.
|
||||
- Width/font/layout-context changes may rebuild the plan, but they map
|
||||
the preserved frontier into that plan rather than reading transient
|
||||
point.
|
||||
- Point-only motion, including leaving the hard line, performs zero work.
|
||||
The next real edit elsewhere, hard newline, or explicit refill owns the
|
||||
completed-paragraph transition.
|
||||
- Core DP, C ABI, DP schema, source text, and the no-overlay contract do
|
||||
not change.
|
||||
- **Fix:** `task029` adds a source-relative frontier marker to live state,
|
||||
derives the active semantic index from it, moves it only after real
|
||||
changes, preserves it through reflow, and removes point-driven
|
||||
publication from `post-command-hook`. The hard-line completion path is
|
||||
unchanged.
|
||||
- **Verification:**
|
||||
- The focused point-motion test failed before implementation because
|
||||
cursor movement removed the owned projection; the reflow regression
|
||||
also selected point's line instead of the edit frontier. Both pass
|
||||
after the fix.
|
||||
- Independent focused ERT passes 10/10; buffer ERT passes 93/93.
|
||||
Default, seed-`20260729`, and isolated full suites pass 182/182;
|
||||
C/Elisp fuzz passes 300/300.
|
||||
- Warning-as-error compilation, checkdoc, pinned package-lint, release,
|
||||
dictionary, pinned-source, no-overlay, no-stale-symbol, and diff gates
|
||||
pass.
|
||||
- Point motion measures 0.033 ms p99 on the C backend and 0.037 ms in an
|
||||
independent Elisp-backend review, with zero planner/cache calls.
|
||||
- The reviewed GUI run
|
||||
`/private/tmp/ekp-frontier-live-v3-66WYRW` records 39 manifest lines
|
||||
over a 26.6-second real screen capture. Backward and forward motion
|
||||
preserve projection hash, plan object, generation, cache size, active
|
||||
index, and frontier. Delete, yank, real undo, narrow/restore resize,
|
||||
and hard newline also pass; source is exact, overlays are zero,
|
||||
`hscroll` is zero, no black segment is detected, and the evidence
|
||||
report returns PASS.
|
||||
- Independent architecture review is CLEAR. Independent code review
|
||||
reports zero blockers; its only residual is the separate narrow-width
|
||||
append latency tracked by `issue018`/`task030`.
|
||||
- **User Confirmation:** Pending. Keep this issue open until the user
|
||||
confirms the editing experience.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Pending.
|
||||
- **Commit:** Pending.
|
||||
@ -0,0 +1,97 @@
|
||||
# Issue: Semantic Live Prefix 2026-07-29
|
||||
|
||||
## issue016 [ ] Live editing freezes native rows instead of projecting a semantic hard-line prefix
|
||||
|
||||
- **Status:** The whole-hard-line commit result is retained, but per-edit
|
||||
replanning is superseded by `issue019`/`task031`.
|
||||
- **Summary:** Before `task028`, live editing committed completed native
|
||||
visual rows independently. User testing showed that earlier soft-wrapped
|
||||
rows in the same hard line did not become globally KP-aligned while the
|
||||
user continued typing. The behavior was row-local, not a dynamic
|
||||
projection of one semantic hard-line plan.
|
||||
- **Environment:** Graphical Emacs 30.2; `ekp-auto-justify-mode`; mixed
|
||||
Latin/CJK hard lines that wrap across multiple visual rows.
|
||||
- **Repro:**
|
||||
1. Enable `ekp-auto-justify-mode`.
|
||||
2. Type one long hard line that naturally soft-wraps into multiple rows.
|
||||
3. Continue editing on the current unfinished row without inserting a
|
||||
hard newline.
|
||||
4. Observe the rows before point.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: EKP sends the complete current hard-line source text to the
|
||||
existing `ekp-layout-plan` and projects only the complete semantic
|
||||
lines before the line containing the latest real source edit. Those
|
||||
preceding lines may change breaks and glue together whenever text,
|
||||
width, font, or layout options change. The editing-frontier line and
|
||||
all following source stay naturally editable. Point-only navigation
|
||||
does not change the projection.
|
||||
- Actual before `task028`: the live model derived native visual rows and
|
||||
committed them one at a time. Once a row was treated as completed,
|
||||
later edits could not let the whole hard-line KP plan revise earlier
|
||||
breaks and glue as a single semantic decision.
|
||||
- **Root Cause:** `ekp-buffer` owned live editing but used native visual-row
|
||||
boundaries as the persistent layout unit. That was the wrong owner
|
||||
boundary. Core DP already computed the correct complete-text plan; the
|
||||
buffer layer needed to consume that plan and decide which prefix was safe
|
||||
to project, not invent a row-freezing live algorithm.
|
||||
- **Required Outcome:** Replace native-row commitment with semantic prefix
|
||||
projection:
|
||||
- keep the core DP, C ABI, DP schema, and `ekp-layout-plan` contract
|
||||
unchanged;
|
||||
- pass the full current hard line to `ekp-layout-plan`;
|
||||
- find the plan line containing the latest real source edit;
|
||||
- project only the complete plan lines before that line;
|
||||
- leave the edit-frontier plan line and all following source natural;
|
||||
- compare old/new line signatures so unchanged prefixes are not
|
||||
reinstalled;
|
||||
- cache recent full hard-line plans in `ekp-buffer` only;
|
||||
- fail closed to native display for conflicts, oversized hard lines, or
|
||||
stale generations;
|
||||
- never use overlays or insert source layout characters.
|
||||
- **Fix:** `task028` replaces the native-row commit state with one current
|
||||
whole-hard-line semantic plan. `task029` makes the latest real source
|
||||
edit, rather than transient point, own the natural-suffix frontier.
|
||||
`ekp-buffer` projects only prior lines, owns break whitespace on the
|
||||
preceding semantic line, keeps the editable suffix property-free,
|
||||
reuses a 16-entry buffer-local plan LRU, and updates changed projection
|
||||
suffixes from common-prefix line signatures. The core DP, C ABI, schema,
|
||||
and `ekp-layout-plan` contract are unchanged.
|
||||
- **Refinement:** `issue017`/`task029` supersede the point-driven boundary
|
||||
portion of `task028`. The persistent natural-suffix boundary belongs to
|
||||
the latest source edit; transient point motion is display read-only.
|
||||
- **Later Refinement:** `issue019`/`task031` supersede immediate
|
||||
whole-hard-line replanning after every edit. The plan remains authoritative
|
||||
at structural commits; stable same-row editing is owned by a saved
|
||||
committed projection and local dirty transaction.
|
||||
- **Verification:**
|
||||
- Public-path and edge-case ERT pass in the 181-test default suite, the
|
||||
seed-`20260729` suite, and 181 isolated Emacs processes. C/Elisp fuzz
|
||||
passes 300/300.
|
||||
- Warning-as-error compilation, checkdoc, the CI-pinned package-lint,
|
||||
release, dictionary, pinned-source, no-overlay, and source-clean gates
|
||||
pass.
|
||||
- The main dynamic GUI run
|
||||
`/private/tmp/ekp-semantic-live-v4-vFZTkr` exercises typing, deletion,
|
||||
backward/forward point motion, yank, real undo, narrow/restore resize,
|
||||
and hard newline. Its 38 checkpoints have exact source text, zero
|
||||
overlays, `hscroll=0`, no pending transaction, no failed assertion,
|
||||
and a reviewed temporal verdict of PASS.
|
||||
- The 44-column split-window run
|
||||
`/private/tmp/ekp-semantic-split-v3-uPwuOi` proves native incremental
|
||||
soft wrapping, one semantic projected prefix line, a natural active
|
||||
line, exact source text, zero overlays, `hscroll=0`, clean disable, and
|
||||
a reviewed temporal verdict of PASS.
|
||||
- A subsequent task029 audit could not reproduce the original 6.399 ms
|
||||
append p99 with the current checked-in benchmark. At its synthetic
|
||||
80-pixel width, repeated GC-excluded runs instead record one plan per
|
||||
unique append and roughly 33–85 ms p99. `issue018`/`task030` track this
|
||||
separate performance debt. Point motion remains a zero-work path at
|
||||
about 0.03 ms p99.
|
||||
- Final independent code review returns APPROVE; independent architecture
|
||||
review returns CLEAR. Both reviewed the latest font-context
|
||||
invalidation delta.
|
||||
- **User Confirmation:** Pending. Keep this issue open until the user
|
||||
personally confirms the delivered editing behavior.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** Pending.
|
||||
- **Commit:** Pending.
|
||||
@ -0,0 +1,72 @@
|
||||
# Issue: Stable Live Edit Transaction 2026-07-29
|
||||
|
||||
## issue019 [ ] Per-edit frontier replanning destabilizes live projection and cannot restore reversible edits exactly
|
||||
|
||||
- **Status:** Implemented and developer-verified by `task031`; awaiting the
|
||||
required user-visible confirmation.
|
||||
- **Summary:** The current live path makes one `frontier` simultaneously own
|
||||
the latest edit, the natural suffix, and projection reuse. Before every
|
||||
edit it clears the touched projected line and every following line; after
|
||||
every edit it replans the complete hard line. This makes unrelated rows
|
||||
move during continuous input and turns narrow append latency into a
|
||||
symptom of the same ownership error.
|
||||
- **Environment:** Graphical Emacs 30.2; `ekp-auto-justify-mode`; long mixed
|
||||
Latin/CJK hard lines with an installed live prefix.
|
||||
- **Repro:**
|
||||
1. Type a hard line until multiple visual rows exist.
|
||||
2. Continue typing within the unfinished row, or edit a previously
|
||||
projected middle row.
|
||||
3. Observe earlier/succeeding rows and the owned text properties after each
|
||||
source change.
|
||||
4. Delete a projected space and insert the identical space again.
|
||||
- **Expected vs Actual:**
|
||||
- Expected: ordinary same-row input leaves the committed projection
|
||||
untouched. A middle-row edit opens one local dirty island while
|
||||
unaffected break anchors remain installed. Small changes are absorbed by
|
||||
existing glue/native wrapping; local words move only when the dirty row
|
||||
no longer fits. Restoring the exact source restores the exact projection
|
||||
immediately.
|
||||
- Actual: `before-change` invalidates the touched line and its complete
|
||||
projected suffix. `after-change` moves `frontier`, runs or reuses a
|
||||
whole-hard-line plan, and republishes immediately. The transaction stores
|
||||
only an old paragraph marker and deleted fragment, so it cannot restore a
|
||||
committed baseline as a first-class state transition.
|
||||
- **Root Cause:** `frontier` conflates three independent owners: source-edit
|
||||
location, unprojected edit boundary, and plan/projection reuse boundary.
|
||||
Signature diffing reduces writes after a plan is chosen, but cannot prevent
|
||||
global optimal breaks from changing when a new plan is computed after
|
||||
every key.
|
||||
- **Required Outcome:**
|
||||
- committed projection owns the last published source/plan/signatures/spans;
|
||||
- a dirty edit transaction snapshots that baseline and owns only the
|
||||
affected local island;
|
||||
- ordinary same-row edits do no whole-hard-line DP and do not rewrite
|
||||
unaffected projection;
|
||||
- crossing a native visual-row boundary atomically replans the completed
|
||||
hard-line prefix, leaving the new current row natural;
|
||||
- the next real edit elsewhere, hard newline/paragraph completion,
|
||||
explicit refill, or width/font/layout-context change is a commit event;
|
||||
- point-only motion performs zero planning, cache, property, or layout work;
|
||||
- a reversible edit restores source and the complete owned projection
|
||||
`equal-including-properties` without depending on a best-effort replan;
|
||||
- completed paragraphs still use the existing globally optimal core DP;
|
||||
- no overlays or source layout characters are introduced.
|
||||
- **Fix:** `task031` deletes frontier-owned immediate publication. Committed
|
||||
live state now owns source/key/plan/signatures/prefix/spans; one edit
|
||||
transaction snapshots that state and marker offsets, naturalizes only its
|
||||
dirty island, restores exact baselines directly, and permits replanning
|
||||
only at native-row or structural commit events. Static lazy-reflow chunks
|
||||
exclude the active paragraph. The core DP and `ekp.el` are unchanged.
|
||||
- **Verification:** Focused public-path regressions were observed red before
|
||||
implementation and green afterward. Final buffer/default/random/isolated
|
||||
ERT runs pass 99/99, 188/188, 188/188, and 188/188; C/Elisp fuzz passes
|
||||
300/300; compile, checkdoc, package-lint, release, and local dictionary
|
||||
gates pass. The reviewed dynamic run
|
||||
`/private/tmp/ekp-stable-transaction-final5-2BFryc` is PASS with 55
|
||||
checkpoints, no failed assertion, a completed run-end, and no black
|
||||
segment or visual noise. Same-row and point-motion benchmark scenarios
|
||||
perform zero plans; 291 appends now contain only 15 structural plans.
|
||||
- **User Confirmation:** Required after temporal GUI evidence is delivered.
|
||||
- **Resolved At:** Unresolved.
|
||||
- **Resolved By:** `task031` developer verification; user confirmation pending.
|
||||
- **Commit:** Pending.
|
||||
@ -0,0 +1,87 @@
|
||||
# Plan: Buffer Module Naming 2026-07-28
|
||||
|
||||
## Goal
|
||||
|
||||
Rename the editor-facing buffer integration module from `ekp-region` to
|
||||
`ekp-buffer` so its file, feature, customization group, configuration
|
||||
variables, private namespace, tests, and documentation match the
|
||||
responsibility it actually owns.
|
||||
|
||||
## Intended User Path
|
||||
|
||||
- Load buffer integration with `(require 'ekp-buffer)`.
|
||||
- Keep public operation names whose suffix describes their real operand:
|
||||
`ekp-justify-region`, `ekp-unjustify-region`, `ekp-justify-buffer`,
|
||||
`ekp-unjustify-buffer`, and the region protection commands.
|
||||
- Keep `ekp-auto-justify-mode`, `ekp-org-setup`, `ekp-markdown-setup`,
|
||||
`ekp-refill-paragraph`, and `ekp-diagnose`; they already follow the
|
||||
package-level `ekp-` namespace and describe behavior rather than the old
|
||||
module filename.
|
||||
- Rename the module-owned public configuration surface and all private
|
||||
implementation/test symbols from `ekp-region*` to `ekp-buffer*`.
|
||||
|
||||
## Scope and Order
|
||||
|
||||
1. Rename `ekp-region.el` to `ekp-buffer.el` and
|
||||
`tests/ekp-region-tests.el` to `tests/ekp-buffer-tests.el`.
|
||||
2. Rename the provided feature, customization group, module-owned settings,
|
||||
internal functions/state, and test namespace.
|
||||
3. Update test runners, GUI/showcase helpers, CI, contributor commands, and
|
||||
release checks that name the files or symbols.
|
||||
4. Update current public/developer/audit documentation and add an
|
||||
Unreleased breaking migration note.
|
||||
5. Preserve historical phase/postmortem wording; add a new decision record
|
||||
instead of rewriting history.
|
||||
|
||||
## Compatibility Decision
|
||||
|
||||
This is an explicit breaking rename requested for the current Unreleased
|
||||
line. Do not retain `ekp-region.el`, provide `ekp-region`, define obsolete
|
||||
aliases, or add variable aliases. The migration is one direct replacement:
|
||||
`ekp-region` becomes `ekp-buffer`. Public region commands retain their
|
||||
semantic names.
|
||||
|
||||
## Behavior Lock
|
||||
|
||||
- Existing full ERT suite: 130 tests before the rename.
|
||||
- Existing buffer tests cover exact round-trip, save failure, isearch,
|
||||
kill/yank, undo, mode lifecycle, resize/lazy reflow, protection commands,
|
||||
and whole-buffer/DWIM paths.
|
||||
- The rename changes lookup names only; rendered output and persisted bytes
|
||||
must remain identical.
|
||||
|
||||
## Cleanup and Fallback Inventory
|
||||
|
||||
- Smell: the `ekp-region` namespace names only one operation scope while
|
||||
the module owns the entire buffer/editor lifecycle.
|
||||
- No dead-code, duplication, error-handling, dependency, or UI pass is in
|
||||
scope.
|
||||
- The selected-window fallback in width calculation is a grounded
|
||||
undisplayed-buffer boundary and remains unchanged.
|
||||
- No masking fallback, swallowed error, broad compatibility shim, or
|
||||
escalation candidate was found.
|
||||
|
||||
## Validation
|
||||
|
||||
- Focused load and buffer ERT through `ekp-buffer`.
|
||||
- Default, seeded-permuted, and fresh-process-isolated full ERT.
|
||||
- C/Elisp fuzz to prove rendered behavior remains unchanged.
|
||||
- Warning-as-error byte compilation, checkdoc, package-lint/release gates,
|
||||
shell syntax, and `git diff --check`.
|
||||
- Scan active runtime, tests, CI, and current documentation for stale
|
||||
`ekp-region` / `ekp-region-tests` references; historical records are the
|
||||
only allowed occurrences.
|
||||
- Read the complete diff before closing the task.
|
||||
|
||||
## Stop Condition
|
||||
|
||||
The new feature loads from `ekp-buffer.el`, all renamed tests and automation
|
||||
pass, current documentation presents only the new API, historical references
|
||||
are explicitly contextual, and no behavior or unrelated source changed.
|
||||
|
||||
## Result
|
||||
|
||||
Completed as `task017`. The approved direct rename is mechanically
|
||||
equivalent to the prior runtime/test implementation apart from the
|
||||
customization parent and explicitly private protrusion helper. All validation
|
||||
listed above passed; no compatibility shim or unrelated cleanup was added.
|
||||
@ -0,0 +1,283 @@
|
||||
# Plan: Text-Property KP Layout Engine 2026-07-29
|
||||
|
||||
The authoritative user-visible contract is
|
||||
`spec_text_property_layout_engine_20260729.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Resolve `issue011` at the representation owner. The real buffer keeps only
|
||||
the user's logical characters. EKP computes Knuth-Plass breaks, glue widths,
|
||||
indentation, and discretionary hyphens as a reusable layout plan, then a
|
||||
buffer renderer projects that plan with text properties on existing source
|
||||
characters.
|
||||
|
||||
No buffer path may insert layout spaces, soft newlines, or discretionary
|
||||
hyphens. No buffer path may use overlays.
|
||||
|
||||
## User Contract
|
||||
|
||||
- `buffer-string`, `buffer-substring-no-properties`, direct character
|
||||
access, search, syntax, markers, point, save, and ordinary editing observe
|
||||
the original character sequence.
|
||||
- Manual commands produce the same KP break and glue decisions as the
|
||||
string API.
|
||||
- `ekp-auto-justify-mode` keeps the current unfinished visual row natural and
|
||||
preserves unaffected committed break anchors during continuous editing.
|
||||
- Ordinary same-row edits do not call whole-hard-line planning. A dirty edit
|
||||
transaction owns the local natural island and a committed projection owns
|
||||
the saved source, plan, signatures, spans, and anchors.
|
||||
- Native soft-wrap crossing atomically replans the completed hard-line prefix
|
||||
once. Signature diffing limits the property update; core DP remains the
|
||||
owner of the committed result.
|
||||
- Hard newline/paragraph completion, the next real edit outside the dirty
|
||||
island, explicit refill, and width/font/layout-context change are commit
|
||||
events. Point-only motion is never one.
|
||||
- Reversible source edits restore the saved projection exactly rather than
|
||||
relying on a recomputation to happen to reproduce it.
|
||||
- Layout-property updates do not change the modified flag, undo history,
|
||||
character-modified tick, point, mark, or external modification hooks.
|
||||
- Turning the mode off removes only EKP-owned projection properties and
|
||||
restores every pre-existing property exactly.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core owner: `ekp.el`
|
||||
|
||||
Introduce one semantic paragraph plan built from the existing `ekp-para`,
|
||||
DP breaks, line glues, and source box offsets. A plan contains ordered
|
||||
lines; each line records its source/box bounds, leading indentation,
|
||||
interior glue targets, chosen break, and discretionary-hyphen state.
|
||||
|
||||
The plan contains no buffer positions, markers, overlays, or display
|
||||
property forms.
|
||||
|
||||
`ekp-pixel-justify` remains the public string formatter. Its renderer
|
||||
consumes the plan and preserves the existing reversible string output and
|
||||
tests. The buffer renderer consumes the same plan but has different
|
||||
representation rights.
|
||||
|
||||
### Buffer owner: `ekp-buffer.el`
|
||||
|
||||
The buffer renderer maps plan operations to existing source characters:
|
||||
|
||||
- ASCII source-space glue:
|
||||
`((space-width FACTOR) (min-width ((TARGET))))`.
|
||||
- Zero-source CJK/mixed glue: `min-width` on the preceding complete
|
||||
grapheme, with a target equal to the grapheme's natural width plus glue.
|
||||
- Leading indentation/alignment: `line-prefix` over the complete planned
|
||||
display line.
|
||||
- Break at source whitespace: the first break-space displays as a newline;
|
||||
remaining boundary whitespace displays as empty.
|
||||
- CJK break: the preceding complete grapheme displays as
|
||||
`GRAPHEME + NEWLINE`.
|
||||
- Latin discretionary break: the preceding complete grapheme displays as
|
||||
`GRAPHEME + HYPHEN + NEWLINE`, with cursor anchoring on the reproduced
|
||||
grapheme.
|
||||
- Paragraph-edge whitespace stripped by the KP model remains in the buffer
|
||||
and displays as empty.
|
||||
|
||||
All projection properties are installed through `with-silent-modifications`
|
||||
and are nonsticky. Copy/kill strips only EKP projection metadata so stale
|
||||
layout cannot be yanked elsewhere.
|
||||
|
||||
### Display ownership
|
||||
|
||||
EKP never overwrites an unowned replacing `display`, `line-prefix`, or
|
||||
`wrap-prefix` value. A paragraph with a property that cannot be composed
|
||||
losslessly is kept verbatim and reported by diagnostics. Removing a layout
|
||||
restores the exact pre-existing property values.
|
||||
|
||||
### Multiple windows
|
||||
|
||||
Text properties are buffer-wide. One buffer therefore has one
|
||||
authoritative plan: the narrowest live window showing that buffer, matching
|
||||
the current `ekp-buffer--effective-width` behavior. Wider windows may have
|
||||
unused right-side space; no window may receive an overflowing plan.
|
||||
Simultaneous different KP plans for one buffer are explicitly not claimed.
|
||||
|
||||
## Live Editing State Machine
|
||||
|
||||
1. The active hard line owns one committed projection: baseline source,
|
||||
whole-hard-line plan, line signatures, projected spans, and anchors.
|
||||
2. The first edit in one visual row snapshots that state and opens a dirty
|
||||
island. `before-change` removes only the affected projection; it never
|
||||
clears an unrelated suffix.
|
||||
3. Same-row edits update source and dirty bounds only. Existing glue and
|
||||
native soft wrapping absorb local edits without whole-hard-line DP.
|
||||
4. Restoring the baseline source restores the saved owned properties and
|
||||
committed state immediately and closes the transaction.
|
||||
5. Crossing a native soft-wrap boundary commits once: recompute or reuse the
|
||||
whole-hard-line plan, derive the completed prefix, and apply the
|
||||
signature-diff update as one silent publication. The new current row is
|
||||
natural.
|
||||
6. A real edit outside the dirty island commits the old transaction before
|
||||
opening a new one. Hard newline/paragraph completion, explicit refill, and
|
||||
width/font/layout-context changes are also commit boundaries.
|
||||
7. Point-only motion anywhere is display read-only and cannot commit,
|
||||
finalize, plan, touch cache identity, or write projection properties.
|
||||
8. IME composition stays entirely native until commit. Stale generations,
|
||||
foreign display ownership, unsupported shrink, oversized hard lines, or
|
||||
projection failures fail closed to native display for that hard line.
|
||||
9. Hard-paragraph completion uses one existing full KP quality pass and
|
||||
starts a new natural active hard line.
|
||||
|
||||
## Milestones and Gates
|
||||
|
||||
### M1 — Core layout plan and string parity
|
||||
|
||||
- Add plan structs and `ekp-layout-plan`.
|
||||
- Make the string renderer consume the plan.
|
||||
- Gate: every existing core rendered string remains
|
||||
`equal-including-properties`; C/Elisp parity and fuzz remain green.
|
||||
|
||||
### M2 — Static text-property buffer renderer
|
||||
|
||||
- Replace delete/insert justification with property projection.
|
||||
- Remove physical inversion/save/isearch adapters that no longer own a
|
||||
character transformation.
|
||||
- Gate: source characters, positions, modified state, undo, hooks, and
|
||||
foreign properties are invariant; exact GUI glue/break/hyphen/indent
|
||||
probes pass.
|
||||
|
||||
### M3 — Seamless live-editing foundation
|
||||
|
||||
- Implement composition deferral, resize generation cancellation,
|
||||
visible-first large-buffer work, the single-paragraph planning guard, and
|
||||
exact editor-state preservation.
|
||||
- Gate: source, point/mark, whitespace, IME, resize, and teardown regressions
|
||||
are locked before selecting the final live row-boundary model.
|
||||
|
||||
### M4 — Product and repository closure
|
||||
|
||||
- Update bilingual user/developer documentation, `issue011`, task/change
|
||||
records, changelog, and a design postmortem.
|
||||
- Run default/permuted/isolated ERT, fuzz, byte compilation with warnings as
|
||||
errors, checkdoc, C parity/build gates, GUI matrix, dynamic verification,
|
||||
anti-slop cleanup, and independent architecture/code review.
|
||||
|
||||
### M5 — Interaction regression closure
|
||||
|
||||
- Preserve the mark marker and `mark-active` as independent editor state
|
||||
across every reprojection, including showcase width changes.
|
||||
- Keep active-line edge whitespace natural while retaining edge cleanup on
|
||||
committed static lines.
|
||||
- Gate: focused red/green ERT, a public-command interaction matrix, clean
|
||||
GUI width-key/single-space evidence, full repository gates, and user
|
||||
confirmation for `issue012` and `issue013`.
|
||||
|
||||
### M6 — Native progressive editing
|
||||
|
||||
- Delete the partial-KP live-flow model and its lookahead/convergence state.
|
||||
- Keep the active source tail under native Emacs redisplay ownership.
|
||||
- Align only completed native screen rows without live break/hyphen
|
||||
projection; run complete KP at hard-paragraph completion.
|
||||
- Own native soft wrapping while auto mode is active, including narrow
|
||||
partial-width windows, and restore the previous display-variable
|
||||
ownership on teardown.
|
||||
- Gate: per-keystroke mixed-text GUI evidence, backward-edit invalidation,
|
||||
paragraph-completion transition, narrow split-window soft wrapping, full
|
||||
repository gates, and user confirmation for `issue014` and `issue015`.
|
||||
|
||||
Historical note: M6 is superseded for live planning by M7. Its active-tail
|
||||
and soft-wrap lifecycle decisions remain prerequisites, but native visual
|
||||
rows are not the durable planning unit.
|
||||
|
||||
### M7 — Semantic hard-line prefix editing
|
||||
|
||||
- Status: implementation, automated/performance/GUI gates, and independent
|
||||
code/architecture reviews are complete. `issue016` remains open for the
|
||||
user's visible editing confirmation.
|
||||
- Replace native-row commitment with whole-hard-line plan consumption in
|
||||
`ekp-buffer`.
|
||||
- Keep `ekp.el`, core DP semantics, C ABI, DP schema, and
|
||||
`ekp-layout-plan` contracts unchanged.
|
||||
- Project only complete semantic plan lines before point; keep the
|
||||
point-containing plan line and all following source natural.
|
||||
- Add buffer-local history cache entries keyed by hard-line text,
|
||||
text-property/layout context, authoritative width, font/face/text-scale
|
||||
context, and EKP layout parameters.
|
||||
- Use semantic line signatures to avoid rewriting unchanged prefixes and
|
||||
to prove later edits can revise earlier breaks/glue together.
|
||||
- Gate: public-path red/green ERT, GUI dynamic recording, latency/cache
|
||||
benchmark, full repository gates, independent review, and user
|
||||
confirmation for `issue016`.
|
||||
|
||||
### M8 — Source-edit-owned live frontier
|
||||
|
||||
- Status: superseded by M10 for edit-trigger ownership. The zero-work
|
||||
point-motion invariant remains current; `issue017` remains open for the
|
||||
user's visible confirmation.
|
||||
- Preserve the latest real source-edit position as the live frontier.
|
||||
- Make point-only motion within the active hard line perform no DP, cache,
|
||||
or text-property work.
|
||||
- Preserve the frontier across width/font/layout-context reflow and map it
|
||||
into the resulting whole-hard-line plan.
|
||||
- Gate: focused property-identity RED/GREEN ERT, public earlier-line edit,
|
||||
reflow-after-motion regression, full repository gates, dynamic GUI
|
||||
point-motion evidence, independent review, and user confirmation.
|
||||
|
||||
### M9 — Narrow unique-append latency
|
||||
|
||||
- Status: re-profiled as `issue018`/`task030` after M10; ready for work.
|
||||
Ordinary same-row planning is gone, so only structural-commit spikes are
|
||||
in scope.
|
||||
- Establish a repeatable width/length matrix before optimizing.
|
||||
- Reduce new-source-state planning cost without stale plan reuse,
|
||||
debounce, skipped publication, global GC changes, or weaker layout
|
||||
semantics.
|
||||
- Keep task029's point-motion zero-work invariant as a permanent gate.
|
||||
|
||||
### M10 — Stable live projection transactions
|
||||
|
||||
- Status: complete as `issue019`/`task031`; automated, static, benchmark,
|
||||
and reviewed temporal GUI gates pass. `issue019` remains open only for
|
||||
user-visible confirmation.
|
||||
- Replace frontier-owned native suffix invalidation with a committed
|
||||
projection baseline and one persistent dirty edit transaction.
|
||||
- Preserve unaffected break anchors during middle-line edits; use native
|
||||
soft wrapping for local word migration.
|
||||
- Replan only at soft-wrap or structural commit boundaries and publish the
|
||||
changed prefix atomically.
|
||||
- Restore reversible edits exactly from the saved baseline.
|
||||
- Preserve core DP/C ABI/schema/plan contracts and the point-motion
|
||||
zero-work invariant.
|
||||
- Gate: focused RED/GREEN public-path ERT, complete repository gates,
|
||||
re-profiled live benchmarks, and temporal GUI evidence.
|
||||
|
||||
### M11 — C-backed resize latency
|
||||
|
||||
- Status: `task032` developer-complete; `issue020` awaits user confirmation.
|
||||
- Freeze the current portable module and measure an interleaved
|
||||
baseline/candidate width-and-length matrix with raw p50/p95 evidence.
|
||||
- Attribute complete resize time across plan construction, Emacs/C
|
||||
marshalling, C DP, and projection publication before selecting a change.
|
||||
- Require exact frozen-C and Elisp layout parity; no stale width reuse,
|
||||
debounce inflation, skipped reflows, approximate planning, or global GC
|
||||
workaround is acceptable.
|
||||
- Gate: at least 20% paired p50/p95 improvement, candidate p95 at or below
|
||||
50 ms, complete automated/static gates, and temporal GUI resize evidence.
|
||||
- Result: core p50/p95 improved by 33.25%/34.09% to 15.318/27.687 ms;
|
||||
complete resize improved by 43.51%/41.03% to 15.900/27.487 ms. Exact
|
||||
frozen-C/Elisp parity and all automated, static, and GUI gates pass.
|
||||
|
||||
## Stop Gates
|
||||
|
||||
- Stop the affected paragraph instead of stealing a foreign replacing
|
||||
display owner.
|
||||
- Stop exact shrink projection for tabs or non-ASCII whitespace when
|
||||
`space-width` cannot express it; keep that paragraph verbatim and report
|
||||
why.
|
||||
- Stop if the live-edit implementation appears to require a core DP, C
|
||||
ABI, DP schema, or `ekp-layout-plan` semantic change; that means the
|
||||
buffer/core boundary has been crossed incorrectly.
|
||||
- Stop and return to diagnosis after two failed fixes for the same display
|
||||
or live-edit invariant.
|
||||
- Do not claim completion while any source-character, undo, modified-state,
|
||||
property-restoration, GUI, or dynamic-edit invariant is unproved.
|
||||
|
||||
## Rollback
|
||||
|
||||
The string renderer can be restored to the current direct implementation
|
||||
because M1 preserves its public result. The buffer renderer can be reverted
|
||||
to the last released physical representation only as a full rollback; no
|
||||
compatibility shim or mixed physical/property backend will be retained.
|
||||
@ -0,0 +1,192 @@
|
||||
# Spec: Source-Clean Text-Property KP Layout 2026-07-29
|
||||
|
||||
## Summary
|
||||
|
||||
`ekp-buffer.el` presents Knuth-Plass layout without rewriting the buffer's
|
||||
character stream. The core computes a reusable semantic layout plan;
|
||||
manual buffer commands and `ekp-auto-justify-mode` project that plan using
|
||||
text properties on existing source characters only.
|
||||
|
||||
The character sequence remains the document. Visual glue, breaks,
|
||||
indentation, and discretionary hyphens are display metadata: EKP creates no
|
||||
overlay and inserts no layout character.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Keep `buffer-string`, `buffer-substring-no-properties`, direct character
|
||||
access, search, syntax, markers, saving, and ordinary editing on the
|
||||
original character sequence.
|
||||
2. Use the same core glue, break, indentation, and hyphen decisions for the
|
||||
compatible string renderer and the non-mutating buffer renderer.
|
||||
3. Make automatic layout stable while editing: committed prefix lines keep
|
||||
their projection until a structural commit, the current unfinished visual
|
||||
row stays under Emacs redisplay ownership, and a middle-line edit
|
||||
naturalizes only its local dirty island while preserving unaffected break
|
||||
anchors.
|
||||
4. Preserve undo, modified state, character-modified tick, point, mark,
|
||||
copy/kill, isearch, IME composition, narrowing, and mode lifecycle.
|
||||
5. Fail closed around display ownership: unsupported or conflicting hard
|
||||
paragraphs remain naturally displayed and are diagnosable.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Hiding EKP metadata from Lisp that explicitly inspects text properties.
|
||||
The clean-text guarantee concerns the character sequence; copy/kill strips
|
||||
EKP-owned projection metadata.
|
||||
- Independent simultaneous layouts for different-width windows showing the
|
||||
same buffer. The narrowest live window is authoritative.
|
||||
- Shrinking tabs or non-ASCII whitespace with `space-width`.
|
||||
- Claiming verified bidi, Arabic, Hebrew, Indic shaping, ligatures, or
|
||||
arbitrary multi-codepoint grapheme behavior beyond the tested LTR
|
||||
Latin/CJK boundary.
|
||||
- Retaining `(require 'ekp-region)` or old private/configuration names.
|
||||
|
||||
## User Flows
|
||||
|
||||
### Load and use buffer commands
|
||||
|
||||
1. The user adds the package to `load-path`.
|
||||
2. The user loads `(require 'ekp-buffer)`.
|
||||
3. `M-x ekp-justify-region` projects the active region, or the hard
|
||||
paragraph at point when no region is active.
|
||||
4. `M-x ekp-justify-buffer` projects the accessible buffer.
|
||||
5. `M-x ekp-unjustify-region` or `ekp-unjustify-buffer` removes only
|
||||
EKP-owned projection properties; source characters do not need
|
||||
restoration.
|
||||
|
||||
### Edit with automatic layout
|
||||
|
||||
1. The user enables `M-x ekp-auto-justify-mode`.
|
||||
2. A committed projection records the complete source baseline, one
|
||||
whole-hard-line `ekp-layout-plan`, line signatures, projected spans, and
|
||||
break anchors. Core DP receives no point, marker, window, or edit-state
|
||||
input.
|
||||
3. Ordinary input within the current native visual row performs no
|
||||
whole-hard-line planning. Previously committed rows remain
|
||||
property-identical and the current row stays natural.
|
||||
4. Editing a committed middle row starts a dirty transaction. Only the
|
||||
affected local island becomes natural; following and preceding unaffected
|
||||
break anchors remain installed. Native wrapping absorbs local push/pull.
|
||||
5. Crossing into the next native visual row atomically replans the completed
|
||||
hard-line prefix once. Line-signature diffing limits property writes; the
|
||||
new current row remains natural.
|
||||
6. A hard newline/paragraph end, the next real edit outside the dirty island,
|
||||
explicit refill, or width/font/layout-context change commits the
|
||||
transaction. Point-only motion anywhere performs zero plan, cache,
|
||||
property, or layout work.
|
||||
7. Restoring the baseline source restores the saved projection immediately
|
||||
and exactly, including text properties, plan signatures, and spans.
|
||||
8. Completed paragraphs use the existing full Knuth-Plass projection.
|
||||
9. Disabling the mode cancels pending work, detaches markers, restores any
|
||||
prior copy filter, and removes every EKP-owned projection.
|
||||
|
||||
### Use logical text during projection
|
||||
|
||||
1. Search and isearch operate directly on source characters, including
|
||||
across display-only breaks and discretionary hyphens.
|
||||
2. Save and `write-region` serialize source characters; visual layout never
|
||||
reaches disk.
|
||||
3. Copy/kill composes with an existing
|
||||
`filter-buffer-substring-function`, removes only EKP-owned projection
|
||||
metadata from the copied string, and preserves delete semantics.
|
||||
4. Lisp that needs a property-free string uses
|
||||
`buffer-substring-no-properties`; property-aware APIs may inspect the
|
||||
EKP display metadata without seeing synthesized layout characters.
|
||||
|
||||
### Diagnose an unsupported paragraph
|
||||
|
||||
1. EKP detects foreign `display`, `line-prefix`, `wrap-prefix`,
|
||||
`composition`, or `invisible` ownership, or an exact shrink request for
|
||||
unsupported whitespace.
|
||||
2. EKP leaves only that hard paragraph natural instead of stealing or
|
||||
approximating the foreign representation.
|
||||
3. `M-x ekp-diagnose` reports the authoritative width and recorded
|
||||
conflicts. The user may remove the conflict or explicitly refill after
|
||||
changing the content/configuration.
|
||||
|
||||
## Display Contract
|
||||
|
||||
- ASCII source spaces use:
|
||||
|
||||
```elisp
|
||||
((space-width FACTOR)
|
||||
(min-width ((TARGET-PIXELS))))
|
||||
```
|
||||
|
||||
- A zero-source CJK/mixed gap applies `min-width` to the preceding complete
|
||||
grapheme, with a target equal to its natural advance plus the planned
|
||||
glue.
|
||||
- Indentation uses `line-prefix`.
|
||||
- A source-whitespace break displays the first boundary character as a
|
||||
newline and any remaining boundary whitespace as empty.
|
||||
- A CJK or Latin discretionary break uses a replacing display string on an
|
||||
existing complete grapheme. It reproduces that grapheme, appends the
|
||||
optional hyphen, then a visual newline.
|
||||
- Every public projection value has an EKP owner property. Cleanup removes
|
||||
a public value only while it is still identical to the owned value.
|
||||
- All projection updates use `with-silent-modifications`; owned properties
|
||||
are nonsticky.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Active IME composition defers projection and retries only for the current
|
||||
generation.
|
||||
- Stale resize/background generations cannot publish.
|
||||
- A hard paragraph over `ekp-auto-justify-paragraph-limit` remains natural
|
||||
during automatic work; explicit `ekp-refill-paragraph` is unbounded.
|
||||
- Large buffers reflow visible-first in time-bounded hard-paragraph chunks.
|
||||
- Foreign property conflicts are isolated to their hard paragraph.
|
||||
- Narrowing does not cause projection cleanup or installation outside the
|
||||
intended accessible operation; mode teardown widens to remove all owned
|
||||
state.
|
||||
- Major-mode changes and mode disable leave the logical text and prior
|
||||
integrations intact.
|
||||
- Reprojection preserves point, the mark marker, and `mark-active`
|
||||
independently. An inactive historical mark must never become a visible
|
||||
selection merely because width or layout options changed.
|
||||
- Semantic live-prefix editing still requires real Emacs soft wrapping for
|
||||
the active natural suffix.
|
||||
`ekp-auto-justify-mode` temporarily disables both `truncate-lines` and
|
||||
narrow partial-window truncation, then restores their prior values and
|
||||
local/global binding state when the mode ends.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `ekp.el` exposes a semantic layout plan containing source offsets,
|
||||
per-line glue targets, break kinds, indentation, and discretionary
|
||||
hyphen decisions; the public string API remains
|
||||
`equal-including-properties` compatible.
|
||||
2. No EKP buffer path creates an overlay or inserts glue spaces, soft
|
||||
newlines, or discretionary hyphens into source text.
|
||||
3. ASCII gaps use the exact combined `space-width`/absolute-pixel
|
||||
`min-width` form; zero-source gaps, indentation, breaks, and hyphens use
|
||||
the display contract above.
|
||||
4. Source characters, positions, point/mark, modified state, undo history,
|
||||
character-modified tick, save output, search, and syntax remain logical.
|
||||
5. Automatic editing proves committed-projection and dirty-transaction
|
||||
ownership: ordinary same-row edits perform no whole-hard-line planning or
|
||||
unaffected property writes; middle-row edits preserve unrelated anchors;
|
||||
reversible edits restore the complete projection
|
||||
`equal-including-properties`; soft-wrap crossing publishes one atomic
|
||||
whole-hard-line prefix update; structural commit events use the existing
|
||||
`ekp-layout-plan`; and point-only motion performs zero layout work. IME
|
||||
deferral, stale resize rejection, and exactly one complete KP pass after
|
||||
hard-paragraph completion remain required. Narrow side-by-side windows
|
||||
soft-wrap rather than horizontally scroll. A single leading/trailing
|
||||
space or tab is visible in the same command-loop turn, including
|
||||
whitespace exposed by deletion.
|
||||
6. Copy/kill, isearch, foreign ownership, multiwindow narrowest-width
|
||||
policy, narrowing, resize, major-mode change, and teardown have
|
||||
deterministic tests.
|
||||
7. Exact GUI probes cover 1–64px ASCII and zero-source CJK glue,
|
||||
display-only break/hyphen/indentation, point, vertical motion, region,
|
||||
mouse, scaling/remapping/fringes, unchanged source, and zero overlays.
|
||||
8. Dynamic per-keystroke evidence proves mixed Latin/CJK same-row typing
|
||||
preserves the committed projection, a real visual-row crossing atomically
|
||||
publishes all completed rows from one hard-line plan, exact source
|
||||
reversal restores the baseline projection, point-only motion preserves
|
||||
every state object and property exactly, and paragraph completion performs
|
||||
the full completed-paragraph KP transition.
|
||||
9. Default, permuted, and isolated ERT; C/Elisp fuzz; warning-as-error
|
||||
Elisp/C builds; checkdoc; package/static/release gates; full diff review;
|
||||
independent code review; and independent architecture review pass.
|
||||
@ -225,3 +225,470 @@
|
||||
profiles, release/dictionary/static gates, and GUI matrix 7/7 passed.
|
||||
Independent review confirmed the implementation fix and architecture
|
||||
ownership; `issue010` is resolved.
|
||||
|
||||
- task017 [x] Rename the editor integration module around its buffer owner.
|
||||
- Source: user request and `plan_buffer_module_naming_20260728.md`.
|
||||
- Behavior lock: run the existing 130-test ERT suite before editing; the
|
||||
rename must preserve rendered output, logical serialization, and every
|
||||
public region/buffer workflow.
|
||||
- Change: rename the file/feature/customization/configuration/private/test
|
||||
namespaces from `ekp-region` to `ekp-buffer`, while keeping public
|
||||
operation names that correctly describe region or buffer operands.
|
||||
- Validation: focused/default/permuted/isolated ERT, C/Elisp fuzz,
|
||||
warning-as-error byte compilation, checkdoc, release/package/static
|
||||
gates, stale-name scan, and full diff review.
|
||||
- Impact: editor integration source/tests, automation, bilingual public
|
||||
and developer documentation, current audit records, phase records,
|
||||
changelog, and a naming decision postmortem.
|
||||
- Completed: 2026-07-28; pre-change ERT 130/130; focused buffer ERT 44/44;
|
||||
default and seeded-permuted ERT 130/130; every one of 130 ERT tests
|
||||
passed in a fresh Emacs process; C/Elisp fuzz 300/300; warning-as-error
|
||||
byte compilation, checkdoc, pinned package-lint, release/shell/diff
|
||||
gates, active stale-name scan, and mechanical equivalence check passed.
|
||||
|
||||
- task018 [x] Record the text-property-only layout and hyphen feasibility.
|
||||
- Source: `issue011`, the user's 2026-07-29 display-property constraints,
|
||||
and `tech-refer_text_property_layout_20260729.md`.
|
||||
- Scope: documentation and clean GUI probes only; no runtime source or
|
||||
test implementation changes.
|
||||
- Evidence: confirm absolute-pixel `min-width`, real-space
|
||||
shrink/stretch through `space-width`, `line-prefix` indentation, and a
|
||||
replacing display string that renders a source grapheme plus
|
||||
discretionary hyphen and visual newline without changing source
|
||||
characters.
|
||||
- Validation: official GNU Emacs documentation/source review; clean
|
||||
Emacs 30.2 GUI glue, hyphen/newline, vertical-motion, and line-prefix
|
||||
probes; document link/traceability review; `git diff --check`.
|
||||
- Impact: `issue011`, one new technical reference, current OMX plan/test
|
||||
supersession notices, phase/global change records; no runtime behavior.
|
||||
- Completed: 2026-07-29. Static LTR feasibility is recorded; `issue011`
|
||||
remains open for the full GUI/editor-semantic gate, live editing, and
|
||||
the one-buffer/multiple-window decision.
|
||||
|
||||
- task019 [x] Extract a semantic KP layout plan and preserve string output.
|
||||
- Source: `issue011`,
|
||||
`spec_text_property_layout_engine_20260729.md`, and
|
||||
`plan_text_property_layout_engine_20260729.md` M1.
|
||||
- Red tests: require line/source/gap/break/hyphen plan data and require the
|
||||
existing string renderer to consume that plan without changing its
|
||||
`equal-including-properties` output.
|
||||
- Validation: focused red/green ERT, full core ERT, C/Elisp parity, fuzz,
|
||||
warning-as-error byte compilation, and checkdoc.
|
||||
- Impact: `ekp.el`, core tests, developer reference, and phase records.
|
||||
- Completed: 2026-07-29; focused plan tests 0/3 → 3/3; the public string
|
||||
renderer consumes `ekp-layout-plan`; full ERT passed 133/133, C/Elisp
|
||||
fuzz passed 300/300, and warning-as-error compilation, checkdoc, and
|
||||
diff checks were clean.
|
||||
|
||||
- task020 [x] Replace physical buffer formatting with text-property projection.
|
||||
- Source: `issue011`,
|
||||
`spec_text_property_layout_engine_20260729.md`, and
|
||||
`plan_text_property_layout_engine_20260729.md` M2.
|
||||
- Red tests: unchanged source characters/positions/ticks/undo/modified
|
||||
state; exact cleanup and foreign-property restoration; no overlays or
|
||||
synthesized layout characters; copy/save/isearch see logical text.
|
||||
- Validation: focused ERT plus clean Emacs 30.2 GUI glue, CJK break,
|
||||
Latin discretionary hyphen, indentation, point, region, and mouse probes.
|
||||
- Impact: `ekp-buffer.el`, buffer/GUI tests, commands, integrations,
|
||||
bilingual docs, and phase records.
|
||||
- Completed: 2026-07-29. `ekp-buffer.el` now projects only onto existing
|
||||
source characters with owned, nonsticky text properties and creates no
|
||||
overlay. Buffer ERT passed 71/71. A clean Emacs 30.2 GUI probe rendered
|
||||
exact 1–64px ASCII and zero-source CJK glue, display-only Latin hyphens
|
||||
and breaks, `line-prefix`, point/vertical motion/region/mouse behavior,
|
||||
unchanged source text, and zero overlays. The 7-case scale/remap/fringe/
|
||||
width matrix passed.
|
||||
|
||||
- task021 [x] Implement active-line and bounded-suffix live layout.
|
||||
- Source: the user's seamless-editing requirement,
|
||||
`spec_text_property_layout_engine_20260729.md`, and
|
||||
`plan_text_property_layout_engine_20260729.md` M3.
|
||||
- Red tests: unfinished line stays natural; overflow pushes a minimal
|
||||
suffix; deletion pulls back; stable prefix does not move; stale
|
||||
generations cancel; composition defers; no delayed whole-paragraph snap.
|
||||
- Validation: deterministic ERT, performance budgets, and dynamic GUI
|
||||
recordings with temporal review.
|
||||
- Impact: `ekp-buffer.el`, live-edit state/tests/benchmarks, mode help,
|
||||
bilingual docs, and phase records.
|
||||
- Completed: 2026-07-29. Live edits publish synchronously from a stable
|
||||
active-flow anchor, preserve a natural unfinished line, push the
|
||||
minimal overflow suffix, pull text back on deletion, converge on an
|
||||
unchanged line signature, defer IME composition, reject stale
|
||||
generations, and bound both lookahead and automatic paragraph work.
|
||||
The type/delete round-trip regression failed with an extra break before
|
||||
the stable-anchor fix and now restores exact prior break positions.
|
||||
Buffer ERT passed 71/71. The 31.71s dynamic GUI artifact recorded
|
||||
before/immediate/settled overflow and deletion checkpoints; source and
|
||||
projection hashes were stable after redisplay, deletion restored the
|
||||
original hashes and two breaks, all assertions passed, zero overlays
|
||||
were present, and temporal review returned PASS.
|
||||
|
||||
- task022 [x] Close the text-property layout architecture and quality gates.
|
||||
- Source: `issue011`,
|
||||
`spec_text_property_layout_engine_20260729.md`, and
|
||||
`plan_text_property_layout_engine_20260729.md` M4.
|
||||
- Work: resolve documentation drift, record the one-width-per-buffer and
|
||||
display-conflict decisions, remove dead physical-layout adapters, run
|
||||
anti-slop cleanup, and obtain independent reviews.
|
||||
- Validation: default/permuted/isolated ERT, fuzz, warning-clean Elisp/C
|
||||
builds, checkdoc/static/release gates, GUI matrix, dynamic evidence,
|
||||
full diff review, code-reviewer APPROVE, and architect CLEAR.
|
||||
- Impact: runtime/tests/docs/postmortem and `issue011` closure.
|
||||
- Completed: 2026-07-29. Buffer ERT passed 71/71; full default and
|
||||
seeded-permuted ERT passed 162/162; every one of 162 tests passed in a
|
||||
fresh Emacs process; C/Elisp fuzz passed 300/300. Warning-as-error
|
||||
byte compilation, checkdoc, pinned package-lint, release, bundled and
|
||||
pinned-upstream dictionary, shell, diff, stale-name, no-overlay, and
|
||||
dead-private-function gates passed. Portable, native, debug, and
|
||||
sanitizer C profiles built cleanly. Static GUI verification passed
|
||||
exact 1–64px ASCII/CJK glue and the seven-case display matrix; dynamic
|
||||
verification passed the overflow/type/delete round trip with zero
|
||||
overlays and stable source/projection hashes. Anti-slop cleanup removed
|
||||
the dead `ekp-buffer--justified-spans` and `ekp--para-glue-shrink`
|
||||
functions. Independent code review returned `APPROVE` with no findings;
|
||||
independent architecture review returned `CLEAR`. `issue011` remains
|
||||
open only for the required user-visible behavior confirmation.
|
||||
- Completion-audit correction: the final objective named a phase spec,
|
||||
but the phase initially had none. Added
|
||||
`spec_text_property_layout_engine_20260729.md` and linked it from the
|
||||
plan, tasks, issue, and change record before claiming goal completion.
|
||||
The same audit corrected the dynamic duration to the manifest's
|
||||
31.707-second `run-end` value and strengthened the existing display
|
||||
tests to assert the exact nested `space-width`, `min-width`, and
|
||||
`line-prefix` values. The strengthened focused tests passed 3/3; the
|
||||
current full suite passed 162/162; warning-as-error test compilation,
|
||||
checkdoc, release, artifact-cleanliness, and diff gates remained clean.
|
||||
|
||||
- task023 [x] Preserve editor selection state and natural active-line edges.
|
||||
- Source: `issue012`, `issue013`, and
|
||||
`spec_text_property_layout_engine_20260729.md`.
|
||||
- Red tests: reprojection must preserve an inactive mark; one
|
||||
command-loop space/tab at either active-line edge and whitespace
|
||||
exposed by deletion must carry no EKP replacing display.
|
||||
- Work: restore the mark marker without changing `mark-active`; prevent
|
||||
completed-paragraph edge hiding from running on the active live line.
|
||||
- Validation: focused red/green ERT plus clean GUI width-key and
|
||||
single-space checkpoints.
|
||||
- Impact: `ekp-buffer.el`, buffer/GUI tests, user docs, issue/change
|
||||
records, and the live-layout decision record.
|
||||
- Completed: `ekp-justify-region` now restores the mark marker and
|
||||
`mark-active` independently. Live projection never applies static edge
|
||||
hiding to the active line and clears from the source span beginning
|
||||
when its first line is active. Focused red 0/3 → green 3/3; clean GUI
|
||||
width and single-space checkpoints pass.
|
||||
|
||||
- task024 [x] Audit the live interaction matrix for adjacent regressions.
|
||||
- Source: the user's request for detailed seamless-editing review and
|
||||
`issue_live_editing_interaction_20260729.md`.
|
||||
- Work: exercise insertion, consecutive whitespace, deletion/backspace,
|
||||
newline, yank, undo, point, inactive/active mark, visual-break
|
||||
boundaries, paragraph transitions, resize, and teardown through public
|
||||
command paths.
|
||||
- Validation: deterministic matrix ERT, full repository gates, and a
|
||||
temporally reviewed GUI recording with explicit invariants.
|
||||
- Impact: tests/verification artifacts and only root-cause runtime fixes
|
||||
for newly reproduced defects.
|
||||
- Completed: added public-command coverage for active/inactive regions,
|
||||
edge whitespace, consecutive spaces/backspace, yank, newline, undo,
|
||||
and deletion-exposed whitespace; existing tests cover the remaining
|
||||
flow, resize, composition, transition, and teardown boundaries. Buffer
|
||||
ERT passes 79/79; default/random/isolated full ERT passes 170/170;
|
||||
fuzz passes 300/300; compiler, lint, release, dictionary, static, diff,
|
||||
and clean dynamic GUI gates pass.
|
||||
|
||||
- task025 [x] Replace partial live KP with natural progressive editing.
|
||||
- Source: `issue014`, the user's approved three-state model, and
|
||||
`spec_text_property_layout_engine_20260729.md`.
|
||||
- Red tests: an underfull active tail owns no EKP layout property; natural
|
||||
wrap aligns only completed rows without a replacing break or live
|
||||
hyphen; editing/backspacing into a committed row restores native
|
||||
display; hard-paragraph completion permits one complete KP pass.
|
||||
- Work: delete the stable-anchor/lookahead/convergence/push-pull live
|
||||
state machine. Keep a natural active tail, project only gaps on rows
|
||||
already left by native screen wrapping, and run full KP only at hard
|
||||
paragraph completion or paragraph exit.
|
||||
- Validation: focused red/green ERT, full repository gates, and
|
||||
per-keystroke mixed Latin/CJK GUI recording across wrap and deletion.
|
||||
- Impact: `ekp-buffer.el`, core live-flow API if it becomes unused,
|
||||
buffer/GUI tests, bilingual docs, spec/plan, issue/change records, and
|
||||
the live-layout postmortem.
|
||||
- Completed: removed stable-anchor/lookahead/convergence/push-pull flow
|
||||
and its unused core signatures. The live path now clears the active
|
||||
hard paragraph to native display, commits only internal gap widths on
|
||||
native rows already left by point, keeps the active tail untouched,
|
||||
and runs complete KP only on hard-paragraph completion or exit. Live
|
||||
rows cannot publish prefixes, replacing breaks, or discretionary
|
||||
hyphens.
|
||||
|
||||
- task026 [x] Close natural-live-edit verification and documentation.
|
||||
- Source: `task025`, `issue014`, and the repository completion contract.
|
||||
- Work: remove obsolete tests/docs/private APIs, update the durable
|
||||
design record, inspect the full diff, and retain deterministic temporal
|
||||
evidence.
|
||||
- Validation: default/random/isolated ERT, fuzz, warning-as-error
|
||||
compilation, checkdoc, pinned package-lint, release/dictionary/static
|
||||
gates, clean fullscreen GUI screenshots, and reviewed recording.
|
||||
- Impact: tests, documentation, phase closure records, and no additional
|
||||
runtime abstraction.
|
||||
- Completed: buffer ERT 75/75; default, seeded-permuted, and isolated full
|
||||
ERT 164/164; fuzz 300/300; compiler, checkdoc, pinned package-lint,
|
||||
release, dictionary, pinned-source, ownership, no-overlay, stale-name,
|
||||
and diff gates pass. Seven static GUI variants pass. The retained
|
||||
14.82-second fullscreen recording
|
||||
`/private/tmp/ekp-native-live-2dYu2U/recording.mov` proves native wrap,
|
||||
deletion restoration, and hard-newline completion without flicker or
|
||||
non-Emacs frames. `issue014` remains open only for required
|
||||
user-visible confirmation.
|
||||
|
||||
- task027 [x] Make native soft wrapping an owned auto-mode precondition.
|
||||
- Source: `issue015`, the user's narrow split-window screenshot, and
|
||||
`spec_text_property_layout_engine_20260729.md`.
|
||||
- Red tests: enabling `ekp-auto-justify-mode` must locally disable both
|
||||
explicit line truncation and Emacs's default narrow partial-window
|
||||
truncation; normal disable, activation failure, and major-mode teardown
|
||||
must restore the exact prior values and local-binding ownership.
|
||||
- Work: give the minor-mode lifecycle temporary ownership of
|
||||
`truncate-lines` and `truncate-partial-width-windows`. Do not synthesize
|
||||
a break, alter source text, or permanently overwrite user settings.
|
||||
- Validation: focused red/green ERT, split-window GUI typing across the
|
||||
native wrap boundary, full repository gates, and temporal review.
|
||||
- Impact: `ekp-buffer.el`, buffer/GUI tests, bilingual docs, spec,
|
||||
issue/change records, and the native-live postmortem.
|
||||
- Completed: the mode owns native soft wrapping before its first reflow,
|
||||
shares one hook lifecycle for activation and cleanup, and uses
|
||||
`unwind-protect` so a failed activation restores state before
|
||||
propagating the original error. Focused lifecycle ERT passes 3/3;
|
||||
buffer ERT 77/77; default, seed-`20260729`, and isolated full ERT
|
||||
166/166; fuzz 300/300; compiler, checkdoc, pinned package-lint,
|
||||
release, dictionary, pinned-source, no-overlay, stale-name, conflict,
|
||||
artifact, and diff gates pass. Retained 44-column split-window
|
||||
evidence proves native wrap with `hscroll=0`, exact source text, zero
|
||||
overlays, and zero live replacing breaks. `issue015` remains open only
|
||||
for required user-visible confirmation.
|
||||
|
||||
- task028 [x] Replace native-row live commitment with semantic hard-line
|
||||
prefix projection.
|
||||
- Source: `issue016`, the user's multi-row live-editing screenshot, and
|
||||
`postmortem/20260729-whole-hard-line-live-prefix.md`.
|
||||
- Red tests: through public editing paths, require the complete current
|
||||
hard line to be planned by the existing `ekp-layout-plan`, require only
|
||||
plan lines before the point-containing line to be projected, require
|
||||
earlier breaks/glue to change together after later edits, and require
|
||||
plan cache hits/zero writes when the semantic plan is unchanged.
|
||||
- Work: delete the native-row freezing model from `ekp-buffer` live
|
||||
editing. Keep the core DP, C ABI, DP schema, and completed-paragraph
|
||||
plan semantics unchanged. Add buffer-local live plan history,
|
||||
line-signature diffing, point-movement boundary updates, fail-closed
|
||||
conflict handling, and exact cleanup without overlays or source layout
|
||||
characters.
|
||||
- Validation: focused red/green ERT for self-insert, yank, delete, real
|
||||
undo/redo, point movement, hard newline, resize, major-mode change, and
|
||||
projection failure; buffer/full/random/isolated ERT; C/Elisp fuzz;
|
||||
warning-as-error compilation; checkdoc; package/release/dictionary/
|
||||
no-overlay/static gates; performance benchmark with latency and cache
|
||||
counters; clean GUI dynamic recording proving aligned semantic prefix,
|
||||
natural point line/suffix, exact source text, zero overlays,
|
||||
`hscroll=0`, and no delayed snap.
|
||||
- Impact: `ekp-buffer.el`, live-edit tests/benchmarks, GUI verification,
|
||||
bilingual user/developer docs, spec/plan/tech reference, issue/change
|
||||
records, and the superseding live-prefix postmortem.
|
||||
- Completed: 2026-07-29. The implementation consumes one complete
|
||||
hard-line `ekp-layout-plan`, projects only semantic lines before the
|
||||
point-containing line, keeps the active line/suffix natural, assigns
|
||||
break whitespace to the preceding semantic owner, uses a bounded
|
||||
16-entry live plan LRU, and publishes only the changed signature
|
||||
suffix. No task028 change was made to core DP semantics, the C ABI,
|
||||
DP schema, or plan contract.
|
||||
- Verification: focused font-context invalidation RED 0/1 → GREEN 1/1;
|
||||
buffer ERT 92/92; default and seed-`20260729` full ERT 181/181;
|
||||
181/181 isolated-process ERT with the added regression independently
|
||||
green; C/Elisp fuzz 300/300; warning-as-error compilation, checkdoc,
|
||||
pinned package-lint, release, dictionary, pinned-source, static
|
||||
ownership, and diff gates pass. Reviewed temporal GUI evidence at
|
||||
`/private/tmp/ekp-semantic-live-v4-vFZTkr` and
|
||||
`/private/tmp/ekp-semantic-split-v3-uPwuOi` returns PASS. The
|
||||
GC-excluded C-backend append p99 is below one frame; default-GC raw p99
|
||||
retains a documented collection-pause risk. Independent code review is
|
||||
APPROVE and architecture review is CLEAR.
|
||||
- Closure: task028's implementation gate is complete. `issue016` remains
|
||||
open until the user personally confirms the visible editing
|
||||
experience.
|
||||
|
||||
- task029 [x] Make the semantic live prefix independent of point-only
|
||||
motion.
|
||||
- Source: `issue017`, the user's clarification that an already published
|
||||
layout is not cursor-owned, and
|
||||
`postmortem/20260729-editing-frontier-not-point.md`.
|
||||
- Red tests: after a real source edit publishes a multi-line prefix,
|
||||
moving point backward and forward inside the same hard line must
|
||||
preserve the exact owned-property projection, editing frontier,
|
||||
active index, signatures, plan/cache counters, modified tick, undo
|
||||
state, and source characters. A real edit in an earlier projected line
|
||||
must still naturalize that line before mutation and relocate the
|
||||
frontier afterward. Reflow after point motion must map the preserved
|
||||
frontier into the new plan.
|
||||
- Work: store the latest real source-edit position in the live state,
|
||||
derive the active semantic line from it, preserve it across
|
||||
width/font/layout reflow, and delete point-driven boundary publication
|
||||
from `post-command-hook`. Keep leaving-hard-line completion unchanged.
|
||||
- Constraints: do not modify core DP semantics, `ekp.el`, C ABI, DP
|
||||
schema, or the layout-plan contract; do not add overlays or source
|
||||
layout characters.
|
||||
- Validation: focused RED/GREEN ERT; complete buffer/default/random/
|
||||
isolated ERT; C/Elisp fuzz; compiler/checkdoc/package/release/
|
||||
dictionary/static gates; benchmark; clean dynamic GUI motion evidence;
|
||||
full diff review; independent code and architecture review.
|
||||
- Impact: `ekp-buffer.el`, buffer and GUI tests, benchmark assertions,
|
||||
bilingual user/developer docs, spec/plan/technical reference,
|
||||
issue/change records, and the superseding frontier postmortem.
|
||||
- Completed: 2026-07-29. Live state now owns a source-relative frontier
|
||||
marker at the latest real edit. Projection derives its active semantic
|
||||
line from that marker; `after-change` relocates it, reflow preserves
|
||||
it, and `post-command-hook` no longer republishes inside the active
|
||||
hard line. Leaving the hard line and hard-newline completion retain the
|
||||
existing static transition. Core DP semantics, `ekp.el`, C ABI, schema,
|
||||
and the layout-plan contract are unchanged.
|
||||
- Verification: the two focused regressions failed before the fix and
|
||||
passed afterward; independent review ran 10/10 focused tests and the
|
||||
buffer suite passes 93/93. Default, seed-`20260729`, and isolated full
|
||||
suites pass 182/182; C/Elisp fuzz passes 300/300. Warning-as-error
|
||||
compilation, checkdoc, pinned package-lint, release, dictionary,
|
||||
pinned-source, no-overlay, no-stale-symbol, and diff gates pass.
|
||||
Point-motion benchmark p99 is 0.033 ms on the C backend and 0.037 ms
|
||||
in the independent Elisp-backend review, with zero planner/cache calls
|
||||
in both. Reviewed 26.6-second GUI evidence at
|
||||
`/private/tmp/ekp-frontier-live-v3-66WYRW` contains 39 manifest lines,
|
||||
all staged assertions green, no black segment, and a PASS report.
|
||||
Architecture review is CLEAR; code review reports zero blockers and
|
||||
isolates the unrelated append latency as `issue018`/`task030`.
|
||||
- Closure: task029's developer gate is complete. `issue016` and
|
||||
`issue017` remain open until the user personally confirms the visible
|
||||
editing experience.
|
||||
|
||||
- task030 [ ] Diagnose and optimize unique-state live append planning at
|
||||
very narrow widths.
|
||||
- Source: `issue018` and
|
||||
`postmortem/20260729-narrow-live-append-replanning.md`.
|
||||
- Problem: after task031, the checked-in 80-pixel workload performs zero
|
||||
planning on ordinary same-row edits but still records 15 structural
|
||||
boundary plans across 291 appends. A fresh GC-excluded run measured
|
||||
C median 2.177 ms/p99 51.170 ms and Elisp median 2.176 ms/p99
|
||||
187.499 ms; the high percentiles now belong to permitted commit spikes,
|
||||
not per-key frontier replanning.
|
||||
- Work: profile a documented width/length and commit-event matrix,
|
||||
identify the surviving structural-planning owner of the cost, and
|
||||
implement the smallest exact optimization that reduces commit latency.
|
||||
- Constraints: preserve KP output semantics, exact cache identity,
|
||||
task031's stable transaction/structural-commit contract, source-clean
|
||||
text properties, zero-work point-only motion, and the existing core
|
||||
DP/C ABI/schema contracts unless a separately approved architecture
|
||||
decision changes them. Do not use debounce, stale reuse, skipped edits,
|
||||
timers, or global GC changes to hide mutator latency.
|
||||
- Validation: result-equivalence ERT/fuzz, profiler before/after evidence,
|
||||
repeatable Elisp/C default-GC and GC-excluded width-matrix benchmarks,
|
||||
complete repository gates, and dynamic GUI typing evidence.
|
||||
- Dependency: `task031` is complete. Optimize only the surviving
|
||||
structural-commit path; the obsolete per-keystroke whole-hard-line path
|
||||
no longer exists.
|
||||
|
||||
- task031 [x] Replace the overloaded live frontier with committed projection,
|
||||
a dirty edit transaction, and atomic structural commits.
|
||||
- Source: `issue019`, the user's stable-editing correction, and
|
||||
`postmortem/20260729-stable-live-transaction.md`.
|
||||
- Red tests: drive installed before/after-change and command hooks to prove
|
||||
that ordinary same-row edits perform no whole-hard-line planning, a
|
||||
middle-line edit preserves unaffected projection anchors, deleting and
|
||||
reinserting the same space restores the exact
|
||||
`equal-including-properties` projection, point-only motion across or
|
||||
outside the active hard line performs zero planning/property writes, and
|
||||
the next real edit elsewhere commits the prior dirty transaction.
|
||||
- Work: delete `frontier = natural suffix` ownership. Store the committed
|
||||
source, plan, line signatures, spans, and owned projection baseline in
|
||||
the edit transaction. Naturalize only the dirty edit island, preserve
|
||||
unaffected break anchors, let native soft wrapping absorb local
|
||||
push/pull, and replan atomically only when input crosses a visual-row
|
||||
boundary or another structural commit event occurs.
|
||||
- Commit events: native soft-wrap crossing, hard newline/paragraph end, the
|
||||
next real source edit outside the dirty island, explicit paragraph
|
||||
refill, and width/font/layout-context change. Cursor motion is never a
|
||||
commit event.
|
||||
- Constraints: keep core DP, `ekp.el`, C ABI, DP schema, and layout-plan
|
||||
semantics unchanged; never add overlays or source layout characters;
|
||||
preserve exact source/editor state and fail closed on conflicts.
|
||||
- Validation: focused RED/GREEN ERT, complete buffer/default/random/
|
||||
isolated ERT, C/Elisp fuzz, warning-as-error compile/checkdoc/package/
|
||||
release/static gates, a re-profiled live benchmark, and temporal GUI
|
||||
evidence for stable normal input, middle-line edits, reversible edits,
|
||||
local word migration, hard completion, and point-only motion.
|
||||
- Impact: `ekp-buffer.el`, buffer/live benchmark/GUI tests, bilingual
|
||||
user/developer docs, spec/plan/technical reference, issue/change records,
|
||||
changelog, and the superseding postmortem.
|
||||
- Implementation: `ekp-buffer--live-state` owns the committed
|
||||
source/key/plan/signatures/prefix/spans; `ekp-buffer--live-edit` snapshots
|
||||
that state plus marker offsets and one dirty island.
|
||||
`ekp-buffer--start-live-edit` naturalizes only the affected span range,
|
||||
`ekp-buffer--ordinary-live-edit-finished` restores an exact baseline or
|
||||
publishes only on a real native-row crossing, and hard/elsewhere/
|
||||
refill/context events commit through their existing boundaries. Lazy
|
||||
reflow excludes the active paragraph so static chunks cannot detach
|
||||
live-owned spans. No task031 change was made to `ekp.el` or the core DP.
|
||||
- Verification: focused RED tests first failed for same-row planning,
|
||||
middle-anchor invalidation, exact reversal, point-motion work, backward
|
||||
row crossing, and lazy-reflow ownership, then passed after the owning
|
||||
fixes. Final runs pass buffer 99/99 and default, seed-`20260729`, and
|
||||
isolated suites 188/188 each; C/Elisp fuzz passes 300/300. Production
|
||||
and task test files compile with warnings as errors; checkdoc is empty;
|
||||
pinned package-lint exits 0; release and 49-entry dictionary-manifest
|
||||
gates pass. The exact pinned-source gate passed earlier in this task
|
||||
with unchanged dictionary bytes; two redundant final downloads ended in
|
||||
GitHub transport `early EOF`, not a content mismatch.
|
||||
- Performance: at 80 px with GC excluded, same-row cache-revisit work
|
||||
performs zero plans (C p99 1.627 ms; Elisp p99 1.502 ms), point motion
|
||||
performs zero plan/cache calls (C p99 0.017 ms; Elisp p99 0.015 ms), and
|
||||
291 appends contain only 15 permitted structural plans. The remaining
|
||||
commit spikes are the re-profiled `task030` surface.
|
||||
- Dynamic evidence: the reviewed 48.95-second fullscreen run at
|
||||
`/private/tmp/ekp-stable-transaction-final5-2BFryc` contains 55 ordered
|
||||
checkpoints (11 actions × 5 phases), 17 green assertions at every
|
||||
checkpoint, one 1434×900 target window, a completed run-end, no black
|
||||
segment, and a PASS report. It covers middle-row locality, exact
|
||||
reversal, forward structural crossing, backward deletion stability,
|
||||
point motion, public yank/undo, resize/context commits, and hard
|
||||
paragraph completion.
|
||||
- Closure: developer verification is complete. `issue019` remains open
|
||||
only for the required user-visible confirmation; `task030` is unblocked.
|
||||
|
||||
- task032 [x] Reduce C-backed resize/reflow latency with exact layout parity.
|
||||
- Source: `issue020` and the user's observed 60–70 ms C resize latency.
|
||||
- Baseline first: freeze the current portable C module and build a
|
||||
same-machine evaluator that separates complete reflow, plan construction,
|
||||
Emacs/C marshalling, C DP, and projection publication. Exclude startup,
|
||||
resize debounce, and GC pauses from the mutator measurement.
|
||||
- Performance gate: an interleaved baseline/candidate width-and-length
|
||||
matrix must improve both p50 and p95 by at least 20%, keep candidate p95
|
||||
at or below 50 ms, and report raw samples rather than a best-of run.
|
||||
- Correctness gate: every candidate C result must match the frozen C
|
||||
baseline and Elisp layout exactly; complete ERT, C/Elisp fuzz,
|
||||
warning-as-error builds, static/release checks, and temporal GUI resize
|
||||
evidence remain mandatory.
|
||||
- Constraints: do not weaken cache identity, KP output semantics, the
|
||||
stable transaction model, or source-clean projection. Do not hide work
|
||||
with longer debounce, skipped widths, stale results, approximate reuse,
|
||||
background publication, or global GC changes.
|
||||
- Stop condition: optimize only the layer proven dominant by the baseline
|
||||
profile. If C DP is not the owner of most resize time, fix the actual
|
||||
Elisp/C or projection boundary instead of micro-optimizing the DP.
|
||||
- Impact: evaluator/benchmark, the proven runtime owner, C module tests,
|
||||
performance documentation, issue/change records, and a decision
|
||||
postmortem when the selected optimization is non-obvious.
|
||||
- Implementation: resolve one paragraph/DP payload per plan, cache natural
|
||||
gap geometry, omit true no-op gaps, consolidate owned property writes,
|
||||
and avoid static-plus-live double publication of the active paragraph.
|
||||
- Result: four interleaved rounds reduce core p50/p95 from
|
||||
22.949/42.006 ms to 15.318/27.687 ms and complete resize p50/p95 from
|
||||
28.149/46.611 ms to 15.900/27.487 ms. Exact frozen-C/Elisp parity holds.
|
||||
- Verification: normal and permuted 193-test suites, the per-test isolated
|
||||
runner, 300-case fuzzing, warning-as-error compilation, package-lint,
|
||||
checkdoc, release checks, and reviewed dynamic GUI evidence pass.
|
||||
- Closure: developer verification is complete. `issue020` remains open
|
||||
only for the required user-visible confirmation.
|
||||
|
||||
@ -0,0 +1,408 @@
|
||||
# Technical Reference: Text-Property-Only KP Layout 2026-07-29
|
||||
|
||||
## Status and Reader Task
|
||||
|
||||
Implemented by `task019` through `task022` for static/source-clean
|
||||
projection, then refined by later live-editing tasks. `task028` supersedes
|
||||
the native-row live planning unit with semantic hard-line prefix
|
||||
projection. This record is the technical reference for the semantic layout
|
||||
plan, text-property-only buffer projection, live editing boundary, and
|
||||
their explicit constraints.
|
||||
|
||||
## User Constraints
|
||||
|
||||
- Do not use overlays.
|
||||
- Do not insert glue spaces, soft newlines, or discretionary hyphens into
|
||||
buffer text.
|
||||
- Express layout through text properties on existing source characters.
|
||||
- Keep the source character sequence and all logical buffer positions
|
||||
unchanged.
|
||||
- An unfinished active line and later live-edit behavior remain separate
|
||||
product concerns; a static display proof does not establish seamless
|
||||
editing.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Emacs 29.1 and later expose enough display primitives to make a
|
||||
text-property-only KP renderer plausible:
|
||||
|
||||
- `(min-width ((PIXELS)))` gives a source span an absolute minimum display
|
||||
width and pads only in the display layer.
|
||||
- `(space-width FACTOR)` scales existing ASCII space glyphs without
|
||||
replacing their source characters.
|
||||
- Combining `space-width` with `min-width` expresses both shrink and stretch
|
||||
while using `min-width` as an exact pixel floor after floating-point
|
||||
rounding.
|
||||
- `line-prefix` supplies indentation for non-continuation display lines.
|
||||
- A replacing `display` string attached to an existing grapheme can show
|
||||
that grapheme followed by a discretionary hyphen and a display-only
|
||||
newline. The original grapheme remains in the buffer.
|
||||
|
||||
These primitives close the representation gap for the implemented
|
||||
single-authoritative-width buffer renderer. Foreign ownership, multiple
|
||||
windows, and seamless incremental editing are governed by the decisions
|
||||
below rather than inferred from the primitives alone.
|
||||
|
||||
The package baseline does not need to move: Emacs `NEWS.29` records
|
||||
`min-width` as a new `display` property, and the repository's CI exercises
|
||||
the declared Emacs 29.1 floor.
|
||||
|
||||
## Confirmed Emacs Semantics
|
||||
|
||||
### Absolute pixel minimum width
|
||||
|
||||
The `min-width` display specification has the form
|
||||
`(min-width (WIDTH))`. The outer one-element list identifies a contiguous
|
||||
display run by `eq`. A pixel expression of the form `(N)` means exactly
|
||||
`N` pixels, so the fully nested form for 20 pixels is:
|
||||
|
||||
```elisp
|
||||
(min-width ((20)))
|
||||
```
|
||||
|
||||
Emacs displays the underlying text normally and appends display-only white
|
||||
space if the rendered span is narrower than the requested minimum.
|
||||
|
||||
Each independently padded adjacent span must receive a distinct identity
|
||||
list. Reusing the same inner list object can make Emacs treat adjacent
|
||||
spans as one run and add padding only at the end of the combined run.
|
||||
|
||||
References:
|
||||
|
||||
- [Other Display Specifications](https://www.gnu.org/software/emacs/manual/html_node/elisp/Other-Display-Specs.html)
|
||||
- [Pixel Specification for Spaces](https://www.gnu.org/software/emacs/manual/html_node/elisp/Pixel-Specification.html)
|
||||
- [Emacs 30.2 display implementation](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L5606-L5686)
|
||||
|
||||
### Scaling real spaces
|
||||
|
||||
`(space-width FACTOR)` multiplies the rendered width of every ASCII space
|
||||
covered by the property. A factor below 1 shrinks; a factor above 1
|
||||
stretches.
|
||||
|
||||
The Emacs 30.2 implementation adds three boundaries beyond the manual's
|
||||
summary:
|
||||
|
||||
- `FACTOR` must be a number greater than zero.
|
||||
- The graphical display path checks specifically for ASCII `SPC`.
|
||||
- The specification is ignored on a non-window-system frame.
|
||||
|
||||
It does not affect tabs. NBSP, ideographic spaces, tabs, and other Unicode
|
||||
space characters therefore need their own policy; they must not be assumed
|
||||
to behave like an ASCII word space.
|
||||
|
||||
References:
|
||||
|
||||
- [Other Display Specifications](https://www.gnu.org/software/emacs/manual/html_node/elisp/Other-Display-Specs.html)
|
||||
- [Emacs 30.2 `space-width` parsing](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L6039-L6052)
|
||||
- [Emacs 30.2 ASCII-space scaling](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L32723-L32729)
|
||||
|
||||
### Combining the two specifications
|
||||
|
||||
For a real ASCII whitespace run whose measured natural width is `N` and
|
||||
whose KP target width is `G > 0`, use the conceptual display value:
|
||||
|
||||
```elisp
|
||||
((space-width FACTOR)
|
||||
(min-width ((G))))
|
||||
```
|
||||
|
||||
where `FACTOR = G / N`.
|
||||
|
||||
`space-width` first makes shrink possible. The display engine rounds the
|
||||
scaled glyph width to pixels; `min-width` then pads any downward rounding
|
||||
error to the exact target. For a target at least as wide as the natural
|
||||
run, `min-width` alone is sufficient, but one combined representation can
|
||||
keep the renderer uniform.
|
||||
|
||||
A zero-width source-space run is not represented by `(space-width 0)`,
|
||||
because zero is rejected. Line-edge spaces must instead be consumed by the
|
||||
chosen break representation, retained naturally, or handled by a separately
|
||||
verified display rule.
|
||||
|
||||
### Indentation
|
||||
|
||||
The `line-prefix` text property adds a display-only prefix to every
|
||||
non-continuation line. It must cover the complete affected text range so
|
||||
redisplay can find it at each line start.
|
||||
|
||||
If EKP relies on Emacs's native wrapping, continuation lines use
|
||||
`wrap-prefix`, not `line-prefix`. If EKP realizes each KP break as a newline
|
||||
inside a replacing display string, the following row is a non-continuation
|
||||
display line and `line-prefix` is applicable.
|
||||
|
||||
Reference:
|
||||
|
||||
- [Special Properties: `line-prefix`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Special-Properties.html)
|
||||
|
||||
## Mapping KP Output to Existing Source Text
|
||||
|
||||
| KP result | Text-property-only projection |
|
||||
| --- | --- |
|
||||
| Ordinary box | Display the original source grapheme or word unchanged. |
|
||||
| Latin word glue over ASCII spaces | Apply `space-width`; add `min-width` as the exact pixel floor. |
|
||||
| CJK or mixed glue with no source space | Apply `min-width` to the preceding source box with target `natural-box-width + glue-width`. |
|
||||
| Leading indentation | Apply `line-prefix`; use `wrap-prefix` only if the selected break backend creates continuation lines. |
|
||||
| Break at an existing single space | Replace that space on display with a newline while leaving the source space in the buffer. |
|
||||
| CJK break with no boundary character | Make the preceding grapheme display as `GRAPHEME + NEWLINE`. |
|
||||
| Latin discretionary hyphen break | Make the preceding grapheme display as `GRAPHEME + HYPHEN + NEWLINE`. |
|
||||
| Final line | Remove forced-break/hyphen properties and leave it ragged unless alignment requires a leading prefix. |
|
||||
|
||||
The current algorithm already computes exact per-gap pixel results for
|
||||
stretch and shrink. The planned change is a renderer substitution, not a
|
||||
new KP cost or distribution model.
|
||||
|
||||
## Discretionary Latin Hyphen Without Source Pollution
|
||||
|
||||
### Proposed break owner
|
||||
|
||||
For a KP break between source positions `P` and `P+1` inside a Latin word:
|
||||
|
||||
1. Identify the complete grapheme immediately before the break. Never split
|
||||
a combining sequence.
|
||||
2. Leave every source character untouched.
|
||||
3. Attach a replacing `display` text property to that grapheme.
|
||||
4. Build the replacement string from the same visible grapheme, followed by
|
||||
the configured hyphen glyph and `"\n"`.
|
||||
5. Copy the grapheme's relevant face/font properties to the replacement
|
||||
string, but remove recursive `display` state.
|
||||
6. Put a `cursor` property on the reproduced grapheme so point on the
|
||||
covered source positions is drawn on the grapheme rather than on the
|
||||
synthetic hyphen or newline.
|
||||
7. Include the hyphen width in the KP line measurement exactly as the
|
||||
current renderer already does.
|
||||
8. Remove or move the property atomically when a later plan chooses a
|
||||
different break.
|
||||
|
||||
Conceptually, breaking `abcdefgh` after `d` displays:
|
||||
|
||||
```text
|
||||
abcd-
|
||||
efgh
|
||||
```
|
||||
|
||||
while the source characters remain exactly:
|
||||
|
||||
```text
|
||||
abcdefgh
|
||||
```
|
||||
|
||||
The hyphen and newline exist only inside the `display` property's
|
||||
replacement string. `char-after`, character positions, regexp search, and
|
||||
`buffer-substring-no-properties` continue to operate on the original word.
|
||||
|
||||
### Why the preceding grapheme owns the replacement
|
||||
|
||||
Attaching `"-\nE"` to the next grapheme would make point on that source
|
||||
grapheme default to the hyphen or newline portion. Attaching
|
||||
`"D-\n"` to the preceding grapheme keeps the visible source glyph at the
|
||||
property's logical anchor, and the next source grapheme begins naturally on
|
||||
the following display line.
|
||||
|
||||
### Runtime evidence on Emacs 30.2
|
||||
|
||||
A clean GUI Emacs 30.2 probe applied a `display` text property containing
|
||||
`"d-\n"` with `cursor 1` to the source `d` in `abcdefgh`.
|
||||
|
||||
Observed result:
|
||||
|
||||
- `substring-no-properties (buffer-string)` remained `"abcdefgh"`.
|
||||
- `buffer-size` remained 8.
|
||||
- `count-screen-lines` reported 2.
|
||||
- `vertical-motion` by one display line moved point from position 1 to
|
||||
position 5, the source `e`.
|
||||
- A 20px `line-prefix` placed both visual rows at x=20.
|
||||
|
||||
This proves the minimal LTR shape is expressible. It does not yet prove
|
||||
mouse selection, active-region highlighting, isearch faces, bidi text,
|
||||
multi-codepoint graphemes, IME composition, or foreign display-property
|
||||
composition.
|
||||
|
||||
References:
|
||||
|
||||
- [Display strings](https://www.gnu.org/software/emacs/manual/html_node/elisp/Other-Display-Specs.html)
|
||||
- [Cursor placement in display strings](https://www.gnu.org/software/emacs/manual/html_node/elisp/Special-Properties.html)
|
||||
- [Emacs 30.2 cursor handling around display-string newlines](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L1899-L1956)
|
||||
|
||||
## Runtime Evidence for Exact Glue
|
||||
|
||||
A clean GUI Emacs 30.2 probe measured the current font's ASCII space at
|
||||
7px, then applied the combined `space-width` and `min-width` display value
|
||||
to two real source spaces:
|
||||
|
||||
- target 3px rendered as exactly 3px;
|
||||
- target 20px rendered as exactly 20px;
|
||||
- the buffer still returned the unchanged source characters `"a b"`.
|
||||
|
||||
This proves one-font, one-window shrink and stretch. The production gate
|
||||
must repeat the probe across text scaling, face remapping, font fallback,
|
||||
mixed property runs, and window resizing.
|
||||
|
||||
## Text API Boundary
|
||||
|
||||
This direction removes character pollution but not all property metadata.
|
||||
|
||||
- `buffer-substring-no-properties`, direct character access, search,
|
||||
syntax parsing, markers, point, and character counts see the original
|
||||
source characters.
|
||||
- `buffer-string` and `buffer-substring` copy text properties into the
|
||||
returned string. Lisp that explicitly examines properties can observe
|
||||
EKP's display metadata.
|
||||
- Property-only updates must use `with-silent-modifications` so layout
|
||||
refresh does not alter the modified flag, ordinary undo history, or
|
||||
external change hooks.
|
||||
- EKP-owned properties must be nonsticky so new user input does not inherit
|
||||
a stale break, hyphen, width, or prefix.
|
||||
- Copy/yank and insertion of propertized strings require an ownership rule
|
||||
that strips or regenerates EKP layout properties rather than carrying a
|
||||
stale layout into another location.
|
||||
|
||||
References:
|
||||
|
||||
- [Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Text-Properties.html)
|
||||
- [Examining Buffer Contents](https://www.gnu.org/software/emacs/manual/html_node/elisp/Buffer-Contents.html)
|
||||
- [Changing Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Changing-Properties.html)
|
||||
|
||||
## Implementation Decisions and Remaining Boundaries
|
||||
|
||||
### Chosen break semantics
|
||||
|
||||
The replacement-string approach forces the exact KP break and makes
|
||||
`line-prefix` usable. A native-wrap alternative might avoid replacing the
|
||||
break-owner grapheme by filling the line exactly with `min-width`, but it
|
||||
still needs proof that Emacs never wraps at an earlier legal boundary and
|
||||
that exact-edge stretch glyphs remain on the intended line.
|
||||
|
||||
The implementation uses the replacing-string backend because it represents
|
||||
the chosen KP break explicitly. Native wrapping is not a fallback.
|
||||
|
||||
### Foreign `display` ownership
|
||||
|
||||
Text has one effective `display` property value at each position. Parallel
|
||||
non-replacing specifications can coexist in one value, but a foreign
|
||||
replacing string, image, invisibility rule, composition, or font-lock
|
||||
manager can conflict with EKP's width or break owner.
|
||||
|
||||
EKP rejects the affected hard paragraph with an explicit diagnostic. It
|
||||
never erases, merges, or silently reorders a foreign replacing
|
||||
specification.
|
||||
|
||||
### Graphemes, bidi, and shaping
|
||||
|
||||
A replacing display string is treated as one display object for
|
||||
bidirectional reordering. The first implementation slice should therefore
|
||||
target LTR Latin hyphenation and CJK. Arabic, Hebrew, Indic shaping,
|
||||
ligatures, and multi-codepoint graphemes remain gated until their cursor,
|
||||
selection, and shaping behavior is proven.
|
||||
|
||||
### Multiple windows
|
||||
|
||||
Text properties belong to the buffer, not a window. One property set cannot
|
||||
simultaneously encode two different KP break plans for the same buffer shown
|
||||
at different widths.
|
||||
|
||||
The narrowest live window showing the buffer is authoritative. Wider
|
||||
windows can retain right-side space but cannot overflow. Independent
|
||||
simultaneous width plans are explicitly outside this text-property-only
|
||||
architecture.
|
||||
|
||||
### Live editing
|
||||
|
||||
The current target policy separates stable editing from global KP
|
||||
commitment. One committed projection owns the hard-line baseline source,
|
||||
semantic plan, line signatures, projected spans, and break anchors. One
|
||||
dirty edit transaction snapshots that baseline and naturalizes only the
|
||||
affected local island.
|
||||
|
||||
Ordinary input inside the same native visual row performs no whole-hard-line
|
||||
planning. Existing glue and native soft wrapping absorb local changes while
|
||||
unaffected anchors remain installed. If the row overflows, native wrapping
|
||||
moves only the required local words. Crossing into a new native visual row
|
||||
is a structural commit: `ekp-buffer` calls or reuses the existing
|
||||
whole-hard-line `ekp-layout-plan` once, then old/new semantic signatures
|
||||
limit one silent prefix publication.
|
||||
|
||||
Hard newline/paragraph completion, the next real edit outside the dirty
|
||||
island, explicit refill, and width/font/layout-context changes are the other
|
||||
commit events. Point-only motion is not a layout event anywhere. A reversible
|
||||
edit restores the saved owned projection directly and
|
||||
`equal-including-properties`.
|
||||
|
||||
This remains a buffer-layer policy, not a DP variant. Core DP plans complete
|
||||
text and receives no point, marker, window, buffer, redisplay, transaction,
|
||||
or composition state. The C ABI, DP schema, and plan contract do not change.
|
||||
|
||||
Static paragraph-edge hiding applies only to committed projected lines. The
|
||||
dirty edit island retains leading/trailing source whitespace with no
|
||||
replacing display, so a single space or tab is visible in the same
|
||||
command-loop turn. Reprojection restores the mark marker and `mark-active`
|
||||
as separate editor state; `set-mark` is not a valid positional restoration
|
||||
API because it activates an otherwise inactive mark.
|
||||
|
||||
Automatic planning is bounded by
|
||||
`ekp-auto-justify-paragraph-limit`. An oversized single hard line stays
|
||||
natural and diagnostic rather than blocking input; the explicit
|
||||
`ekp-refill-paragraph` command bypasses this limit.
|
||||
|
||||
## Rejected Shortcuts
|
||||
|
||||
- Insert U+00AD, `-`, whitespace, or newline characters: violates the
|
||||
logical-text requirement.
|
||||
- Overlay `before-string` or `after-string`: explicitly excluded by the
|
||||
user.
|
||||
- Use `min-width` alone for shrink: it only adds padding.
|
||||
- Use `space-width` alone as an exact-width guarantee: floating-point
|
||||
scaling is rounded to pixels.
|
||||
- Treat tabs or all Unicode whitespace as ASCII spaces: contradicted by the
|
||||
documented and source-level behavior.
|
||||
- Claim the GUI probe proves seamless editing: it covers a static LTR
|
||||
display case only.
|
||||
|
||||
## Runtime Verification Gate
|
||||
|
||||
1. Exact glue:
|
||||
- 1px through 64px targets;
|
||||
- shrink and stretch;
|
||||
- CJK, ASCII space, mixed gap, and repeated spaces;
|
||||
- text scaling, face remap, and fallback fonts.
|
||||
2. Breaks and hyphens:
|
||||
- Latin word break with hyphen;
|
||||
- CJK break without whitespace;
|
||||
- break at one and multiple source spaces;
|
||||
- final-line removal and breakpoint movement.
|
||||
3. Editor semantics:
|
||||
- point at every source position;
|
||||
- `C-n`/`C-p`, visual beginning/end of line, mouse hit testing;
|
||||
- active region, isearch, kill/yank, undo/redo, and narrowing.
|
||||
4. Ownership:
|
||||
- font-lock refontification;
|
||||
- a foreign replacing `display` property;
|
||||
- composition and IME preedit;
|
||||
- exact restoration of pre-existing properties.
|
||||
5. State invariants:
|
||||
- unchanged source characters and `buffer-chars-modified-tick`;
|
||||
- unchanged modified flag, undo list, and external change-hook count;
|
||||
- no stale property inheritance after insert, delete, split, or join.
|
||||
6. Architecture decision:
|
||||
- explicitly resolve or relax simultaneous different-width windows
|
||||
before calling the renderer complete.
|
||||
|
||||
## Stop Condition
|
||||
|
||||
This research task is complete when the confirmed primitives, proposed
|
||||
hyphen owner, known limitations, and runtime evidence are recorded.
|
||||
The original static feasibility and one-buffer/multiple-window gates were
|
||||
satisfied by `task019`–`task022`.
|
||||
|
||||
`task028` proved whole-hard-line plan consumption and semantic signature
|
||||
publication. `task029` proved that point-only motion must perform zero work.
|
||||
`issue019`/`task031` supersede their per-edit trigger ownership: a frontier
|
||||
cannot simultaneously be latest edit, natural suffix, and projection reuse
|
||||
boundary. Whole-hard-line planning remains valid at structural commits, not
|
||||
after every key.
|
||||
|
||||
The task029 performance audit recorded 291 unique append plans and zero cache
|
||||
hits at 80 pixels. `task031` now removes ordinary same-row planning: the same
|
||||
291-edit workload records 15 structural plans, while same-row revisit and
|
||||
point-motion scenarios record zero plans. The remaining C/Elisp p99 spikes
|
||||
coincide with permitted commit events; `issue018`/`task030` now own that
|
||||
unblocked, narrower performance surface.
|
||||
93
CHANGELOG.md
93
CHANGELOG.md
@ -8,6 +8,20 @@ project aims to follow [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Fixed
|
||||
|
||||
- C-backed core layout and complete resize/reflow now reuse prepared
|
||||
paragraph, DP, and gap geometry and avoid duplicate projection
|
||||
publication. The four-round same-machine gate records p95 values of
|
||||
27.687 ms and 27.487 ms respectively, with exact frozen-C/Elisp parity.
|
||||
- Reprojecting a buffer now preserves the mark marker and `mark-active`
|
||||
independently. Showcase width keys and resize reflow no longer turn an
|
||||
old inactive mark into a highlighted region.
|
||||
- The unfinished live line now keeps its source-edge whitespace natural.
|
||||
A leading/trailing space or tab appears in the same input turn, and
|
||||
whitespace exposed by backspace is no longer hidden by `display ""`
|
||||
until another glyph arrives.
|
||||
- Enabling or disabling a theme, or changing a frame's default font, now
|
||||
invalidates live plan history and reflows every active auto-justify
|
||||
buffer with the new glyph metrics.
|
||||
- Changing any Knuth-Plass cost parameter now selects a correctly keyed
|
||||
DP/render result immediately; cached paragraphs no longer require
|
||||
`ekp-clear-caches`. Structurally equal non-zero-looseness signatures now
|
||||
@ -15,14 +29,21 @@ project aims to follow [Semantic Versioning](https://semver.org/).
|
||||
- Changing `ekp-default-cws-stretch-pixel` in automatic spacing mode now
|
||||
invalidates both paragraph-cache lookup paths immediately. Unchanged
|
||||
spacing signatures still reuse the cached paragraph.
|
||||
- Saving justified buffers now writes from a logical-text copy instead of
|
||||
temporarily unformatting the display buffer. Filesystem, encoding, and
|
||||
interruption failures therefore leave the visible layout intact and
|
||||
retryable.
|
||||
- Buffer layout no longer rewrites the source character stream. Saving,
|
||||
region writes, direct Elisp reads, syntax, and search therefore operate
|
||||
on logical text without a temporary serialization copy.
|
||||
- Live editing now separates committed projection from a local dirty edit
|
||||
transaction. Same-row typing performs no whole-hard-line planning,
|
||||
middle-row edits preserve unaffected break anchors, and exact source
|
||||
reversal restores the saved projection `equal-including-properties`.
|
||||
- Point-only motion no longer changes live state or projection anywhere,
|
||||
including after leaving the active hard paragraph. Structural commits are
|
||||
owned by visual-row crossing, hard completion, the next real edit
|
||||
elsewhere, explicit refill, and width/font/layout-context changes.
|
||||
- Kill/copy integration now composes with and restores an existing
|
||||
`filter-buffer-substring-function`, including DELETE operations. Removing
|
||||
the final layout span outside auto mode also removes unused save/search/
|
||||
copy integrations.
|
||||
`filter-buffer-substring-function`, including DELETE operations, and
|
||||
strips only EKP-owned projection properties from copied text. Removing
|
||||
the final layout span outside auto mode restores the exact prior filter.
|
||||
- Dictionary syntax no longer degrades silently: files containing
|
||||
libhyphen replacement/slash patterns fail with an explicit condition
|
||||
because the fixed-width DP cannot model their conditional rewrites.
|
||||
@ -32,6 +53,41 @@ project aims to follow [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Changed
|
||||
|
||||
- The core now exposes semantic `ekp-layout-plan`, `ekp-layout-line`, and
|
||||
`ekp-layout-gap` records. The compatible string renderer and the buffer
|
||||
renderer consume the same break, glue, indentation, and discretionary-
|
||||
hyphen decisions.
|
||||
- Buffer/region layout is now a pure text-property projection on existing
|
||||
source characters: ASCII spaces combine `space-width` and absolute-pixel
|
||||
`min-width`; zero-source CJK/mixed glue uses `min-width` on a complete
|
||||
grapheme; indentation uses `line-prefix`; and visual breaks/hyphens use
|
||||
replacing display strings. EKP creates no overlay and inserts no layout
|
||||
character into a buffer.
|
||||
- Auto-justify now uses stable hard-line transactions. A dirty island stays
|
||||
native between commit events, so ordinary keys neither run DP nor rewrite
|
||||
unrelated properties. Native visual-row crossing atomically republishes
|
||||
every completed row from the unchanged whole-hard-line core plan; the new
|
||||
row remains natural. A buffer-local 16-entry LRU reuses recent
|
||||
text/context plans, and common-prefix signatures minimize writes at
|
||||
commits. IME and resize publication remain generation-checked; no
|
||||
edit-idle or cursor-motion formatter exists.
|
||||
- Auto-justify now owns its native soft-wrap precondition. It temporarily
|
||||
disables `truncate-lines` and narrow partial-window truncation, including
|
||||
Emacs's default sub-50-column behavior, then restores the prior values and
|
||||
buffer-local ownership on teardown.
|
||||
- One buffer uses the narrowest displayed window as its authoritative
|
||||
width. Foreign replacing display owners and unsupported non-ASCII
|
||||
whitespace shrink keep only the affected hard paragraph natural and are
|
||||
reported by `ekp-diagnose`.
|
||||
- Automatic work bounds a single hard paragraph with
|
||||
`ekp-auto-justify-paragraph-limit` (default 2,048 characters). Longer
|
||||
paragraphs remain naturally editable; `ekp-refill-paragraph` is the
|
||||
explicit unbounded quality pass.
|
||||
- **Breaking:** the editor integration module is now `ekp-buffer.el` and
|
||||
provides `ekp-buffer`; replace `(require 'ekp-region)` with
|
||||
`(require 'ekp-buffer)`. Module-owned configuration names now use the
|
||||
`ekp-buffer-` prefix. Public commands whose names describe an actual
|
||||
region or buffer operand are unchanged.
|
||||
- The bundled dictionary inventory is now 49 reproducible entries with a
|
||||
pinned LibreOffice commit, per-file SHA-256/source/license manifest, and a
|
||||
portable verifier/exporter. Sanskrit was removed because the pinned
|
||||
@ -44,6 +100,21 @@ project aims to follow [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Tests
|
||||
|
||||
- ERT now covers source/tick/undo invariants, exact ASCII and CJK glue
|
||||
projection, display-only static breaks and hyphens, zero-projection
|
||||
underfilled input, whole-hard-line semantic-prefix selection, non-frozen
|
||||
earlier breaks, common-prefix differential writes, plan-cache reuse,
|
||||
point-motion projection stability, stable dirty transactions, exact
|
||||
reversible restoration, atomic visual-row commits, hard-paragraph
|
||||
completion, IME/stale generations, foreign ownership,
|
||||
narrowest-window policy, inactive/active mark preservation, immediate
|
||||
edge whitespace, consecutive spaces, newline/yank/real-undo paths, and
|
||||
the oversized-paragraph guard.
|
||||
GUI probes verify pixel widths. Reviewed temporal recordings verify
|
||||
width-key selection and immediate edge-whitespace states, plus stable
|
||||
middle-row edits, exact reversible restoration, atomic visual-row
|
||||
commits, zero-work point motion, public yank/undo, resize commits, and
|
||||
hard-paragraph completion without delayed snap or overlays.
|
||||
- ERT fixtures now restore every mutable EKP option they isolate. The
|
||||
parshape C-bypass regression drives the public formatter, and reusable
|
||||
permuted-order plus per-test fresh-process runners prevent alphabetical
|
||||
@ -57,6 +128,14 @@ project aims to follow [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Performance
|
||||
|
||||
- The live-path benchmark records append, historical-plan reuse,
|
||||
point-motion, and hard-boundary latency with plan/cache counters.
|
||||
At the synthetic 80-pixel width, same-row editing and point motion perform
|
||||
zero planning; point-motion p99 is 0.017 ms on C and 0.015 ms on Elisp.
|
||||
Across 291 appends, only 15 visual-row crossings invoke the planner.
|
||||
Those structural commits still exceed the 16 ms p99 frame budget on both
|
||||
backends and are tracked as `issue018`/`task030`; no stale reuse, debounce,
|
||||
skipped publication, or global GC workaround hides the miss.
|
||||
- Tokenization now accumulates fragments and joins once per emitted box;
|
||||
dense hyphen insertion likewise joins original word slices once. On the
|
||||
1,000–8,000-character adversarial benchmark, the 8,000-character cases
|
||||
|
||||
@ -11,7 +11,7 @@ EMACS=emacs # or /path/to/Emacs
|
||||
# Byte-compile with warnings as errors (CI does this)
|
||||
$EMACS -Q --batch -L . \
|
||||
--eval '(setq byte-compile-error-on-warn t)' \
|
||||
-f batch-byte-compile ekp.el ekp-utils.el ekp-hyphen.el ekp-region.el
|
||||
-f batch-byte-compile ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
|
||||
|
||||
# Run the ERT suite (C-module tests auto-skip if not built)
|
||||
tests/run-tests.sh $EMACS
|
||||
@ -36,21 +36,23 @@ dictionaries/update.sh check
|
||||
to the demerits or line-metric formulas must touch both
|
||||
`ekp--dp-run-1d` (Elisp) and `dp_process_position` (`ekp_c/ekp_kp.c`),
|
||||
and the fuzz suite must stay at 300/300.
|
||||
- **The layout is lossless.** The renderer's marker properties
|
||||
(`ekp-glue`, `ekp-soft-break`, `ekp-soft-hyphen`, `ekp-hidden`) must
|
||||
round-trip exactly through `ekp-unjustify-region`.
|
||||
- **The two renderers have different rights.** The string renderer's
|
||||
physical marker vocabulary (`ekp-glue`, `ekp-soft-break`,
|
||||
`ekp-soft-hyphen`, `ekp-hidden`) stays lossless and compatible. The
|
||||
buffer renderer must create no overlay or source character and may use
|
||||
only EKP-owned text properties on existing characters.
|
||||
- Any C-module API change bumps `EKP_VERSION_MINOR` and the matching
|
||||
`ekp-c-module-required-version`, and rebuilds the module.
|
||||
- New behavior needs an ERT test. Buffer-level behavior (save,
|
||||
isearch, undo, kill/yank, mode interactions) goes in
|
||||
`tests/ekp-region-tests.el`.
|
||||
`tests/ekp-buffer-tests.el`.
|
||||
|
||||
## Style
|
||||
|
||||
- `lexical-binding: t` everywhere; keep byte-compilation warning-free.
|
||||
- `checkdoc` clean (CI enforces it): imperative docstring first lines,
|
||||
arguments mentioned in uppercase, two spaces after a sentence.
|
||||
- `package-lint` clean: the `ekp-` / `ekp-region-` namespaces, proper
|
||||
- `package-lint` clean: the `ekp-` / `ekp-buffer-` namespaces, proper
|
||||
autoload cookies on interactive entry points.
|
||||
- Match the surrounding code; keep comments about *why*, not *what*.
|
||||
|
||||
|
||||
159
DEVELOPER.md
159
DEVELOPER.md
@ -153,52 +153,127 @@ optimality. Both engines implement the identical strategy.
|
||||
- Last lines are ragged-right (ideal glues + trailing filler);
|
||||
single-box lines get a trailing filler clamped at ≥ 0.
|
||||
|
||||
`ekp--pixel-justify` then strips leading space boxes (except on the
|
||||
first line — indentation) and trailing space boxes, and appends a
|
||||
hyphen — propertized like the word it breaks — where a line ends at a
|
||||
hyphenation point. Stripped widths are *not* redistributed: the DP
|
||||
already excluded them (§3).
|
||||
`ekp-layout-plan` is the representation boundary between layout and
|
||||
rendering. It combines the paragraph boxes/source offsets, DP breaks,
|
||||
per-line glue targets, indentation, stripped edges, and discretionary
|
||||
hyphen decisions into `ekp-layout-plan`, `ekp-layout-line`, and
|
||||
`ekp-layout-gap` records. The plan contains no buffer positions or display
|
||||
mechanism. `ekp-render-layout-string` consumes it for the public string API;
|
||||
the buffer integration can consume the same decisions without re-running or
|
||||
reinterpreting the KP algorithm.
|
||||
|
||||
Glues become `(space :width (N))` display properties, so justification
|
||||
is pixel-exact in GUI Emacs and column-exact in batch/tty.
|
||||
The two consumers deliberately have different representation rights.
|
||||
|
||||
The output is **lossless**: boxes are located in the source string
|
||||
(`ekp--box-offsets`), and every synthesized or hidden piece records the
|
||||
original text it stands for —
|
||||
#### String renderer
|
||||
|
||||
`ekp-render-layout-string` preserves the public string API. It strips
|
||||
leading/trailing space boxes, synthesizes display spaces and visual
|
||||
newlines, and appends a propertized discretionary hyphen where selected.
|
||||
The returned string remains lossless through four private markers:
|
||||
|
||||
| property | on | value / meaning |
|
||||
|-------------------|-----------------------|--------------------------------|
|
||||
| `ekp-glue` | synthesized glue space| original text it replaced |
|
||||
| `ekp-soft-break` | inserted `\n` | whitespace swallowed at break |
|
||||
| `ekp-soft-hyphen` | inserted hyphen | marker only |
|
||||
| `ekp-hidden` | paragraph-edge text | kept verbatim, `display ""` |
|
||||
|-------------------|------------------------|-------------------------------|
|
||||
| `ekp-glue` | synthesized glue space | original text it replaced |
|
||||
| `ekp-soft-break` | synthesized `\n` | swallowed boundary whitespace |
|
||||
| `ekp-soft-hyphen` | synthesized hyphen | marker only |
|
||||
| `ekp-hidden` | paragraph-edge text | source retained, display empty|
|
||||
|
||||
Zero-width glue with a non-empty original renders as the hidden
|
||||
original itself, so no character is ever dropped. `ekp-region.el`
|
||||
inverts these four structurally (`ekp-unjustify-region`) — exact even
|
||||
after the justified text was edited — and builds
|
||||
`ekp-justify-region` / `ekp-auto-justify-mode` on top.
|
||||
`ekp--layout-marker-properties` owns the complete renderer/region marker
|
||||
vocabulary and its non-inheritance contract.
|
||||
This physical representation exists only in the returned string. It is
|
||||
kept for API compatibility and is not installed into a source buffer.
|
||||
`ekp--layout-marker-properties` owns its marker vocabulary and
|
||||
non-inheritance contract.
|
||||
|
||||
Saving is a non-mutating serialization boundary.
|
||||
`ekp-region--write-logical-buffer` runs first in the buffer-local
|
||||
`write-region-annotate-functions`, switches whole-buffer writes to a hidden
|
||||
logical copy, and leaves the display buffer untouched. Subsequent annotation
|
||||
functions and coding conversion operate on that copy. A successful write
|
||||
disposes it immediately; a failed write keeps at most one copy, which the
|
||||
next write or integration teardown replaces. Region-only `write-region`
|
||||
calls intentionally retain Emacs's physical-buffer semantics; the logical
|
||||
serialization boundary is the whole-buffer save path.
|
||||
#### Buffer renderer
|
||||
|
||||
Copy filtering has explicit single-slot ownership. EKP records whether the
|
||||
previous `filter-buffer-substring-function` was local, temporarily restores
|
||||
that value, and invokes the public `filter-buffer-substring` dispatcher to
|
||||
preserve its transform and DELETE behavior. EKP then structurally removes
|
||||
layout markers from the returned string. DELETE lifecycle cleanup runs after
|
||||
the temporary binding is unwound, so removing the final justified span
|
||||
outside auto mode restores that exact local value or reveals the inherited
|
||||
value, and removes the save/search/change hooks.
|
||||
`ekp-buffer.el` keeps the buffer's character sequence untouched and
|
||||
projects the same plan with text properties on existing source characters:
|
||||
|
||||
- source ASCII spaces:
|
||||
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`;
|
||||
- zero-source CJK/mixed glue: `min-width` on the preceding complete
|
||||
grapheme, targeting its natural advance plus glue;
|
||||
- indentation: `line-prefix`;
|
||||
- a source-whitespace break: the first boundary character displays as
|
||||
newline and the rest as empty;
|
||||
- a CJK or discretionary-hyphen break: a replacing display string
|
||||
reproduces the existing complete grapheme, appends the optional hyphen,
|
||||
then a visual newline.
|
||||
|
||||
`ekp-buffer--display` and `ekp-buffer--line-prefix` record exact ownership.
|
||||
Removal clears the public property only when its value is still identical
|
||||
to EKP's owner value, so a later foreign change is not erased. Paragraphs
|
||||
with foreign `display`, `line-prefix`, `wrap-prefix`, `composition`, or
|
||||
`invisible` ownership stay verbatim. Exact shrink of tabs/non-ASCII
|
||||
whitespace is also refused because `space-width` affects ASCII spaces only.
|
||||
|
||||
All installation/removal runs inside `with-silent-modifications`, and owned
|
||||
properties are nonsticky. Buffer characters, point/mark, modified state,
|
||||
undo, character-modified tick, and external change hooks therefore remain
|
||||
source-owned. Reprojection restores the mark marker without calling
|
||||
`set-mark`, then restores `mark-active` independently; an inactive mark
|
||||
cannot become a region as a layout side effect. No EKP buffer path creates
|
||||
an overlay.
|
||||
|
||||
Saving, ordinary search, syntax, and direct Elisp character APIs need no
|
||||
logical-text adapter: the real buffer is already logical. Copy filtering
|
||||
remains necessary because `buffer-substring` intentionally preserves text
|
||||
properties. EKP composes with the previous
|
||||
`filter-buffer-substring-function`, then removes only its projection
|
||||
metadata from the copied string.
|
||||
|
||||
#### Live flow
|
||||
|
||||
Live editing uses the ordinary whole-text Knuth-Plass plan without giving
|
||||
editing state to the core planner:
|
||||
|
||||
1. Live state owns the last committed hard-line source, normal
|
||||
`ekp-layout-plan`, semantic signatures, projected spans, and stable break
|
||||
anchors at the narrowest-window authoritative width.
|
||||
2. The first real change opens one edit transaction. It snapshots that
|
||||
committed state, then removes EKP properties only from the smallest
|
||||
projected span range containing the edit. Span objects and unaffected
|
||||
anchors remain registered.
|
||||
3. Further changes inside the dirty island use native soft wrapping and do
|
||||
no whole-hard-line planning. If the logical source returns exactly to the
|
||||
snapshot, EKP restores the saved owned-property runs and marker offsets
|
||||
directly; the state, plan, signatures, and spans retain object identity.
|
||||
4. Crossing a native visual-row boundary is a commit. EKP computes or reuses
|
||||
one whole-hard-line plan, derives every completed semantic row, and
|
||||
publishes their changed suffix silently as one command-loop transition.
|
||||
The new current row remains natural.
|
||||
5. The other commits are a hard newline/paragraph completion, the next real
|
||||
edit outside the dirty island, explicit refill, and width/font/face/theme
|
||||
or layout-context change. Point motion is never a commit, even across hard
|
||||
paragraphs; there is no live `post-command-hook`.
|
||||
6. Stable line signatures minimize property writes at a commit, while the
|
||||
16-entry buffer-local LRU reuses recent text/context plans. Neither
|
||||
mechanism decides when layout is allowed to change; the transaction owns
|
||||
that policy.
|
||||
7. IME preedit, foreign display ownership, unsupported shrink, oversized
|
||||
hard lines, stale generations, or publication errors fail closed to
|
||||
native display. A partial projection is rolled back and the original
|
||||
error is surfaced.
|
||||
|
||||
Native wrapping is a state-machine precondition, not a user preference the
|
||||
mode can merely hope is enabled. On activation, the mode snapshots the
|
||||
values and local-binding ownership of `truncate-lines` and
|
||||
`truncate-partial-width-windows`, then makes both buffer-local and nil.
|
||||
Teardown restores local values or removes the temporary bindings so global
|
||||
ownership resumes. This prevents Emacs's default 50-column partial-window
|
||||
threshold from silently turning a narrow split into horizontal scrolling.
|
||||
|
||||
This state model needs no live lookahead, push/pull convergence, per-key
|
||||
whole-line planner, or idle formatter. A real edit after point motion may
|
||||
commit the previous active hard line, including when narrowing makes that
|
||||
line inaccessible; point motion itself remains a strict no-op.
|
||||
|
||||
No edit-idle whole-paragraph formatter exists. Resize/background work is
|
||||
generation-owned. Large buffers are processed visible-first in hard-
|
||||
paragraph chunks; a single hard paragraph above
|
||||
`ekp-auto-justify-paragraph-limit` remains natural during automatic work
|
||||
and requires explicit `ekp-refill-paragraph` for an unbounded quality pass.
|
||||
Because text properties are buffer-wide, the narrowest live window supplies
|
||||
the one authoritative width.
|
||||
|
||||
### 5.1 Break permissions, alignment, protrusion, shapes
|
||||
|
||||
@ -403,11 +478,11 @@ ekp.el Core: para struct, caching, DP (1D + looseness),
|
||||
ekp-utils.el Tokenizer (boxes, kinsoku), font detection with
|
||||
batch/tty fallbacks, C module loading
|
||||
ekp-hyphen.el Liang hyphenation + dictionary registry
|
||||
ekp-region.el Buffer/region commands, ekp-auto-justify-mode, and
|
||||
editor integration (save, isearch, kill-ring, undo)
|
||||
ekp-buffer.el Text-property-only buffer/region projection, synchronous
|
||||
live flow, window lifecycle, copy filtering, diagnostics
|
||||
ekp_c/ C dynamic module (see ekp_c/README.md)
|
||||
dictionaries/ Hunspell hyphenation patterns (from LibreOffice)
|
||||
tests/ ekp-tests.el, ekp-region-tests.el (ERT),
|
||||
tests/ ekp-tests.el, ekp-buffer-tests.el (ERT),
|
||||
ekp-fuzz.el (parity fuzz), ekp-bench.el,
|
||||
ekp-demo.el, ekp-showcase.el, sample texts,
|
||||
run-tests.sh
|
||||
|
||||
132
DEVELOPER_ZH.md
132
DEVELOPER_ZH.md
@ -136,46 +136,106 @@ penalty/flagged 断点),主流程无 `q`/looseness(见 §6),相邻松紧惩
|
||||
≥ 0。
|
||||
- 末行右侧不齐(理想 glue + 尾部填充);单盒行的尾部填充钳制 ≥ 0。
|
||||
|
||||
`ekp--pixel-justify` 随后剥离行首空格盒(首行除外——缩进)与行尾空格
|
||||
盒,在断词处附加连字符(继承所断单词的文本属性)。剥离的宽度**不再**
|
||||
重新分配:DP 已经排除了它们(§3)。
|
||||
`ekp-layout-plan` 是布局决策与渲染方式之间的表示边界。它把段落盒子
|
||||
及其原文 offset、DP 断点、逐行 glue 目标、缩进、边缘剥离和可选断词
|
||||
决策组合成 `ekp-layout-plan`、`ekp-layout-line` 和 `ekp-layout-gap`
|
||||
记录。plan 不包含 buffer 位置或具体显示机制。
|
||||
`ekp-render-layout-string` 用它服务现有字符串 API;buffer 集成可复用
|
||||
完全相同的决策,无需重新运行或重新解释 KP 算法。
|
||||
|
||||
Glue 渲染为 `(space :width (N))` display 属性,GUI 下像素级精确,
|
||||
batch/tty 下按字符列精确。
|
||||
两个消费方拥有不同且明确的表示权限。
|
||||
|
||||
渲染输出是**无损**的:先用 `ekp--box-offsets` 在原串中定位每个盒子,
|
||||
然后每一处合成/隐藏内容都记录它所对应的原文——
|
||||
#### 字符串渲染器
|
||||
|
||||
`ekp-render-layout-string` 保持公开字符串 API 兼容。它剥离首尾空格
|
||||
盒、合成 display 空格与视觉换行,并在选中断词点附加继承样式的连字符。
|
||||
返回字符串通过四类私有标记保持无损:
|
||||
|
||||
| 属性 | 位置 | 值 / 含义 |
|
||||
|-------------------|-----------------|----------------------------|
|
||||
| `ekp-glue` | 合成的 glue 空格| 它所替换的原文 |
|
||||
| `ekp-soft-break` | 插入的 `\n` | 断点处被吞掉的空白 |
|
||||
| `ekp-soft-hyphen` | 插入的连字符 | 仅作标记 |
|
||||
| `ekp-hidden` | 段落边缘文本 | 原样保留,`display ""` 隐藏|
|
||||
|-------------------|------------------|---------------------------|
|
||||
| `ekp-glue` | 合成的 glue 空格 | 它所替换的原文 |
|
||||
| `ekp-soft-break` | 合成的 `\n` | 断点处吞掉的边界空白 |
|
||||
| `ekp-soft-hyphen` | 合成的连字符 | 仅作标记 |
|
||||
| `ekp-hidden` | 段落边缘文本 | 保留源文本、显示为空 |
|
||||
|
||||
零宽 glue 若对应非空原文,直接渲染为隐藏的原文本身,因此任何字符都
|
||||
不会丢失。`ekp-region.el` 对这四类标记做纯结构逆变换
|
||||
(`ekp-unjustify-region`)——即使排版后又被编辑过也能精确还原——并在
|
||||
其上实现 `ekp-justify-region` / `ekp-auto-justify-mode`。
|
||||
`ekp--layout-marker-properties` 统一拥有 renderer/region 的完整标记
|
||||
词汇表及其不向新输入继承的契约。
|
||||
这份物理表示只存在于返回字符串中,用于兼容既有 API,绝不会安装到源
|
||||
buffer。`ekp--layout-marker-properties` 统一拥有其标记词汇表与不继承
|
||||
契约。
|
||||
|
||||
保存是非修改式序列化边界。buffer-local
|
||||
`write-region-annotate-functions` 中最先运行
|
||||
`ekp-region--write-logical-buffer`,把整 buffer 写入切换到隐藏的逻辑
|
||||
副本,显示 buffer 始终不变;后续 annotation 与编码转换继续处理该副本。
|
||||
成功写入立即销毁副本;失败时每个源 buffer 最多保留一份,下次写入或
|
||||
integration teardown 会替换并清理它。只写局部的 `write-region`
|
||||
有意保留 Emacs 的物理 buffer 语义;逻辑序列化边界只覆盖整 buffer
|
||||
保存路径。
|
||||
#### Buffer 渲染器
|
||||
|
||||
复制过滤有明确的单槽 owner。EKP 记录原
|
||||
`filter-buffer-substring-function` 是否为 buffer-local,临时恢复该值
|
||||
并调用公开的 `filter-buffer-substring` dispatcher,以保留转换与
|
||||
DELETE 语义,再从返回字符串中结构化移除 EKP 布局标记。DELETE 的
|
||||
lifecycle 清理在临时绑定解除后执行;auto mode 外最后一个排版区间
|
||||
消失时,会恢复原 local 值或重新暴露继承值,同时移除
|
||||
save/search/change hooks。
|
||||
`ekp-buffer.el` 保持 buffer 字符序列不变,只在现有源字符上用文本属性
|
||||
投影同一份 plan:
|
||||
|
||||
- 源 ASCII 空格:
|
||||
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`;
|
||||
- 无源空格的 CJK/混排 glue:把 `min-width` 加在前一个完整字素上,
|
||||
目标为其自然 advance 加 glue;
|
||||
- 缩进:`line-prefix`;
|
||||
- 源空白断点:第一个边界字符显示成换行,其余显示为空;
|
||||
- CJK 或拉丁断词断点:replacing display string 重现已有完整字素,
|
||||
接上可选连字符和视觉换行。
|
||||
|
||||
`ekp-buffer--display` 与 `ekp-buffer--line-prefix` 记录精确 owner。
|
||||
移除时只有公开属性的值仍与 EKP owner 值相同时才清除,因此后来的外部
|
||||
修改不会被误删。带外部 `display`、`line-prefix`、`wrap-prefix`、
|
||||
`composition` 或 `invisible` owner 的段落保持 verbatim。若精确排版
|
||||
要求缩窄 tab/非 ASCII 空白也会拒绝,因为 `space-width` 只影响 ASCII
|
||||
空格。
|
||||
|
||||
所有安装/移除都在 `with-silent-modifications` 内完成,自有属性设为
|
||||
nonsticky。因此 buffer 字符、point/mark、modified 状态、undo、字符
|
||||
修改 tick 与外部 change hook 仍完全由源编辑拥有。任何 EKP buffer
|
||||
路径都不会创建 overlay。重投影直接恢复 mark marker,不会调用会激活
|
||||
选区的 `set-mark`,随后独立恢复 `mark-active`;因此布局不能把旧 mark
|
||||
变成选区。
|
||||
|
||||
保存、普通搜索、语法和直接 Elisp 字符 API 不再需要逻辑文本 adapter:
|
||||
真实 buffer 本来就是逻辑文本。复制过滤仍有必要,因为
|
||||
`buffer-substring` 按设计保留文本属性。EKP 先组合原有
|
||||
`filter-buffer-substring-function`,再只移除自己在复制字符串中的投影
|
||||
元数据。
|
||||
|
||||
#### 实时流式排版
|
||||
|
||||
实时编辑直接消费正常的整段 Knuth-Plass plan,不会把编辑状态交给
|
||||
核心规划器:
|
||||
|
||||
1. live state 按最窄窗口权威宽度持有最后一次提交的硬行源文本、正常
|
||||
`ekp-layout-plan`、语义签名、投影 span 与稳定断行锚点。
|
||||
2. 第一次真实变更打开一个编辑事务。事务先快照提交态,再只移除包含该
|
||||
编辑的最小投影 span 范围中的 EKP 属性;span 对象与无关锚点仍保持
|
||||
注册。
|
||||
3. 脏岛内的后续变更交给原生软折行,不规划整条硬行。若逻辑源文本精确
|
||||
回到快照,EKP 直接恢复保存的自有属性区间与 marker 偏移;state、plan、
|
||||
signatures 和 spans 都保持对象身份。
|
||||
4. 跨过原生视觉行边界才提交。EKP 计算或复用一次整条硬行 plan,得到
|
||||
全部已完成语义行,并在同一个 command-loop 转换中静默发布真正变化的
|
||||
后缀;新的当前行保持自然。
|
||||
5. 其他提交事件只有硬换行/段落完成、下一次真实编辑发生在脏岛之外、
|
||||
显式 refill,以及宽度/字体/face/主题或布局上下文变化。point 移动永远
|
||||
不是提交,即使跨越硬段落也一样;live 路径没有 `post-command-hook`。
|
||||
6. 稳定行签名只负责在提交时减少属性写入,buffer-local 16 项 LRU 只负责
|
||||
复用近期文本/上下文 plan。何时允许布局变化由事务控制,不是二者控制。
|
||||
7. IME preedit、外部显示所有权、不支持的收缩、超长硬行、过期
|
||||
generation 或发布错误都会 fail closed 到原生显示。半成品投影会
|
||||
回滚,原始错误继续向外报告。
|
||||
|
||||
原生软折行是状态机的前提,不能只希望用户碰巧开启它。mode 启用时先
|
||||
保存 `truncate-lines` 和 `truncate-partial-width-windows` 的值与
|
||||
buffer-local 所有权,再把二者设为 buffer-local `nil`;teardown 时恢复
|
||||
原局部值,或移除临时局部绑定让全局值重新接管。这样 Emacs 默认的
|
||||
50 列分栏阈值就不会悄悄把窄分栏变成横向滚动。
|
||||
|
||||
这个状态模型不需要 live lookahead、push/pull 收敛、逐键整行规划器或
|
||||
idle formatter。point 移动本身严格零副作用;之后若在别处发生真实编辑,
|
||||
则允许提交先前活动硬行,即使 narrowing 让该行已不在可访问区内。
|
||||
|
||||
不存在编辑空闲后整段 formatter。resize/后台工作都带 generation。
|
||||
大 buffer 按可见优先的硬段落 chunk 处理;单个硬段落超过
|
||||
`ekp-auto-justify-paragraph-limit` 时,自动路径保持自然显示,只有显式
|
||||
`ekp-refill-paragraph` 才执行无界完整质量 pass。文本属性属于 buffer,
|
||||
所以以最窄活动窗口作为唯一权威宽度。
|
||||
|
||||
### 5.1 断行许可、对齐、悬挂、段形
|
||||
|
||||
@ -346,11 +406,11 @@ ekp.el 核心:para 结构、缓存、DP(1D + looseness)、
|
||||
ekp-utils.el 分词器(盒子、避头尾)、带 batch/tty 回退的字体
|
||||
检测、C 模块加载
|
||||
ekp-hyphen.el Liang 断词 + 词典注册
|
||||
ekp-region.el buffer/region 命令、ekp-auto-justify-mode,以及
|
||||
编辑器集成(保存、isearch、kill-ring、undo)
|
||||
ekp-buffer.el 纯文本属性 buffer/region 投影、同步实时流动、窗口
|
||||
lifecycle、复制过滤与诊断
|
||||
ekp_c/ C 动态模块(见 ekp_c/README.md)
|
||||
dictionaries/ Hunspell 断词模式(来自 LibreOffice)
|
||||
tests/ ekp-tests.el、ekp-region-tests.el(ERT)、
|
||||
tests/ ekp-tests.el、ekp-buffer-tests.el(ERT)、
|
||||
ekp-fuzz.el(一致性 fuzz)、ekp-bench.el、
|
||||
ekp-demo.el、ekp-showcase.el、示例文本、run-tests.sh
|
||||
```
|
||||
|
||||
@ -44,6 +44,9 @@
|
||||
- `task015` 已完成终局清理与门禁:删除九个死 accessor 和依赖私有
|
||||
substring-filter API 的调用;最终独立代码审查 `APPROVE`、架构审查
|
||||
`CLEAR`,全部仓库内 issue/task 均已闭环。
|
||||
- `task017` 已将当前编辑器集成模块直接改名为 `ekp-buffer.el`;
|
||||
下文的 `ekp-region.el` 与 `ekp-region-*` 仍保留为审计基线的历史
|
||||
证据,不代表当前加载入口。
|
||||
- 下文保留 `main@29cef97` 的原始证据与判断,不把历史问题改写成从未发生。
|
||||
|
||||
## 结论先行
|
||||
|
||||
2011
ekp-buffer.el
Normal file
2011
ekp-buffer.el
Normal file
File diff suppressed because it is too large
Load Diff
1079
ekp-region.el
1079
ekp-region.el
File diff suppressed because it is too large
Load Diff
331
ekp.el
331
ekp.el
@ -158,8 +158,8 @@ A line ending in punctuation lets part of that glyph hang past the
|
||||
flush edge, per `ekp-protrusion-ratios' — CLREQ line-end punctuation
|
||||
squeeze and microtype-style hanging periods/hyphens in one mechanism.
|
||||
Left-edge protrusion is not implemented: Emacs cannot render text
|
||||
before the line origin. When enabled, reserve the protrusion width
|
||||
in the layout (see `ekp-region-protrusion-reserve')."
|
||||
before the line origin. Buffer integration reserves the protrusion
|
||||
width in its layout when enabled."
|
||||
:type 'boolean
|
||||
:group 'ekp)
|
||||
|
||||
@ -241,10 +241,27 @@ when non-zero the C module is bypassed automatically."
|
||||
;; Lazily memoized (START . END) offsets of each box in the source
|
||||
;; string (render-time lossless payloads); content-invariant.
|
||||
(box-offsets-memo nil)
|
||||
;; Natural pixel width of each source gap (indexed by right box).
|
||||
;; Width-independent projection geometry is measured once per paragraph.
|
||||
(gap-naturals-memo nil)
|
||||
;; Glue params snapshot at para creation time (plist)
|
||||
glue-params
|
||||
(dp-cache nil :type hash-table))
|
||||
|
||||
(cl-defstruct (ekp-layout-gap (:constructor ekp-layout-gap--create))
|
||||
"One planned glue between two source boxes."
|
||||
kind left-box right-box source-start source-end natural-pixel target-pixel)
|
||||
|
||||
(cl-defstruct (ekp-layout-line (:constructor ekp-layout-line--create))
|
||||
"One display line in an `ekp-layout-plan'."
|
||||
index box-start box-end source-start source-end glues gaps
|
||||
leading-pixel trailing-pixel hyphen-p break-kind
|
||||
break-source-start break-source-end signature)
|
||||
|
||||
(cl-defstruct (ekp-layout-plan (:constructor ekp-layout-plan--create))
|
||||
"Semantic KP layout independent of any output representation."
|
||||
string line-pixel boxes offsets lines)
|
||||
|
||||
(defvar ekp--para-cache nil
|
||||
"Cache: equal-keyed table, content key → ekp-para struct.")
|
||||
|
||||
@ -606,14 +623,6 @@ Positions right after HYPHEN-POSITIONS are forced to `nws'."
|
||||
((eq 'mws type) (plist-get params :mws-ideal))
|
||||
((eq 'cws type) (plist-get params :cws-ideal)))))
|
||||
|
||||
(defun ekp--para-glue-shrink (para type)
|
||||
"Get shrink amount for TYPE using PARA's stored glue params."
|
||||
(let ((params (ekp-para-glue-params para)))
|
||||
(cond ((or (null type) (eq 'nws type)) 0)
|
||||
((eq 'lws type) (plist-get params :lws-shrink))
|
||||
((eq 'mws type) (plist-get params :mws-shrink))
|
||||
((eq 'cws type) (plist-get params :cws-shrink)))))
|
||||
|
||||
(defun ekp--para-glue-stretch (para type)
|
||||
"Get stretch amount for TYPE using PARA's stored glue params."
|
||||
(let ((params (ekp-para-glue-params para)))
|
||||
@ -1650,18 +1659,19 @@ HYPHEN-COUNT)."
|
||||
0
|
||||
(ekp--ragged-extra-stretch)))))
|
||||
|
||||
(defun ekp--dp-cache-para (para line-pixel)
|
||||
"Return PARA's DP result at LINE-PIXEL, computing it when absent."
|
||||
(or (ekp--dp-get-cached para line-pixel)
|
||||
(if (ekp--c-available-p)
|
||||
(ekp--dp-cache-via-c para line-pixel)
|
||||
(ekp--dp-cache-elisp para line-pixel))))
|
||||
|
||||
(defun ekp-dp-cache (string line-pixel)
|
||||
"Compute optimal line breaks for STRING at LINE-PIXEL width.
|
||||
Uses Knuth-Plass dynamic programming with demerits.
|
||||
If `ekp-use-c-module' is non-nil and the C module is available (and
|
||||
`ekp-looseness' is 0), the C module computes the DP."
|
||||
(let* ((para (ekp--get-para string))
|
||||
(cached (ekp--dp-get-cached para line-pixel)))
|
||||
(cond
|
||||
(cached cached)
|
||||
((ekp--c-available-p)
|
||||
(ekp--dp-cache-via-c para line-pixel))
|
||||
(t (ekp--dp-cache-elisp para line-pixel)))))
|
||||
(ekp--dp-cache-para (ekp--get-para string) line-pixel))
|
||||
|
||||
(defun ekp--lines-data-from-breaks (para line-pixel breaks)
|
||||
"Compute (RESTS . GAPS) lists for BREAKS of PARA at LINE-PIXEL.
|
||||
@ -1912,18 +1922,15 @@ deficit) spread across GAPS-LIST using PARA's stored glue params."
|
||||
para glues-types distribution stretch-p)))
|
||||
(append '(0) glue-pixels '(0)))))
|
||||
|
||||
(defun ekp-line-glues (string line-pixel)
|
||||
"Compute glue pixels for each line after breaking STRING at LINE-PIXEL.
|
||||
Returns vector of vectors, each inner vector is glue pixels for one line.
|
||||
Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
||||
(let* ((para (ekp--get-para string))
|
||||
(boxes-num (length (ekp-para-boxes para)))
|
||||
(defun ekp--line-glues-from-data (para line-pixel dp)
|
||||
"Compute glue vectors from prepared PARA at LINE-PIXEL using DP.
|
||||
Each line's glues are [0 glue1 glue2 ... trailing-space]."
|
||||
(let* ((boxes-num (length (ekp-para-boxes para)))
|
||||
(glues-types (ekp-para-glues-types para))
|
||||
(alignment (or (plist-get (ekp-para-glue-params para) :alignment)
|
||||
'justify))
|
||||
(ragged (not (eq alignment 'justify)))
|
||||
(hyphen-positions (ekp-para-hyphen-positions para))
|
||||
(dp (ekp-dp-data string line-pixel))
|
||||
(breaks (plist-get dp :breaks))
|
||||
(lines-rests (plist-get dp :rests))
|
||||
(lines-gaps (plist-get dp :gaps))
|
||||
@ -2011,6 +2018,13 @@ Each line's glues: [0 glue1 glue2 ... trailing-space]."
|
||||
(setq start end)))
|
||||
line-glues))
|
||||
|
||||
(defun ekp-line-glues (string line-pixel)
|
||||
"Compute glue pixels for each line after breaking STRING at LINE-PIXEL.
|
||||
Returns a vector of per-line glue vectors."
|
||||
(let ((para (ekp--get-para string)))
|
||||
(ekp--line-glues-from-data
|
||||
para line-pixel (ekp--dp-cache-para para line-pixel))))
|
||||
|
||||
;;;; Rendering
|
||||
|
||||
(defun ekp--box-space-p (box)
|
||||
@ -2068,6 +2082,144 @@ leftmost scan aligns them unambiguously."
|
||||
(setq i (1+ i))))
|
||||
offsets))
|
||||
|
||||
(defun ekp--gap-natural-pixels (para offsets)
|
||||
"Return memoized natural gap widths for PARA at OFFSETS."
|
||||
(or (ekp-para-gap-naturals-memo para)
|
||||
(let* ((string (ekp-para-string para))
|
||||
(boxes (ekp-para-boxes para))
|
||||
(naturals (make-vector (length boxes) 0)))
|
||||
(cl-loop
|
||||
for right from 1 below (length boxes)
|
||||
for left = (1- right)
|
||||
for start = (cdr (aref offsets left))
|
||||
for end = (car (aref offsets right))
|
||||
for source = (if (< start end)
|
||||
(substring string start end)
|
||||
(car (last (string-glyph-split
|
||||
(aref boxes left)))))
|
||||
do (aset naturals right (ekp--measured-width source)))
|
||||
(setf (ekp-para-gap-naturals-memo para) naturals))))
|
||||
|
||||
(defun ekp--layout-line-gaps
|
||||
(para offsets naturals box-start box-end glues)
|
||||
"Build semantic gaps for one line of PARA.
|
||||
OFFSETS maps source boxes and NATURALS their measured gap widths.
|
||||
BOX-START and BOX-END delimit the kept boxes; GLUES contains the
|
||||
corresponding leading/interior/trailing pixel widths."
|
||||
(let ((types (ekp-para-glues-types para))
|
||||
(glue-index 1)
|
||||
gaps)
|
||||
(cl-loop for right from (1+ box-start) below box-end do
|
||||
(let* ((left (1- right))
|
||||
(source-start (cdr (aref offsets left)))
|
||||
(source-end (car (aref offsets right)))
|
||||
(natural (aref naturals right))
|
||||
(target (aref glues glue-index)))
|
||||
(when (or (< source-start source-end)
|
||||
(> target 0))
|
||||
(push (ekp-layout-gap--create
|
||||
:kind (aref types right)
|
||||
:left-box left
|
||||
:right-box right
|
||||
:source-start source-start
|
||||
:source-end source-end
|
||||
:natural-pixel natural
|
||||
:target-pixel target)
|
||||
gaps))
|
||||
(setq glue-index (1+ glue-index))))
|
||||
(vconcat (nreverse gaps))))
|
||||
|
||||
(defun ekp--layout-line-signature (line)
|
||||
"Return the stable layout signature for LINE."
|
||||
(list (ekp-layout-line-source-start line)
|
||||
(ekp-layout-line-source-end line)
|
||||
(ekp-layout-line-break-kind line)
|
||||
(append (ekp-layout-line-glues line) nil)))
|
||||
|
||||
(defun ekp--layout-break-kind (string line next-line)
|
||||
"Classify the visual break from LINE to NEXT-LINE in STRING."
|
||||
(cond
|
||||
((ekp-layout-line-hyphen-p line) 'hyphen)
|
||||
((string-blank-p
|
||||
(substring string
|
||||
(ekp-layout-line-source-end line)
|
||||
(ekp-layout-line-source-start next-line)))
|
||||
'space)
|
||||
(t 'cjk)))
|
||||
|
||||
(defun ekp--finalize-layout-breaks (string lines)
|
||||
"Add source break ranges, kinds, and signatures to LINES for STRING."
|
||||
(dotimes (i (length lines))
|
||||
(let* ((line (aref lines i))
|
||||
(next (and (< i (1- (length lines))) (aref lines (1+ i))))
|
||||
(start (ekp-layout-line-source-end line))
|
||||
(end (if next (ekp-layout-line-source-start next) start)))
|
||||
(setf (ekp-layout-line-break-source-start line) start
|
||||
(ekp-layout-line-break-source-end line) end
|
||||
(ekp-layout-line-break-kind line)
|
||||
(and next (ekp--layout-break-kind string line next))
|
||||
(ekp-layout-line-signature line)
|
||||
(ekp--layout-line-signature line))))
|
||||
lines)
|
||||
|
||||
(defun ekp--make-layout-line
|
||||
(para offsets naturals index start end line-glues last-line-p)
|
||||
"Build one semantic layout line from PARA's DP slice START through END.
|
||||
OFFSETS maps boxes to source positions and NATURALS stores gap widths.
|
||||
INDEX is the line number, and LINE-GLUES holds its pixel widths.
|
||||
LAST-LINE-P suppresses a terminal discretionary hyphen."
|
||||
(let* ((boxes (ekp-para-boxes para))
|
||||
(stripped (ekp--strip-line-spaces
|
||||
(cl-subseq boxes start end) line-glues
|
||||
(> index 0) t))
|
||||
(kept (nth 0 stripped))
|
||||
(glues (vconcat (nth 1 stripped)))
|
||||
(box-start (+ start (nth 2 stripped)))
|
||||
(box-end (+ box-start (length kept)))
|
||||
(hyphen-p (and (not last-line-p)
|
||||
(ekp--hyphenate-p
|
||||
(ekp-para-hyphen-positions para) (1- end)))))
|
||||
(when kept
|
||||
(ekp-layout-line--create
|
||||
:index index :box-start box-start :box-end box-end
|
||||
:source-start (car (aref offsets box-start))
|
||||
:source-end (cdr (aref offsets (1- box-end)))
|
||||
:glues glues
|
||||
:gaps (ekp--layout-line-gaps
|
||||
para offsets naturals box-start box-end glues)
|
||||
:leading-pixel (aref glues 0)
|
||||
:trailing-pixel (aref glues (1- (length glues)))
|
||||
:hyphen-p hyphen-p))))
|
||||
|
||||
(defun ekp-layout-plan (string line-pixel)
|
||||
"Return a semantic KP layout plan for STRING at LINE-PIXEL.
|
||||
The plan records source offsets, glue targets, breaks, indentation,
|
||||
and discretionary hyphens without choosing a string or buffer display
|
||||
representation."
|
||||
(let* ((para (ekp--get-para string))
|
||||
(boxes (ekp-para-boxes para))
|
||||
(offsets (or (ekp-para-box-offsets-memo para)
|
||||
(setf (ekp-para-box-offsets-memo para)
|
||||
(ekp--box-offsets string (append boxes nil)))))
|
||||
(naturals (ekp--gap-natural-pixels para offsets))
|
||||
(dp (ekp--dp-cache-para para line-pixel))
|
||||
(breaks (plist-get dp :breaks))
|
||||
(line-glues (ekp--line-glues-from-data para line-pixel dp))
|
||||
(start 0)
|
||||
lines)
|
||||
(dotimes (i (length breaks))
|
||||
(let* ((end (nth i breaks))
|
||||
(line (ekp--make-layout-line
|
||||
para offsets naturals i start end (aref line-glues i)
|
||||
(= i (1- (length breaks))))))
|
||||
(when line (push line lines))
|
||||
(setq start end)))
|
||||
(setq lines (ekp--finalize-layout-breaks
|
||||
string (vconcat (nreverse lines))))
|
||||
(ekp-layout-plan--create
|
||||
:string string :line-pixel line-pixel
|
||||
:boxes boxes :offsets offsets :lines lines)))
|
||||
|
||||
(defconst ekp--layout-marker-properties
|
||||
'(ekp-glue ekp-soft-break ekp-soft-hyphen ekp-hidden ekp-justified)
|
||||
"Text properties owned by the lossless render/inversion protocol.")
|
||||
@ -2121,6 +2273,57 @@ The `ekp-soft-hyphen' property marks it as synthesized, so
|
||||
(text-properties-at (1- (length box)) box))))
|
||||
(apply #'propertize "-" 'ekp-soft-hyphen t props)))
|
||||
|
||||
(defun ekp--render-layout-line-string (plan line)
|
||||
"Render LINE from PLAN using the reversible string marker protocol."
|
||||
(let* ((string (ekp-layout-plan-string plan))
|
||||
(boxes (ekp-layout-plan-boxes plan))
|
||||
(offsets (ekp-layout-plan-offsets plan))
|
||||
(start (ekp-layout-line-box-start line))
|
||||
(end (ekp-layout-line-box-end line))
|
||||
(glues (ekp-layout-line-glues line))
|
||||
parts)
|
||||
(cl-loop for box-index from start below end
|
||||
for glue-index from 0 do
|
||||
(push (ekp--render-glue
|
||||
(aref glues glue-index)
|
||||
(if (= box-index start) ""
|
||||
(substring string
|
||||
(cdr (aref offsets (1- box-index)))
|
||||
(car (aref offsets box-index)))))
|
||||
parts)
|
||||
(push (aref boxes box-index) parts))
|
||||
(when (ekp-layout-line-hyphen-p line)
|
||||
(push (ekp--hyphen-for-box (aref boxes (1- end))) parts))
|
||||
(push (ekp--render-glue (aref glues (1- (length glues))) "") parts)
|
||||
(apply #'concat (nreverse parts))))
|
||||
|
||||
(defun ekp-render-layout-string (plan)
|
||||
"Render PLAN as the public reversible justified string."
|
||||
(let* ((string (ekp-layout-plan-string plan))
|
||||
(lines (ekp-layout-plan-lines plan)))
|
||||
(if (= (length lines) 0)
|
||||
(ekp--hide-string string)
|
||||
(let ((parts (list (ekp--hide-string
|
||||
(substring string 0
|
||||
(ekp-layout-line-source-start
|
||||
(aref lines 0)))))))
|
||||
(dotimes (i (length lines))
|
||||
(let ((line (aref lines i)))
|
||||
(when (> i 0)
|
||||
(let ((prev (aref lines (1- i))))
|
||||
(push (propertize
|
||||
"\n" 'ekp-soft-break
|
||||
(substring string
|
||||
(ekp-layout-line-source-end prev)
|
||||
(ekp-layout-line-source-start line)))
|
||||
parts)))
|
||||
(push (ekp--render-layout-line-string plan line) parts)))
|
||||
(let ((last (aref lines (1- (length lines)))))
|
||||
(push (ekp--hide-string
|
||||
(substring string (ekp-layout-line-source-end last)))
|
||||
parts))
|
||||
(apply #'concat (nreverse parts))))))
|
||||
|
||||
(defun ekp--pixel-justify (string line-pixel)
|
||||
"Justify single-paragraph STRING to LINE-PIXEL, with render caching.
|
||||
The rendered string for a (paragraph, width) pair is deterministic,
|
||||
@ -2150,85 +2353,7 @@ The output is lossless with respect to STRING:
|
||||
survives as zero-display `ekp-hidden' text,
|
||||
- break hyphens carry `ekp-soft-hyphen'.
|
||||
`ekp-unjustify-region' inverts all four structurally."
|
||||
(let* ((para (ekp--get-para string))
|
||||
(boxes (append (ekp-para-boxes para) nil))
|
||||
(offsets (or (ekp-para-box-offsets-memo para)
|
||||
(setf (ekp-para-box-offsets-memo para)
|
||||
(ekp--box-offsets string boxes))))
|
||||
(breaks (ekp-line-breaks string line-pixel))
|
||||
(num (length breaks))
|
||||
(lines-glues (ekp-line-glues string line-pixel))
|
||||
(hyphen-positions (ekp-para-hyphen-positions para))
|
||||
(start 0)
|
||||
;; (rendered-text first-box-idx last-box-idx) per visible line
|
||||
(lines nil))
|
||||
(dotimes (i num)
|
||||
(let* ((end (nth i breaks))
|
||||
(line-boxes (cl-subseq boxes start end))
|
||||
(glue-pixels (append (aref lines-glues i) nil))
|
||||
;; Strip space boxes:
|
||||
;; - First line (i=0): keep leading spaces (indentation)
|
||||
;; - Other lines: strip leading spaces (break artifacts)
|
||||
;; - All lines: strip trailing spaces
|
||||
(is-first-line (= i 0))
|
||||
(stripped (ekp--strip-line-spaces line-boxes glue-pixels
|
||||
(not is-first-line)
|
||||
t))
|
||||
(kept (nth 0 stripped))
|
||||
(kept-glues (nth 1 stripped))
|
||||
(first-idx (+ start (nth 2 stripped)))
|
||||
;; Check if last box of this line needs hyphen
|
||||
(need-hyphen
|
||||
(and (< i (1- num)) ; not last line
|
||||
(ekp--hyphenate-p hyphen-positions (1- end)))))
|
||||
(when kept
|
||||
(let ((parts nil) (idx first-idx) (glues kept-glues) (n 0))
|
||||
(dolist (box kept)
|
||||
(push (ekp--render-glue
|
||||
(pop glues)
|
||||
(if (> idx first-idx)
|
||||
(substring string
|
||||
(cdr (aref offsets (1- idx)))
|
||||
(car (aref offsets idx)))
|
||||
;; leading glue of a line is always 0px and
|
||||
;; replaces nothing; edge text is handled by
|
||||
;; soft breaks / hidden runs below
|
||||
""))
|
||||
parts)
|
||||
(push box parts)
|
||||
(setq idx (1+ idx) n (1+ n)))
|
||||
(when need-hyphen
|
||||
(push (ekp--hyphen-for-box (car (last kept))) parts))
|
||||
;; trailing filler glue (synthesized, replaces nothing)
|
||||
(push (ekp--render-glue (car glues) "") parts)
|
||||
(push (list (apply #'concat (nreverse parts))
|
||||
first-idx (+ first-idx n -1))
|
||||
lines)))
|
||||
(setq start end)))
|
||||
(setq lines (nreverse lines))
|
||||
(if (null lines)
|
||||
;; Defensive: no visible box at all (blank paragraphs are
|
||||
;; filtered before this function).
|
||||
(ekp--hide-string string)
|
||||
(let* ((first-line (car lines))
|
||||
(last-line (car (last lines)))
|
||||
(parts (list (ekp--hide-string
|
||||
(substring string 0
|
||||
(car (aref offsets (nth 1 first-line)))))))
|
||||
(prev nil))
|
||||
(dolist (line lines)
|
||||
(when prev
|
||||
(push (propertize "\n" 'ekp-soft-break
|
||||
(substring string
|
||||
(cdr (aref offsets (nth 2 prev)))
|
||||
(car (aref offsets (nth 1 line)))))
|
||||
parts))
|
||||
(push (nth 0 line) parts)
|
||||
(setq prev line))
|
||||
(push (ekp--hide-string
|
||||
(substring string (cdr (aref offsets (nth 2 last-line)))))
|
||||
parts)
|
||||
(apply #'concat (nreverse parts))))))
|
||||
(ekp-render-layout-string (ekp-layout-plan string line-pixel)))
|
||||
|
||||
(defun ekp--validate-width (line-pixel)
|
||||
"Signal a user error unless LINE-PIXEL is a positive integer."
|
||||
|
||||
101
postmortem/20260728-buffer-module-naming.md
Normal file
101
postmortem/20260728-buffer-module-naming.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Name Editor Integration After Its Buffer Owner
|
||||
|
||||
## Context
|
||||
|
||||
`ekp-region.el` began as an in-place region formatter, but its stable
|
||||
responsibility grew to the whole buffer lifecycle: whole-buffer commands,
|
||||
window-width tracking, incremental reflow, save serialization, isearch,
|
||||
kill/yank filtering, modified-state preservation, mode presets, and teardown.
|
||||
The old module name described one public operation scope rather than the
|
||||
state and external protocols the module owns.
|
||||
|
||||
That mismatch also spread into the feature name, customization group,
|
||||
configuration variables, private implementation prefix, test file, CI, and
|
||||
documentation. A reader looking for buffer integration had to know the
|
||||
historical `region` name first.
|
||||
|
||||
## Decision
|
||||
|
||||
Rename the module directly to `ekp-buffer.el` and provide `ekp-buffer`.
|
||||
Rename the customization group, module-owned configuration surface, private
|
||||
implementation namespace, test file, and test namespace to `ekp-buffer*`.
|
||||
Attach the subgroup to the top-level `ekp` customization group.
|
||||
|
||||
Keep public operation names whose final component describes the real
|
||||
operand or workflow:
|
||||
|
||||
- `ekp-justify-region` and `ekp-unjustify-region`
|
||||
- `ekp-justify-buffer` and `ekp-unjustify-buffer`
|
||||
- the no-break/verbatim region commands
|
||||
- `ekp-auto-justify-mode`, setup commands, refill, and diagnostics
|
||||
|
||||
The unexported protrusion-reserve calculation now uses the conventional
|
||||
double-hyphen private name `ekp-buffer--protrusion-reserve`.
|
||||
|
||||
## Compatibility Decision
|
||||
|
||||
This is an explicit breaking change in the Unreleased line. Remove the old
|
||||
file, feature, variables, and internal names instead of keeping aliases or a
|
||||
loader shim. The migration is recorded in `CHANGELOG.md`:
|
||||
|
||||
```elisp
|
||||
(require 'ekp-buffer)
|
||||
```
|
||||
|
||||
Configurations that set the module-owned `ekp-region-*` variables must use
|
||||
their corresponding `ekp-buffer-*` names.
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Keep `ekp-region`
|
||||
|
||||
This preserves source compatibility but keeps the wrong owner in every file
|
||||
and symbol lookup. The mismatch is concrete, not hypothetical: most of the
|
||||
module handles buffer-wide state or editor protocols.
|
||||
|
||||
### Rename to `ekp-mode`
|
||||
|
||||
The minor mode is only one entry point. Manual region and whole-buffer
|
||||
commands install the same integration lifecycle without enabling the mode.
|
||||
|
||||
### Split Region Commands from Buffer Integration
|
||||
|
||||
The commands, reversible markers, serialization, search, copy filtering, and
|
||||
mode state share one lifecycle. Splitting them would add declarations and
|
||||
cross-file glue without creating independent owners.
|
||||
|
||||
### Keep a Compatibility Loader or Aliases
|
||||
|
||||
Two feature names and parallel variable surfaces would preserve the naming
|
||||
debt and create an indefinite compatibility boundary. The requested
|
||||
breaking migration has one direct replacement and no persisted-data format
|
||||
change.
|
||||
|
||||
## Consequences
|
||||
|
||||
- File and feature lookup now identify the actual buffer integration owner.
|
||||
- Public commands remain semantically precise instead of receiving a
|
||||
redundant `buffer` prefix.
|
||||
- Customization appears under the main EKP group.
|
||||
- Test names, CI commands, and documentation use the same vocabulary.
|
||||
- Existing configurations must update their `require` and any
|
||||
module-owned settings; layout behavior and saved bytes do not change.
|
||||
|
||||
## Verification
|
||||
|
||||
The pre-change behavior lock passed 130/130 ERT. After the rename, focused
|
||||
buffer ERT passed 44/44; default and seeded-permuted full ERT passed 130/130;
|
||||
all 130 selected ERT tests passed one per fresh Emacs process; and C/Elisp
|
||||
fuzz passed 300/300.
|
||||
|
||||
Warnings-as-errors byte compilation, checkdoc, pinned package-lint, release
|
||||
invariants, shell syntax, diff checks, and active-surface stale-name scans
|
||||
passed. A mechanical equivalence check compared the new runtime and test
|
||||
files with the old files transformed by the approved rename table; the only
|
||||
additional runtime differences were the EKP customization parent and the
|
||||
private protrusion helper name.
|
||||
|
||||
## Rollback
|
||||
|
||||
Reverse the direct file/feature/symbol mapping and the documentation/CI
|
||||
references together. No data migration or C module rebuild is required.
|
||||
60
postmortem/20260729-active-line-edge-state.md
Normal file
60
postmortem/20260729-active-line-edge-state.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Keep Static Edge Policy out of Live Editor State
|
||||
|
||||
## Context
|
||||
|
||||
The source-clean renderer had two interaction regressions after its initial
|
||||
quality gate.
|
||||
|
||||
First, every reprojection copied the old mark position and restored it with
|
||||
`set-mark`. Emacs treats mark position and `mark-active` as separate state,
|
||||
but `set-mark` changes both. A showcase width key therefore turned an old
|
||||
inactive mark into a highlighted region.
|
||||
|
||||
Second, the live renderer correctly skipped glue justification on the
|
||||
point-containing line, then reused static paragraph-edge cleanup. The KP
|
||||
plan excludes leading/trailing whitespace from its content bounds, so that
|
||||
cleanup assigned `display ""` to a newly typed edge space. The source
|
||||
character existed immediately, but the user saw it only after typing the
|
||||
next glyph.
|
||||
|
||||
## Decision
|
||||
|
||||
Editor state is restored by its actual owners:
|
||||
|
||||
- set the existing mark marker's position directly, then restore
|
||||
`mark-active` independently;
|
||||
- retain static edge hiding for committed lines;
|
||||
- never hide source-edge whitespace on the active live line;
|
||||
- when the first planned line becomes active, clear projection from the
|
||||
span's source start so previously hidden leading whitespace becomes
|
||||
natural too.
|
||||
|
||||
No showcase-specific selection workaround and no space self-insert advice
|
||||
is added. Both symptoms are fixed in `ekp-buffer.el`, where reprojection and
|
||||
live-line presentation are owned.
|
||||
|
||||
## Why the Initial Tests Missed It
|
||||
|
||||
The live typing test inserted `" x"` as one operation. By assertion time,
|
||||
the space was interior and no longer eligible for edge cleanup. The GUI
|
||||
matrix also tested an intentionally active region, but not an existing
|
||||
inactive mark.
|
||||
|
||||
The replacement gates stop at the temporal boundary:
|
||||
|
||||
- inspect state immediately after one space, before another glyph;
|
||||
- cover leading/trailing space, tab, CJK, deletion-exposed whitespace,
|
||||
consecutive spaces, newline, yank, and undo;
|
||||
- test inactive and active mark states separately;
|
||||
- record the real showcase width command and command-loop space insertion
|
||||
in a clean full-screen GUI run.
|
||||
|
||||
## Consequences
|
||||
|
||||
One space now moves point/cursor immediately and carries no EKP replacing
|
||||
display. Backspace cannot re-hide the exposed whitespace. Width changes
|
||||
preserve point, mark position, and region activation exactly.
|
||||
|
||||
Committed static paragraphs still hide stripped edge whitespace, so this
|
||||
correction does not change completed KP layout or the source-clean
|
||||
representation contract.
|
||||
104
postmortem/20260729-editing-frontier-not-point.md
Normal file
104
postmortem/20260729-editing-frontier-not-point.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Let Source Edits Own the Live Editing Frontier
|
||||
|
||||
> Superseded note (2026-07-29): point-only motion remains a zero-work
|
||||
> invariant. `20260729-stable-live-transaction.md` replaces the frontier's
|
||||
> natural-suffix and immediate-publication ownership with a saved committed
|
||||
> projection and a local dirty edit transaction.
|
||||
|
||||
## Context
|
||||
|
||||
`task028` fixed the planning boundary: all projected rows in one hard line
|
||||
now come from one complete `ekp-layout-plan`. It nevertheless used current
|
||||
point to choose the natural suffix whenever `post-command-hook` ran.
|
||||
|
||||
That conflated two independent facts:
|
||||
|
||||
- point is transient navigation state;
|
||||
- the editing frontier records which semantic line is currently unfinished.
|
||||
|
||||
As a result, cursor motion alone removed and reinstalled projection
|
||||
properties. It did no DP work, but it still changed the visual document.
|
||||
Worse, a later width or font reflow could read the cursor's temporary
|
||||
position and silently move the natural suffix boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
The latest real source edit owns the editing frontier.
|
||||
|
||||
1. A live hard line keeps a source marker for the changed region's new end.
|
||||
2. Before a real edit, the existing prefix line containing the edit is
|
||||
returned to native display together with its suffix.
|
||||
3. After the edit, the frontier marker moves to the changed region's new
|
||||
end and the whole-hard-line plan is recomputed or reused once.
|
||||
4. The active semantic index is derived from that marker, never from
|
||||
transient point.
|
||||
5. Point-only motion within the active hard line does nothing.
|
||||
6. Width, font, and layout-context changes preserve the marker and map it
|
||||
into the new plan.
|
||||
7. Leaving the hard line still completes the old hard line with the
|
||||
existing static KP path.
|
||||
|
||||
The core DP remains unaware of point, markers, buffers, and editing state.
|
||||
|
||||
## Why This Is Simpler
|
||||
|
||||
`active-index` is derived plan state; the frontier marker is the one stable
|
||||
source fact. There is no point-motion state machine and no attempt to infer
|
||||
editing intent from navigation. One owner event—source mutation—moves the
|
||||
frontier.
|
||||
|
||||
The change deletes the point-boundary publication path instead of adding
|
||||
another condition to it.
|
||||
|
||||
## Superseded Decision
|
||||
|
||||
This record supersedes only the point-driven boundary statements in
|
||||
`20260729-whole-hard-line-live-prefix.md`. That record's main decisions
|
||||
remain valid:
|
||||
|
||||
- one complete hard line is the planning unit;
|
||||
- all projected prefix lines come from the same core plan;
|
||||
- the frontier line and suffix stay natural;
|
||||
- prior breaks may change together after later source edits.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Moving point cannot cause visual reflow or text-property writes.
|
||||
- Editing an earlier projected line still makes that actual edit line and
|
||||
its suffix natural before mutation, then republishes from the updated
|
||||
whole-hard-line plan.
|
||||
- Resizing can change breaks and glue, but not because point happened to be
|
||||
elsewhere inside the hard line.
|
||||
- The frontier remains display-layer state. Core DP, the C ABI, and the
|
||||
source-clean representation are unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
The regression gate compares the complete owned-property projection,
|
||||
frontier marker, active index, signatures, spans, source characters,
|
||||
modified tick, undo state, plan calls, cache calls, and property writes
|
||||
before and after backward/forward point motion. It failed before the change
|
||||
and now passes; reflow and deferred IME completion also preserve the
|
||||
frontier instead of reading transient point.
|
||||
|
||||
Default, seed-`20260729`, and isolated full ERT pass 182/182; buffer ERT
|
||||
passes 93/93 and C/Elisp fuzz passes 300/300. Point-motion p99 is
|
||||
0.033 ms on the C backend with zero plan/cache calls.
|
||||
|
||||
Reviewed dynamic GUI evidence at
|
||||
`/private/tmp/ekp-frontier-live-v3-66WYRW` contains a 26.6-second screen
|
||||
recording and 39 manifest lines. Backward/forward commands preserve the
|
||||
projection hash, plan, generation, cache size, active index, and frontier
|
||||
through every required checkpoint. Delete, yank, real undo, resize,
|
||||
restore, and hard-newline transitions also pass; source text remains exact,
|
||||
overlays and horizontal scroll remain zero, and the final report is PASS.
|
||||
|
||||
Independent architecture review is CLEAR. Independent code review finds
|
||||
zero task029 blockers; the separate unique-append performance debt is
|
||||
recorded as `issue018`/`task030`.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert this editing-frontier change as one unit if a public editing or GUI
|
||||
gate fails. Do not restore point-driven projection changes as a fallback;
|
||||
navigation is not a layout invalidation event.
|
||||
57
postmortem/20260729-narrow-live-append-replanning.md
Normal file
57
postmortem/20260729-narrow-live-append-replanning.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Keep Narrow Append Optimization Separate from Frontier Correctness
|
||||
|
||||
## Context
|
||||
|
||||
Task029 removes point from live-layout ownership. Its point-motion workload
|
||||
is now a zero-work path, but the same benchmark exposed a different
|
||||
problem: at a synthetic width of 80 pixels, every appended character
|
||||
creates a new whole-hard-line cache key and therefore invokes the complete
|
||||
planner again.
|
||||
|
||||
The earlier task028 record reported a 6.399 ms GC-excluded append p99. That
|
||||
number is not reproducible with the current checked-in benchmark. Repeated
|
||||
audits instead measured roughly 33–85 ms, while point motion remained near
|
||||
0.03 ms with zero planner and cache calls.
|
||||
|
||||
## Decision
|
||||
|
||||
Close task029 on its correctness and navigation-performance invariants, and
|
||||
track unique-state narrow append latency independently as issue018/task030.
|
||||
|
||||
This is not permission to weaken live layout. Task030 must retain:
|
||||
|
||||
- exact whole-hard-line KP semantics;
|
||||
- exact cache identity;
|
||||
- immediate source-edit publication;
|
||||
- source-clean text-property display;
|
||||
- zero-work point-only motion;
|
||||
- unchanged core DP/C ABI/schema contracts unless a later architecture
|
||||
decision explicitly proves a core change is necessary.
|
||||
|
||||
## Why
|
||||
|
||||
The two paths have different owners and different evidence:
|
||||
|
||||
- point motion previously performed unnecessary display work and is fixed
|
||||
by source-edit-owned frontier state;
|
||||
- unique append legitimately changes the source and currently pays for a
|
||||
new whole-hard-line plan.
|
||||
|
||||
Combining them would either delay a verified correctness fix or encourage a
|
||||
wrong-layer shortcut such as stale reuse, debounce, skipped publication, or
|
||||
point-dependent planning.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Task029 may complete when its automated, GUI, and independent-review
|
||||
gates pass.
|
||||
- Current documentation must report the narrow-width miss rather than the
|
||||
stale 6.399 ms claim.
|
||||
- Issue018 remains open until task030 produces equivalent layout results
|
||||
within an explicitly documented width/latency envelope.
|
||||
|
||||
## Rollback
|
||||
|
||||
There is no runtime change in this decision record. If later optimization
|
||||
cannot preserve exact layout and editing semantics, abandon that
|
||||
optimization and leave issue018 open rather than weakening the contract.
|
||||
128
postmortem/20260729-native-progressive-live-editing.md
Normal file
128
postmortem/20260729-native-progressive-live-editing.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Let Native Redisplay Own Unfinished Text
|
||||
|
||||
## Context
|
||||
|
||||
The first text-property live engine correctly kept synthesized layout out
|
||||
of the source character stream, but it still treated an unfinished hard
|
||||
paragraph as a partial KP document. Stable anchors, lookahead, push/pull,
|
||||
and convergence limited the amount of recomputation; they did not change
|
||||
the user-visible ownership error.
|
||||
|
||||
Near the right edge, that engine could choose several visual breaks and a
|
||||
discretionary hyphen while the user was still typing. Calling one row
|
||||
“natural” only suppressed its glue adjustment. The surrounding partial KP
|
||||
plan still controlled wrapping, so editing did not behave like ordinary
|
||||
Emacs.
|
||||
|
||||
The product requirement is stricter: before the user naturally leaves a
|
||||
visual row, EKP must have no opinion about its layout.
|
||||
|
||||
## Decision
|
||||
|
||||
Use three states with distinct owners:
|
||||
|
||||
1. The point-containing, underfilled source tail belongs entirely to native
|
||||
Emacs redisplay. EKP performs no KP planning and publishes no layout
|
||||
property there.
|
||||
2. Once native redisplay has moved point to a later visual row, EKP may
|
||||
align internal gaps on rows already left behind. This projection uses
|
||||
only `space-width` and `min-width`; it cannot add `line-prefix`, a
|
||||
replacing break, or a discretionary hyphen.
|
||||
3. Once a hard paragraph ends or point leaves it, EKP runs one complete KP
|
||||
pass. Only this state may publish planned visual breaks, indentation,
|
||||
and discretionary hyphens.
|
||||
|
||||
Before a source edit, the active hard paragraph is restored to native
|
||||
display. If editing crosses into a previously committed row, that prefix is
|
||||
therefore invalidated before Emacs changes the source. After the edit and
|
||||
redisplay, completed native row boundaries are derived with
|
||||
`vertical-motion`; gap targets are then computed for those fixed source
|
||||
ranges. The row boundary itself remains Emacs-owned.
|
||||
|
||||
IME composition defers projection. Resize work is generation-checked.
|
||||
Narrowing may release an inaccessible previous live paragraph but must not
|
||||
modify it outside the accessible restriction. Foreign replacing display
|
||||
ownership causes the affected row to remain natural rather than inviting a
|
||||
second owner.
|
||||
|
||||
## Why This Is Simpler
|
||||
|
||||
The live engine no longer predicts how an incomplete paragraph will flow.
|
||||
It has no lookahead limit, stable-line signature, anchor-creep rule,
|
||||
pullback algorithm, or convergence state. Each transition follows a
|
||||
visible editor event:
|
||||
|
||||
- still on the same native row: do nothing;
|
||||
- native wrap completed a row: align that row's gaps;
|
||||
- hard paragraph completed or was left: run full KP.
|
||||
|
||||
The completed-paragraph planner and the live editor now share only
|
||||
semantic width/glue computations. They do not pretend to share the same
|
||||
control flow.
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Keep partial KP and tune the active-line exception
|
||||
|
||||
This retains the wrong owner. Suppressing glue on one chosen row cannot
|
||||
prevent adjacent planned breaks or hyphens from appearing while the
|
||||
paragraph is unfinished.
|
||||
|
||||
### Run full KP synchronously after every edit
|
||||
|
||||
Removing an idle timer does not make completed-paragraph optimization an
|
||||
editing algorithm. It would still move unfinished text and would make
|
||||
typing latency proportional to paragraph planning.
|
||||
|
||||
### Insert soft newlines or hyphen characters
|
||||
|
||||
That would restore the original source-pollution defect. Direct Elisp
|
||||
character APIs, save, search, and syntax must continue to observe only
|
||||
logical text.
|
||||
|
||||
### Use overlays for transient rows
|
||||
|
||||
The project has one text-property projection ownership model, and the user
|
||||
explicitly excluded overlays. A second transient representation would
|
||||
complicate invalidation without correcting the row-ownership rule.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Typing before native wrap looks and behaves like auto layout is disabled.
|
||||
- A committed live row can improve spacing without changing its native
|
||||
wrap boundary.
|
||||
- Live editing deliberately has no discretionary hyphenation. Hyphens
|
||||
appear only in the completed paragraph's display projection and never in
|
||||
source text.
|
||||
- Deleting back across a native wrap removes the committed row projection
|
||||
before the source edit, so Emacs immediately restores its natural flow.
|
||||
- One authoritative graphical window determines native row boundaries.
|
||||
Text properties cannot represent simultaneous plans for different window
|
||||
widths.
|
||||
- Automatic work remains bounded by the existing hard-paragraph limit;
|
||||
explicit refill is the opt-in path for an oversized paragraph.
|
||||
|
||||
## Verification
|
||||
|
||||
Behavioral ERT proves the three state transitions through the installed
|
||||
before/after-change path, including underfilled tails, native wrap,
|
||||
backward invalidation, hard newline, paragraph exit, IME, narrowing,
|
||||
resize, foreign ownership, and teardown.
|
||||
|
||||
Static GUI probes prove exact `space-width`/`min-width` projection from
|
||||
1–64 pixels, display-only completed-paragraph breaks/hyphens/indentation,
|
||||
unchanged source, and zero overlays across scale, remap, fringe, and width
|
||||
variants.
|
||||
|
||||
The retained dynamic artifact
|
||||
`/private/tmp/ekp-native-live-2dYu2U` records native mixed Latin/CJK wrap,
|
||||
deletion back across the boundary, and hard-paragraph completion. Every
|
||||
immediate and post-redisplay checkpoint passes, and temporal review finds
|
||||
no flicker, black segment, or non-Emacs frame.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the native progressive live transition as one unit. Do not restore
|
||||
the partial-KP state machine piecemeal or add a compatibility shim. The
|
||||
semantic completed-paragraph planner and text-property projection layer
|
||||
remain independently valid.
|
||||
90
postmortem/20260729-native-soft-wrap-precondition.md
Normal file
90
postmortem/20260729-native-soft-wrap-precondition.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Own the Native Soft-Wrap Precondition
|
||||
|
||||
## Context
|
||||
|
||||
The native progressive editor deliberately lets Emacs decide when the
|
||||
active source line becomes another visual row. That model worked in a
|
||||
full-width verification window but failed in the user's narrow
|
||||
side-by-side window: the line horizontally scrolled and displayed a `$`
|
||||
truncation indicator.
|
||||
|
||||
The source was clean and the KP planner was not involved. Emacs 30.2
|
||||
defaults `truncate-partial-width-windows` to `50`; a partial-width window
|
||||
below that many columns truncates even when `truncate-lines` is nil.
|
||||
Without a native visual row, the live engine has no completed row to
|
||||
observe or align.
|
||||
|
||||
## Decision
|
||||
|
||||
`ekp-auto-justify-mode` owns the display precondition its state machine
|
||||
requires:
|
||||
|
||||
1. On activation, snapshot the values and buffer-local ownership of
|
||||
`truncate-lines` and `truncate-partial-width-windows`.
|
||||
2. Make both variables buffer-local and nil before the first reflow.
|
||||
3. On mode disable or major-mode teardown, restore prior local values or
|
||||
remove the temporary local bindings so global ownership resumes.
|
||||
4. Treat activation as a transaction: if width discovery, initial reflow,
|
||||
hook installation, or integration setup signals, remove partial
|
||||
lifecycle state, restore the display variables, set the mode back to
|
||||
disabled, and let the original error propagate.
|
||||
|
||||
This changes only redisplay policy. It creates no source character,
|
||||
overlay, replacing display break, or independent wrapping algorithm.
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Treat truncation as a user configuration error
|
||||
|
||||
The failing value is an Emacs default in narrow split windows. A mode whose
|
||||
algorithm requires native visual rows cannot leave that precondition
|
||||
implicit and still promise natural editing.
|
||||
|
||||
### Compute hidden visual rows while Emacs truncates
|
||||
|
||||
That would make EKP simulate a display the user cannot see. It would also
|
||||
leave the reported horizontal-scrolling experience unchanged.
|
||||
|
||||
### Set the variables without restoring them
|
||||
|
||||
That would fix typing by permanently changing unrelated buffer behavior.
|
||||
Minor-mode ownership must end with the mode.
|
||||
|
||||
### Change the global defaults
|
||||
|
||||
The requirement belongs to one buffer while its auto mode is active.
|
||||
Changing global behavior would affect unrelated buffers and packages.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Full-width and narrow side-by-side windows use the same native
|
||||
progressive state transitions.
|
||||
- Existing `visual-line-mode`/`word-wrap` behavior remains available;
|
||||
EKP only prevents truncation.
|
||||
- Disabling auto mode restores the exact prior buffer-local ownership.
|
||||
When the original state was global, removing the temporary binding lets
|
||||
the current global value resume.
|
||||
- Multiple windows showing the same buffer share the buffer-local
|
||||
soft-wrap policy, matching the existing buffer-wide projection model.
|
||||
|
||||
## Verification
|
||||
|
||||
A public minor-mode lifecycle ERT reproduces the failure red, then proves
|
||||
both variables are nil and buffer-local while enabled and that their prior
|
||||
local/global ownership is restored on disable. Separate regressions force
|
||||
initial reflow failure and switch major modes, proving those teardown paths
|
||||
restore the same ownership and never leave a half-enabled mode.
|
||||
|
||||
The retained graphical run
|
||||
`/private/tmp/ekp-soft-wrap-final-pass-PIUigY` types mixed Latin/CJK text
|
||||
one character at a time in a 44-column left split. The selected window
|
||||
advances from one to two visual rows with `hscroll=0`, zero live replacing
|
||||
breaks, zero overlays, exact source text, and no pending transaction.
|
||||
Temporal review finds only the intended Emacs split, natural continuation,
|
||||
and a stable final frame.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the wrap-state snapshot, activation, and restoration together.
|
||||
Leaving only the forced values or only the restoration would violate mode
|
||||
ownership.
|
||||
117
postmortem/20260729-stable-live-transaction.md
Normal file
117
postmortem/20260729-stable-live-transaction.md
Normal file
@ -0,0 +1,117 @@
|
||||
# Separate Stable Editing From Global KP Commitment
|
||||
|
||||
## Context
|
||||
|
||||
`task028` fixed the planning unit by consuming one whole-hard-line KP plan.
|
||||
`task029` then removed point-only invalidation by preserving a source-edit
|
||||
frontier. User testing exposed that the combined model still gives the
|
||||
frontier three jobs: latest edit position, natural-suffix boundary, and
|
||||
projection reuse boundary.
|
||||
|
||||
That ownership cannot deliver stable editing. Replanning the complete hard
|
||||
line after every key permits the globally optimal breakpoint set to change
|
||||
after every key. Line-signature diffing can reduce property writes only after
|
||||
those new decisions exist; it cannot make the decisions stable. Clearing the
|
||||
touched line and its entire suffix before every edit also destroys anchors
|
||||
that were not causally affected.
|
||||
|
||||
The narrow unique-append cost recorded by `issue018` is therefore downstream
|
||||
of the correctness model, not the first problem to optimize.
|
||||
|
||||
## Decision
|
||||
|
||||
Use two edit-time layers and one completed-layout owner:
|
||||
|
||||
1. A committed projection owns the last published source, core plan, line
|
||||
signatures, projected spans, and break anchors.
|
||||
2. A dirty edit transaction snapshots that complete baseline and
|
||||
naturalizes only the affected local island.
|
||||
3. The existing core DP remains the sole owner of globally optimal committed
|
||||
and completed layout.
|
||||
|
||||
Ordinary input inside the same native visual row does not run whole-hard-line
|
||||
DP and does not rewrite unaffected properties. Existing glue and native soft
|
||||
wrapping absorb small local changes. If the dirty row no longer fits, native
|
||||
wrapping moves the necessary local words while following committed break
|
||||
anchors remain fixed.
|
||||
|
||||
A transaction commits only when:
|
||||
|
||||
- input crosses a native soft-wrap boundary;
|
||||
- a hard newline/paragraph end is inserted or removed;
|
||||
- the next real source edit occurs outside the dirty island;
|
||||
- the user explicitly refills the paragraph; or
|
||||
- width, font, or layout context changes.
|
||||
|
||||
Point motion is never a commit event. On a soft-wrap commit, the complete
|
||||
hard-line source may be replanned once and the completed prefix is updated as
|
||||
one silent publication; the new current row remains natural. On hard
|
||||
paragraph completion, the existing full-quality KP path remains unchanged.
|
||||
|
||||
If an edit restores the baseline source, the transaction restores the saved
|
||||
owned projection and committed state directly. Exact restoration is a state
|
||||
transition, not a hope that recomputation happens to reproduce equivalent
|
||||
properties.
|
||||
|
||||
## Superseded Decisions
|
||||
|
||||
This record supersedes:
|
||||
|
||||
- `20260729-whole-hard-line-live-prefix.md` where it permits earlier
|
||||
projected rows to change after every source edit;
|
||||
- `20260729-editing-frontier-not-point.md` where the edit frontier also owns
|
||||
the natural suffix and immediate post-edit publication.
|
||||
|
||||
It preserves their valid decisions:
|
||||
|
||||
- the core DP and layout-plan contract stay unchanged;
|
||||
- committed prefix lines come from one semantic hard-line plan;
|
||||
- the current unfinished row is natural;
|
||||
- point-only motion performs zero work;
|
||||
- hard-paragraph completion receives full-quality KP layout.
|
||||
|
||||
It also restores the useful local-stability principle from the earlier
|
||||
bounded-flow work without restoring its core continuation API, helper stack,
|
||||
or per-key DP control flow.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Editing stability and final optimality have different, explicit triggers.
|
||||
- Unaffected visual rows retain their break anchors during continuous local
|
||||
editing.
|
||||
- Reversible source edits restore the exact projection immediately.
|
||||
- Per-key unique-state planning disappears from ordinary same-row append; the
|
||||
remaining `task030` surface must be measured after this change.
|
||||
- The live buffer layer owns more baseline state, but the core algorithm,
|
||||
source-clean representation, and projection primitives remain unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
Public hook/command paths must prove ordinary same-row no-plan behavior,
|
||||
local dirty-island ownership, unaffected-anchor stability, exact
|
||||
`equal-including-properties` reversal, soft-wrap atomic commit, hard
|
||||
completion, next-edit-elsewhere commit, zero-work point motion, IME
|
||||
deferral, width/font/context commit, and fail-closed conflicts. Dynamic GUI
|
||||
evidence is required because batch tests cannot prove temporal stability or
|
||||
native word migration.
|
||||
|
||||
## Outcome
|
||||
|
||||
The implementation confirmed the ownership split. Ordinary same-row edits
|
||||
and point motion now make zero planner calls; the 291-edit narrow benchmark
|
||||
contains 15 plans, each tied to a real structural crossing. Middle-row edits
|
||||
retain unrelated span objects, and exact source reversal restores the saved
|
||||
projection `equal-including-properties` with the original plan, signatures,
|
||||
and spans.
|
||||
|
||||
The final 48.95-second GUI run records 11 public actions at five phases each.
|
||||
All 55 checkpoints pass, the run completes in one fullscreen target window,
|
||||
and temporal review finds no black, blank, split, stale-buffer, or client-
|
||||
message frame. The surviving structural-commit latency is correctly left to
|
||||
`task030`; it is no longer evidence against the transaction model.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the transaction change as one unit if the public-path or GUI gates
|
||||
fail. Do not fall back to debounce, idle whole-paragraph formatting, or the
|
||||
per-key frontier planner; all three retain the same trigger-ownership error.
|
||||
163
postmortem/20260729-text-property-live-layout.md
Normal file
163
postmortem/20260729-text-property-live-layout.md
Normal file
@ -0,0 +1,163 @@
|
||||
# Keep KP Decisions Semantic and Buffer Layout Presentational
|
||||
|
||||
> Historical note: the text-property projection decision remains current.
|
||||
> The partial-KP live state machine described below was replaced by the
|
||||
> native progressive model in
|
||||
> `20260729-native-progressive-live-editing.md`.
|
||||
|
||||
## Context
|
||||
|
||||
The original editor integration inserted a reversible rendered string into
|
||||
the real buffer. Its marker properties made the transform recoverable, and
|
||||
save/copy/search adapters hid much of the representation, but arbitrary
|
||||
Elisp character APIs still observed synthesized spaces, newlines, and
|
||||
discretionary hyphens.
|
||||
|
||||
The required outcome was stronger than reversible formatting: the buffer's
|
||||
character stream itself had to remain the logical document. The user also
|
||||
excluded overlays and required existing-character text properties,
|
||||
specifically absolute-pixel `min-width`, real-space `space-width`, and
|
||||
`line-prefix`.
|
||||
|
||||
This exposed a second mistake in the old live mode. A function that produces
|
||||
an optimal completed paragraph is not automatically an editing algorithm.
|
||||
Running it after an idle delay caused unfinished lines to snap, performed
|
||||
whole-paragraph work for local edits, and had no explicit overflow,
|
||||
pullback, composition, or convergence policy.
|
||||
|
||||
The first deletion implementation also recalculated its look-behind from
|
||||
each backspace. Once two visual lines merged, the next deletion began at a
|
||||
new line boundary and moved the anchor left again. The source round trip was
|
||||
correct, but the projection accumulated an extra break because the
|
||||
recomputation boundary crept into the formerly stable prefix.
|
||||
|
||||
## Decision
|
||||
|
||||
Separate layout decisions from representation.
|
||||
|
||||
`ekp-layout-plan` owns boxes/source offsets, line ranges, exact glue targets,
|
||||
indentation, chosen breaks, and discretionary-hyphen decisions. The public
|
||||
string renderer retains its compatible lossless physical returned string.
|
||||
The buffer renderer consumes the same plan but has no right to insert a
|
||||
character.
|
||||
|
||||
The buffer projection is:
|
||||
|
||||
- source ASCII space:
|
||||
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`;
|
||||
- zero-source CJK/mixed gap: `min-width` on the preceding complete
|
||||
grapheme, targeting natural advance plus glue;
|
||||
- indentation: `line-prefix`;
|
||||
- explicit break/hyphen: a replacing display string on an existing
|
||||
complete grapheme that reproduces the grapheme, appends the optional
|
||||
hyphen, then a visual newline.
|
||||
|
||||
Every public property value is mirrored by an EKP owner property. Cleanup
|
||||
removes the public value only while it is still identical to the owned
|
||||
value. Updates use `with-silent-modifications`; owned properties are
|
||||
nonsticky. No buffer path creates an overlay.
|
||||
|
||||
Live editing uses a separate bounded state machine:
|
||||
|
||||
1. keep one stable source-marker anchor for a continuous editing flow, then
|
||||
snapshot and clear only its affected projected suffix;
|
||||
2. keep the point-containing unfinished line naturally spaced;
|
||||
3. commit the longest fitting prefix on overflow;
|
||||
4. pull following material back after deletion;
|
||||
5. stop when source position and a position-independent line signature
|
||||
match the old plan;
|
||||
6. defer composition and reject stale generation work;
|
||||
7. allow a complete quality pass only after leaving the paragraph or an
|
||||
explicit refill command.
|
||||
|
||||
The active anchor is released when the flow ends: leaving the paragraph,
|
||||
explicit refill, a hard-boundary reflow, conflict abandonment, or mode
|
||||
teardown. A new deletion starts one line earlier only when it first begins
|
||||
exactly at a visual-line boundary; subsequent backspaces reuse the same
|
||||
anchor. This makes overflow followed by deletion projection-identical.
|
||||
|
||||
Automatic work has a hard single-paragraph limit. An oversized paragraph
|
||||
stays naturally editable and diagnostic rather than entering an unbounded
|
||||
DP during mode enable, paste, or typing. `ekp-refill-paragraph` is the
|
||||
explicit request to pay that cost.
|
||||
|
||||
Text properties are buffer-wide, so the narrowest live window supplies one
|
||||
authoritative width. Wider windows may show unused space. Simultaneous
|
||||
different-width KP plans are not claimed.
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Keep physical buffer text and add more adapters
|
||||
|
||||
No finite adapter list can change what arbitrary `buffer-string`,
|
||||
`char-after`, syntax, or third-party Elisp reads. This compensates at the
|
||||
wrong layer.
|
||||
|
||||
### Use overlays
|
||||
|
||||
The user explicitly excluded them. Keeping an overlay backend would also
|
||||
create two projection ownership models and make lifecycle behavior harder
|
||||
to reason about.
|
||||
|
||||
### Use `min-width` alone
|
||||
|
||||
It can only add width. Existing ASCII spaces sometimes need shrinking;
|
||||
`space-width` supplies that shrink and `min-width` supplies the exact pixel
|
||||
floor after rounding.
|
||||
|
||||
### Use `space-width` alone
|
||||
|
||||
The factor is rounded by redisplay and is not an exact absolute-pixel
|
||||
guarantee. It also has no effect on tabs.
|
||||
|
||||
### Reuse completed-paragraph formatting after every edit
|
||||
|
||||
Changing its trigger from idle to synchronous would remove the delay but
|
||||
still perform the wrong unit of work and would justify an unfinished line.
|
||||
The editing path needs its own active-line and convergence semantics.
|
||||
|
||||
### Promise independent widths in multiple windows
|
||||
|
||||
One set of buffer text properties cannot encode two different break plans.
|
||||
Pretending otherwise would publish an overflowing plan in at least one
|
||||
window.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Direct character APIs, search, syntax, save, point, and markers operate on
|
||||
source characters.
|
||||
- APIs that preserve or inspect text properties can still observe EKP
|
||||
projection metadata; copy/kill removes that owned metadata.
|
||||
- Foreign replacing display ownership makes only the affected hard
|
||||
paragraph natural and diagnostic.
|
||||
- Tabs/non-ASCII whitespace remain natural if an exact plan would require
|
||||
shrinking them.
|
||||
- Layout property changes do not create layout-only undo or modified-state
|
||||
changes.
|
||||
- Ordinary edits perform bounded local flow with no delayed
|
||||
whole-paragraph snap.
|
||||
- Very long single paragraphs trade automatic KP quality for bounded input
|
||||
latency until the user explicitly requests refill.
|
||||
|
||||
## Verification
|
||||
|
||||
The architecture requires three evidence layers:
|
||||
|
||||
- ERT for core plan/string parity, source and lifecycle invariants, exact
|
||||
property forms, live overflow/pullback/convergence, composition, undo,
|
||||
ownership, resize, multiwindow policy, and overload behavior;
|
||||
- clean GUI probes for exact pixel glue, break/hyphen/indent rendering,
|
||||
point, region, mouse, and source invariants;
|
||||
- dynamic before/immediate/settled recording for overflow and pullback,
|
||||
including source/projection hashes, zero overlays, pixel-fit assertions,
|
||||
and temporal review.
|
||||
|
||||
Final evidence and counts are recorded in the phase change/task documents,
|
||||
not retroactively copied into this decision record.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the text-property buffer renderer and live state machine together.
|
||||
Do not retain a mixed physical/text-property backend. The semantic plan and
|
||||
compatible string renderer can remain independently because their boundary
|
||||
does not depend on buffer projection.
|
||||
147
postmortem/20260729-whole-hard-line-live-prefix.md
Normal file
147
postmortem/20260729-whole-hard-line-live-prefix.md
Normal file
@ -0,0 +1,147 @@
|
||||
# Let Live Editing Consume the Whole Hard-Line Plan
|
||||
|
||||
> Superseded note (2026-07-29): the whole-hard-line planning decision
|
||||
> remains current, but `20260729-editing-frontier-not-point.md` supersedes
|
||||
> this record's point-driven projection-boundary statements.
|
||||
>
|
||||
> Later supersession (2026-07-29):
|
||||
> `20260729-stable-live-transaction.md` preserves whole-hard-line planning at
|
||||
> structural commits but supersedes replanning after every source edit.
|
||||
|
||||
## Context
|
||||
|
||||
The native progressive live model fixed the most visible editing problems:
|
||||
typing before native wrap stayed natural, narrow split windows soft-wrapped,
|
||||
and live mode no longer published discretionary hyphens or replacing
|
||||
breaks into the active tail.
|
||||
|
||||
User testing then exposed the next owner mistake. A hard line that naturally
|
||||
wraps into several visual rows still needs the rows before point to be
|
||||
decisions from one Knuth-Plass plan. The native-row model aligned rows after
|
||||
Emacs had already wrapped them, then treated those row boundaries as local
|
||||
facts. That cannot produce global KP behavior because later source text can
|
||||
change earlier optimal breaks and glue.
|
||||
|
||||
The failed assumption was subtle: "native redisplay owns the active edit
|
||||
tail" does not imply "native rows are the durable planning unit." Native
|
||||
redisplay should still own the point-containing unfinished area, but the
|
||||
completed prefix should be projected from the semantic KP plan for the
|
||||
whole current hard line.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the core algorithm unchanged. `ekp-layout-plan` already computes the
|
||||
correct plan for complete text. `ekp-buffer` must consume that plan for the
|
||||
current hard line and choose the editable projection boundary.
|
||||
|
||||
The live pipeline is:
|
||||
|
||||
1. Read the complete source text of the current hard line.
|
||||
2. Call the existing `ekp-layout-plan` with the authoritative width and
|
||||
current layout context.
|
||||
3. Find the semantic plan line that contains point.
|
||||
4. Project only complete plan lines before that line.
|
||||
5. Leave the point-containing plan line and every later source character
|
||||
completely natural.
|
||||
6. Compare old and new semantic line signatures so unchanged prefixes stay
|
||||
installed and changed suffixes are removed/reinstalled atomically.
|
||||
|
||||
This state belongs entirely to `ekp-buffer`: hard-line bounds, source and
|
||||
context signatures, the latest plan, line signatures, installed prefix
|
||||
range, generation, and a small buffer-local history cache for undo/redo and
|
||||
width/text round trips.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Do not change core DP semantics.
|
||||
- Do not add continuation DP, live DP, or final-line special cases.
|
||||
- Do not change the C ABI, DP schema, or core/Elisp algorithm contract.
|
||||
- Do not copy the DP into `ekp-buffer`.
|
||||
- Do not add buffer, point, window, marker, or redisplay state to the core
|
||||
planner.
|
||||
- Do not use overlays.
|
||||
- Do not insert source spaces, source newlines, or source hyphen
|
||||
characters.
|
||||
|
||||
The completed-paragraph planner remains the owner of optimal breaks and
|
||||
glue. The buffer layer owns edit-time projection, invalidation, and
|
||||
redisplay safety.
|
||||
|
||||
## Superseded Model
|
||||
|
||||
`20260729-native-progressive-live-editing.md` remains useful history for
|
||||
two decisions that still stand:
|
||||
|
||||
- the point-containing active area must remain natural;
|
||||
- full static KP projection is allowed after hard-paragraph completion.
|
||||
|
||||
It is superseded for the planning unit. Completed native visual rows are
|
||||
not stable committed rows. They are only a symptom that there is now a
|
||||
semantic prefix before point that may be safe to project.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Earlier displayed rows in the same hard line can change together when
|
||||
later input changes the optimal KP plan.
|
||||
- Moving point into an earlier semantic line immediately makes that line
|
||||
and the following source natural again.
|
||||
- Point movement without text change still matters because it changes the
|
||||
projection boundary.
|
||||
- Plan cache hits are possible when undo/redo, deletion, width changes, or
|
||||
point movement revisit a known hard-line text/context signature.
|
||||
- Projection failure must not advance a tail marker or leave half of a KP
|
||||
prefix installed; the affected hard line returns to native display.
|
||||
- First-line underfilled editing remains indistinguishable from ordinary
|
||||
Emacs because there is no complete semantic prefix before point.
|
||||
|
||||
## Verification
|
||||
|
||||
The implementation proves the behavior through public editing paths, not
|
||||
by stubbing row lists:
|
||||
|
||||
- `self-insert-command`, yank, delete, real undo/redo, point movement,
|
||||
hard newline, resize, and major-mode change;
|
||||
- point in last, middle, and first semantic plan line;
|
||||
- early plan breaks changing after later edits, proving rows are not frozen;
|
||||
- plan-cache hits and zero property writes when the semantic plan is
|
||||
unchanged;
|
||||
- clean GUI dynamic evidence with exact source text, zero overlays,
|
||||
`hscroll=0`, natural point line, aligned prefix lines, and no delayed
|
||||
idle snap.
|
||||
|
||||
## Implementation Outcome
|
||||
|
||||
`task028` implemented this decision entirely in `ekp-buffer`. The core DP,
|
||||
C ABI, DP schema, and `ekp-layout-plan` contract were not changed. The live
|
||||
state now uses whole-hard-line plans, a 16-entry buffer-local history LRU,
|
||||
semantic line signatures, common-prefix differential publication, and
|
||||
transactional fail-closed cleanup.
|
||||
|
||||
Default and seed-`20260729` ERT pass 181/181, all 181 tests pass in isolated
|
||||
Emacs processes, and C/Elisp fuzz passes 300/300. The reviewed main dynamic
|
||||
GUI run `/private/tmp/ekp-semantic-live-v4-vFZTkr` and 44-column split run
|
||||
`/private/tmp/ekp-semantic-split-v3-uPwuOi` both return PASS with exact
|
||||
source text, zero overlays, `hscroll=0`, a natural point line, and no
|
||||
temporal snap or stale projection.
|
||||
|
||||
The original C-backend run recorded append p99 6.399 ms with GC excluded.
|
||||
A later task029 audit could not reproduce that number with the current
|
||||
checked-in benchmark: the synthetic 80-pixel workload instead records one
|
||||
plan per unique append and roughly 33–85 ms p99. That separate performance
|
||||
debt is now `issue018`/`task030`; see
|
||||
`20260729-narrow-live-append-replanning.md`. The implementation does not
|
||||
hide it with stale reuse, debounce, skipped publication, global GC changes,
|
||||
or a timer workaround.
|
||||
|
||||
Final independent code review returns APPROVE and independent architecture
|
||||
review returns CLEAR, including the theme-disable and frame-font
|
||||
invalidation delta.
|
||||
|
||||
Developer verification does not close `issue016`; the user must still
|
||||
confirm the editing experience personally.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the semantic-prefix live implementation as one unit if it fails its
|
||||
public-path or GUI gates. Do not restore native-row freezing as the final
|
||||
model; it is known to be the wrong planning boundary for global KP layout.
|
||||
53
postmortem/20260730-c-resize-hot-path-ownership.md
Normal file
53
postmortem/20260730-c-resize-hot-path-ownership.md
Normal file
@ -0,0 +1,53 @@
|
||||
# C Resize Hot-Path Ownership
|
||||
|
||||
## Context
|
||||
|
||||
The reported 60–70 ms resize number was initially described as C dynamic
|
||||
module performance. The measured operation actually crossed core planning,
|
||||
the Emacs module boundary, projection construction, text-property
|
||||
publication, and live-prefix replacement.
|
||||
|
||||
## What the profile showed
|
||||
|
||||
The portable C call averaged roughly 1.8 ms in the frozen four-round
|
||||
baseline. Repeated paragraph lookup and plan construction dominated the core
|
||||
path, while redundant gap measurement and property publication dominated
|
||||
the buffer projection path. Resize also published the active paragraph
|
||||
statically before replacing it with the live prefix.
|
||||
|
||||
An apparent remaining 60–70 ms GUI spike was a separate phenomenon:
|
||||
`set-window-margins` allocated in the Emacs window system and a later EKP
|
||||
callback paid the resulting GC pause. The EKP mutator stayed below the
|
||||
50 ms contract when total, GC, and mutator time were recorded separately.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the C ABI and Knuth-Plass algorithm unchanged. Move prepared paragraph,
|
||||
DP, and natural gap geometry through the existing core-to-buffer path,
|
||||
remove only true no-op projection records, consolidate owned property
|
||||
publication, and make resize publish the active paragraph only through its
|
||||
live owner.
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- Micro-optimizing C DP: it was not the dominant measured layer.
|
||||
- Increasing resize debounce or skipping widths: this hides work and makes
|
||||
projection stale.
|
||||
- Reusing approximate plans across widths: this breaks exact layout parity.
|
||||
- Raising the global GC threshold: this changes process-wide behavior and
|
||||
misattributes window-system allocation to EKP.
|
||||
|
||||
## Consequences
|
||||
|
||||
The four-round gate reduces core p95 from 42.006 ms to 27.687 ms and complete
|
||||
resize p95 from 46.611 ms to 27.487 ms, with 34.09% and 41.03% improvements.
|
||||
Frozen C, candidate C, and Elisp layout hashes remain identical. External GC
|
||||
can still increase wall-clock totals, but it is explicitly observable and
|
||||
is not concealed by runtime policy changes.
|
||||
|
||||
## Verification and rollback
|
||||
|
||||
The evaluator, normal/permuted/isolated ERT suites, fuzzing, warning-as-error
|
||||
builds, static and release gates, and reviewed temporal GUI evidence pass.
|
||||
Rollback is a direct revert of the prepared-data and publication changes;
|
||||
no compatibility layer or data migration is required.
|
||||
125
readme.md
125
readme.md
@ -15,9 +15,14 @@ typesetting, entirely inside Emacs.
|
||||
one); dedicated inter-CJK and CJK↔Latin spacing.
|
||||
- **Hyphenation** — Frank Liang's algorithm (the TeX algorithm) with 49
|
||||
checksum-pinned Hunspell pattern dictionaries bundled.
|
||||
- **Pixel-accurate justification** — every justified line renders at
|
||||
exactly the requested pixel width, using `display (space :width ...)`
|
||||
properties; works with variable-width fonts.
|
||||
- **Pixel-accurate justification** — one semantic layout plan drives both
|
||||
renderers. The string API uses pixel spaces; buffer layout combines
|
||||
`space-width` with absolute-pixel `min-width`, so it works with
|
||||
variable-width fonts without inserting layout characters.
|
||||
- **Clean editable buffers** — buffer commands create no overlays and add
|
||||
no glue spaces, soft newlines, or discretionary hyphens to the character
|
||||
stream. `buffer-string`, `char-after`, search, syntax, save, and ordinary
|
||||
Elisp text consumers see the source characters.
|
||||
- **Text properties preserved** — faces, colors and other properties
|
||||
survive justification; inserted hyphens inherit the face of the word
|
||||
they break.
|
||||
@ -40,7 +45,7 @@ Clone the repository and add it to your `load-path` (the
|
||||
```elisp
|
||||
(add-to-list 'load-path "/path/to/emacs-kp")
|
||||
(require 'ekp)
|
||||
(require 'ekp-region) ; buffer/region commands
|
||||
(require 'ekp-buffer) ; buffer/region commands
|
||||
```
|
||||
|
||||
Byte-compiling is strongly recommended — the Elisp engine is about
|
||||
@ -82,48 +87,77 @@ rebuild.
|
||||
|
||||
## Interactive Use (buffer & region)
|
||||
|
||||
`ekp-region.el` turns the string API into buffer-level commands:
|
||||
`ekp-buffer.el` turns the string API into buffer-level commands:
|
||||
|
||||
```elisp
|
||||
(require 'ekp-region)
|
||||
(require 'ekp-buffer)
|
||||
```
|
||||
|
||||
- `M-x ekp-justify-region` — justify the region to the window text
|
||||
width (with a numeric prefix argument, to that many pixels). With
|
||||
no active region, it justifies the paragraph at point.
|
||||
- `M-x ekp-justify-buffer` — justify the whole buffer.
|
||||
- `M-x ekp-unjustify-region` / `ekp-unjustify-buffer` — restore the
|
||||
original text **exactly**, including collapsed whitespace runs.
|
||||
Justification is lossless: every synthesized space, soft line break,
|
||||
and soft hyphen carries the original text it replaced, so restoring
|
||||
is a structural transform that also works after you edited the
|
||||
justified text.
|
||||
- `M-x ekp-auto-justify-mode` — keep the whole buffer justified to the
|
||||
window width. Re-flows (debounced by
|
||||
`ekp-auto-justify-resize-delay`) when the window width changes, and
|
||||
after edits re-justifies only the touched paragraphs
|
||||
(`ekp-auto-justify-edit-delay`), so unchanged paragraphs hit the
|
||||
paragraph cache. Turning the mode off restores the buffer exactly.
|
||||
- `M-x ekp-unjustify-region` / `ekp-unjustify-buffer` — remove EKP's
|
||||
display projection. The source text does not need restoration because
|
||||
buffer layout never replaced it.
|
||||
- `M-x ekp-auto-justify-mode` — keep completed hard paragraphs justified
|
||||
while ordinary typing remains stable. The active hard line has a committed
|
||||
projection plus one local edit transaction. Typing within the same native
|
||||
visual row does no whole-line planning and leaves unaffected projected rows
|
||||
untouched. Editing a projected middle row naturalizes only that row's dirty
|
||||
island, so later break anchors stay in place and native wrapping handles
|
||||
local word migration. When input naturally crosses into the next visual
|
||||
row, EKP runs or reuses one complete `ekp-layout-plan` and atomically
|
||||
publishes all completed rows; the new row remains natural. Deleting and
|
||||
reinserting the same source restores the saved projection exactly,
|
||||
including text properties.
|
||||
Point motion never plans or writes layout properties, even when point
|
||||
leaves the paragraph. Global commits happen only at a visual-row crossing,
|
||||
hard newline/paragraph completion, the next real edit elsewhere, explicit
|
||||
refill, or a width/font/layout-context change. There is no edit-idle
|
||||
whole-paragraph snap. Window resize re-flow is debounced by
|
||||
`ekp-auto-justify-resize-delay`.
|
||||
The mode temporarily disables both explicit line truncation and Emacs's
|
||||
narrow partial-window truncation, so a side-by-side editing window still
|
||||
soft-wraps normally. Disabling the mode restores the prior buffer-local
|
||||
or global ownership of both settings.
|
||||
A leading or trailing space/tab on the active line is visible in the
|
||||
same input turn; deleting a following glyph does not hide that source
|
||||
whitespace. Reprojection also preserves an inactive mark as inactive, so
|
||||
width changes do not create an accidental selection.
|
||||
While active, the standard **EKP** menu exposes formatting, protection,
|
||||
and window-fit diagnostic commands; `C-h m` describes the same workflow.
|
||||
|
||||
The buffer is treated as a live document, not just a canvas:
|
||||
The projection uses text properties on existing source graphemes only:
|
||||
|
||||
- **Saving** writes the *logical* text — soft line breaks, glue
|
||||
spaces and break hyphens never reach disk; the on-screen buffer
|
||||
stays justified even if writing fails or is interrupted.
|
||||
This guarantee applies to whole-buffer saves; an explicit region-only
|
||||
`write-region` writes the selected physical buffer representation.
|
||||
- **Searching** (isearch) sees the logical text, so CJK phrases and
|
||||
hyphenated words are found across the layout.
|
||||
- **Copying** puts the logical text on the kill ring, so pasted text
|
||||
carries words, not pixel spacing. Existing mode/user substring filters
|
||||
remain active and are restored when the final layout span is removed.
|
||||
- Merely enabling the mode never marks the buffer modified (no stray
|
||||
lock files or auto-saves), and `undo` is not fought by the re-flow
|
||||
timer.
|
||||
- Existing ASCII spaces receive
|
||||
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`.
|
||||
- A CJK or mixed gap with no source space adds `min-width` to the preceding
|
||||
complete grapheme; the target is its natural advance plus the glue.
|
||||
- `line-prefix` supplies indentation. A break or discretionary hyphen is
|
||||
a replacing display string on an existing complete grapheme.
|
||||
- EKP never creates an overlay. It also never steals a foreign replacing
|
||||
`display`, `line-prefix`, `wrap-prefix`, `composition`, or `invisible`
|
||||
owner; that hard paragraph stays natural and `M-x ekp-diagnose` reports
|
||||
the conflict.
|
||||
|
||||
`ekp-region-margin-pixel` (default 2) is subtracted from the window
|
||||
Consequently:
|
||||
|
||||
- **Elisp APIs and saving** see the original character sequence. Visual
|
||||
spaces, newlines, and hyphens cannot reach disk or syntax/search logic.
|
||||
`buffer-substring` can still carry the EKP display properties because it
|
||||
preserves text properties; `buffer-substring-no-properties` is the plain
|
||||
source string.
|
||||
- **Searching** (including isearch) operates directly on source text, so a
|
||||
word remains one word across a visual discretionary break.
|
||||
- **Copying and killing** strip EKP-owned projection properties while
|
||||
composing with any existing substring filter. Pasted text contains only
|
||||
the logical content and its non-EKP properties.
|
||||
- Projection updates run inside `with-silent-modifications`: enabling,
|
||||
editing, resizing, and disabling layout do not create layout-only undo
|
||||
entries, modified-state changes, or character-modified ticks.
|
||||
|
||||
`ekp-buffer-margin-pixel` (default 2) is subtracted from the window
|
||||
width as a rounding safety margin.
|
||||
|
||||
Large buffers (over `ekp-auto-justify-lazy-threshold` characters,
|
||||
@ -132,6 +166,13 @@ synchronously and the rest follows in idle background chunks, with a
|
||||
per-tick time budget (`ekp-auto-justify-tick-budget`) and priority
|
||||
for whatever you scroll to.
|
||||
|
||||
Automatic planning is also bounded per hard paragraph.
|
||||
`ekp-auto-justify-paragraph-limit` defaults to 2 048 characters. A longer
|
||||
single paragraph stays naturally wrapped and fully editable instead of
|
||||
blocking input in an unbounded Knuth-Plass pass; `M-x ekp-diagnose` reports
|
||||
the reason. Run `M-x ekp-refill-paragraph` when you explicitly want the
|
||||
unbounded full-quality pass for that paragraph.
|
||||
|
||||
Mode presets for verbatim protection — one call each:
|
||||
|
||||
```elisp
|
||||
@ -146,8 +187,8 @@ in Org and Markdown buffers when you have not configured your own.
|
||||
|
||||
- Block level: paragraphs carrying the `ekp-verbatim` text property
|
||||
(`M-x ekp-verbatim-region`), wearing a face listed in
|
||||
`ekp-region-skip-faces` (e.g. `org-block`, `markdown-code-face`), or
|
||||
matched by the buffer-local function `ekp-region-skip-predicate`
|
||||
`ekp-buffer-skip-faces` (e.g. `org-block`, `markdown-code-face`), or
|
||||
matched by the buffer-local function `ekp-buffer-skip-predicate`
|
||||
pass through completely untouched.
|
||||
- Inline level: spans carrying `ekp-no-break`
|
||||
(`M-x ekp-no-break-region`) become rigid atoms — never broken,
|
||||
@ -158,7 +199,7 @@ Manual properties are deliberately **current-buffer-session only**:
|
||||
plain-text saving and reopening do not persist them. Use
|
||||
`M-x ekp-allow-break-region` / `ekp-clear-verbatim-region` to remove them.
|
||||
For protection derived from persistent document syntax, use mode faces or
|
||||
the buffer-local `ekp-region-skip-predicate` (the Org/Markdown presets do
|
||||
the buffer-local `ekp-buffer-skip-predicate` (the Org/Markdown presets do
|
||||
this automatically).
|
||||
|
||||
## Typography
|
||||
@ -292,6 +333,16 @@ the point.)
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Text properties are buffer-wide, so one buffer cannot carry independent
|
||||
plans for windows of different widths. EKP uses the narrowest live
|
||||
window as the authoritative width; wider windows can show unused space
|
||||
but never overflow.
|
||||
- EKP-owned layout properties are visible to APIs that explicitly inspect
|
||||
text properties. The character stream is clean; copy/kill removes the
|
||||
owned projection metadata.
|
||||
- Tabs and non-ASCII whitespace cannot be shrunk with `space-width`. If an
|
||||
exact plan would require that operation, EKP leaves the affected hard
|
||||
paragraph natural and reports the conflict.
|
||||
- Measurement follows the current buffer's face remappings
|
||||
(`text-scale-mode`, themes, `ekp-org-setup`-style tweaks) and
|
||||
reserves the truncation-indicator column in windows without
|
||||
@ -336,7 +387,7 @@ emacs -Q -L /path/to/emacs-kp -L /path/to/emacs-kp/tests \
|
||||
|
||||
The matrix prints every row and exits with status 1 if any fit check fails,
|
||||
so the same command can gate local release automation. The verifier is a
|
||||
developer tool under `tests/`; it is not loaded by `(require 'ekp-region)`.
|
||||
developer tool under `tests/`; it is not loaded by `(require 'ekp-buffer)`.
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
99
readme_zh.md
99
readme_zh.md
@ -14,8 +14,12 @@ Emacs-kp 在 Emacs 内部完整实现了 Knuth-Plass 最优断行算法,支持
|
||||
独立可调。
|
||||
- **连字符断词** — Frank Liang 算法(TeX 同款),内置 49 份带固定来源
|
||||
与 SHA-256 的 Hunspell pattern 词典。
|
||||
- **像素级两端对齐** — 每一行渲染宽度精确等于目标像素宽度(通过
|
||||
`display (space :width ...)` 属性实现),支持变宽字体。
|
||||
- **像素级两端对齐** — 一份语义 layout plan 同时驱动字符串与 buffer
|
||||
渲染器。字符串 API 使用像素空格;buffer 组合 `space-width` 与绝对
|
||||
像素 `min-width`,不插入排版字符也能支持变宽字体。
|
||||
- **干净且可编辑的 buffer** — buffer 命令不创建 overlay,也不会向字符流
|
||||
加入 glue 空格、软换行或断词连字符。`buffer-string`、`char-after`、
|
||||
搜索、语法、保存及普通 Elisp 文本 API 看到的都是源字符。
|
||||
- **文本属性保留** — face、颜色等属性完整保留;断词插入的连字符继承所
|
||||
在单词的样式。
|
||||
- **困难输入不丢内容** — 超长不可断 token(URL、窄栏长词)退化为紧急
|
||||
@ -35,7 +39,7 @@ Emacs-kp 在 Emacs 内部完整实现了 Knuth-Plass 最优断行算法,支持
|
||||
```elisp
|
||||
(add-to-list 'load-path "/path/to/emacs-kp")
|
||||
(require 'ekp)
|
||||
(require 'ekp-region) ; buffer/region 命令
|
||||
(require 'ekp-buffer) ; buffer/region 命令
|
||||
```
|
||||
|
||||
强烈建议字节编译——编译后 Elisp 引擎约快 10 倍。
|
||||
@ -72,47 +76,75 @@ Elisp。已启用模块若 signal,则作为后端契约错误直接呈现。若
|
||||
|
||||
## 交互使用(buffer 与 region)
|
||||
|
||||
`ekp-region.el` 把字符串 API 变成 buffer 级命令:
|
||||
`ekp-buffer.el` 把字符串 API 变成 buffer 级命令:
|
||||
|
||||
```elisp
|
||||
(require 'ekp-region)
|
||||
(require 'ekp-buffer)
|
||||
```
|
||||
|
||||
- `M-x ekp-justify-region` — 把选区排版到窗口文本宽度(数字前缀参数
|
||||
可指定像素宽)。没有激活选区时,排版光标所在段落。
|
||||
- `M-x ekp-justify-buffer` — 排版整个 buffer。
|
||||
- `M-x ekp-unjustify-region` / `ekp-unjustify-buffer` — **精确**还原
|
||||
原文,包括被折叠的连续空格。排版是无损的:每个合成空隙、软换行、
|
||||
软连字符都携带它所替换的原文,还原是纯结构变换,即使排版后又编辑
|
||||
过也能正确还原。
|
||||
- `M-x ekp-auto-justify-mode` — 让整个 buffer 保持按窗口宽度排版。
|
||||
窗口宽度变化时自动重排(防抖延迟 `ekp-auto-justify-resize-delay`);
|
||||
编辑后只重排被改动的段落(空闲延迟 `ekp-auto-justify-edit-delay`),
|
||||
未变段落直接命中段落缓存。关闭 mode 时 buffer 精确恢复原状。
|
||||
- `M-x ekp-unjustify-region` / `ekp-unjustify-buffer` — 移除 EKP 的
|
||||
显示投影。buffer 排版从未替换源文本,因此不需要“还原字符”。
|
||||
- `M-x ekp-auto-justify-mode` — 让已完成硬段落保持按窗口宽度排版,
|
||||
同时让普通输入保持稳定。活动硬行由“已提交投影 + 一个局部编辑事务”
|
||||
组成。同一原生视觉行内输入时,不会规划整条硬行,也不会改写无关的已
|
||||
投影行。编辑中间已投影行时,只让该行的真实脏岛恢复自然;后续断行锚点
|
||||
保持不动,局部单词迁移交给 Emacs 原生软折行。输入自然跨入下一视觉行
|
||||
时,EKP 才调用或复用一次完整 `ekp-layout-plan`,并原子发布所有已完成
|
||||
行;新行继续自然显示。删除后插回完全相同的源文本会立即逐属性恢复保存
|
||||
的完整投影。
|
||||
仅移动 point 永远不会规划或写布局属性,即使 point 离开段落也是如此。
|
||||
全局提交只发生在视觉行跨越、硬换行/段落完成、下一次真实编辑发生在
|
||||
别处、显式 refill,或宽度/字体/布局上下文变化时。不存在编辑后空闲触发
|
||||
的整段跳变。窗口尺寸变化仍通过
|
||||
`ekp-auto-justify-resize-delay` 防抖。
|
||||
mode 启用期间会临时关闭显式行截断以及 Emacs 的窄分栏窗口截断,
|
||||
因此左右分栏再窄也会正常软折行;关闭 mode 时会精确恢复这两个变量
|
||||
原来的 buffer-local 或全局所有权。
|
||||
活动行首尾输入一个空格或 tab 会在同一次输入中立即可见;删除其后的
|
||||
字符也不会把该源空白隐藏。重投影还会分别保持 mark 位置与
|
||||
`mark-active`,因此改变宽度不会把旧 mark 变成意外选区。
|
||||
mode 激活时,标准 **EKP** 菜单提供排版、保护与窗口适配诊断命令;
|
||||
`C-h m` 也会说明同一套流程。
|
||||
|
||||
buffer 被当作活的文档,而不只是画布:
|
||||
投影只使用现有源字素上的文本属性:
|
||||
|
||||
- **保存**时写入的是**逻辑文本**——软换行、glue 空格、断词连字符属于
|
||||
排版而非内容,不会落盘;即使写盘失败或中断,屏幕 buffer 也保持排版态。
|
||||
该保证适用于整 buffer 保存;显式只写局部的 `write-region` 会写入所选
|
||||
区域当前的物理 buffer 表示。
|
||||
- **搜索**(isearch)看到的是逻辑文本,中文短语与被断词的英文单词
|
||||
都能跨排版找到。
|
||||
- **复制**放进 kill ring 的是逻辑文本,粘贴出去的是文字而非像素间距;
|
||||
已有 mode/user substring filter 会继续生效,最后一个排版区间移除后
|
||||
精确恢复。
|
||||
- 仅仅开启 mode 不会把 buffer 标记为已修改(不产生锁文件或 auto-save),
|
||||
重排定时器也不再与 `undo` 打架。
|
||||
- 源 ASCII 空格使用
|
||||
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`。
|
||||
- 没有源空格的 CJK/混排间距,把 `min-width` 加到前一个完整字素上,
|
||||
目标值为“字素自然 advance + glue”。
|
||||
- 缩进使用 `line-prefix`;视觉断行与断词连字符使用挂在现有完整字素上的
|
||||
replacing display string。
|
||||
- EKP 绝不创建 overlay,也不抢占外部的 replacing `display`、
|
||||
`line-prefix`、`wrap-prefix`、`composition` 或 `invisible`。有冲突
|
||||
的硬段落保持自然显示,`M-x ekp-diagnose` 会报告原因。
|
||||
|
||||
`ekp-region-margin-pixel`(默认 2)是从窗口宽度中扣除的取整安全边距。
|
||||
因此:
|
||||
|
||||
- **Elisp API 与保存**直接看到原始字符序列,视觉空格、换行和连字符不可能
|
||||
进入磁盘、语法或搜索逻辑。`buffer-substring` 会保留文本属性,所以可能
|
||||
携带 EKP 的显示属性;`buffer-substring-no-properties` 是纯源字符串。
|
||||
- **搜索**(包括 isearch)直接搜索源文本,一个拉丁单词不会因为视觉断词而
|
||||
变成两个词。
|
||||
- **复制/剪切**会在组合已有 substring filter 的同时移除 EKP 自有投影
|
||||
属性;粘贴内容只包含逻辑文本及非 EKP 属性。
|
||||
- 投影更新包在 `with-silent-modifications` 中:启用、编辑、缩放及关闭
|
||||
排版不会制造仅由布局引起的 undo 条目、modified 状态或字符修改 tick。
|
||||
|
||||
`ekp-buffer-margin-pixel`(默认 2)是从窗口宽度中扣除的取整安全边距。
|
||||
|
||||
大 buffer(超过 `ekp-auto-justify-lazy-threshold` 字符,默认 2 万)
|
||||
自动改为可视优先重排:屏幕内的部分同步完成,其余在空闲时后台分块
|
||||
补齐,每个时间片有时间预算(`ekp-auto-justify-tick-budget`),并优先
|
||||
处理你滚动到的区域。
|
||||
|
||||
自动规划还按硬段落设有上限。`ekp-auto-justify-paragraph-limit` 默认
|
||||
2 048 字符。更长的单个硬段落保持自然折行与完整可编辑性,避免一次无界
|
||||
Knuth-Plass 计算阻塞输入;`M-x ekp-diagnose` 会报告这一原因。确实需要
|
||||
对该段执行无上限完整质量排版时,显式运行 `M-x ekp-refill-paragraph`。
|
||||
|
||||
各 mode 的 verbatim 保护预设——各一行:
|
||||
|
||||
```elisp
|
||||
@ -126,9 +158,9 @@ buffer 被当作活的文档,而不只是画布:
|
||||
### 保护代码块与 verbatim 文本
|
||||
|
||||
- 段落级:携带 `ekp-verbatim` 文本属性(`M-x ekp-verbatim-region`)、
|
||||
face 在 `ekp-region-skip-faces` 列表中(如 `org-block`、
|
||||
face 在 `ekp-buffer-skip-faces` 列表中(如 `org-block`、
|
||||
`markdown-code-face`)、或被 buffer-local 的
|
||||
`ekp-region-skip-predicate` 判定的段落**原样跳过**,一个字节都不动。
|
||||
`ekp-buffer-skip-predicate` 判定的段落**原样跳过**,一个字节都不动。
|
||||
- 行内级:带 `ekp-no-break` 属性的区间(`M-x ekp-no-break-region`)
|
||||
成为刚性原子——不断行、不断词、空格保持字面宽度——适合行内代码、
|
||||
产品名、数字加单位。
|
||||
@ -136,7 +168,7 @@ buffer 被当作活的文档,而不只是画布:
|
||||
手动属性明确只在**当前 buffer 会话**有效:普通文本保存与重新打开不会
|
||||
恢复它们。使用 `M-x ekp-allow-break-region` /
|
||||
`ekp-clear-verbatim-region` 清除。需要从持久文档语法派生保护时,使用
|
||||
mode face 或 buffer-local 的 `ekp-region-skip-predicate`(Org/Markdown
|
||||
mode face 或 buffer-local 的 `ekp-buffer-skip-predicate`(Org/Markdown
|
||||
预设会自动这样做)。
|
||||
|
||||
## 排版特性
|
||||
@ -250,6 +282,13 @@ Silicon 测得;方法见 DEVELOPER_ZH.md:
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 文本属性属于 buffer,不能为同一 buffer 在不同宽度窗口保存两套 plan。
|
||||
EKP 以显示该 buffer 的最窄活动窗口为权威宽度;较宽窗口可能右侧留白,
|
||||
但不会溢出。
|
||||
- 显式检查文本属性的 Elisp API 能看到 EKP 自有布局属性;干净保证针对
|
||||
字符流。复制/剪切会移除这些投影元数据。
|
||||
- `space-width` 不能缩窄 tab 或非 ASCII 空白。若精确 plan 要求这种操作,
|
||||
EKP 会让受影响硬段落保持自然显示并报告冲突。
|
||||
- 测量会跟随当前 buffer 的 face 重映射(`text-scale-mode`、主题等),
|
||||
并在无 fringe 的窗口里为截断指示符预留一列,排版行贴合真实显示。
|
||||
若在特殊配置下仍出现截断或偏短,在该 buffer 里执行
|
||||
@ -287,7 +326,7 @@ emacs -Q -L /path/to/emacs-kp -L /path/to/emacs-kp/tests \
|
||||
|
||||
矩阵会打印全部行;任一贴合检查失败时以状态码 1 退出,因此同一命令可
|
||||
作为本地发布门禁。验证器是 `tests/` 下的开发工具,
|
||||
`(require 'ekp-region)` 不会加载它。
|
||||
`(require 'ekp-buffer)` 不会加载它。
|
||||
|
||||
## 致谢
|
||||
|
||||
|
||||
239
tests/ekp-buffer-live-bench.el
Normal file
239
tests/ekp-buffer-live-bench.el
Normal file
@ -0,0 +1,239 @@
|
||||
;;; ekp-buffer-live-bench.el --- Live edit benchmarks for EKP -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Reproducible public-path latency measurements for `ekp-auto-justify-mode'.
|
||||
;;
|
||||
;; emacs -Q --batch -L . -L tests -l tests/ekp-buffer-live-bench.el
|
||||
;;
|
||||
;; To benchmark the C backend, load it before this file. Batch glyph
|
||||
;; measurements are internally comparable but are not a replacement for
|
||||
;; the graphical dynamic verification. Bind `gc-cons-threshold' to
|
||||
;; `most-positive-fixnum' to separate mutator latency from GC pauses.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ekp-buffer)
|
||||
|
||||
(defconst ekp-buffer-live-bench--width 80)
|
||||
|
||||
(defconst ekp-buffer-live-bench--base
|
||||
(concat
|
||||
"alpha beta gamma delta epsilon zeta eta theta iota kappa lambda "
|
||||
"中文混排让前面的完整语义行共同调整 while the active line stays natural"))
|
||||
|
||||
(defvar ekp-buffer-live-bench--expected nil)
|
||||
(defvar ekp-buffer-live-bench--times nil)
|
||||
(defvar ekp-buffer-live-bench--gc-events 0)
|
||||
(defvar ekp-buffer-live-bench--gc-seconds 0.0)
|
||||
|
||||
(defun ekp-buffer-live-bench--percentile (values percentile)
|
||||
"Return PERCENTILE from numeric VALUES."
|
||||
(let* ((sorted (sort (copy-sequence values) #'<))
|
||||
(index (1- (ceiling (* percentile (length sorted))))))
|
||||
(nth (max 0 (min index (1- (length sorted)))) sorted)))
|
||||
|
||||
(defun ekp-buffer-live-bench--statistics ()
|
||||
"Return latency statistics for the current scenario."
|
||||
(let ((times (nreverse ekp-buffer-live-bench--times)))
|
||||
(list :edits (length times)
|
||||
:median (ekp-buffer-live-bench--percentile times 0.50)
|
||||
:p95 (ekp-buffer-live-bench--percentile times 0.95)
|
||||
:p99 (ekp-buffer-live-bench--percentile times 0.99)
|
||||
:max (apply #'max times)
|
||||
:gc-events ekp-buffer-live-bench--gc-events
|
||||
:gc-ms (* 1000 ekp-buffer-live-bench--gc-seconds))))
|
||||
|
||||
(defun ekp-buffer-live-bench--record-command (command expected)
|
||||
"Run public COMMAND, verify EXPECTED source, and record its latency."
|
||||
(let ((started (float-time))
|
||||
(gcs-before gcs-done)
|
||||
(gc-before gc-elapsed))
|
||||
(funcall command)
|
||||
(run-hooks 'post-command-hook)
|
||||
(push (* 1000 (- (float-time) started))
|
||||
ekp-buffer-live-bench--times)
|
||||
(cl-incf ekp-buffer-live-bench--gc-events
|
||||
(- gcs-done gcs-before))
|
||||
(cl-incf ekp-buffer-live-bench--gc-seconds
|
||||
(- gc-elapsed gc-before)))
|
||||
(setq ekp-buffer-live-bench--expected expected)
|
||||
(unless (equal (substring-no-properties (buffer-string)) expected)
|
||||
(error "Live benchmark source mismatch"))
|
||||
(when (overlays-in (point-min) (point-max))
|
||||
(error "Live benchmark created an overlay")))
|
||||
|
||||
(defun ekp-buffer-live-bench--insert (character)
|
||||
"Insert CHARACTER through `self-insert-command'."
|
||||
(ekp-buffer-live-bench--record-command
|
||||
(lambda ()
|
||||
(let ((last-command-event character))
|
||||
(call-interactively #'self-insert-command)))
|
||||
(concat ekp-buffer-live-bench--expected (string character))))
|
||||
|
||||
(defun ekp-buffer-live-bench--delete-backward ()
|
||||
"Delete one character through `delete-backward-char'."
|
||||
(ekp-buffer-live-bench--record-command
|
||||
(lambda () (call-interactively #'delete-backward-char))
|
||||
(substring ekp-buffer-live-bench--expected 0 -1)))
|
||||
|
||||
(defun ekp-buffer-live-bench--append-workload ()
|
||||
"Type enough mixed prose to cross several semantic boundaries."
|
||||
(dotimes (_ 3)
|
||||
(mapc #'ekp-buffer-live-bench--insert
|
||||
(string-to-list
|
||||
(concat
|
||||
" extraordinary editing continues smoothly 中文拉丁混排"
|
||||
" and all earlier complete lines may move together")))))
|
||||
|
||||
(defun ekp-buffer-live-bench--cache-workload ()
|
||||
"Revisit two historical paragraph states repeatedly."
|
||||
(dotimes (_ 80)
|
||||
(ekp-buffer-live-bench--insert ?x)
|
||||
(ekp-buffer-live-bench--delete-backward)))
|
||||
|
||||
(defun ekp-buffer-live-bench--motion-workload ()
|
||||
"Move point across semantic boundaries without changing projection."
|
||||
(let* ((state ekp-buffer--live-state)
|
||||
(transaction ekp-buffer--live-edit)
|
||||
(projection (buffer-substring (point-min) (point-max)))
|
||||
(active (ekp-buffer--live-state-active-index state))
|
||||
(signatures (ekp-buffer--live-state-signatures state))
|
||||
(prefix-end (ekp-buffer--live-state-prefix-end state))
|
||||
(prefix-position (marker-position prefix-end))
|
||||
(edit-end
|
||||
(and transaction
|
||||
(ekp-buffer--live-edit-edit-end transaction)))
|
||||
(edit-position (and edit-end (marker-position edit-end))))
|
||||
(dotimes (_ 48)
|
||||
(ekp-buffer-live-bench--record-command
|
||||
(lambda () (call-interactively #'backward-char))
|
||||
ekp-buffer-live-bench--expected))
|
||||
(dotimes (_ 48)
|
||||
(ekp-buffer-live-bench--record-command
|
||||
(lambda () (call-interactively #'forward-char))
|
||||
ekp-buffer-live-bench--expected))
|
||||
(unless (and (eq ekp-buffer--live-state state)
|
||||
(= (ekp-buffer--live-state-active-index state) active)
|
||||
(eq (ekp-buffer--live-state-signatures state) signatures)
|
||||
(eq ekp-buffer--live-edit transaction)
|
||||
(eq (ekp-buffer--live-state-prefix-end state) prefix-end)
|
||||
(= (marker-position prefix-end) prefix-position)
|
||||
(or (null edit-end)
|
||||
(and (eq (ekp-buffer--live-edit-edit-end transaction)
|
||||
edit-end)
|
||||
(= (marker-position edit-end) edit-position)))
|
||||
(equal-including-properties
|
||||
(buffer-substring (point-min) (point-max))
|
||||
projection))
|
||||
(error "Point motion changed the live projection"))))
|
||||
|
||||
(defun ekp-buffer-live-bench--boundary-workload ()
|
||||
"Create and remove hard boundaries through public commands."
|
||||
(dotimes (_ 24)
|
||||
(ekp-buffer-live-bench--record-command
|
||||
(lambda () (call-interactively #'newline))
|
||||
(concat ekp-buffer-live-bench--expected "\n"))
|
||||
(ekp-buffer-live-bench--delete-backward)))
|
||||
|
||||
(defun ekp-buffer-live-bench--instrument (workload)
|
||||
"Run WORKLOAD and return planner/cache counters."
|
||||
(let ((plan-calls 0)
|
||||
(cache-gets 0)
|
||||
(cache-hits 0)
|
||||
(original-plan (symbol-function 'ekp-layout-plan))
|
||||
(original-get (symbol-function 'ekp-buffer--live-cache-get)))
|
||||
(cl-letf (((symbol-function 'ekp-layout-plan)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf plan-calls)
|
||||
(apply original-plan arguments)))
|
||||
((symbol-function 'ekp-buffer--live-cache-get)
|
||||
(lambda (key)
|
||||
(cl-incf cache-gets)
|
||||
(let ((plan (funcall original-get key)))
|
||||
(when plan (cl-incf cache-hits))
|
||||
plan))))
|
||||
(funcall workload))
|
||||
(list :plan-calls plan-calls
|
||||
:cache-hits cache-hits
|
||||
:cache-misses (- cache-gets cache-hits))))
|
||||
|
||||
(defun ekp-buffer-live-bench--scenario (name workload)
|
||||
"Run named live-edit WORKLOAD and return its measurements."
|
||||
(with-temp-buffer
|
||||
(text-mode)
|
||||
(insert ekp-buffer-live-bench--base)
|
||||
(goto-char (point-max))
|
||||
(cl-letf (((symbol-function 'ekp-buffer--window-pixel)
|
||||
(lambda (&optional _window)
|
||||
ekp-buffer-live-bench--width)))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(let ((ekp-buffer-live-bench--expected
|
||||
(substring-no-properties (buffer-string)))
|
||||
(ekp-buffer-live-bench--times nil)
|
||||
(ekp-buffer-live-bench--gc-events 0)
|
||||
(ekp-buffer-live-bench--gc-seconds 0.0))
|
||||
(garbage-collect)
|
||||
(unwind-protect
|
||||
(append (list :name name)
|
||||
(ekp-buffer-live-bench--statistics-after
|
||||
workload))
|
||||
(ekp-auto-justify-mode -1))))))
|
||||
|
||||
(defun ekp-buffer-live-bench--statistics-after (workload)
|
||||
"Run WORKLOAD with instrumentation and return combined statistics."
|
||||
(let ((counters (ekp-buffer-live-bench--instrument workload)))
|
||||
(append (ekp-buffer-live-bench--statistics)
|
||||
counters
|
||||
(list :source-ok t
|
||||
:conflicts (length ekp-buffer--conflicts)
|
||||
:cache-size (length ekp-buffer--live-plan-cache)))))
|
||||
|
||||
(defun ekp-buffer-live-bench--format (result)
|
||||
"Format live benchmark RESULT as one report row."
|
||||
(format
|
||||
"%-15s %5d %8.3f %8.3f %8.3f %8.3f %6d %6d %6d %3d %7.2f %5d %s"
|
||||
(plist-get result :name)
|
||||
(plist-get result :edits)
|
||||
(plist-get result :median)
|
||||
(plist-get result :p95)
|
||||
(plist-get result :p99)
|
||||
(plist-get result :max)
|
||||
(plist-get result :plan-calls)
|
||||
(plist-get result :cache-hits)
|
||||
(plist-get result :cache-misses)
|
||||
(plist-get result :gc-events)
|
||||
(plist-get result :gc-ms)
|
||||
(plist-get result :cache-size)
|
||||
(if (<= (plist-get result :p99) 16.0) "PASS" "MISS")))
|
||||
|
||||
;;;###autoload
|
||||
(defun ekp-buffer-live-bench-run ()
|
||||
"Run live-edit scenarios and print percentile/caching evidence."
|
||||
(let ((scenarios
|
||||
`(("append" . ,#'ekp-buffer-live-bench--append-workload)
|
||||
("cache-revisit" . ,#'ekp-buffer-live-bench--cache-workload)
|
||||
("point-motion" . ,#'ekp-buffer-live-bench--motion-workload)
|
||||
("hard-boundary" . ,#'ekp-buffer-live-bench--boundary-workload))))
|
||||
(princ
|
||||
(format "EKP live edit benchmark: engine=%s width=%dpx\n"
|
||||
(if (and ekp-use-c-module (ekp--c-available-p)) "C" "elisp")
|
||||
ekp-buffer-live-bench--width))
|
||||
(princ
|
||||
(concat
|
||||
"scenario edits median p95 p99 max"
|
||||
" plans hits misses gc gc-ms cache frame\n"))
|
||||
(dolist (scenario scenarios)
|
||||
(princ
|
||||
(concat
|
||||
(ekp-buffer-live-bench--format
|
||||
(ekp-buffer-live-bench--scenario
|
||||
(car scenario) (cdr scenario)))
|
||||
"\n")))))
|
||||
|
||||
(ekp-buffer-live-bench-run)
|
||||
|
||||
(provide 'ekp-buffer-live-bench)
|
||||
|
||||
;;; ekp-buffer-live-bench.el ends here
|
||||
2310
tests/ekp-buffer-tests.el
Normal file
2310
tests/ekp-buffer-tests.el
Normal file
File diff suppressed because it is too large
Load Diff
344
tests/ekp-c-resize-evaluator.el
Normal file
344
tests/ekp-c-resize-evaluator.el
Normal file
@ -0,0 +1,344 @@
|
||||
;;; ekp-c-resize-evaluator.el --- C resize performance gate -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; One process measures one frozen-baseline or candidate round. Compare mode
|
||||
;; aggregates the interleaved JSONL records and enforces the task032 contract.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'json)
|
||||
|
||||
(defvar ekp-use-c-module)
|
||||
(defvar ekp-auto-justify-mode)
|
||||
(defvar ekp-auto-justify-lazy-threshold)
|
||||
(defvar ekp-buffer--auto-width)
|
||||
(declare-function ekp-showcase--sample "ekp-showcase")
|
||||
(declare-function ekp-buffer--paragraph-ranges "ekp-buffer")
|
||||
(declare-function ekp-buffer--skip-paragraph-p "ekp-buffer")
|
||||
(declare-function ekp-buffer--reflow "ekp-buffer")
|
||||
(declare-function ekp--get-para "ekp")
|
||||
(declare-function ekp-para-dp-cache "ekp")
|
||||
(declare-function ekp-justify-region "ekp-buffer")
|
||||
(declare-function ekp-clear-caches "ekp")
|
||||
(declare-function ekp-c-module-load "ekp-utils")
|
||||
(declare-function ekp--c-available-p "ekp")
|
||||
(declare-function ekp-c-version "ext:ekp")
|
||||
|
||||
(defconst ekp-c-resize-evaluator--widths
|
||||
'(480 452 424 396 368 340 312 284 256 228 200 172))
|
||||
|
||||
(defconst ekp-c-resize-evaluator--warm-width 520)
|
||||
(defconst ekp-c-resize-evaluator--target-ms 50.0)
|
||||
(defconst ekp-c-resize-evaluator--minimum-improvement 20.0)
|
||||
|
||||
(defvar ekp-c-resize-evaluator--plan-ms 0.0)
|
||||
(defvar ekp-c-resize-evaluator--module-ms 0.0)
|
||||
(defvar ekp-c-resize-evaluator--install-ms 0.0)
|
||||
(defvar ekp-c-resize-evaluator--clear-ms 0.0)
|
||||
|
||||
(defun ekp-c-resize-evaluator--fixtures ()
|
||||
"Return the fixed length matrix used by the evaluator."
|
||||
(let ((sample (ekp-showcase--sample)))
|
||||
`(("showcase" . ,sample)
|
||||
("showcase-2x" . ,(concat sample "\n\n" sample)))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--timed-call (counter function arguments)
|
||||
"Call FUNCTION with ARGUMENTS and add elapsed milliseconds to COUNTER."
|
||||
(let ((start (float-time)))
|
||||
(prog1 (apply function arguments)
|
||||
(set counter
|
||||
(+ (symbol-value counter)
|
||||
(* 1000.0 (- (float-time) start)))))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--reset-profile ()
|
||||
"Reset per-reflow layer counters."
|
||||
(setq ekp-c-resize-evaluator--plan-ms 0.0
|
||||
ekp-c-resize-evaluator--module-ms 0.0
|
||||
ekp-c-resize-evaluator--install-ms 0.0
|
||||
ekp-c-resize-evaluator--clear-ms 0.0))
|
||||
|
||||
(defun ekp-c-resize-evaluator--layout-hash ()
|
||||
"Return a stable hash of the complete source and projection."
|
||||
(secure-hash
|
||||
'sha256
|
||||
(prin1-to-string (buffer-substring (point-min) (point-max)))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--paragraphs ()
|
||||
"Return cached paragraph objects for the current logical buffer."
|
||||
(cl-loop for range in
|
||||
(ekp-buffer--paragraph-ranges (point-min) (point-max))
|
||||
for text = (buffer-substring (car range) (cdr range))
|
||||
unless (ekp-buffer--skip-paragraph-p text)
|
||||
collect (ekp--get-para text)))
|
||||
|
||||
(defun ekp-c-resize-evaluator--clear-dp (paragraphs)
|
||||
"Clear only width-dependent DP entries in PARAGRAPHS."
|
||||
(dolist (paragraph paragraphs)
|
||||
(clrhash (ekp-para-dp-cache paragraph))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--profiled-call (function arguments)
|
||||
"Call FUNCTION with ARGUMENTS and return total plus layer timings."
|
||||
(let ((plan (symbol-function 'ekp-layout-plan))
|
||||
(module (symbol-function 'ekp-c-break-with-arrays))
|
||||
(install (symbol-function 'ekp-buffer--install-plan))
|
||||
(live-install (symbol-function 'ekp-buffer--install-live-prefix))
|
||||
(clear (symbol-function 'ekp-buffer--clear-projection))
|
||||
(start (float-time)))
|
||||
(ekp-c-resize-evaluator--reset-profile)
|
||||
(cl-letf (((symbol-function 'ekp-layout-plan)
|
||||
(lambda (&rest args)
|
||||
(ekp-c-resize-evaluator--timed-call
|
||||
'ekp-c-resize-evaluator--plan-ms plan args)))
|
||||
((symbol-function 'ekp-c-break-with-arrays)
|
||||
(lambda (&rest args)
|
||||
(ekp-c-resize-evaluator--timed-call
|
||||
'ekp-c-resize-evaluator--module-ms module args)))
|
||||
((symbol-function 'ekp-buffer--install-plan)
|
||||
(lambda (&rest args)
|
||||
(ekp-c-resize-evaluator--timed-call
|
||||
'ekp-c-resize-evaluator--install-ms install args)))
|
||||
((symbol-function 'ekp-buffer--install-live-prefix)
|
||||
(lambda (&rest args)
|
||||
(ekp-c-resize-evaluator--timed-call
|
||||
'ekp-c-resize-evaluator--install-ms live-install args)))
|
||||
((symbol-function 'ekp-buffer--clear-projection)
|
||||
(lambda (&rest args)
|
||||
(ekp-c-resize-evaluator--timed-call
|
||||
'ekp-c-resize-evaluator--clear-ms clear args))))
|
||||
(apply function arguments))
|
||||
`((total_ms . ,(* 1000.0 (- (float-time) start)))
|
||||
(plan_ms . ,ekp-c-resize-evaluator--plan-ms)
|
||||
(module_ms . ,ekp-c-resize-evaluator--module-ms)
|
||||
(install_ms . ,ekp-c-resize-evaluator--install-ms)
|
||||
(clear_ms . ,ekp-c-resize-evaluator--clear-ms))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--measure-core-fixture (fixture)
|
||||
"Measure every uncached core width for FIXTURE."
|
||||
(with-temp-buffer
|
||||
(insert (cdr fixture))
|
||||
(let ((paragraphs (ekp-c-resize-evaluator--paragraphs))
|
||||
samples)
|
||||
(ekp-justify-region
|
||||
(point-min) (point-max) ekp-c-resize-evaluator--warm-width)
|
||||
(dolist (width ekp-c-resize-evaluator--widths)
|
||||
(ekp-c-resize-evaluator--clear-dp paragraphs)
|
||||
(let ((before gcs-done)
|
||||
(sample
|
||||
(ekp-c-resize-evaluator--profiled-call
|
||||
(symbol-function 'ekp-justify-region)
|
||||
(list (point-min) (point-max) width))))
|
||||
(unless (= before gcs-done)
|
||||
(error "GC occurred inside resize sample"))
|
||||
(push (append `((path . "core")
|
||||
(fixture . ,(car fixture)) (width . ,width))
|
||||
sample)
|
||||
samples)))
|
||||
(nreverse samples))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--measure-resize-fixture (fixture)
|
||||
"Measure every uncached full resize width for FIXTURE."
|
||||
(with-temp-buffer
|
||||
(insert (cdr fixture))
|
||||
(let ((paragraphs (ekp-c-resize-evaluator--paragraphs))
|
||||
(ekp-auto-justify-mode t)
|
||||
(ekp-auto-justify-lazy-threshold most-positive-fixnum)
|
||||
samples)
|
||||
(ekp-buffer--reflow
|
||||
(current-buffer) ekp-c-resize-evaluator--warm-width)
|
||||
(dolist (width ekp-c-resize-evaluator--widths)
|
||||
(ekp-c-resize-evaluator--clear-dp paragraphs)
|
||||
(let ((before gcs-done)
|
||||
(sample
|
||||
(ekp-c-resize-evaluator--profiled-call
|
||||
(symbol-function 'ekp-buffer--reflow)
|
||||
(list (current-buffer) width))))
|
||||
(unless (= before gcs-done)
|
||||
(error "GC occurred inside resize sample"))
|
||||
(push (append `((path . "resize")
|
||||
(fixture . ,(car fixture)) (width . ,width))
|
||||
sample)
|
||||
samples)))
|
||||
(nreverse samples))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--layout-core-fixture (fixture)
|
||||
"Return core projection hashes for every width of FIXTURE."
|
||||
(with-temp-buffer
|
||||
(insert (cdr fixture))
|
||||
(cl-loop for width in ekp-c-resize-evaluator--widths
|
||||
do (ekp-justify-region (point-min) (point-max) width)
|
||||
collect `((path . "core") (fixture . ,(car fixture))
|
||||
(width . ,width)
|
||||
(hash . ,(ekp-c-resize-evaluator--layout-hash))))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--layout-resize-fixture (fixture)
|
||||
"Return full resize projection hashes for every width of FIXTURE."
|
||||
(with-temp-buffer
|
||||
(insert (cdr fixture))
|
||||
(let ((ekp-auto-justify-mode t)
|
||||
(ekp-auto-justify-lazy-threshold most-positive-fixnum))
|
||||
(cl-loop for width in ekp-c-resize-evaluator--widths
|
||||
do (ekp-buffer--reflow (current-buffer) width)
|
||||
collect `((path . "resize") (fixture . ,(car fixture))
|
||||
(width . ,width)
|
||||
(hash . ,(ekp-c-resize-evaluator--layout-hash)))))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--layout-matrix (use-c)
|
||||
"Return the fixed layout matrix with USE-C selecting the engine."
|
||||
(let ((ekp-use-c-module use-c))
|
||||
(ekp-clear-caches)
|
||||
(let ((core
|
||||
(cl-mapcan #'ekp-c-resize-evaluator--layout-core-fixture
|
||||
(ekp-c-resize-evaluator--fixtures))))
|
||||
(ekp-clear-caches)
|
||||
(append
|
||||
core
|
||||
(cl-mapcan #'ekp-c-resize-evaluator--layout-resize-fixture
|
||||
(ekp-c-resize-evaluator--fixtures))))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--measure-round ()
|
||||
"Measure one C round and return a JSON-compatible record."
|
||||
(require 'ekp)
|
||||
(require 'ekp-buffer)
|
||||
(require 'ekp-showcase)
|
||||
(ekp-c-module-load)
|
||||
(unless (ekp--c-available-p)
|
||||
(error "C module did not load"))
|
||||
(let ((gc-cons-threshold most-positive-fixnum)
|
||||
(label (or (getenv "EKP_RESIZE_LABEL") "unknown"))
|
||||
(round (string-to-number (or (getenv "EKP_RESIZE_ROUND") "0"))))
|
||||
(garbage-collect)
|
||||
(ekp-clear-caches)
|
||||
(let* ((core
|
||||
(cl-mapcan #'ekp-c-resize-evaluator--measure-core-fixture
|
||||
(ekp-c-resize-evaluator--fixtures)))
|
||||
(_ (ekp-clear-caches))
|
||||
(resize
|
||||
(cl-mapcan #'ekp-c-resize-evaluator--measure-resize-fixture
|
||||
(ekp-c-resize-evaluator--fixtures)))
|
||||
(samples (append core resize))
|
||||
(c-layouts (ekp-c-resize-evaluator--layout-matrix t))
|
||||
(elisp-layouts (ekp-c-resize-evaluator--layout-matrix nil)))
|
||||
`((label . ,label) (round . ,round)
|
||||
(module_version . ,(ekp-c-version))
|
||||
(c_elisp_parity . ,(equal c-layouts elisp-layouts))
|
||||
(layouts . ,c-layouts) (samples . ,samples)))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--write-record (record)
|
||||
"Append JSON RECORD to `EKP_RESIZE_OUTPUT'."
|
||||
(let ((path (getenv "EKP_RESIZE_OUTPUT")))
|
||||
(unless path (error "EKP_RESIZE_OUTPUT is required"))
|
||||
(write-region (concat (json-encode record) "\n") nil path t 'silent)))
|
||||
|
||||
(defun ekp-c-resize-evaluator--read-jsonl (path)
|
||||
"Read JSON objects from PATH."
|
||||
(with-temp-buffer
|
||||
(insert-file-contents path)
|
||||
(mapcar (lambda (line)
|
||||
(json-parse-string line :object-type 'alist
|
||||
:array-type 'list))
|
||||
(split-string (buffer-string) "\n" t))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--sample-values (records path key)
|
||||
"Return numeric KEY values from PATH samples in RECORDS."
|
||||
(cl-loop for record in records
|
||||
append (cl-loop for sample in (alist-get 'samples record)
|
||||
when (equal (alist-get 'path sample) path)
|
||||
collect (alist-get key sample))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--percentile (values percentile)
|
||||
"Return nearest-rank PERCENTILE from VALUES."
|
||||
(let* ((sorted (sort (copy-sequence values) #'<))
|
||||
(rank (max 0 (1- (ceiling (* percentile (length sorted)))))))
|
||||
(nth rank sorted)))
|
||||
|
||||
(defun ekp-c-resize-evaluator--improvement (baseline candidate)
|
||||
"Return percentage improvement from BASELINE to CANDIDATE."
|
||||
(* 100.0 (/ (- baseline candidate) baseline)))
|
||||
|
||||
(defun ekp-c-resize-evaluator--all-parity-p (records)
|
||||
"Return non-nil when every RECORD matches its Elisp engine."
|
||||
(cl-every (lambda (record) (eq (alist-get 'c_elisp_parity record) t))
|
||||
records))
|
||||
|
||||
(defun ekp-c-resize-evaluator--cross-parity-p (baseline candidate)
|
||||
"Return non-nil when BASELINE and CANDIDATE layouts match exactly."
|
||||
(and (equal (alist-get 'layouts (car baseline))
|
||||
(alist-get 'layouts (car candidate)))
|
||||
(cl-every
|
||||
(lambda (record)
|
||||
(equal (alist-get 'layouts record)
|
||||
(alist-get 'layouts (car baseline))))
|
||||
(append baseline candidate))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--path-comparison (baseline candidate path)
|
||||
"Return metrics for PATH in BASELINE and CANDIDATE records."
|
||||
(let* ((base (ekp-c-resize-evaluator--sample-values
|
||||
baseline path 'total_ms))
|
||||
(cand (ekp-c-resize-evaluator--sample-values
|
||||
candidate path 'total_ms))
|
||||
(base-p50 (ekp-c-resize-evaluator--percentile base 0.50))
|
||||
(base-p95 (ekp-c-resize-evaluator--percentile base 0.95))
|
||||
(cand-p50 (ekp-c-resize-evaluator--percentile cand 0.50))
|
||||
(cand-p95 (ekp-c-resize-evaluator--percentile cand 0.95))
|
||||
(p50-gain (ekp-c-resize-evaluator--improvement base-p50 cand-p50))
|
||||
(p95-gain (ekp-c-resize-evaluator--improvement base-p95 cand-p95)))
|
||||
`((baseline_p50_ms . ,base-p50) (baseline_p95_ms . ,base-p95)
|
||||
(candidate_p50_ms . ,cand-p50) (candidate_p95_ms . ,cand-p95)
|
||||
(p50_improvement_pct . ,p50-gain)
|
||||
(p95_improvement_pct . ,p95-gain)
|
||||
(candidate_target_pass
|
||||
. ,(<= cand-p95 ekp-c-resize-evaluator--target-ms))
|
||||
(improvement_pass
|
||||
. ,(and (>= p50-gain ekp-c-resize-evaluator--minimum-improvement)
|
||||
(>= p95-gain ekp-c-resize-evaluator--minimum-improvement))))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--comparison (baseline candidate)
|
||||
"Return core and resize metrics for BASELINE and CANDIDATE records."
|
||||
`((core . ,(ekp-c-resize-evaluator--path-comparison
|
||||
baseline candidate "core"))
|
||||
(resize . ,(ekp-c-resize-evaluator--path-comparison
|
||||
baseline candidate "resize"))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--path-pass-p (metrics)
|
||||
"Return non-nil when one path's METRICS pass both performance gates."
|
||||
(and (eq (alist-get 'candidate_target_pass metrics) t)
|
||||
(eq (alist-get 'improvement_pass metrics) t)))
|
||||
|
||||
(defun ekp-c-resize-evaluator--report (baseline candidate)
|
||||
"Return the final evaluator report for BASELINE and CANDIDATE."
|
||||
(let* ((metrics (ekp-c-resize-evaluator--comparison baseline candidate))
|
||||
(parity (and (ekp-c-resize-evaluator--all-parity-p baseline)
|
||||
(ekp-c-resize-evaluator--all-parity-p candidate)
|
||||
(ekp-c-resize-evaluator--cross-parity-p
|
||||
baseline candidate)))
|
||||
(pass (and parity
|
||||
(ekp-c-resize-evaluator--path-pass-p
|
||||
(alist-get 'core metrics))
|
||||
(ekp-c-resize-evaluator--path-pass-p
|
||||
(alist-get 'resize metrics)))))
|
||||
`((pass . ,pass) (layout_parity . ,parity)
|
||||
(core . ,(alist-get 'core metrics))
|
||||
(resize . ,(alist-get 'resize metrics)))))
|
||||
|
||||
(defun ekp-c-resize-evaluator--compare ()
|
||||
"Compare raw JSONL paths from the environment and enforce the contract."
|
||||
(let* ((baseline (ekp-c-resize-evaluator--read-jsonl
|
||||
(getenv "EKP_RESIZE_BASELINE_JSONL")))
|
||||
(candidate (ekp-c-resize-evaluator--read-jsonl
|
||||
(getenv "EKP_RESIZE_CANDIDATE_JSONL")))
|
||||
(report (ekp-c-resize-evaluator--report baseline candidate))
|
||||
(path (getenv "EKP_RESIZE_REPORT")))
|
||||
(when path
|
||||
(write-region (concat (json-encode report) "\n") nil path nil 'silent))
|
||||
(princ (concat (json-encode report) "\n"))
|
||||
(unless (eq (alist-get 'pass report) t)
|
||||
(kill-emacs 1))))
|
||||
|
||||
(if (equal (getenv "EKP_RESIZE_MODE") "compare")
|
||||
(ekp-c-resize-evaluator--compare)
|
||||
(ekp-c-resize-evaluator--write-record
|
||||
(ekp-c-resize-evaluator--measure-round)))
|
||||
|
||||
;;; ekp-c-resize-evaluator.el ends here
|
||||
@ -11,6 +11,10 @@
|
||||
(require 'cl-lib)
|
||||
(require 'ekp)
|
||||
|
||||
(declare-function ekp-c-break-batch "ext:ekp")
|
||||
(declare-function ekp-c-break-with-arrays "ext:ekp")
|
||||
(declare-function ekp-c-set-penalties "ext:ekp")
|
||||
|
||||
(defun ekp-c-tests--available ()
|
||||
"Return non-nil when a compatible C module can be loaded."
|
||||
(or ekp-c-module-loaded
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,831 +0,0 @@
|
||||
;;; ekp-region-tests.el --- Tests for ekp-region.el -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Batch-safe ERT tests for the buffer-level justification layer.
|
||||
;; Widths are always passed explicitly, so no window is required.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ekp-region)
|
||||
|
||||
(defconst ekp-region-test--samples
|
||||
(list "简单的中文段落测试内容,排版效果应当良好稳定。"
|
||||
"The quick brown fox jumps over the lazy dog several times today."
|
||||
"Mixed 中英文 paragraph with double spaces inside and a tail "
|
||||
"para one\n\npara two 混排 content here\nthird para"
|
||||
" leading indent 段落内容 preserved intact"
|
||||
"短\n\n\n多个空段落之间的内容")
|
||||
"Logical texts covering CJK, Latin, mixed, blanks, indent, tails.")
|
||||
|
||||
(defconst ekp-region-test--widths '(30 80 200 400)
|
||||
"Pixel widths from emergency-narrow to comfortable.")
|
||||
|
||||
(defmacro ekp-region-test--with-text (text &rest body)
|
||||
"Run BODY in a temp buffer containing TEXT."
|
||||
(declare (indent 1))
|
||||
`(with-temp-buffer
|
||||
(insert ,text)
|
||||
,@body))
|
||||
|
||||
;;;; Roundtrip exactness
|
||||
|
||||
(ert-deftest ekp-region-test-roundtrip-exact ()
|
||||
"justify + unjustify restores text and properties exactly."
|
||||
(dolist (text ekp-region-test--samples)
|
||||
(dolist (w ekp-region-test--widths)
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) w)
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal-including-properties (buffer-string) text))))))
|
||||
|
||||
(ert-deftest ekp-region-test-roundtrip-propertized ()
|
||||
"Roundtrip preserves user text properties."
|
||||
(let ((text (concat (propertize "加粗的中文开头内容" 'face 'bold)
|
||||
" plain middle part "
|
||||
(propertize "italic tail words" 'face 'italic))))
|
||||
(dolist (w '(60 250))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) w)
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal-including-properties (buffer-string) text))))))
|
||||
|
||||
(ert-deftest ekp-region-test-hard-newlines-preserved ()
|
||||
"Hard newline count survives justification."
|
||||
(ekp-region-test--with-text "a 段落 one\n\nb 段落 two\nc 段落 three"
|
||||
(ekp-justify-region (point-min) (point-max) 100)
|
||||
(let ((hard 0))
|
||||
(goto-char (point-min))
|
||||
(while (search-forward "\n" nil t)
|
||||
(unless (get-text-property (match-beginning 0) 'ekp-soft-break)
|
||||
(setq hard (1+ hard))))
|
||||
(should (= hard 3)))))
|
||||
|
||||
;;;; Justified-state invariants
|
||||
|
||||
(ert-deftest ekp-region-test-justified-marked ()
|
||||
"Justified region carries the ekp-justified width property."
|
||||
(ekp-region-test--with-text "中文内容需要标记属性验证正确性"
|
||||
(ekp-justify-region (point-min) (point-max) 120)
|
||||
(should (eq (get-text-property (point-min) 'ekp-justified) 120))
|
||||
(should-not (text-property-not-all (point-min) (point-max)
|
||||
'ekp-justified 120))))
|
||||
|
||||
(ert-deftest ekp-region-test-rejustify-idempotent ()
|
||||
"Justifying at a new width equals a fresh justification at that width."
|
||||
(let ((text "The idempotence check 中英混排 must hold across widths."))
|
||||
(let (fresh)
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 150)
|
||||
(setq fresh (buffer-string)))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 300)
|
||||
(ekp-justify-region (point-min) (point-max) 150)
|
||||
(should (equal-including-properties (buffer-string) fresh))))))
|
||||
|
||||
;;;; Edit robustness
|
||||
|
||||
(ert-deftest ekp-region-test-edit-then-unjustify ()
|
||||
"Text typed into a justified buffer survives unjustification."
|
||||
(ekp-region-test--with-text "abcdef ghijkl 中文内容 mnopqr stuvwx"
|
||||
(ekp-justify-region (point-min) (point-max) 80)
|
||||
;; Insert inside the first word: physical == logical there.
|
||||
(goto-char (+ (point-min) 2))
|
||||
(insert "XY")
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal (buffer-string)
|
||||
"abXYcdef ghijkl 中文内容 mnopqr stuvwx"))))
|
||||
|
||||
(ert-deftest ekp-region-test-point-stable ()
|
||||
"Point returns to its logical position after a roundtrip."
|
||||
(ekp-region-test--with-text "abcdef ghijkl mnopqr stuvwx yzabcd"
|
||||
(goto-char (+ (point-min) 9)) ; inside "ghijkl"
|
||||
(ekp-justify-region (point-min) (point-max) 60)
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (= (point) (+ (point-min) 9)))))
|
||||
|
||||
;;;; Auto-justify mode
|
||||
|
||||
(defmacro ekp-region-test--with-mode (text width &rest body)
|
||||
"Enable `ekp-auto-justify-mode' on TEXT at WIDTH, run BODY, disable."
|
||||
(declare (indent 2))
|
||||
`(ekp-region-test--with-text ,text
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) ,width)))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(unwind-protect
|
||||
(progn ,@body)
|
||||
(ekp-auto-justify-mode -1)))))
|
||||
|
||||
(ert-deftest ekp-region-test-mode-roundtrip ()
|
||||
"Enabling then disabling the mode restores the buffer exactly."
|
||||
(let ((text "first paragraph 内容 aaa bbb ccc\nsecond paragraph 内容 ddd"))
|
||||
(ekp-region-test--with-mode text 150
|
||||
(should ekp-region--auto-width)
|
||||
(should (get-text-property (point-min) 'ekp-justified)))
|
||||
;; body ran; with-mode disabled the mode on exit — verify restore
|
||||
(ekp-region-test--with-text text
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 150)))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(ekp-auto-justify-mode -1)
|
||||
(should (equal-including-properties (buffer-string) text))))))
|
||||
|
||||
(ert-deftest ekp-region-test-mode-incremental-edit ()
|
||||
"Edits re-justify only the touched paragraph, content stays correct."
|
||||
(let ((text "aaa bbb ccc ddd eee fff\nggg hhh iii jjj kkk lll")
|
||||
(calls nil))
|
||||
(ekp-region-test--with-mode text 100
|
||||
(let ((orig (symbol-function 'ekp-justify-region)))
|
||||
(cl-letf (((symbol-function 'ekp-justify-region)
|
||||
(lambda (b e &optional px)
|
||||
(push (cons (marker-position (copy-marker b))
|
||||
(marker-position (copy-marker e)))
|
||||
calls)
|
||||
(funcall orig b e px))))
|
||||
;; Edit inside paragraph 1.
|
||||
(goto-char (+ (point-min) 4))
|
||||
(insert "zz")
|
||||
(should ekp-region--dirty)
|
||||
(ekp-region--flush-dirty (current-buffer))
|
||||
;; Exactly one incremental call, confined before the hard \n.
|
||||
(should (= (length calls) 1))
|
||||
(let ((hard-nl (save-excursion
|
||||
(goto-char (point-min))
|
||||
(catch 'nl
|
||||
(while (search-forward "\n" nil t)
|
||||
(unless (get-text-property (match-beginning 0)
|
||||
'ekp-soft-break)
|
||||
(throw 'nl (match-beginning 0))))))))
|
||||
(should (<= (cdar calls) hard-nl)))))
|
||||
;; Logical text after disable = original with the edit applied.
|
||||
(ekp-auto-justify-mode -1)
|
||||
(should (equal (buffer-string)
|
||||
"aaa zzbbb ccc ddd eee fff\nggg hhh iii jjj kkk lll"))
|
||||
;; re-enable so with-mode's cleanup disable is a no-op state-wise
|
||||
(ekp-auto-justify-mode 1))))
|
||||
|
||||
(ert-deftest ekp-region-test-verbatim-paragraph-skipped ()
|
||||
"A code-block paragraph stays byte-identical; prose around it justifies."
|
||||
(let* ((code (propertize "(defun foo (x) (list 1 2))"
|
||||
'ekp-verbatim t 'face 'font-lock-keyword-face))
|
||||
(text (concat "prose before with words enough to wrap lines\n"
|
||||
code
|
||||
"\nprose after also long enough to wrap lines")))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
;; the code line is still there, character-exact, spacing intact
|
||||
(goto-char (point-min))
|
||||
(should (search-forward "(defun foo (x) (list 1 2))" nil t))
|
||||
;; prose got justified (soft breaks appeared)
|
||||
(should (text-property-not-all (point-min) (point-max)
|
||||
'ekp-soft-break nil))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal-including-properties (buffer-string) text)))))
|
||||
|
||||
(ert-deftest ekp-region-test-skip-faces ()
|
||||
"Paragraphs wearing a skip face stay verbatim."
|
||||
(let* ((ekp-region-skip-faces '(font-lock-comment-face))
|
||||
(code (propertize ";; a comment line kept as-is"
|
||||
'face 'font-lock-comment-face))
|
||||
(text (concat "prose paragraph long enough to wrap\n" code)))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 15)
|
||||
(goto-char (point-min))
|
||||
(should (search-forward ";; a comment line kept as-is" nil t))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal-including-properties (buffer-string) text)))))
|
||||
|
||||
(ert-deftest ekp-region-test-skip-predicate ()
|
||||
"The paragraph predicate is the general escape hatch."
|
||||
(let ((text "keepme raw spacing\nnormal prose that wraps around"))
|
||||
(ekp-region-test--with-text text
|
||||
(setq-local ekp-region-skip-predicate
|
||||
(lambda (p) (string-prefix-p "keepme" p)))
|
||||
(ekp-justify-region (point-min) (point-max) 12)
|
||||
(goto-char (point-min))
|
||||
(should (looking-at-p "keepme raw spacing$"))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal (buffer-string) text)))))
|
||||
|
||||
(ert-deftest ekp-region-test-indent-roundtrip ()
|
||||
"First-line indent spacers vanish exactly on unjustify."
|
||||
(let ((ekp-first-line-indent 6)
|
||||
(text "首行缩进往返检查内容足够长断行几次"))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 30)
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (equal-including-properties (buffer-string) text)))))
|
||||
|
||||
(ert-deftest ekp-region-test-resize-hook-window-arg ()
|
||||
"The resize hook handles its WINDOW argument and foreign current buffer.
|
||||
Regression: buffer-local `window-size-change-functions' members get
|
||||
the displaying WINDOW, with an arbitrary buffer current."
|
||||
(let ((text "resize hook 检查 aaa bbb ccc ddd eee fff"))
|
||||
(ekp-region-test--with-mode text 200
|
||||
(let ((buf (current-buffer))
|
||||
(win (selected-window)))
|
||||
(set-window-buffer win buf)
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 120)))
|
||||
;; simulate redisplay: window argument, unrelated buffer current
|
||||
(with-temp-buffer
|
||||
(ekp-region--on-resize win)))
|
||||
(with-current-buffer buf
|
||||
(should (timerp ekp-region--resize-timer))
|
||||
(cancel-timer ekp-region--resize-timer)
|
||||
;; run what the timer would have run
|
||||
(ekp-region--reflow buf 120)
|
||||
(should (= ekp-region--auto-width 120)))))))
|
||||
|
||||
(ert-deftest ekp-region-test-mode-reflow-width ()
|
||||
"Reflow to a new width matches a fresh justification at that width."
|
||||
(let ((text "reflow 检查 aaa bbb ccc ddd eee fff ggg hhh")
|
||||
fresh)
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 90)
|
||||
(setq fresh (buffer-substring (point-min) (point-max))))
|
||||
(ekp-region-test--with-mode text 200
|
||||
(ekp-region--reflow (current-buffer) 90)
|
||||
(should (= ekp-region--auto-width 90))
|
||||
(let ((got (buffer-substring (point-min) (point-max))))
|
||||
;; ekp-justified was written at two widths; ignore that prop
|
||||
(remove-text-properties 0 (length got) '(ekp-justified nil) got)
|
||||
(remove-text-properties 0 (length fresh) '(ekp-justified nil) fresh)
|
||||
(should (equal-including-properties got fresh))))))
|
||||
|
||||
|
||||
(ert-deftest ekp-region-test-lazy-reflow-equals-oneshot ()
|
||||
"Visible-first chunked re-flow converges to the one-shot result."
|
||||
(let ((ekp-auto-justify-lazy-threshold 100)
|
||||
(ekp-auto-justify-chunk-size 3)
|
||||
(text (mapconcat #'identity
|
||||
(make-list 12 "段落内容 some words 足够长会换行的样子")
|
||||
"\n")))
|
||||
(ekp-region-test--with-text text
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 60))
|
||||
((symbol-function 'ekp-region--visible-span)
|
||||
(lambda () (cons (point-min) (min (point-max) 80)))))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(ekp-region--reflow (current-buffer) 50)
|
||||
(should ekp-region--pending)
|
||||
;; drain the background queue synchronously
|
||||
(let ((guard 0))
|
||||
(while (and ekp-region--pending (< guard 100))
|
||||
(when (timerp ekp-region--chunk-timer)
|
||||
(cancel-timer ekp-region--chunk-timer)
|
||||
(setq ekp-region--chunk-timer nil))
|
||||
(ekp-region--process-chunk (current-buffer))
|
||||
(setq guard (1+ guard))))
|
||||
(should-not ekp-region--pending)
|
||||
(let ((lazy (buffer-string)))
|
||||
(ekp-auto-justify-mode -1)
|
||||
(ekp-justify-region (point-min) (point-max) 50)
|
||||
(should (equal-including-properties (buffer-string) lazy)))))))
|
||||
|
||||
;;;; Editor-state integrity (save / modified / undo / stickiness)
|
||||
|
||||
(ert-deftest ekp-region-test-typed-char-inherits-no-marker ()
|
||||
"Text typed right after a glue must not inherit renderer markers.
|
||||
Regression: `self-insert-command' uses insert-and-inherit; a char
|
||||
inheriting `ekp-glue' was deleted as a synthesized space by the next
|
||||
unjustification."
|
||||
(ekp-region-test--with-text "aaa bbb 中文 ccc"
|
||||
(ekp-justify-region (point-min) (point-max) 200)
|
||||
(let ((glue-pos (text-property-not-all (point-min) (point-max)
|
||||
'ekp-glue nil)))
|
||||
(should glue-pos)
|
||||
(goto-char (1+ glue-pos))
|
||||
(insert-and-inherit "X")
|
||||
(let ((x (1+ glue-pos)))
|
||||
(should-not (get-text-property x 'ekp-glue))
|
||||
(should-not (get-text-property x 'display))
|
||||
(should-not (get-text-property x 'ekp-soft-break))))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (= 1 (cl-count ?X (buffer-string))))))
|
||||
|
||||
(ert-deftest ekp-region-test-save-writes-logical-text ()
|
||||
"Saving a justified file buffer writes the logical text to disk,
|
||||
keeps the buffer justified, and leaves it unmodified."
|
||||
(let* ((file (make-temp-file "ekp-save-test"))
|
||||
(text "中文保存测试内容足够长会断行的样子,再加一句凑长度。")
|
||||
(make-backup-files nil)
|
||||
(create-lockfiles nil))
|
||||
(unwind-protect
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(insert text)
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(should (> (cl-count ?\n (buffer-string)) 0))
|
||||
(save-buffer)
|
||||
;; Disk: logical text only, no layout newlines.
|
||||
(should (equal (with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(buffer-string))
|
||||
text))
|
||||
;; Buffer: still justified, and not "modified" vs its file.
|
||||
(should (get-text-property (point-min) 'ekp-justified))
|
||||
(should-not (buffer-modified-p))
|
||||
;; And a second save still works (state was reset).
|
||||
(insert "x")
|
||||
(goto-char (point-min))
|
||||
(save-buffer)
|
||||
(should (equal (with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(buffer-string))
|
||||
(concat text "x")))
|
||||
(let ((kill-buffer-query-functions nil))
|
||||
(kill-buffer)))
|
||||
(delete-file file))))
|
||||
|
||||
(ert-deftest ekp-region-test-failed-save-preserves-layout ()
|
||||
"A filesystem save failure must not leave the buffer unformatted."
|
||||
(let* ((dir (make-temp-file "ekp-save-fail-" t))
|
||||
(file (expand-file-name "file.txt" dir))
|
||||
(text "保存失败以后屏幕仍然保持排版状态 and remains editable")
|
||||
(make-backup-files nil)
|
||||
(create-lockfiles nil))
|
||||
(unwind-protect
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(setq-local require-final-newline nil)
|
||||
(insert text)
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(let ((layout (buffer-substring (point-min) (point-max))))
|
||||
(delete-directory dir t)
|
||||
(cl-letf (((symbol-function 'y-or-n-p)
|
||||
(lambda (&rest _) nil)))
|
||||
(should-error (save-buffer)))
|
||||
(should (equal-including-properties
|
||||
(buffer-substring (point-min) (point-max)) layout))
|
||||
(should (get-text-property (point-min) 'ekp-justified))
|
||||
(should (buffer-modified-p)))
|
||||
(let ((kill-buffer-query-functions nil))
|
||||
(set-buffer-modified-p nil)
|
||||
(kill-buffer)))
|
||||
(when (file-directory-p dir)
|
||||
(delete-directory dir t)))))
|
||||
|
||||
(ert-deftest ekp-region-test-interrupted-save-preserves-layout ()
|
||||
"A quit during writing must not leave the buffer unformatted."
|
||||
(let* ((file (make-temp-file "ekp-save-quit-"))
|
||||
(text "保存中断以后屏幕排版状态必须原样保留 with logical text")
|
||||
(make-backup-files nil)
|
||||
(create-lockfiles nil))
|
||||
(unwind-protect
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(setq-local require-final-newline nil)
|
||||
(insert text)
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(let ((layout (buffer-substring (point-min) (point-max))))
|
||||
(cl-letf (((symbol-function 'write-region)
|
||||
(lambda (&rest _) (signal 'quit nil))))
|
||||
(should (condition-case nil
|
||||
(progn (save-buffer) nil)
|
||||
(quit t))))
|
||||
(should (equal-including-properties
|
||||
(buffer-substring (point-min) (point-max)) layout))
|
||||
(should (buffer-modified-p)))
|
||||
(let ((kill-buffer-query-functions nil))
|
||||
(set-buffer-modified-p nil)
|
||||
(kill-buffer)))
|
||||
(delete-file file))))
|
||||
|
||||
(ert-deftest ekp-region-test-encoding-save-failure-is-retryable ()
|
||||
"An encoding failure must preserve layout and allow a clean retry."
|
||||
(let* ((file (make-temp-file "ekp-save-encoding-"))
|
||||
(text "编码失败以后仍然保持排版,重试写入 logical text")
|
||||
(make-backup-files nil)
|
||||
(create-lockfiles nil))
|
||||
(unwind-protect
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(setq-local require-final-newline nil)
|
||||
(insert text)
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(let ((layout (buffer-substring (point-min) (point-max))))
|
||||
(set-buffer-file-coding-system 'us-ascii-unix)
|
||||
(cl-letf (((symbol-function 'select-safe-coding-system)
|
||||
(lambda (&rest _) (error "Forced encoding failure"))))
|
||||
(should-error (save-buffer)))
|
||||
(should (equal-including-properties
|
||||
(buffer-substring (point-min) (point-max)) layout))
|
||||
(should (buffer-modified-p))
|
||||
(set-buffer-file-coding-system 'utf-8-unix)
|
||||
(save-buffer)
|
||||
(should (equal-including-properties
|
||||
(buffer-substring (point-min) (point-max)) layout))
|
||||
(should-not (buffer-live-p ekp-region--write-buffer))
|
||||
(should (equal (with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(buffer-string))
|
||||
text)))
|
||||
(let ((kill-buffer-query-functions nil))
|
||||
(kill-buffer)))
|
||||
(delete-file file))))
|
||||
|
||||
(ert-deftest ekp-region-test-justify-preserves-unmodified ()
|
||||
"Pure re-layout must not flip `buffer-modified-p'."
|
||||
(let* ((file (make-temp-file "ekp-mod-test"))
|
||||
(make-backup-files nil)
|
||||
(create-lockfiles nil))
|
||||
(unwind-protect
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(insert "modified 标志保持检查内容足够长断行")
|
||||
(save-buffer)
|
||||
(should-not (buffer-modified-p))
|
||||
(ekp-justify-region (point-min) (point-max) 30)
|
||||
(should-not (buffer-modified-p))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should-not (buffer-modified-p))
|
||||
;; A real edit still marks the buffer modified.
|
||||
(insert "y")
|
||||
(should (buffer-modified-p))
|
||||
(let ((kill-buffer-query-functions nil))
|
||||
(set-buffer-modified-p nil)
|
||||
(kill-buffer)))
|
||||
(delete-file file))))
|
||||
|
||||
(ert-deftest ekp-region-test-undo-changes-not-redirtied ()
|
||||
"Changes applied by undo must not schedule a re-flow."
|
||||
(ekp-region-test--with-mode "undo guard 检查内容 aaa bbb ccc" 100
|
||||
(setq ekp-region--dirty nil)
|
||||
(let ((undo-in-progress t))
|
||||
(ekp-region--after-change (point-min) (1+ (point-min)) 0))
|
||||
(should-not ekp-region--dirty)
|
||||
(let ((undo-in-progress nil))
|
||||
(ekp-region--after-change (point-min) (1+ (point-min)) 0))
|
||||
(should ekp-region--dirty)
|
||||
(dolist (p ekp-region--dirty)
|
||||
(set-marker (car p) nil)
|
||||
(set-marker (cdr p) nil))
|
||||
(setq ekp-region--dirty nil)
|
||||
(when (timerp ekp-region--edit-timer)
|
||||
(cancel-timer ekp-region--edit-timer))))
|
||||
|
||||
(ert-deftest ekp-region-test-major-mode-change-restores ()
|
||||
"Switching major mode tears the justified state down cleanly."
|
||||
(let ((text "major mode 切换检查 aaa bbb ccc ddd"))
|
||||
(ekp-region-test--with-text text
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 80)))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(should (get-text-property (point-min) 'ekp-justified))
|
||||
(fundamental-mode)
|
||||
(should (equal (buffer-string) text))
|
||||
(should-not ekp-auto-justify-mode)))))
|
||||
|
||||
;;;; Ecosystem compatibility (kill ring / isearch / fields / read-only)
|
||||
|
||||
(ert-deftest ekp-region-test-kill-ring-gets-logical-text ()
|
||||
"Copying justified text extracts the logical text.
|
||||
CJK justification injects real space characters between glyphs;
|
||||
they must not travel with a kill/yank."
|
||||
(let ((text "中文复制检查内容足够长会断行 with some latin"))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(should (local-variable-p 'filter-buffer-substring-function))
|
||||
(should (equal (filter-buffer-substring (point-min) (point-max))
|
||||
text)))))
|
||||
|
||||
(ert-deftest ekp-region-test-copy-filter-composes-and-restores ()
|
||||
"EKP must preserve an existing buffer-local substring filter."
|
||||
(let ((text "组合复制过滤器必须保留 logical text and prefix"))
|
||||
(ekp-region-test--with-text text
|
||||
(let ((prior (lambda (beg end &optional delete)
|
||||
(let ((text (buffer-substring beg end)))
|
||||
(when delete (delete-region beg end))
|
||||
(concat "PRE:" text)))))
|
||||
(setq-local filter-buffer-substring-function prior)
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(should (equal (filter-buffer-substring (point-min) (point-max))
|
||||
(concat "PRE:" text)))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should (local-variable-p 'filter-buffer-substring-function))
|
||||
(should (eq filter-buffer-substring-function prior))))))
|
||||
|
||||
(ert-deftest ekp-region-test-kill-filter-composes-delete ()
|
||||
"Composed filtering must preserve DELETE and prior-filter semantics."
|
||||
(let ((text "组合 kill 过滤器删除源文本但返回 logical text"))
|
||||
(ekp-region-test--with-text text
|
||||
(let ((prior (lambda (beg end &optional delete)
|
||||
(let ((text (buffer-substring beg end)))
|
||||
(when delete (delete-region beg end))
|
||||
(concat "PRE:" text)))))
|
||||
(setq-local filter-buffer-substring-function prior)
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(should (equal (filter-buffer-substring
|
||||
(point-min) (point-max) t)
|
||||
(concat "PRE:" text)))
|
||||
(should (= (point-min) (point-max)))
|
||||
(should (eq filter-buffer-substring-function prior))))))
|
||||
|
||||
(ert-deftest ekp-region-test-restores-inherited-copy-filter ()
|
||||
"Final unjustify must reveal an inherited substring filter again."
|
||||
(let ((prior (lambda (beg end &optional delete)
|
||||
(prog1 (buffer-substring beg end)
|
||||
(when delete (delete-region beg end))))))
|
||||
(let ((filter-buffer-substring-function prior))
|
||||
(ekp-region-test--with-text "继承 filter 恢复检查内容"
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should-not (local-variable-p 'filter-buffer-substring-function))
|
||||
(should (eq filter-buffer-substring-function prior))))))
|
||||
|
||||
(ert-deftest ekp-region-test-mode-disable-restores-copy-filter ()
|
||||
"Disabling auto mode must restore the previous local copy filter."
|
||||
(let ((prior (lambda (beg end &optional delete)
|
||||
(prog1 (buffer-substring beg end)
|
||||
(when delete (delete-region beg end))))))
|
||||
(ekp-region-test--with-text "关闭 mode 恢复已有 copy filter"
|
||||
(setq-local filter-buffer-substring-function prior)
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 20)))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(ekp-auto-justify-mode -1))
|
||||
(should (local-variable-p 'filter-buffer-substring-function))
|
||||
(should (eq filter-buffer-substring-function prior)))))
|
||||
|
||||
(ert-deftest ekp-region-test-final-unjustify-removes-integrations ()
|
||||
"Removing the final layout span must remove unused integrations."
|
||||
(ekp-region-test--with-text "最后一个排版区间移除后清理集成 hooks"
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(should (memq #'ekp-region--write-logical-buffer
|
||||
write-region-annotate-functions))
|
||||
(should (memq #'ekp-region--isearch-begin isearch-mode-hook))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(should-not (local-variable-p 'filter-buffer-substring-function))
|
||||
(should-not (memq #'ekp-region--write-logical-buffer
|
||||
write-region-annotate-functions))
|
||||
(should-not (memq #'ekp-region--isearch-begin isearch-mode-hook))))
|
||||
|
||||
(ert-deftest ekp-region-test-isearch-sees-logical-text ()
|
||||
"The isearch hooks expose the logical text, then restore the layout."
|
||||
(let ((text "跨行搜索的目标短语必须能找到 internationalization word"))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(let ((justified (buffer-string)))
|
||||
;; Sanity: layout breaks the phrase apart.
|
||||
(should (> (cl-count ?\n justified) 0))
|
||||
(ekp-region--isearch-begin)
|
||||
;; Logical view: the full phrase and the long word are findable.
|
||||
(goto-char (point-min))
|
||||
(should (search-forward "目标短语必须能找到" nil t))
|
||||
(goto-char (point-min))
|
||||
(should (search-forward "internationalization" nil t))
|
||||
(ekp-region--isearch-end)
|
||||
;; Layout restored byte-identically.
|
||||
(should (equal-including-properties (buffer-string) justified))))))
|
||||
|
||||
(ert-deftest ekp-region-test-field-paragraph-skipped ()
|
||||
"Paragraphs containing field or read-only text stay verbatim."
|
||||
(let* ((prompt (propertize "shell> " 'field 'output))
|
||||
(text (concat prompt "command output here\n"
|
||||
"prose paragraph long enough to wrap around")))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-region (point-min) (point-max) 15)
|
||||
(goto-char (point-min))
|
||||
(should (search-forward "command output here" nil t)))))
|
||||
|
||||
(ert-deftest ekp-region-test-read-only-command-barfs ()
|
||||
"Interactive justify on a read-only buffer signals, not corrupts."
|
||||
(ekp-region-test--with-text "read only 检查内容"
|
||||
(set-mark (point-min))
|
||||
(goto-char (point-max))
|
||||
(activate-mark)
|
||||
(read-only-mode 1)
|
||||
(should-error (call-interactively #'ekp-justify-region)
|
||||
:type 'buffer-read-only)))
|
||||
|
||||
;;;; Commands and mode integration
|
||||
|
||||
(ert-deftest ekp-region-test-no-break-public-commands ()
|
||||
"Interactive no-break commands affect the public formatter and report scope."
|
||||
(ekp-region-test--with-text "prefix AA BB suffix words"
|
||||
(let (messages)
|
||||
(set-mark 8)
|
||||
(goto-char 13)
|
||||
(activate-mark)
|
||||
(cl-letf (((symbol-function 'message)
|
||||
(lambda (format-string &rest args)
|
||||
(push (apply #'format format-string args) messages))))
|
||||
(call-interactively #'ekp-no-break-region))
|
||||
(should (eq (get-text-property 8 'ekp-no-break) t))
|
||||
(ekp-justify-region (point-min) (point-max) 4)
|
||||
(should (string-match-p "AA BB" (buffer-string)))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(set-mark 8)
|
||||
(goto-char 13)
|
||||
(activate-mark)
|
||||
(cl-letf (((symbol-function 'message)
|
||||
(lambda (format-string &rest args)
|
||||
(push (apply #'format format-string args) messages))))
|
||||
(call-interactively #'ekp-allow-break-region))
|
||||
(should-not (get-text-property 8 'ekp-no-break))
|
||||
(should (= (length messages) 2))
|
||||
(should (cl-every
|
||||
(lambda (text)
|
||||
(string-match-p "current buffer session" text))
|
||||
messages)))))
|
||||
|
||||
(ert-deftest ekp-region-test-verbatim-public-commands ()
|
||||
"Interactive verbatim commands protect the real region formatter."
|
||||
(ekp-region-test--with-text
|
||||
"literal block stays exactly here\nordinary prose wraps here"
|
||||
(goto-char (point-min))
|
||||
(let ((first-end (line-end-position)) messages)
|
||||
(set-mark (point-min))
|
||||
(goto-char first-end)
|
||||
(activate-mark)
|
||||
(cl-letf (((symbol-function 'message)
|
||||
(lambda (format-string &rest args)
|
||||
(push (apply #'format format-string args) messages))))
|
||||
(call-interactively #'ekp-verbatim-region))
|
||||
(ekp-justify-region (point-min) (point-max) 8)
|
||||
(should (equal (buffer-substring-no-properties
|
||||
(point-min) (line-end-position))
|
||||
"literal block stays exactly here"))
|
||||
(ekp-unjustify-region (point-min) (point-max))
|
||||
(set-mark (point-min))
|
||||
(goto-char first-end)
|
||||
(activate-mark)
|
||||
(cl-letf (((symbol-function 'message)
|
||||
(lambda (format-string &rest args)
|
||||
(push (apply #'format format-string args) messages))))
|
||||
(call-interactively #'ekp-clear-verbatim-region))
|
||||
(ekp-justify-region (point-min) first-end 8)
|
||||
(should (get-text-property (point-min) 'ekp-justified))
|
||||
(should (= (length messages) 2))
|
||||
(should (cl-every
|
||||
(lambda (text)
|
||||
(string-match-p "current buffer session" text))
|
||||
messages)))))
|
||||
|
||||
(ert-deftest ekp-region-test-protection-workflows-discoverable ()
|
||||
"Mode help and menu expose the existing protection workflows."
|
||||
(should (string-match-p
|
||||
"current buffer session"
|
||||
(documentation #'ekp-auto-justify-mode)))
|
||||
(let ((menu (lookup-key ekp-auto-justify-mode-map [menu-bar ekp])))
|
||||
(should (keymapp menu))
|
||||
(should (where-is-internal
|
||||
#'ekp-no-break-region ekp-auto-justify-mode-map))
|
||||
(should (where-is-internal
|
||||
#'ekp-verbatim-region ekp-auto-justify-mode-map))))
|
||||
|
||||
(ert-deftest ekp-region-test-justify-buffer-roundtrip ()
|
||||
"ekp-justify-buffer / ekp-unjustify-buffer cover the whole buffer."
|
||||
(let ((text "第一段内容足够长断行\n\n第二段 also long enough to wrap"))
|
||||
(ekp-region-test--with-text text
|
||||
(ekp-justify-buffer 25)
|
||||
(should (get-text-property (point-min) 'ekp-justified))
|
||||
(ekp-unjustify-buffer)
|
||||
(should (equal-including-properties (buffer-string) text)))))
|
||||
|
||||
(ert-deftest ekp-region-test-justify-dwim-paragraph ()
|
||||
"Without an active region, the commands act on the paragraph at point."
|
||||
(ekp-region-test--with-text
|
||||
"para one short\npara two 目标段落内容足够长会断行几次\npara three"
|
||||
(goto-char (point-min))
|
||||
(search-forward "目标")
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 20)))
|
||||
(call-interactively #'ekp-justify-region))
|
||||
;; Only paragraph two is justified.
|
||||
(goto-char (point-min))
|
||||
(should-not (get-text-property (point) 'ekp-justified))
|
||||
(search-forward "目标")
|
||||
(should (get-text-property (match-beginning 0) 'ekp-justified))
|
||||
(goto-char (point-max))
|
||||
(should-not (get-text-property (1- (point)) 'ekp-justified))
|
||||
;; And unjustify DWIM restores just as well.
|
||||
(goto-char (point-min))
|
||||
(search-forward "目标")
|
||||
(call-interactively #'ekp-unjustify-region)
|
||||
(should (equal (buffer-string)
|
||||
"para one short\npara two 目标段落内容足够长会断行几次\npara three"))))
|
||||
|
||||
(ert-deftest ekp-region-test-refill-paragraph ()
|
||||
"`ekp-refill-paragraph' re-justifies the paragraph at point."
|
||||
(ekp-region-test--with-text "refill 检查内容足够长会断行几次的样子\nsecond para"
|
||||
(goto-char (point-min))
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 20)))
|
||||
(ekp-refill-paragraph))
|
||||
(should (get-text-property (point-min) 'ekp-justified))
|
||||
(goto-char (point-max))
|
||||
(should-not (get-text-property (1- (point)) 'ekp-justified))))
|
||||
|
||||
(ert-deftest ekp-region-test-markdown-setup ()
|
||||
"ekp-markdown-setup stops font-lock from managing `display'."
|
||||
(with-temp-buffer
|
||||
(setq-local font-lock-extra-managed-props '(display composition))
|
||||
(ekp-markdown-setup)
|
||||
(should (equal font-lock-extra-managed-props '(composition)))
|
||||
(should (equal ekp-region-skip-faces ekp-region-markdown-skip-faces))))
|
||||
|
||||
(ert-deftest ekp-region-test-org-auto-preset ()
|
||||
"Enabling the mode in an Org buffer applies the Org skip preset."
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert "普通正文段落内容足够长断行几次的样子")
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 100)))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(unwind-protect
|
||||
(should (equal ekp-region-skip-faces ekp-region-org-skip-faces))
|
||||
(ekp-auto-justify-mode -1)))))
|
||||
|
||||
;;;; Lazy re-flow scheduling
|
||||
|
||||
(ert-deftest ekp-region-test-huge-edit-goes-lazy ()
|
||||
"A dirty region larger than the lazy threshold is chunked, not sync."
|
||||
(let ((ekp-auto-justify-lazy-threshold 50)
|
||||
(ekp-auto-justify-chunk-size 2)
|
||||
(text (mapconcat #'identity
|
||||
(make-list 10 "大量粘贴模拟内容足够长会断行")
|
||||
"\n")))
|
||||
(ekp-region-test--with-text text
|
||||
(cl-letf (((symbol-function 'ekp-region--window-pixel)
|
||||
(lambda (&optional _) 60)))
|
||||
(ekp-auto-justify-mode 1)
|
||||
(unwind-protect
|
||||
(progn
|
||||
;; drain the enable-time lazy queue first
|
||||
(let ((ekp-auto-justify-tick-budget 10.0) (guard 0))
|
||||
(while (and ekp-region--pending (< guard 100))
|
||||
(when (timerp ekp-region--chunk-timer)
|
||||
(cancel-timer ekp-region--chunk-timer)
|
||||
(setq ekp-region--chunk-timer nil))
|
||||
(ekp-region--process-chunk (current-buffer))
|
||||
(setq guard (1+ guard))))
|
||||
;; simulate a huge edit: whole buffer marked dirty
|
||||
(push (cons (copy-marker (point-min))
|
||||
(copy-marker (point-max)))
|
||||
ekp-region--dirty)
|
||||
(when (timerp ekp-region--edit-timer)
|
||||
(cancel-timer ekp-region--edit-timer))
|
||||
(ekp-region--flush-dirty (current-buffer))
|
||||
;; not processed synchronously: a queue exists
|
||||
(should ekp-region--pending)
|
||||
;; drain and verify convergence to the one-shot result
|
||||
(let ((ekp-auto-justify-tick-budget 10.0) (guard 0))
|
||||
(while (and ekp-region--pending (< guard 100))
|
||||
(when (timerp ekp-region--chunk-timer)
|
||||
(cancel-timer ekp-region--chunk-timer)
|
||||
(setq ekp-region--chunk-timer nil))
|
||||
(ekp-region--process-chunk (current-buffer))
|
||||
(setq guard (1+ guard))))
|
||||
(should-not ekp-region--pending)
|
||||
(let ((lazy (buffer-string)))
|
||||
(ekp-auto-justify-mode -1)
|
||||
(ekp-justify-region (point-min) (point-max) 60)
|
||||
(should (equal-including-properties (buffer-string) lazy))
|
||||
(ekp-auto-justify-mode 1)))
|
||||
(ekp-auto-justify-mode -1))))))
|
||||
|
||||
(ert-deftest ekp-region-test-prioritize-visible-chunks ()
|
||||
"Chunks intersecting the visible span move to the queue front."
|
||||
(ekp-region-test--with-text "abc"
|
||||
(setq ekp-region--auto-width 100)
|
||||
(let* ((mk (lambda (a b) (cons (copy-marker a) (copy-marker b))))
|
||||
(c1 (funcall mk 1 2))
|
||||
(c2 (funcall mk 2 3))
|
||||
(c3 (funcall mk 3 4)))
|
||||
(setq ekp-region--pending (cons 100 (list c1 c2 c3)))
|
||||
(cl-letf (((symbol-function 'ekp-region--visible-span)
|
||||
(lambda () (cons 3 4))))
|
||||
(ekp-region--prioritize-visible))
|
||||
(should (eq (cadr ekp-region--pending) c3))
|
||||
(ekp-region--cancel-pending))))
|
||||
|
||||
(ert-deftest ekp-region-test-tick-budget-batches-chunks ()
|
||||
"A generous tick budget drains several chunks in one tick;
|
||||
a zero budget still makes progress (exactly one chunk)."
|
||||
(let ((text (mapconcat #'identity
|
||||
(make-list 6 "分块预算检查内容足够长")
|
||||
"\n")))
|
||||
(ekp-region-test--with-text text
|
||||
(setq ekp-region--auto-width 40)
|
||||
(setq ekp-region--pending
|
||||
(cons 40 (ekp-region--make-chunks (point-min) (point-max))))
|
||||
(setq-local ekp-auto-justify-chunk-size 1)
|
||||
;; zero budget: one chunk per tick
|
||||
(let ((ekp-auto-justify-tick-budget 0)
|
||||
(before (length (cdr ekp-region--pending))))
|
||||
(cl-letf (((symbol-function 'input-pending-p) #'ignore))
|
||||
(let ((ekp-auto-justify-mode t))
|
||||
(ekp-region--process-chunk (current-buffer))))
|
||||
(should (= (length (cdr ekp-region--pending)) (1- before))))
|
||||
(when (timerp ekp-region--chunk-timer)
|
||||
(cancel-timer ekp-region--chunk-timer)
|
||||
(setq ekp-region--chunk-timer nil))
|
||||
;; big budget: the rest drains in one tick
|
||||
(let ((ekp-auto-justify-tick-budget 10.0))
|
||||
(cl-letf (((symbol-function 'input-pending-p) #'ignore))
|
||||
(let ((ekp-auto-justify-mode t))
|
||||
(ekp-region--process-chunk (current-buffer)))))
|
||||
(should-not ekp-region--pending))))
|
||||
|
||||
(provide 'ekp-region-tests)
|
||||
|
||||
;;; ekp-region-tests.el ends here
|
||||
@ -22,7 +22,7 @@
|
||||
;;; Code:
|
||||
|
||||
(require 'ekp)
|
||||
(require 'ekp-region)
|
||||
(require 'ekp-buffer)
|
||||
|
||||
(defvar ekp-showcase-min-width 140)
|
||||
(defvar ekp-showcase-max-width 1200)
|
||||
|
||||
@ -750,6 +750,92 @@ module is bypassed automatically (it has no looseness support)."
|
||||
(dolist (property ekp--layout-marker-properties)
|
||||
(should (eq (alist-get property text-property-default-nonsticky) t))))
|
||||
|
||||
(ert-deftest ekp-test-layout-plan-maps-source-gaps-and-breaks ()
|
||||
"The semantic plan must retain source offsets for every visual decision."
|
||||
(ekp-tests--with-clean-state
|
||||
(let* ((text "中文 Latin mixed paragraph with enough words to wrap")
|
||||
(plan (ekp-layout-plan text 24))
|
||||
(lines (ekp-layout-plan-lines plan))
|
||||
(last-end 0))
|
||||
(should (ekp-layout-plan-p plan))
|
||||
(should (equal (ekp-layout-plan-string plan) text))
|
||||
(should (= (ekp-layout-plan-line-pixel plan) 24))
|
||||
(should (> (length lines) 1))
|
||||
(dotimes (i (length lines))
|
||||
(let ((line (aref lines i)))
|
||||
(should (<= last-end (ekp-layout-line-source-start line)))
|
||||
(should (< (ekp-layout-line-source-start line)
|
||||
(ekp-layout-line-source-end line)))
|
||||
(dolist (gap (append (ekp-layout-line-gaps line) nil))
|
||||
(should (memq (ekp-layout-gap-kind gap) '(lws mws cws nws)))
|
||||
(should (<= (ekp-layout-gap-source-start gap)
|
||||
(ekp-layout-gap-source-end gap)))
|
||||
(should (>= (ekp-layout-gap-target-pixel gap) 0)))
|
||||
(when (< i (1- (length lines)))
|
||||
(should (memq (ekp-layout-line-break-kind line)
|
||||
'(space cjk hyphen))))
|
||||
(setq last-end (ekp-layout-line-source-end line)))))))
|
||||
|
||||
(ert-deftest ekp-test-layout-plan-resolves-paragraph-once ()
|
||||
"One plan must not rebuild the same paragraph cache key downstream."
|
||||
(ekp-tests--with-clean-state
|
||||
(let ((calls 0)
|
||||
(get-para (symbol-function 'ekp--get-para)))
|
||||
(cl-letf (((symbol-function 'ekp--get-para)
|
||||
(lambda (string)
|
||||
(setq calls (1+ calls))
|
||||
(funcall get-para string))))
|
||||
(ekp-layout-plan
|
||||
"A mixed 中文 paragraph should resolve one cached paragraph object."
|
||||
24))
|
||||
(should (= calls 1)))))
|
||||
|
||||
(ert-deftest ekp-test-layout-plan-omits-zero-source-zero-width-gaps ()
|
||||
"The projection plan must omit gaps that cannot install a property."
|
||||
(ekp-tests--with-clean-state
|
||||
(let ((plan (ekp-layout-plan
|
||||
"中文 mixed paragraph keeps natural gaps off the hot path."
|
||||
480)))
|
||||
(cl-loop
|
||||
for line across (ekp-layout-plan-lines plan)
|
||||
do
|
||||
(cl-loop
|
||||
for gap across (ekp-layout-line-gaps line)
|
||||
do
|
||||
(should
|
||||
(or (< (ekp-layout-gap-source-start gap)
|
||||
(ekp-layout-gap-source-end gap))
|
||||
(> (ekp-layout-gap-target-pixel gap) 0))))))))
|
||||
|
||||
(ert-deftest ekp-test-layout-plan-records-discretionary-hyphen ()
|
||||
"A chosen Latin discretionary break must be explicit in the core plan."
|
||||
(ekp-tests--with-clean-state
|
||||
(let* ((plan (ekp-layout-plan
|
||||
"extraordinary hyphenation demonstration paragraph" 15))
|
||||
(line (seq-find
|
||||
(lambda (candidate)
|
||||
(eq (ekp-layout-line-break-kind candidate) 'hyphen))
|
||||
(append (ekp-layout-plan-lines plan) nil))))
|
||||
(should line)
|
||||
(should (ekp-layout-line-hyphen-p line))
|
||||
(should (= (ekp-layout-line-break-source-start line)
|
||||
(ekp-layout-line-break-source-end line))))))
|
||||
|
||||
(ert-deftest ekp-test-public-string-renderer-consumes-layout-plan ()
|
||||
"The public formatter must render the shared semantic plan."
|
||||
(ekp-tests--with-clean-state
|
||||
(let ((calls 0)
|
||||
(original (symbol-function 'ekp-layout-plan)))
|
||||
(cl-letf (((symbol-function 'ekp-layout-plan)
|
||||
(lambda (string width)
|
||||
(cl-incf calls)
|
||||
(funcall original string width))))
|
||||
(should (stringp
|
||||
(ekp-pixel-justify
|
||||
"Shared plans keep the string and buffer renderers aligned"
|
||||
24)))
|
||||
(should (> calls 0))))))
|
||||
|
||||
(ert-deftest ekp-test-gaps-between-brute-force ()
|
||||
"`ekp--gaps-between' must equal naive counting."
|
||||
(ekp-tests--with-clean-state
|
||||
|
||||
72
tests/run-c-resize-evaluator.sh
Executable file
72
tests/run-c-resize-evaluator.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
# Evaluate task032 against the frozen portable baseline.
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
||||
GOAL="$ROOT/.omx/goals/performance/c-resize-latency"
|
||||
BASELINE_ROOT="$GOAL/baseline/source"
|
||||
RAW="$GOAL/raw"
|
||||
ROUNDS=${EKP_RESIZE_ROUNDS:-4}
|
||||
|
||||
if test -n "${EMACS:-}"; then
|
||||
EMACS_BIN=$EMACS
|
||||
elif command -v emacs >/dev/null 2>&1; then
|
||||
EMACS_BIN=$(command -v emacs)
|
||||
elif test -x /Applications/Emacs.app/Contents/MacOS/Emacs-arm64-11; then
|
||||
EMACS_BIN=/Applications/Emacs.app/Contents/MacOS/Emacs-arm64-11
|
||||
else
|
||||
printf '%s\n' "resize-evaluator: Emacs executable not found" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
test -f "$BASELINE_ROOT/ekp.el"
|
||||
test -f "$BASELINE_ROOT/ekp-buffer.el"
|
||||
test -f "$BASELINE_ROOT/ekp_c/ekp.dylib"
|
||||
|
||||
make -C "$ROOT/ekp_c" clean all PROFILE=portable
|
||||
mkdir -p "$RAW"
|
||||
BASELINE_JSONL="$RAW/baseline.jsonl"
|
||||
CANDIDATE_JSONL="$RAW/candidate.jsonl"
|
||||
REPORT="$GOAL/latest-report.json"
|
||||
: >"$BASELINE_JSONL"
|
||||
: >"$CANDIDATE_JSONL"
|
||||
|
||||
run_round()
|
||||
{
|
||||
code_root=$1
|
||||
label=$2
|
||||
round=$3
|
||||
output=$4
|
||||
EKP_RESIZE_LABEL=$label \
|
||||
EKP_RESIZE_ROUND=$round \
|
||||
EKP_RESIZE_OUTPUT=$output \
|
||||
"$EMACS_BIN" -Q --batch -L "$code_root" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-c-resize-evaluator.el"
|
||||
}
|
||||
|
||||
round=1
|
||||
while test "$round" -le "$ROUNDS"; do
|
||||
if test $((round % 2)) -eq 1; then
|
||||
run_round "$BASELINE_ROOT" baseline "$round" "$BASELINE_JSONL"
|
||||
run_round "$ROOT" candidate "$round" "$CANDIDATE_JSONL"
|
||||
else
|
||||
run_round "$ROOT" candidate "$round" "$CANDIDATE_JSONL"
|
||||
run_round "$BASELINE_ROOT" baseline "$round" "$BASELINE_JSONL"
|
||||
fi
|
||||
round=$((round + 1))
|
||||
done
|
||||
|
||||
EKP_RESIZE_MODE=compare \
|
||||
EKP_RESIZE_BASELINE_JSONL="$BASELINE_JSONL" \
|
||||
EKP_RESIZE_CANDIDATE_JSONL="$CANDIDATE_JSONL" \
|
||||
EKP_RESIZE_REPORT="$REPORT" \
|
||||
"$EMACS_BIN" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-c-resize-evaluator.el"
|
||||
|
||||
"$ROOT/tests/run-tests.sh" "$EMACS_BIN"
|
||||
"$EMACS_BIN" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-fuzz.el"
|
||||
"$ROOT/tests/check-release.sh"
|
||||
|
||||
printf '%s\n' "resize-evaluator: performance, parity, ERT, fuzz, and release gates pass"
|
||||
@ -10,7 +10,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
TESTS=$(
|
||||
"$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-region-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
--eval '(dolist (test (ert-select-tests "^ekp-\\(?:test\\|[[:alnum:]-]+-test\\)-" t))
|
||||
@ -21,7 +21,7 @@ for test_name in $TESTS; do
|
||||
echo "isolated ERT: $test_name"
|
||||
"$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-region-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
--eval "(ert-run-tests-batch-and-exit '$test_name)"
|
||||
|
||||
@ -10,7 +10,7 @@ case "$ORDER" in
|
||||
"")
|
||||
exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-region-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
@ -18,7 +18,7 @@ case "$ORDER" in
|
||||
--random-order)
|
||||
exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-region-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
-l "$ROOT/tests/run-tests-random-order.el" \
|
||||
|
||||
Loading…
Reference in New Issue
Block a user