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:
parent
a12a3c1191
commit
f0c7927644
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user