Commit Graph

12 Commits

Author SHA1 Message Date
Kinneyzhang
4dee06ec47 fix(showcase): follow-window right-edge truncation; checkdoc clean
showcase: the demo font was applied with face-remap-add-relative, but
string-pixel-width (how ekp measures) ignores buffer face remapping
while honoring an explicit :family — so measurement used the session
default font and rendering used the demo font.  On a wide follow-
window that per-glyph discrepancy accumulated past the safety margin
and every justified line overflowed the right edge (invisible at the
narrow 480px default).  Pin the font as an appended :family face text
property instead, so measurement and rendering agree; inline atom/
code faces keep precedence.

checkdoc: all four elisp files are now checkdoc-clean (docstrings,
argument mentions, sentence spacing) with no logic change; the three
validation messages read naturally again (dropped the capitalized
"Ekp:" prefix checkdoc's capital-letter rule had forced).

92 ERT green; fuzz 300/300 with C parity; byte-compile warning-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 02:28:29 +08:00
Kinneyzhang
bc4907e1fc perf(region): smarter lazy re-flow scheduling
- a dirty area larger than the lazy threshold (big paste, revert)
  is chunked into the background queue instead of freezing the
  command loop with one synchronous re-justification
- queued chunks intersecting the visible span move to the queue
  front on scroll (window-scroll-functions): scrolling into an
  unprocessed area no longer waits for the whole queue
- background ticks work under a time budget
  (ekp-auto-justify-tick-budget, default 5 ms): several small
  chunks per tick, guaranteed progress of at least one, peek-then-
  pop so an aborted justification never loses a chunk
- ticks also yield while an input-method composition is active

3 new ERT tests (84 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:11:05 +08:00
Kinneyzhang
f0c7927644 feat: whole-buffer commands, DWIM, M-q remap, org/markdown wiring
- new commands ekp-justify-buffer / ekp-unjustify-buffer
- ekp-justify-region / ekp-unjustify-region without an active region
  now act on the hard paragraph at point (DWIM) instead of erroring
- ekp-auto-justify-mode remaps fill-paragraph to the new
  ekp-refill-paragraph — plain M-q treated glue spaces and soft
  breaks as content and destroyed the original whitespace
- ekp-org-setup / ekp-markdown-setup wire the existing skip-face
  presets in one call; enabling the mode in an Org/Markdown buffer
  applies them automatically unless the user configured their own.
  The markdown setup also stops font-lock from managing the display
  property: refontification was stripping the pixel glue and
  wrecking the layout permanently.
- justify runs font-lock-ensure over the region first when
  face-based verbatim detection is configured — paragraphs jit-lock
  had never fontified (off-screen code blocks) were not skipped
- showcase/demo buffers disable undo recording (width sweeps
  generated thousands of useless undo entries)

5 new ERT tests (81 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:08:40 +08:00
Kinneyzhang
a12a3c1191 feat: coexist with the Emacs ecosystem — isearch, kill ring, windows
- isearch searches the logical text: entering isearch temporarily
  un-justifies (spans remembered as markers), exiting restores the
  layout.  Justified CJK lines contain real injected spaces, which
  made even same-line phrases unfindable.
- the kill ring receives the logical text via a buffer-local
  filter-buffer-substring-function: yanking justified text elsewhere
  transports words, not the source window's pixel layout.
- multi-window: the layout width is now the narrowest window showing
  the buffer (was: whichever window resized last, leaving the other
  window overflowing), and window-configuration-change-hook catches
  the buffer becoming displayed, splits, and closed windows.
- paragraphs containing field or read-only text (comint prompts)
  are never re-written; interactive justify/unjustify on read-only
  buffers signals instead of editing through inhibit-read-only.
- edit re-flows wait for an active input-method composition (quail
  preedit) to finish instead of re-writing the text under it.

4 new ERT tests (76 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:05:15 +08:00
Kinneyzhang
ede518f10f fix: editor-state integrity — saving, stickiness, modified flag, undo
Justification is a re-layout of the same logical text; it now behaves
that way toward the rest of the editor:

- saving a justified buffer writes the LOGICAL text: before-save-hook
  unjustifies (spans remembered as markers), after-save-hook restores
  the justified view; the deterministic pair stays off undo history.
  Previously soft newlines and break hyphens were silently persisted
  to disk and the original whitespace was unrecoverable after reopen.
- renderer marker properties (ekp-glue/soft-break/soft-hyphen/hidden/
  justified) are registered in text-property-default-nonsticky: text
  typed after a glue no longer inherits the marker and is no longer
  deleted as a synthesized space by the next re-flow.
- pure re-layout preserves buffer-modified-p when it was nil: no more
  lock files, auto-saves and "buffer modified" prompts from merely
  enabling ekp-auto-justify-mode.
- changes applied by undo are not re-dirtied (no timer war against
  the user's undo sequence).
- switching major modes tears justified state down cleanly
  (change-major-mode-hook), widening first so narrowing never leaves
  justified orphans.

5 new ERT tests (72 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:00:27 +08:00
Kinneyzhang
9dcaeb0ba3 chore: licensing, customize support, and packaging hygiene
- add COPYING (GPL-3.0-or-later) and license headers to all elisp
  and C sources; real author/maintainer info replaces placeholders
- convert user-facing options to defcustom under new group `ekp'
  (spacing internals managed by ekp-param-set stay defvars)
- autoload user commands: ekp-param-reset, ekp-clear-caches,
  ekp-c-module-load, ekp-c-module-build
- ekp--load-dicts: a missing dictionaries/ directory now only
  disables hyphenation instead of breaking (require 'ekp);
  ekp--split-with-hyphen degrades gracefully (and resolves the
  hyphenator once per call instead of once per word)
- package summary no longer redundantly says "for Emacs"
- Makefile: drop personal Windows EMACS_ROOT default; add guidance
- ekp_c/README.md: fix stale 1.1 version references (module is 1.4)
- DEVELOPER*.md: remove archive/ from file map (not in the repo)
- remove unreferenced 10 MB demo GIF

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 00:52:37 +08:00
Kinneyzhang
dc2f6da846 feat: visible-first lazy re-flow for large buffers; packaging polish
Buffers past ekp-auto-justify-lazy-threshold (20k chars) no longer
re-justify wholesale on a width change: the visible span updates
synchronously and the rest follows in polite background chunks
(run-with-timer ticks that yield to pending input).  Measured on a
300-paragraph, 132k-char article: perceived latency 332 ms → 15 ms,
background completion ~0.5 s.

Fixed in the process: adjacent chunk markers shared a boundary, and
the earlier chunk's delete+insert pushed the next chunk's start
marker back over the freshly inserted text — chunks grew linearly
(2.2k, 4.9k, 7.4k… chars) and background work went quadratic (14 s).
Start markers now use insertion-type t.  Regression test asserts the
chunked result equals the one-shot result property-for-property.

Also: org/markdown skip-face presets (ekp-region-org-skip-faces /
ekp-region-markdown-skip-faces), MELPA header hygiene (Version + URL
on the main file, stale Package-Requires dropped from ekp-hyphen.el),
readme notes.

67 ERT green; fuzz 300/300.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 23:23:25 +08:00
Kinneyzhang
c4d7fdf8bb perf: reclaim feature costs — hot-path caches for smooth reflow
Investigation: the apparent 7× bench regression was interpreted-mode
measurement (tests always deleted .elc); byte-compiled + C the real
P1/P2 cost was +70–90%.  Worktree A/B across phase commits confirmed
the features themselves added ~+20% interpreted.

Optimizations (all verified by the 66-test suite + 300-case fuzz):
- per-char memo table for ekp--str-type (get-char-code-property was
  the para-build hot spot)
- precomputed punctuation char lists (no per-call list rebuild)
- ekp--box-offsets memoized in the paragraph struct
- interned glue strings for the two dominant payload shapes
- rendered-output cache per (paragraph, width) inside dp-cache
  (capped at 64 widths per paragraph), so resize sweeps that revisit
  a width skip rendering entirely
- unjustify walks hop property boundaries instead of chars

Numbers (byte-compiled + C): justify zh w=200 53.8 ms — at parity
with the pre-feature record (57 ms) despite punctuation boxes and
break permissions; range zh 117 ms (+55%, inherent box-count cost).
Continuous reflow, 60-paragraph 26k-char article through the full
region layer: 187 ms → 73 ms per width change; incremental
single-paragraph re-justify after an edit: 62 ms → 17 ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:03:05 +08:00
Kinneyzhang
4d9a018fbf feat: verbatim protection — code blocks and mode hooks
Paragraphs stay verbatim (never justified, byte-identical through
justify/auto-mode/unjustify) when they carry the ekp-verbatim text
property, wear a face from ekp-region-skip-faces (point it at
org-block / markdown-code-face and code blocks are exempt), or match
the buffer-local ekp-region-skip-predicate — three layers of the same
general mechanism, no mode-specific code.  Inline code inside prose
is the existing ekp-no-break atom (rigid, unbreakable, unhyphenated).

New commands: ekp-verbatim-region / ekp-clear-verbatim-region.
Core engine untouched: block protection lives entirely in the region
layer, so skipped text never even reaches the tokenizer.

Tests: 66 ERT green (property / face / predicate paths, exact
roundtrips around justified prose).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 21:40:43 +08:00
Kinneyzhang
f6aa64b3b1 feat: right-edge protrusion — hanging punctuation (C 1.4)
ekp-protrusion enables microtype-style character protrusion at the
right edge: a line ending in fullwidth punctuation (。、」…) hangs
the whitespace half of the glyph past the flush edge — visually
identical to CLREQ line-end punctuation compression — and latin
periods/commas/quotes and soft hyphens protrude by their configured
ratios (ekp-protrusion-ratios).

Mechanics: per-box tail protrusion, folded through trailing space
boxes into a per-gap array; each DP candidate widens its effective
target (lw = width + release), mirrored exactly in the renderer's
per-line effective width and in the C-result reconstruction, so DP,
rendering and both engines stay coherent.  Left-edge protrusion is
documented as out of scope (Emacs cannot render before the line
origin).  ekp-region reserves the protrusion width in auto layouts
(ekp-region-protrusion-reserve).

Off by default; all-zero arrays make it a no-op on the shared path.
C module 1.4: break-with-arrays 12→14 args (tail-protrudes array,
hyphen-protrude scalar), batch vectors 14 elements.

Tests: 59 ERT green (deterministic hang check, flush-when-off,
protrusion C parity); fuzz 300/300.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 21:32:54 +08:00
Kinneyzhang
ea96a6dc96 feat: no-break spans, NBSP/WORD JOINER support, verbatim atoms
- text property ekp-no-break turns any span into a rigid typesetting
  atom: no line break inside, no hyphenation, literal spacing (space
  runs inside the span become preserved space boxes instead of
  stretchable glue) — inline code, product names, numbers with units
- character-driven joins: NBSP, NARROW NBSP, FIGURE SPACE keep their
  neighbors together (they are boxes whose adjacent gaps are
  unbreakable and glue-free — the character supplies its spacing);
  WORD JOINER / ZWNBSP attach invisibly like combining marks
- new commands: ekp-no-break-region / ekp-allow-break-region
- overlong atoms fall back to single emergency lines (atomic-run
  handling from the break-permission layer); zero C changes needed

Tests: 52 ERT green incl. atomicity across widths, hyphenation
suppression, NBSP/WJ adjacency sweeps; fuzz 300/300 with C parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 21:12:07 +08:00
Kinneyzhang
95ed2b32d1 feat: buffer-level justification — region commands and auto-justify mode
The renderer (shared by both engines) is now lossless: synthesized
glue carries the original text it replaced (ekp-glue), soft line
breaks carry the whitespace swallowed around the break
(ekp-soft-break), break hyphens are marked (ekp-soft-hyphen), and
paragraph-edge whitespace survives as zero-display ekp-hidden text.
Orphaned ekp--combine-glues-and-boxes / ekp--interleave removed.

New ekp-region.el:
- ekp-justify-region / ekp-unjustify-region: in-place justification
  with exact structural restore (character- and property-exact),
  robust to edits made while justified
- ekp-auto-justify-mode: keeps the buffer justified at the window
  width; debounced re-flow on window resize, incremental
  per-paragraph re-justification after edits (served by the
  paragraph cache)
- fix found in live GUI testing: buffer-local members of
  window-size-change-functions receive the WINDOW as argument and
  may run with an unrelated buffer current; the handler now resolves
  window and buffer explicitly (regression test included)

Tests: 47 ERT (36 core + 11 region) passing; 300-case fuzz 0
failures (C/elisp parity unchanged); byte-compile clean with
error-on-warn. Verified interactively in GUI Emacs: justified at
1403px maximized, auto-reflowed to 614px on frame resize.

docs: interactive-use section in readme.md / readme_zh.md
ci: byte-compile list includes ekp-region.el

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:23:03 +08:00