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>
This commit is contained in:
Kinneyzhang 2026-07-27 01:08:40 +08:00
parent a12a3c1191
commit f0c7927644
4 changed files with 149 additions and 4 deletions

View File

@ -92,6 +92,27 @@ through untouched. Checked against the `face' property of each
paragraph, symbol or list."
:type '(repeat face))
;;;###autoload
(defun ekp-org-setup ()
"Configure the current (Org) buffer for ekp justification.
Protects source blocks, tables and meta lines from justification.
Typical use: (add-hook \\='org-mode-hook #\\='ekp-org-setup)."
(setq-local ekp-region-skip-faces ekp-region-org-skip-faces))
;;;###autoload
(defun ekp-markdown-setup ()
"Configure the current (Markdown) buffer for ekp justification.
Protects code faces from justification, and stops markdown-mode's
font-lock from managing the `display' property refontification
would otherwise strip the pixel-glue display specs and wreck the
layout. The cost: markdown's own display-based decorations (URL
hiding) are no longer cleaned up by refontification here.
Typical use: (add-hook \\='markdown-mode-hook #\\='ekp-markdown-setup)."
(setq-local ekp-region-skip-faces ekp-region-markdown-skip-faces)
(when (boundp 'font-lock-extra-managed-props)
(setq-local font-lock-extra-managed-props
(remq 'display font-lock-extra-managed-props))))
(defvar-local ekp-region-skip-predicate nil
"When non-nil, a function called with a paragraph string.
Return non-nil to keep that paragraph verbatim (no justification).
@ -245,6 +266,12 @@ is one logical character."
;;;; Commands
(defun ekp-region--dwim-bounds ()
"Region bounds when the region is active, else the paragraph at point."
(if (use-region-p)
(cons (region-beginning) (region-end))
(ekp-region--para-bounds (cons (point) (point)))))
;;;###autoload
(defun ekp-justify-region (beg end &optional pixel)
"Justify the text between BEG and END to PIXEL width.
@ -255,10 +282,16 @@ idempotent and can re-flow to a new width."
(interactive
(progn
(barf-if-buffer-read-only)
(list (region-beginning) (region-end)
(and current-prefix-arg
(prefix-numeric-value current-prefix-arg)))))
(pcase-let ((`(,beg . ,end) (ekp-region--dwim-bounds)))
(list beg end
(and current-prefix-arg
(prefix-numeric-value current-prefix-arg))))))
(setq pixel (or pixel (ekp-region--window-pixel)))
(when (and font-lock-mode
(or ekp-region-skip-faces ekp-region-skip-predicate))
;; Face-based verbatim detection needs real faces: parts of the
;; region jit-lock never displayed are not fontified yet.
(font-lock-ensure (min beg end) (max beg end)))
(let ((beg (copy-marker (min beg end)))
(end (copy-marker (max beg end) t))
(ekp-region--inhibit t)
@ -314,7 +347,11 @@ Idempotent; added by `ekp-justify-region' and `ekp-auto-justify-mode'."
Removes synthesized glue and soft hyphens, replaces soft line breaks
with the whitespace they swallowed, and re-exposes hidden paragraph
tails. Text the user typed into the justified region is preserved."
(interactive "*r")
(interactive
(progn
(barf-if-buffer-read-only)
(pcase-let ((`(,beg . ,end) (ekp-region--dwim-bounds)))
(list beg end))))
(let ((end-m (copy-marker (max beg end) t))
(ekp-region--inhibit t)
(inhibit-read-only t))
@ -452,6 +489,24 @@ words, not the pixel layout of the source window (DELETE as in
`filter-buffer-substring-function')."
(ekp-region--logical-string (buffer-substring--filter beg end delete)))
;;;###autoload
(defun ekp-justify-buffer (&optional pixel)
"Justify the whole accessible portion of the buffer to PIXEL width.
PIXEL defaults to the window text width; interactively, a numeric
prefix argument supplies it explicitly."
(interactive
(progn
(barf-if-buffer-read-only)
(list (and current-prefix-arg
(prefix-numeric-value current-prefix-arg)))))
(ekp-justify-region (point-min) (point-max) pixel))
;;;###autoload
(defun ekp-unjustify-buffer ()
"Restore the logical text of the whole accessible portion."
(interactive "*")
(ekp-unjustify-region (point-min) (point-max)))
;;;###autoload
(defun ekp-no-break-region (beg end)
"Mark the region as an unbreakable typesetting atom.
@ -688,6 +743,21 @@ window splits, and deletions of the narrowest window."
#'ekp-region--process-chunk
buffer))))))))))
(defun ekp-refill-paragraph ()
"Re-justify the hard paragraph at point (ekp's `fill-paragraph').
Bound to \\[fill-paragraph] while `ekp-auto-justify-mode' is on:
plain `fill-paragraph' would treat glue spaces and soft breaks as
content and destroy the original whitespace."
(interactive "*")
(pcase-let ((`(,beg . ,end)
(ekp-region--para-bounds (cons (point) (point)))))
(ekp-justify-region beg end (or ekp-region--auto-width
(ekp-region--window-pixel)))))
(defvar-keymap ekp-auto-justify-mode-map
:doc "Keymap for `ekp-auto-justify-mode'."
"<remap> <fill-paragraph>" #'ekp-refill-paragraph)
;;;###autoload
(define-minor-mode ekp-auto-justify-mode
"Keep the buffer pixel-justified to the window width.
@ -695,8 +765,14 @@ Re-flows when the window width changes and re-justifies edited
paragraphs incrementally. Designed for reading and previewing;
the buffer text is restored exactly when the mode is turned off."
:lighter " EKP"
:keymap ekp-auto-justify-mode-map
(if ekp-auto-justify-mode
(progn
;; Out-of-the-box protection for the common markup modes,
;; unless the user configured their own.
(unless (or ekp-region-skip-faces ekp-region-skip-predicate)
(cond ((derived-mode-p 'org-mode) (ekp-org-setup))
((derived-mode-p 'markdown-mode) (ekp-markdown-setup))))
(setq ekp-region--auto-width (ekp-region--effective-width))
(ekp-region--reflow (current-buffer) ekp-region--auto-width)
(add-hook 'window-size-change-functions #'ekp-region--on-resize nil t)

View File

@ -80,6 +80,7 @@
(delete-other-windows)
(switch-to-buffer buf)
(with-current-buffer buf
(buffer-disable-undo)
(dolist (pixel pixel-lst)
(erase-buffer)
(insert (ekp-pixel-justify str pixel))

View File

@ -441,6 +441,71 @@ they must not travel with a kill/yank."
(should-error (call-interactively #'ekp-justify-region)
:type 'buffer-read-only)))
;;;; Commands and mode integration
(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)))))
(provide 'ekp-region-tests)
;;; ekp-region-tests.el ends here

View File

@ -241,6 +241,9 @@
(ignore-errors (ekp-c-module-load))
(let ((buf (get-buffer-create "*ekp-showcase*")))
(with-current-buffer buf
;; The width sweep re-justifies the whole buffer dozens of
;; times; recording that in undo history is pure garbage.
(buffer-disable-undo)
(let ((inhibit-read-only t))
(erase-buffer)
(ekp-showcase-mode)