From f0c79276447d345ebe2a400e91699c1c5ea42eef Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Mon, 27 Jul 2026 01:08:40 +0800 Subject: [PATCH] feat: whole-buffer commands, DWIM, M-q remap, org/markdown wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ekp-region.el | 84 +++++++++++++++++++++++++++++++++++++-- tests/ekp-demo.el | 1 + tests/ekp-region-tests.el | 65 ++++++++++++++++++++++++++++++ tests/ekp-showcase.el | 3 ++ 4 files changed, 149 insertions(+), 4 deletions(-) diff --git a/ekp-region.el b/ekp-region.el index 518ebae..83b38c7 100644 --- a/ekp-region.el +++ b/ekp-region.el @@ -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'." + " " #'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) diff --git a/tests/ekp-demo.el b/tests/ekp-demo.el index 154fe39..f7199a1 100644 --- a/tests/ekp-demo.el +++ b/tests/ekp-demo.el @@ -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)) diff --git a/tests/ekp-region-tests.el b/tests/ekp-region-tests.el index 89986da..a2e8d08 100644 --- a/tests/ekp-region-tests.el +++ b/tests/ekp-region-tests.el @@ -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 diff --git a/tests/ekp-showcase.el b/tests/ekp-showcase.el index 947cac5..0ba8055 100644 --- a/tests/ekp-showcase.el +++ b/tests/ekp-showcase.el @@ -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)