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>
This commit is contained in:
parent
c4d7fdf8bb
commit
dc2f6da846
@ -3,7 +3,6 @@
|
||||
;; Copyright (C) 2024
|
||||
;; Author: emacs-kp contributors
|
||||
;; Keywords: text, hyphenation, typesetting
|
||||
;; Package-Requires: ((emacs "27.1"))
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
|
||||
125
ekp-region.el
125
ekp-region.el
@ -46,6 +46,27 @@ by the display engine due to rounding."
|
||||
"Idle seconds before edited paragraphs are re-justified."
|
||||
:type 'number)
|
||||
|
||||
(defcustom ekp-auto-justify-lazy-threshold 20000
|
||||
"Buffer size (characters) beyond which re-flows go visible-first.
|
||||
Below it a window-width change re-justifies the whole buffer at
|
||||
once; above it the visible portion is done synchronously and the
|
||||
rest follows in idle background chunks."
|
||||
:type 'natnum)
|
||||
|
||||
(defcustom ekp-auto-justify-chunk-size 10
|
||||
"Paragraphs re-justified per background tick in lazy re-flows."
|
||||
:type 'natnum)
|
||||
|
||||
(defconst ekp-region-org-skip-faces
|
||||
'(org-block org-block-begin-line org-block-end-line org-code
|
||||
org-verbatim org-table org-meta-line)
|
||||
"Reasonable `ekp-region-skip-faces' preset for Org buffers.")
|
||||
|
||||
(defconst ekp-region-markdown-skip-faces
|
||||
'(markdown-code-face markdown-inline-code-face markdown-pre-face
|
||||
markdown-table-face)
|
||||
"Reasonable `ekp-region-skip-faces' preset for Markdown buffers.")
|
||||
|
||||
(defcustom ekp-region-skip-faces nil
|
||||
"Faces whose paragraphs are never justified (kept verbatim).
|
||||
Point major-mode faces here — e.g. `org-block' and `org-code' for
|
||||
@ -72,6 +93,11 @@ they suffice.")
|
||||
(defvar-local ekp-region--dirty nil
|
||||
"Pending edited regions, as a list of (BEG-MARKER . END-MARKER).")
|
||||
|
||||
(defvar-local ekp-region--pending nil
|
||||
"Lazy re-flow state: (WIDTH . CHUNKS), CHUNKS = ((BEG-M . END-M)...).")
|
||||
|
||||
(defvar-local ekp-region--chunk-timer nil)
|
||||
|
||||
;;;; Width
|
||||
|
||||
(defun ekp-region-protrusion-reserve ()
|
||||
@ -282,8 +308,10 @@ unbreakable span inside prose, use `ekp-no-break-region' instead."
|
||||
|
||||
(defun ekp-region--para-bounds (marker-pair)
|
||||
"Hard-paragraph bounds containing MARKER-PAIR, as (BEG . END)."
|
||||
(let ((b (marker-position (car marker-pair)))
|
||||
(e (marker-position (cdr marker-pair))))
|
||||
(let ((b (let ((x (car marker-pair)))
|
||||
(if (markerp x) (marker-position x) x)))
|
||||
(e (let ((x (cdr marker-pair)))
|
||||
(if (markerp x) (marker-position x) x))))
|
||||
(save-excursion
|
||||
(goto-char (max (point-min) (min b (point-max))))
|
||||
(while (and (> (point) (point-min))
|
||||
@ -361,13 +389,99 @@ buffer current — so resolve both explicitly."
|
||||
#'ekp-region--reflow
|
||||
(current-buffer) w)))))))))
|
||||
|
||||
(defun ekp-region--cancel-pending ()
|
||||
"Drop any queued lazy re-flow chunks."
|
||||
(when (timerp ekp-region--chunk-timer)
|
||||
(cancel-timer ekp-region--chunk-timer))
|
||||
(setq ekp-region--chunk-timer nil)
|
||||
(dolist (c (cdr ekp-region--pending))
|
||||
(set-marker (car c) nil)
|
||||
(set-marker (cdr c) nil))
|
||||
(setq ekp-region--pending nil))
|
||||
|
||||
(defun ekp-region--make-chunks (beg end)
|
||||
"Split [BEG, END) into marker-pair chunks of whole hard paragraphs."
|
||||
(let ((chunks nil))
|
||||
(save-excursion
|
||||
(goto-char beg)
|
||||
(while (< (point) end)
|
||||
(let ((cbeg (point)) (paras 0))
|
||||
(while (and (< (point) end)
|
||||
(< paras ekp-auto-justify-chunk-size))
|
||||
(if (search-forward "\n" end 'move)
|
||||
(unless (get-text-property (match-beginning 0)
|
||||
'ekp-soft-break)
|
||||
(setq paras (1+ paras)))
|
||||
nil))
|
||||
(when (> (point) cbeg)
|
||||
;; BEG has insertion-type t: the previous chunk's re-insert
|
||||
;; happens exactly at this boundary, and the marker must
|
||||
;; end up after that text, not before it.
|
||||
(push (cons (copy-marker cbeg t) (copy-marker (point) t))
|
||||
chunks)))))
|
||||
(nreverse chunks)))
|
||||
|
||||
(defun ekp-region--visible-span ()
|
||||
"Visible portion of the current buffer, as (BEG . END)."
|
||||
(let ((win (get-buffer-window (current-buffer))))
|
||||
(if win
|
||||
(cons (window-start win) (or (window-end win t) (point-max)))
|
||||
(cons (point-min) (point-max)))))
|
||||
|
||||
(defun ekp-region--process-chunk (buffer)
|
||||
"Re-justify the next queued chunk of BUFFER, then reschedule."
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(setq ekp-region--chunk-timer nil)
|
||||
(cond
|
||||
((or (not ekp-auto-justify-mode) (null ekp-region--pending))
|
||||
(ekp-region--cancel-pending))
|
||||
;; a newer re-flow superseded this queue
|
||||
((not (eql (car ekp-region--pending) ekp-region--auto-width))
|
||||
(ekp-region--cancel-pending))
|
||||
;; be polite: yield to pending input, try again shortly
|
||||
((input-pending-p)
|
||||
(setq ekp-region--chunk-timer
|
||||
(run-with-timer 0.1 nil #'ekp-region--process-chunk buffer)))
|
||||
(t
|
||||
(let* ((width (car ekp-region--pending))
|
||||
(chunk (pop (cdr ekp-region--pending))))
|
||||
(when chunk
|
||||
(ekp-justify-region (car chunk) (cdr chunk) width)
|
||||
(set-marker (car chunk) nil)
|
||||
(set-marker (cdr chunk) nil))
|
||||
(if (cdr ekp-region--pending)
|
||||
(setq ekp-region--chunk-timer
|
||||
(run-with-timer 0.02 nil
|
||||
#'ekp-region--process-chunk buffer))
|
||||
(setq ekp-region--pending nil))))))))
|
||||
|
||||
(defun ekp-region--reflow (buffer width)
|
||||
"Re-justify all of BUFFER to WIDTH."
|
||||
"Re-justify BUFFER to WIDTH — whole buffer, or visible-first when large."
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(when ekp-auto-justify-mode
|
||||
(setq ekp-region--auto-width width)
|
||||
(ekp-justify-region (point-min) (point-max) width)))))
|
||||
(ekp-region--cancel-pending)
|
||||
(if (< (- (point-max) (point-min))
|
||||
ekp-auto-justify-lazy-threshold)
|
||||
(ekp-justify-region (point-min) (point-max) width)
|
||||
;; visible part now, the rest in background chunks
|
||||
(pcase-let* ((`(,vbeg . ,vend) (ekp-region--visible-span))
|
||||
(`(,pbeg . ,pend)
|
||||
(ekp-region--para-bounds (cons vbeg vend))))
|
||||
(ekp-justify-region pbeg pend width)
|
||||
(let ((chunks (nconc
|
||||
;; start at PEND so the hard newline there
|
||||
;; gets its ekp-justified property too
|
||||
(ekp-region--make-chunks pend (point-max))
|
||||
(ekp-region--make-chunks (point-min) pbeg))))
|
||||
(when chunks
|
||||
(setq ekp-region--pending (cons width chunks))
|
||||
(setq ekp-region--chunk-timer
|
||||
(run-with-timer 0.02 nil
|
||||
#'ekp-region--process-chunk
|
||||
buffer))))))))))
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode ekp-auto-justify-mode
|
||||
@ -380,7 +494,7 @@ the buffer text is restored exactly when the mode is turned off."
|
||||
(progn
|
||||
(setq ekp-region--auto-width
|
||||
(ekp-region--window-pixel (get-buffer-window)))
|
||||
(ekp-justify-region (point-min) (point-max) ekp-region--auto-width)
|
||||
(ekp-region--reflow (current-buffer) ekp-region--auto-width)
|
||||
(add-hook 'window-size-change-functions #'ekp-region--on-resize nil t)
|
||||
(add-hook 'after-change-functions #'ekp-region--after-change nil t))
|
||||
(remove-hook 'window-size-change-functions #'ekp-region--on-resize t)
|
||||
@ -389,6 +503,7 @@ the buffer text is restored exactly when the mode is turned off."
|
||||
(cancel-timer ekp-region--resize-timer))
|
||||
(when (timerp ekp-region--edit-timer)
|
||||
(cancel-timer ekp-region--edit-timer))
|
||||
(ekp-region--cancel-pending)
|
||||
(setq ekp-region--resize-timer nil
|
||||
ekp-region--edit-timer nil
|
||||
ekp-region--dirty nil
|
||||
|
||||
2
ekp.el
2
ekp.el
@ -2,6 +2,8 @@
|
||||
|
||||
;; Copyright (C) 2024
|
||||
;; Author: emacs-kp contributors
|
||||
;; Version: 1.0.0
|
||||
;; URL: https://github.com/Kinneyzhang/emacs-kp
|
||||
;; Keywords: text, typesetting, CJK
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
|
||||
|
||||
19
readme.md
19
readme.md
@ -55,7 +55,7 @@ cd ekp_c && make # requires C11 compiler, produces ekp.dylib/.so/.dll
|
||||
```
|
||||
|
||||
```elisp
|
||||
(ekp-c-module-load) ; prints "ekp-c module loaded (version 1.1, N threads)"
|
||||
(ekp-c-module-load) ; prints "ekp-c module loaded (version 1.4, N threads)"
|
||||
```
|
||||
|
||||
Once loaded (and since `ekp-use-c-module` defaults to `t`), all
|
||||
@ -89,6 +89,21 @@ loading refuses with a message asking you to rebuild.
|
||||
`ekp-region-margin-pixel` (default 2) is subtracted from the window
|
||||
width as a rounding safety margin.
|
||||
|
||||
Large buffers (over `ekp-auto-justify-lazy-threshold` characters,
|
||||
default 20 000) re-flow visible-first: the portion on screen updates
|
||||
synchronously (~15 ms) and the rest follows in idle background chunks.
|
||||
|
||||
Mode presets for verbatim protection:
|
||||
|
||||
```elisp
|
||||
(add-hook 'org-mode-hook
|
||||
(lambda ()
|
||||
(setq-local ekp-region-skip-faces ekp-region-org-skip-faces)))
|
||||
(add-hook 'markdown-mode-hook
|
||||
(lambda ()
|
||||
(setq-local ekp-region-skip-faces ekp-region-markdown-skip-faces)))
|
||||
```
|
||||
|
||||
### Protecting code and other verbatim text
|
||||
|
||||
- Block level: paragraphs carrying the `ekp-verbatim` text property
|
||||
@ -221,7 +236,7 @@ off most for optimal-width search and long multi-paragraph texts.
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
tests/run-tests.sh /path/to/emacs # 36 ERT tests, all batch-safe
|
||||
tests/run-tests.sh /path/to/emacs # 67 ERT tests, all batch-safe
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
19
readme_zh.md
19
readme_zh.md
@ -50,7 +50,7 @@ cd ekp_c && make # 需要 C11 编译器,产出 ekp.dylib/.so/.dll
|
||||
```
|
||||
|
||||
```elisp
|
||||
(ekp-c-module-load) ; 显示 "ekp-c module loaded (version 1.1, N threads)"
|
||||
(ekp-c-module-load) ; 显示 "ekp-c module loaded (version 1.4, N threads)"
|
||||
```
|
||||
|
||||
加载后(`ekp-use-c-module` 默认为 `t`)所有排版调用自动走 C 引擎。
|
||||
@ -77,6 +77,21 @@ Elisp 与 C 两个引擎的输出**完全一致**;Elisp 是永远可用的后备
|
||||
|
||||
`ekp-region-margin-pixel`(默认 2)是从窗口宽度中扣除的取整安全边距。
|
||||
|
||||
大 buffer(超过 `ekp-auto-justify-lazy-threshold` 字符,默认 2 万)
|
||||
自动改为可视优先重排:屏幕内的部分同步完成(约 15ms),其余在空闲
|
||||
时后台分块补齐。
|
||||
|
||||
各 mode 的 verbatim 保护预设:
|
||||
|
||||
```elisp
|
||||
(add-hook 'org-mode-hook
|
||||
(lambda ()
|
||||
(setq-local ekp-region-skip-faces ekp-region-org-skip-faces)))
|
||||
(add-hook 'markdown-mode-hook
|
||||
(lambda ()
|
||||
(setq-local ekp-region-skip-faces ekp-region-markdown-skip-faces)))
|
||||
```
|
||||
|
||||
### 保护代码块与 verbatim 文本
|
||||
|
||||
- 段落级:携带 `ekp-verbatim` 文本属性(`M-x ekp-verbatim-region`)、
|
||||
@ -190,7 +205,7 @@ Silicon 测得;方法见 DEVELOPER_ZH.md:
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
tests/run-tests.sh /path/to/emacs # 36 个 ERT 测试,全部支持 batch
|
||||
tests/run-tests.sh /path/to/emacs # 67 个 ERT 测试,全部支持 batch
|
||||
```
|
||||
|
||||
## 致谢
|
||||
|
||||
@ -255,6 +255,36 @@ the displaying WINDOW, with an arbitrary buffer current."
|
||||
(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)))))))
|
||||
|
||||
(provide 'ekp-region-tests)
|
||||
|
||||
;;; ekp-region-tests.el ends here
|
||||
|
||||
Loading…
Reference in New Issue
Block a user