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."
|
paragraph, symbol or list."
|
||||||
:type '(repeat face))
|
: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
|
(defvar-local ekp-region-skip-predicate nil
|
||||||
"When non-nil, a function called with a paragraph string.
|
"When non-nil, a function called with a paragraph string.
|
||||||
Return non-nil to keep that paragraph verbatim (no justification).
|
Return non-nil to keep that paragraph verbatim (no justification).
|
||||||
@ -245,6 +266,12 @@ is one logical character."
|
|||||||
|
|
||||||
;;;; Commands
|
;;;; 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
|
;;;###autoload
|
||||||
(defun ekp-justify-region (beg end &optional pixel)
|
(defun ekp-justify-region (beg end &optional pixel)
|
||||||
"Justify the text between BEG and END to PIXEL width.
|
"Justify the text between BEG and END to PIXEL width.
|
||||||
@ -255,10 +282,16 @@ idempotent and can re-flow to a new width."
|
|||||||
(interactive
|
(interactive
|
||||||
(progn
|
(progn
|
||||||
(barf-if-buffer-read-only)
|
(barf-if-buffer-read-only)
|
||||||
(list (region-beginning) (region-end)
|
(pcase-let ((`(,beg . ,end) (ekp-region--dwim-bounds)))
|
||||||
(and current-prefix-arg
|
(list beg end
|
||||||
(prefix-numeric-value current-prefix-arg)))))
|
(and current-prefix-arg
|
||||||
|
(prefix-numeric-value current-prefix-arg))))))
|
||||||
(setq pixel (or pixel (ekp-region--window-pixel)))
|
(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)))
|
(let ((beg (copy-marker (min beg end)))
|
||||||
(end (copy-marker (max beg end) t))
|
(end (copy-marker (max beg end) t))
|
||||||
(ekp-region--inhibit 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
|
Removes synthesized glue and soft hyphens, replaces soft line breaks
|
||||||
with the whitespace they swallowed, and re-exposes hidden paragraph
|
with the whitespace they swallowed, and re-exposes hidden paragraph
|
||||||
tails. Text the user typed into the justified region is preserved."
|
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))
|
(let ((end-m (copy-marker (max beg end) t))
|
||||||
(ekp-region--inhibit t)
|
(ekp-region--inhibit t)
|
||||||
(inhibit-read-only 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')."
|
`filter-buffer-substring-function')."
|
||||||
(ekp-region--logical-string (buffer-substring--filter beg end delete)))
|
(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
|
;;;###autoload
|
||||||
(defun ekp-no-break-region (beg end)
|
(defun ekp-no-break-region (beg end)
|
||||||
"Mark the region as an unbreakable typesetting atom.
|
"Mark the region as an unbreakable typesetting atom.
|
||||||
@ -688,6 +743,21 @@ window splits, and deletions of the narrowest window."
|
|||||||
#'ekp-region--process-chunk
|
#'ekp-region--process-chunk
|
||||||
buffer))))))))))
|
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
|
;;;###autoload
|
||||||
(define-minor-mode ekp-auto-justify-mode
|
(define-minor-mode ekp-auto-justify-mode
|
||||||
"Keep the buffer pixel-justified to the window width.
|
"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;
|
paragraphs incrementally. Designed for reading and previewing;
|
||||||
the buffer text is restored exactly when the mode is turned off."
|
the buffer text is restored exactly when the mode is turned off."
|
||||||
:lighter " EKP"
|
:lighter " EKP"
|
||||||
|
:keymap ekp-auto-justify-mode-map
|
||||||
(if ekp-auto-justify-mode
|
(if ekp-auto-justify-mode
|
||||||
(progn
|
(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))
|
(setq ekp-region--auto-width (ekp-region--effective-width))
|
||||||
(ekp-region--reflow (current-buffer) 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 'window-size-change-functions #'ekp-region--on-resize nil t)
|
||||||
|
|||||||
@ -80,6 +80,7 @@
|
|||||||
(delete-other-windows)
|
(delete-other-windows)
|
||||||
(switch-to-buffer buf)
|
(switch-to-buffer buf)
|
||||||
(with-current-buffer buf
|
(with-current-buffer buf
|
||||||
|
(buffer-disable-undo)
|
||||||
(dolist (pixel pixel-lst)
|
(dolist (pixel pixel-lst)
|
||||||
(erase-buffer)
|
(erase-buffer)
|
||||||
(insert (ekp-pixel-justify str pixel))
|
(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)
|
(should-error (call-interactively #'ekp-justify-region)
|
||||||
:type 'buffer-read-only)))
|
: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)
|
(provide 'ekp-region-tests)
|
||||||
|
|
||||||
;;; ekp-region-tests.el ends here
|
;;; ekp-region-tests.el ends here
|
||||||
|
|||||||
@ -241,6 +241,9 @@
|
|||||||
(ignore-errors (ekp-c-module-load))
|
(ignore-errors (ekp-c-module-load))
|
||||||
(let ((buf (get-buffer-create "*ekp-showcase*")))
|
(let ((buf (get-buffer-create "*ekp-showcase*")))
|
||||||
(with-current-buffer buf
|
(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))
|
(let ((inhibit-read-only t))
|
||||||
(erase-buffer)
|
(erase-buffer)
|
||||||
(ekp-showcase-mode)
|
(ekp-showcase-mode)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user