mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-06-14 04:21:24 +00:00
Pixel-direct alignment in visual-wrap-prefix-mode (bug#81039)
`visual-wrap--content-prefix' previously returned a column count
computed as
(max (string-width prefix)
(ceiling (string-pixel-width prefix) avg-space-width))
with two problems:
* `string-width' ignores `buffer-invisibility-spec', so an invisible
prefix (hidden ATX markers under `markdown-ts-hide-markup', for
example) still reserved its character count on line 1 via a
`min-width' display property, shifting the visible heading right.
* With variable-pitch fonts, rounding the prefix width up to whole
columns added visible padding whenever the natural width did not
fall on an exact column boundary.
Return the prefix's natural pixel width via `string-pixel-width'
instead, which accounts for any display transformation applied to
the prefix (invisibility, `display' replacements, text scaling,
proportional fonts). Drop the `min-width' property from
`visual-wrap--apply-to-line' so line 1 renders at its natural width.
Switch the continuation `wrap-prefix' to a mixed-unit `:align-to'
sum form:
(space :align-to (+ (PIX) (EXTRA-INDENT . width)))
where PIX is the prefix's pixel width and EXTRA-INDENT is
`visual-wrap-extra-indent' in canonical character widths. The
display engine resolves each term per the active frame and sums
them, so no Lisp-level unit conversion is needed.
Since `min-width' is no longer installed, the accumulation cycle
that commit 81a5beb8af (bug#73882) worked around cannot recur.
Drop the `min-width' strip from `visual-wrap--content-prefix' and
the `min-width' removal from `visual-wrap--remove-properties'.
Keep `min-width' in `visual-wrap--safe-display-specs' so that
lines where other modes install it are not skipped.
* lisp/visual-wrap.el (visual-wrap--content-prefix): Return pixel
width instead of column count; drop the `min-width' strip.
(visual-wrap--apply-to-line): Drop `min-width' on line 1; use
mixed-unit `:align-to' sum form for the continuation wrap-prefix.
(visual-wrap--adjust-prefix): Handle only string prefixes; the
numeric (pixel) case is now handled inline in `--apply-to-line'
via the mixed-unit `:align-to' sum form.
(visual-wrap--remove-properties): Drop `min-width' removal.
(visual-wrap--safe-display-specs): Add note about `min-width'.
* test/lisp/visual-wrap-tests.el: Update expected `wrap-prefix'
values to the new sum form.
(visual-wrap-tests/invisible-prefix): New test motivated by bug#81039.
(visual-wrap-tests/negative-extra-indent): New test; verify that a
large negative `visual-wrap-extra-indent' produces a valid
wrap-prefix (the display engine clamps the stretch to zero).
* test/manual/visual-wrap-test.el: New file. Manual test suite
for visual-eyeball verification of prefix alignment behavior.
Reported-by: Andrea Alberti <a.alberti82@gmail.com>
Co-authored-by: Stefan Monnier <monnier@iro.umontreal.ca>
This commit is contained in:
parent
02fb01166e
commit
ca5e9976b1
3 changed files with 517 additions and 71 deletions
|
|
@ -34,12 +34,21 @@
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
(defcustom visual-wrap-extra-indent 0
|
(defcustom visual-wrap-extra-indent 0
|
||||||
"Number of extra spaces to indent in `visual-wrap-prefix-mode'.
|
"Number of extra columns to indent in `visual-wrap-prefix-mode'.
|
||||||
|
|
||||||
`visual-wrap-prefix-mode' indents the visual lines to the level
|
`visual-wrap-prefix-mode' indents the visual lines to the level
|
||||||
of the actual line plus `visual-wrap-extra-indent'. A negative
|
of the actual line plus `visual-wrap-extra-indent'. A negative
|
||||||
value will do a relative de-indent.
|
value will do a relative de-indent.
|
||||||
|
|
||||||
|
When the prefix is a repeated string (e.g. `> ' or `;;; '), the extra
|
||||||
|
indent is applied by appending or trimming space characters. When the
|
||||||
|
prefix is whitespace-only indentation, the extra indent is measured in
|
||||||
|
canonical character widths (the default font's average character width),
|
||||||
|
which may differ from the width of a space character in some fonts; the
|
||||||
|
canonical width is used because variable-pitch fonts often have
|
||||||
|
particularly narrow spaces, and the average character width produces
|
||||||
|
more predictable indentation.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
actual indent = 2
|
actual indent = 2
|
||||||
|
|
@ -58,7 +67,7 @@ extra indent = 2
|
||||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
|
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
|
||||||
ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||||
:type 'integer
|
:type 'integer
|
||||||
:safe 'integerp
|
:safe #'integerp
|
||||||
:version "30.1"
|
:version "30.1"
|
||||||
:group 'visual-line)
|
:group 'visual-line)
|
||||||
|
|
||||||
|
|
@ -128,49 +137,47 @@ members of `visual-wrap--safe-display-specs' (which see)."
|
||||||
eol-face)))))))
|
eol-face)))))))
|
||||||
|
|
||||||
(defun visual-wrap--adjust-prefix (prefix)
|
(defun visual-wrap--adjust-prefix (prefix)
|
||||||
"Adjust PREFIX with `visual-wrap-extra-indent'."
|
"Adjust the string PREFIX with `visual-wrap-extra-indent'."
|
||||||
(if (numberp prefix)
|
(let ((prefix-len (string-width prefix)))
|
||||||
(+ visual-wrap-extra-indent prefix)
|
(cond
|
||||||
(let ((prefix-len (string-width prefix)))
|
((= 0 visual-wrap-extra-indent)
|
||||||
(cond
|
prefix)
|
||||||
((= 0 visual-wrap-extra-indent)
|
((< 0 visual-wrap-extra-indent)
|
||||||
prefix)
|
(concat prefix (make-string visual-wrap-extra-indent ?\s)))
|
||||||
((< 0 visual-wrap-extra-indent)
|
((< 0 (+ visual-wrap-extra-indent prefix-len))
|
||||||
(concat prefix (make-string visual-wrap-extra-indent ?\s)))
|
(substring prefix
|
||||||
((< 0 (+ visual-wrap-extra-indent prefix-len))
|
0 (+ visual-wrap-extra-indent prefix-len)))
|
||||||
(substring prefix
|
(t
|
||||||
0 (+ visual-wrap-extra-indent prefix-len)))
|
""))))
|
||||||
(t
|
|
||||||
"")))))
|
|
||||||
|
|
||||||
(defun visual-wrap--apply-to-line ()
|
(defun visual-wrap--apply-to-line ()
|
||||||
"Apply visual-wrapping properties to the logical line starting at point."
|
"Apply visual-wrapping properties to the logical line starting at point."
|
||||||
(when-let* ((first-line-prefix (fill-match-adaptive-prefix))
|
(when-let* ((first-line-prefix (fill-match-adaptive-prefix))
|
||||||
(next-line-prefix (visual-wrap--content-prefix
|
(next-line-prefix (visual-wrap--content-prefix
|
||||||
first-line-prefix (point))))
|
first-line-prefix)))
|
||||||
(when (numberp next-line-prefix)
|
|
||||||
;; Set a minimum width for the prefix so it lines up correctly
|
|
||||||
;; with subsequent lines. Make sure not to do this past the end
|
|
||||||
;; of the line though! (`fill-match-adaptive-prefix' could
|
|
||||||
;; potentially return a prefix longer than the current line in the
|
|
||||||
;; buffer.)
|
|
||||||
(add-display-text-property
|
|
||||||
(point) (min (+ (point) (length first-line-prefix))
|
|
||||||
(pos-eol))
|
|
||||||
'min-width `((,next-line-prefix . width))))
|
|
||||||
(setq next-line-prefix (visual-wrap--adjust-prefix next-line-prefix))
|
|
||||||
(put-text-property
|
(put-text-property
|
||||||
(point) (pos-eol) 'wrap-prefix
|
(point) (pos-eol) 'wrap-prefix
|
||||||
(if (numberp next-line-prefix)
|
(if (numberp next-line-prefix)
|
||||||
`(space :align-to (,next-line-prefix . width))
|
;; Whitespace continuation: use a mixed-unit `:align-to' that
|
||||||
next-line-prefix))))
|
;; combines the pixel width from `visual-wrap--content-prefix'
|
||||||
|
;; with `visual-wrap-extra-indent' specified by the user in
|
||||||
|
;; canonical character widths. The display engine resolves
|
||||||
|
;; each unit per the active frame and sums them. If a large
|
||||||
|
;; negative `visual-wrap-extra-indent' makes the sum negative,
|
||||||
|
;; the display engine clamps the stretch width to zero
|
||||||
|
;; (xdisp.c), so the continuation starts at the left margin.
|
||||||
|
`(space :align-to (+ (,next-line-prefix)
|
||||||
|
(,visual-wrap-extra-indent . width)))
|
||||||
|
;; String prefix (e.g. `> ', `;;; '): adjust for extra
|
||||||
|
;; indent in characters, then use the string directly.
|
||||||
|
(visual-wrap--adjust-prefix next-line-prefix)))))
|
||||||
|
|
||||||
(defun visual-wrap--content-prefix (prefix position)
|
(defun visual-wrap--content-prefix (prefix)
|
||||||
"Get the next-line prefix for the specified first-line PREFIX.
|
"Get the next-line prefix for the specified first-line PREFIX.
|
||||||
POSITION is the position in the buffer where PREFIX is located.
|
POSITION is the position in the buffer where PREFIX is located.
|
||||||
|
|
||||||
This returns a string prefix to use for subsequent lines; an integer,
|
This returns a string prefix to use for subsequent lines; a number,
|
||||||
indicating the number of canonical-width spaces to use; or nil, if
|
indicating the pixel width to use for whitespace alignment; or nil if
|
||||||
PREFIX was empty."
|
PREFIX was empty."
|
||||||
(cond
|
(cond
|
||||||
((string= prefix "")
|
((string= prefix "")
|
||||||
|
|
@ -187,18 +194,13 @@ PREFIX was empty."
|
||||||
(remove-text-properties 0 (length prefix) '(wrap-prefix) prefix)
|
(remove-text-properties 0 (length prefix) '(wrap-prefix) prefix)
|
||||||
prefix)
|
prefix)
|
||||||
(t
|
(t
|
||||||
;; Otherwise, we want the prefix to be whitespace of the same width
|
;; Whitespace continuation: return the natural pixel width of the
|
||||||
;; as the first-line prefix. We want to return an integer width (in
|
;; first-line prefix. Using `string-pixel-width' (rather than a
|
||||||
;; units of the font's average-width) large enough to fit the
|
;; character count) accounts for any display transformation applied
|
||||||
;; first-line prefix.
|
;; to the prefix: invisibility, `display' replacements (e.g. icons,
|
||||||
(let ((avg-space (propertize (buffer-substring position (1+ position))
|
;; `display ""'), text scaling, proportional fonts. Continuation
|
||||||
'display '(space :width (1 . width)))))
|
;; lines then align with whatever line 1 actually renders.
|
||||||
;; Remove any `min-width' display specs since we'll replace with
|
(string-pixel-width prefix (current-buffer)))))
|
||||||
;; our own later in `visual-wrap--apply-to-line' (bug#73882).
|
|
||||||
(add-display-text-property 0 (length prefix) 'min-width nil prefix)
|
|
||||||
(max (string-width prefix)
|
|
||||||
(ceiling (string-pixel-width prefix (current-buffer))
|
|
||||||
(string-pixel-width avg-space (current-buffer))))))))
|
|
||||||
|
|
||||||
(defun visual-wrap-fill-context-prefix (beg end)
|
(defun visual-wrap-fill-context-prefix (beg end)
|
||||||
"Compute visual wrap prefix from text between BEG and END.
|
"Compute visual wrap prefix from text between BEG and END.
|
||||||
|
|
@ -215,8 +217,8 @@ by `visual-wrap-extra-indent'."
|
||||||
;; taskpaper-mode where paragraph-start matches everything).
|
;; taskpaper-mode where paragraph-start matches everything).
|
||||||
(or (let ((paragraph-start regexp-unmatchable))
|
(or (let ((paragraph-start regexp-unmatchable))
|
||||||
(fill-context-prefix beg end))
|
(fill-context-prefix beg end))
|
||||||
;; Note: fill-context-prefix may return nil; See:
|
;; Note: fill-context-prefix may return nil; See:
|
||||||
;; http://article.gmane.org/gmane.emacs.devel/156285
|
;; http://article.gmane.org/gmane.emacs.devel/156285
|
||||||
""))
|
""))
|
||||||
(prefix (visual-wrap--adjust-prefix fcp))
|
(prefix (visual-wrap--adjust-prefix fcp))
|
||||||
(face (visual-wrap--prefix-face fcp beg end)))
|
(face (visual-wrap--prefix-face fcp beg end)))
|
||||||
|
|
@ -226,8 +228,6 @@ by `visual-wrap-extra-indent'."
|
||||||
|
|
||||||
(defun visual-wrap--remove-properties (start end)
|
(defun visual-wrap--remove-properties (start end)
|
||||||
"Remove visual wrapping text properties from START to END."
|
"Remove visual wrapping text properties from START to END."
|
||||||
;; Remove `min-width' from any prefixes we detected.
|
|
||||||
(remove-display-text-property start end 'min-width)
|
|
||||||
;; Remove `wrap-prefix' related properties from any lines with
|
;; Remove `wrap-prefix' related properties from any lines with
|
||||||
;; prefixes we detected.
|
;; prefixes we detected.
|
||||||
(remove-text-properties start end '(wrap-prefix nil)))
|
(remove-text-properties start end '(wrap-prefix nil)))
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@
|
||||||
;;; Commentary:
|
;;; Commentary:
|
||||||
|
|
||||||
;; Tests for `visual-wrap-prefix-mode'.
|
;; Tests for `visual-wrap-prefix-mode'.
|
||||||
|
;;
|
||||||
|
;; Pixel values in these tests assume the batch-mode metric of one
|
||||||
|
;; pixel per canonical character column (`string-pixel-width " "' = 1).
|
||||||
|
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
|
|
@ -36,12 +39,8 @@
|
||||||
(should (equal-including-properties
|
(should (equal-including-properties
|
||||||
(buffer-string)
|
(buffer-string)
|
||||||
#("greetings\n* hello\n* hi"
|
#("greetings\n* hello\n* hi"
|
||||||
10 12 ( wrap-prefix (space :align-to (2 . width))
|
10 17 (wrap-prefix (space :align-to (+ (2) (0 . width))))
|
||||||
display (min-width ((2 . width))))
|
18 22 (wrap-prefix (space :align-to (+ (2) (0 . width)))))))))
|
||||||
12 17 ( wrap-prefix (space :align-to (2 . width)))
|
|
||||||
18 20 ( wrap-prefix (space :align-to (2 . width))
|
|
||||||
display (min-width ((2 . width))))
|
|
||||||
20 22 ( wrap-prefix (space :align-to (2 . width))))))))
|
|
||||||
|
|
||||||
(ert-deftest visual-wrap-tests/safe-display ()
|
(ert-deftest visual-wrap-tests/safe-display ()
|
||||||
"Test adding wrapping properties to text with safe display properties."
|
"Test adding wrapping properties to text with safe display properties."
|
||||||
|
|
@ -51,10 +50,9 @@
|
||||||
(should (equal-including-properties
|
(should (equal-including-properties
|
||||||
(buffer-string)
|
(buffer-string)
|
||||||
#("* hello"
|
#("* hello"
|
||||||
0 2 ( wrap-prefix (space :align-to (2 . width))
|
0 2 (wrap-prefix (space :align-to (+ (2) (0 . width))))
|
||||||
display (min-width ((2 . width))))
|
2 7 (wrap-prefix (space :align-to (+ (2) (0 . width)))
|
||||||
2 7 ( wrap-prefix (space :align-to (2 . width))
|
display (raise 1)))))))
|
||||||
display (raise 1)))))))
|
|
||||||
|
|
||||||
(ert-deftest visual-wrap-tests/unsafe-display/within-line ()
|
(ert-deftest visual-wrap-tests/unsafe-display/within-line ()
|
||||||
"Test adding wrapping properties to text with unsafe display properties.
|
"Test adding wrapping properties to text with unsafe display properties.
|
||||||
|
|
@ -66,10 +64,9 @@ When these properties don't extend across multiple lines,
|
||||||
(should (equal-including-properties
|
(should (equal-including-properties
|
||||||
(buffer-string)
|
(buffer-string)
|
||||||
#("* [img]"
|
#("* [img]"
|
||||||
0 2 ( wrap-prefix (space :align-to (2 . width))
|
0 2 (wrap-prefix (space :align-to (+ (2) (0 . width))))
|
||||||
display (min-width ((2 . width))))
|
2 7 (wrap-prefix (space :align-to (+ (2) (0 . width)))
|
||||||
2 7 ( wrap-prefix (space :align-to (2 . width))
|
display (image :type bmp)))))))
|
||||||
display (image :type bmp)))))))
|
|
||||||
|
|
||||||
(ert-deftest visual-wrap-tests/unsafe-display/spanning-lines ()
|
(ert-deftest visual-wrap-tests/unsafe-display/spanning-lines ()
|
||||||
"Test adding wrapping properties to text with unsafe display properties.
|
"Test adding wrapping properties to text with unsafe display properties.
|
||||||
|
|
@ -126,18 +123,14 @@ See bug#76018."
|
||||||
(should (equal-including-properties
|
(should (equal-including-properties
|
||||||
(buffer-string)
|
(buffer-string)
|
||||||
#("* this zoo contains goats"
|
#("* this zoo contains goats"
|
||||||
0 2 ( wrap-prefix (space :align-to (2 . width))
|
0 25 (wrap-prefix (space :align-to (+ (2) (0 . width)))))))
|
||||||
display (min-width ((2 . width))))
|
|
||||||
2 25 ( wrap-prefix (space :align-to (2 . width))))))
|
|
||||||
(let ((start (point)))
|
(let ((start (point)))
|
||||||
(insert-and-inherit "\n\nit also contains pandas")
|
(insert-and-inherit "\n\nit also contains pandas")
|
||||||
(visual-wrap-prefix-function start (point-max)))
|
(visual-wrap-prefix-function start (point-max)))
|
||||||
(should (equal-including-properties
|
(should (equal-including-properties
|
||||||
(buffer-string)
|
(buffer-string)
|
||||||
#("* this zoo contains goats\n\nit also contains pandas"
|
#("* this zoo contains goats\n\nit also contains pandas"
|
||||||
0 2 ( wrap-prefix (space :align-to (2 . width))
|
0 25 (wrap-prefix (space :align-to (+ (2) (0 . width)))))))))
|
||||||
display (min-width ((2 . width))))
|
|
||||||
2 25 ( wrap-prefix (space :align-to (2 . width))))))))
|
|
||||||
|
|
||||||
(ert-deftest visual-wrap-tests/cleanup ()
|
(ert-deftest visual-wrap-tests/cleanup ()
|
||||||
"Test that deactivating `visual-wrap-prefix-mode' cleans up text properties."
|
"Test that deactivating `visual-wrap-prefix-mode' cleans up text properties."
|
||||||
|
|
@ -146,11 +139,43 @@ See bug#76018."
|
||||||
(visual-wrap-prefix-function (point-min) (point-max))
|
(visual-wrap-prefix-function (point-min) (point-max))
|
||||||
;; Make sure we've added the visual-wrapping properties.
|
;; Make sure we've added the visual-wrapping properties.
|
||||||
(should (equal (text-properties-at (point-min))
|
(should (equal (text-properties-at (point-min))
|
||||||
'( wrap-prefix (space :align-to (2 . width))
|
'(wrap-prefix (space :align-to (+ (2) (0 . width))))))
|
||||||
display (min-width ((2 . width))))))
|
|
||||||
(visual-wrap-prefix-mode -1)
|
(visual-wrap-prefix-mode -1)
|
||||||
(should (equal-including-properties
|
(should (equal-including-properties
|
||||||
(buffer-string)
|
(buffer-string)
|
||||||
"* hello\n* hi"))))
|
"* hello\n* hi"))))
|
||||||
|
|
||||||
|
(ert-deftest visual-wrap-tests/negative-extra-indent ()
|
||||||
|
"A large negative `visual-wrap-extra-indent' does not break alignment.
|
||||||
|
The mixed-unit `:align-to' sum may go negative, but the display engine
|
||||||
|
clamps the stretch width to zero (xdisp.c), so the continuation starts
|
||||||
|
at the left margin."
|
||||||
|
(with-temp-buffer
|
||||||
|
(setq-local visual-wrap-extra-indent -20)
|
||||||
|
(insert "* hello")
|
||||||
|
(visual-wrap-prefix-function (point-min) (point-max))
|
||||||
|
;; The sum (+ (2) (-20 . width)) is negative in batch mode
|
||||||
|
;; (2 - 20 = -18), but the display engine clamps to zero.
|
||||||
|
(should (equal (get-text-property (point-min) 'wrap-prefix)
|
||||||
|
'(space :align-to (+ (2) (-20 . width)))))))
|
||||||
|
|
||||||
|
(ert-deftest visual-wrap-tests/invisible-prefix ()
|
||||||
|
"Invisible prefix characters do not reserve column space.
|
||||||
|
The natural pixel width of a fully invisible prefix is zero, so the
|
||||||
|
continuation `wrap-prefix' aligns to pixel 0 and no `min-width' display
|
||||||
|
property is installed on line 1. See bug#81039."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert (propertize "### " 'invisible t))
|
||||||
|
(insert "Heading")
|
||||||
|
(visual-wrap-prefix-function (point-min) (point-max))
|
||||||
|
(should (equal (get-text-property (point-min) 'wrap-prefix)
|
||||||
|
'(space :align-to (+ (0) (0 . width)))))
|
||||||
|
;; The original bug was that `min-width' got installed on the
|
||||||
|
;; invisible prefix region, padding line 1 even though the prefix
|
||||||
|
;; rendered at zero pixels. The redesign installs no `min-width'
|
||||||
|
;; at all.
|
||||||
|
(should-not (memq 'min-width
|
||||||
|
(ensure-list
|
||||||
|
(get-text-property (point-min) 'display))))))
|
||||||
|
|
||||||
;; visual-wrap-tests.el ends here
|
;; visual-wrap-tests.el ends here
|
||||||
|
|
|
||||||
421
test/manual/visual-wrap-test.el
Normal file
421
test/manual/visual-wrap-test.el
Normal file
|
|
@ -0,0 +1,421 @@
|
||||||
|
;;; visual-wrap-test.el --- Manual tests for visual-wrap-prefix-mode -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2026 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
;; This file is part of GNU Emacs.
|
||||||
|
|
||||||
|
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||||||
|
;; it under the terms of the GNU General Public License as published by
|
||||||
|
;; the Free Software Foundation, either version 3 of the License, or
|
||||||
|
;; (at your option) any later version.
|
||||||
|
|
||||||
|
;; GNU Emacs is distributed in the hope that it will be useful,
|
||||||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
;; GNU General Public License for more details.
|
||||||
|
|
||||||
|
;; You should have received a copy of the GNU General Public License
|
||||||
|
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Manual test suite for `visual-wrap-prefix-mode'. Each test opens a
|
||||||
|
;; buffer named *visual-wrap-test-NNN* with an explanatory banner at
|
||||||
|
;; the top, followed by sample lines. The banner describes what to
|
||||||
|
;; look for; the code below carries no parallel documentation.
|
||||||
|
;;
|
||||||
|
;; Run from `emacs -Q':
|
||||||
|
;;
|
||||||
|
;; emacs -Q -l test/manual/visual-wrap-test.el \
|
||||||
|
;; --eval "(visual-wrap-test-001)"
|
||||||
|
;;
|
||||||
|
;; Append `-nw' to the same invocation to repeat each test in a TTY
|
||||||
|
;; frame. `string-pixel-width' adapts to the frame, so GUI and TTY
|
||||||
|
;; runs share the same expectations modulo test 004 (variable-pitch),
|
||||||
|
;; which degrades silently on a TTY.
|
||||||
|
;;
|
||||||
|
;; Tests:
|
||||||
|
;; 001 Visible fixed-pitch prefix (baseline / regression check).
|
||||||
|
;; 002 Fully invisible prefix (the original bug).
|
||||||
|
;; 003 Partially invisible prefix.
|
||||||
|
;; 004a Variable-pitch narrow prefix `;;; ' (GUI only).
|
||||||
|
;; 004b Variable-pitch wide prefix `%%% ' (GUI only).
|
||||||
|
;; 005 Non-zero `visual-wrap-extra-indent'.
|
||||||
|
;; 006 markdown-ts-mode + `markdown-ts-hide-markup' (real-world repro).
|
||||||
|
;; 007 org-table-style `|' prefix (regression check for bug#73882).
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(defconst visual-wrap-test--long
|
||||||
|
"The quick brown fox jumps over the lazy dog, repeatedly, with great enthusiasm, again and again, until the moon comes up and the cows come home, and then some more for good measure."
|
||||||
|
"A line long enough to overflow any reasonable window.")
|
||||||
|
|
||||||
|
(defun visual-wrap-test--prepare (name)
|
||||||
|
"Create or reset buffer NAME, plain `text-mode', return it."
|
||||||
|
(let ((buf (get-buffer-create name)))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(read-only-mode -1)
|
||||||
|
(erase-buffer)
|
||||||
|
(kill-all-local-variables)
|
||||||
|
(text-mode))
|
||||||
|
buf))
|
||||||
|
|
||||||
|
(defun visual-wrap-test--show (buf)
|
||||||
|
"Enable `visual-wrap-prefix-mode' in BUF and switch to it."
|
||||||
|
(with-current-buffer buf
|
||||||
|
(goto-char (point-min))
|
||||||
|
(visual-wrap-prefix-mode 1))
|
||||||
|
(switch-to-buffer buf))
|
||||||
|
|
||||||
|
(defun visual-wrap-test--insert-invisible (text)
|
||||||
|
"Insert TEXT and mark its character range invisible via text properties.
|
||||||
|
Uses `invisible t', which is matched by the default
|
||||||
|
`buffer-invisibility-spec'."
|
||||||
|
(let ((start (point)))
|
||||||
|
(insert text)
|
||||||
|
(put-text-property start (point) 'invisible t)))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-001 ()
|
||||||
|
"Baseline: visible fixed-pitch prefix. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buf (visual-wrap-test--prepare "*visual-wrap-test-001*")))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "\
|
||||||
|
visual-wrap-test-001 — visible fixed-pitch prefix (baseline)
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Mode: `text-mode'. The paragraph below starts with `> ', which
|
||||||
|
the default `adaptive-fill-regexp' matches as a paragraph prefix.
|
||||||
|
|
||||||
|
`visual-wrap-prefix-mode' is enabled in this buffer. Narrow the
|
||||||
|
window until the long paragraph wraps onto several visual lines.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
* Line 1 is not shifted; `> ' renders at its natural width.
|
||||||
|
* Continuation visual lines align horizontally with the first
|
||||||
|
character that follows `> ' on line 1.
|
||||||
|
|
||||||
|
This case worked correctly before the patch (bug#81039). The test confirms
|
||||||
|
the redesign (bug#81039) has not broken the common fixed-pitch fixed-width
|
||||||
|
case while fixing the invisible-prefix and variable-pitch cases.
|
||||||
|
|
||||||
|
Sample line:
|
||||||
|
|
||||||
|
> ")
|
||||||
|
(insert visual-wrap-test--long "\n"))
|
||||||
|
(visual-wrap-test--show buf)))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-002 ()
|
||||||
|
"Fully invisible prefix. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buf (visual-wrap-test--prepare "*visual-wrap-test-002*")))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "\
|
||||||
|
visual-wrap-test-002 — fully invisible prefix
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
Mode: `text-mode'. The paragraph below starts with `### ', and
|
||||||
|
all four of those characters carry `invisible t' as a text
|
||||||
|
property. The default `buffer-invisibility-spec' includes t, so
|
||||||
|
the display engine renders them at zero pixels.
|
||||||
|
|
||||||
|
The original bug reported in bug#81039: `visual-wrap--content-prefix'
|
||||||
|
used `string-width' to derive a column count, which ignores
|
||||||
|
invisibility. It therefore reserved four columns of `min-width' on
|
||||||
|
line 1 and shifted the visible content rightward.
|
||||||
|
|
||||||
|
`visual-wrap-prefix-mode' is enabled. Narrow the window so the
|
||||||
|
paragraph wraps.
|
||||||
|
|
||||||
|
Expected with the redesign (bug#81039):
|
||||||
|
* Line 1 is NOT shifted; the visible content starts at column 0
|
||||||
|
(the `### ' has zero rendered width).
|
||||||
|
* Continuation visual lines also start at column 0, since the
|
||||||
|
natural pixel width of the prefix is zero.
|
||||||
|
|
||||||
|
Sample line:
|
||||||
|
|
||||||
|
")
|
||||||
|
(visual-wrap-test--insert-invisible "### ")
|
||||||
|
(insert visual-wrap-test--long "\n"))
|
||||||
|
(visual-wrap-test--show buf)))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-003 ()
|
||||||
|
"Partially invisible prefix. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buf (visual-wrap-test--prepare "*visual-wrap-test-003*")))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "\
|
||||||
|
visual-wrap-test-003 — partially invisible prefix
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
Mode: `text-mode'. The paragraph below begins with `### '
|
||||||
|
\(four characters: three hashes and a space). The first two
|
||||||
|
hashes carry `invisible t'; the third hash and the space remain
|
||||||
|
visible. The visible portion of the prefix is therefore `# ',
|
||||||
|
two columns wide.
|
||||||
|
|
||||||
|
`visual-wrap-prefix-mode' is enabled. Narrow the window so the
|
||||||
|
paragraph wraps.
|
||||||
|
|
||||||
|
Expected with the redesign (bug#81039):
|
||||||
|
* Line 1 shows `# ' at column 0, followed by the paragraph
|
||||||
|
text — no extra padding to compensate for the hidden hashes.
|
||||||
|
* Continuation visual lines align with the first character
|
||||||
|
after the visible `# ' on line 1 (i.e. two columns in).
|
||||||
|
|
||||||
|
If line 1's content begins past column 2, or continuations land
|
||||||
|
elsewhere than two columns in, the natural-width computation is
|
||||||
|
not honoring per-character invisibility.
|
||||||
|
|
||||||
|
Sample line:
|
||||||
|
|
||||||
|
")
|
||||||
|
(visual-wrap-test--insert-invisible "##")
|
||||||
|
(insert "# ")
|
||||||
|
(insert visual-wrap-test--long "\n"))
|
||||||
|
(visual-wrap-test--show buf)))
|
||||||
|
|
||||||
|
(defun visual-wrap-test--variable-pitch (name prefix narrow-or-wide)
|
||||||
|
"Set up a variable-pitch test buffer named NAME with PREFIX.
|
||||||
|
NARROW-OR-WIDE is the string \"narrow\" or \"wide\", used only in
|
||||||
|
the banner."
|
||||||
|
(let ((buf (visual-wrap-test--prepare name)))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(when (display-graphic-p)
|
||||||
|
(variable-pitch-mode 1))
|
||||||
|
(unless (display-graphic-p)
|
||||||
|
(insert "\
|
||||||
|
NOTE: this Emacs frame is a TTY. `variable-pitch-mode' has no
|
||||||
|
effect; every glyph is exactly one column wide. The test below
|
||||||
|
therefore degenerates to a visible fixed-pitch prefix (similar to
|
||||||
|
test 001). Re-run inside a GUI frame to actually test the
|
||||||
|
variable-pitch path.
|
||||||
|
|
||||||
|
"))
|
||||||
|
(insert (format "\
|
||||||
|
visual-wrap-test — variable-pitch %s prefix `%s'
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
Mode: `text-mode' + `variable-pitch-mode' (GUI only). The
|
||||||
|
paragraph below starts with `%s', whose natural pixel width in
|
||||||
|
a proportional font is %s than the same number of monospace
|
||||||
|
columns.
|
||||||
|
|
||||||
|
This is the case Jim Porter's 2024 commit was designed to handle:
|
||||||
|
under the old `(max string-width (ceiling pixel/avg-space))'
|
||||||
|
formula, the column-rounded `min-width' on line 1 over-padded the
|
||||||
|
prefix. Under the redesign (bug#81039), the continuation `wrap-prefix' uses
|
||||||
|
the prefix's pixel width directly, so no rounding occurs.
|
||||||
|
|
||||||
|
`visual-wrap-prefix-mode' is enabled. Narrow the window so the
|
||||||
|
paragraph wraps.
|
||||||
|
|
||||||
|
Expected with the redesign (bug#81039):
|
||||||
|
* Line 1 renders `%s' at its natural pixel width.
|
||||||
|
* Continuation visual lines align with the first character that
|
||||||
|
follows `%s' on line 1, in pixels — no visible jitter
|
||||||
|
between line 1 and the wrapped lines.
|
||||||
|
|
||||||
|
To compare against the pre-bug#81039 behavior, re-run this test
|
||||||
|
without `--load'ing the patched `visual-wrap.el' (bug#81039) (i.e. let
|
||||||
|
the built-in version handle the buffer). You should see a small
|
||||||
|
but real horizontal gap between the prefix end on line 1 and the
|
||||||
|
start of continuation lines.
|
||||||
|
|
||||||
|
Sample line:
|
||||||
|
|
||||||
|
%s" narrow-or-wide prefix prefix narrow-or-wide prefix prefix prefix))
|
||||||
|
(insert visual-wrap-test--long "\n\n"
|
||||||
|
(format "\
|
||||||
|
|
||||||
|
=== Appendix: artifact in banner text (out of scope of bug#81039) ===
|
||||||
|
|
||||||
|
On close inspection in GUI, the bullet-prefix line
|
||||||
|
|
||||||
|
* Continuation...
|
||||||
|
|
||||||
|
above sits a few pixels right of its follow-on buffer lines that start
|
||||||
|
with four spaces
|
||||||
|
|
||||||
|
follows `%s' on...
|
||||||
|
|
||||||
|
This is an artifact due to the specific content in the banner. Because
|
||||||
|
the prefix width is computed for each physical line separately, in the
|
||||||
|
case of a variable-pitch font we end up having slightly different widths
|
||||||
|
(in this example, the differences are barely visible by human eye). In
|
||||||
|
fact, ` * ' and ` ' are separate adaptive-fill prefixes on independent
|
||||||
|
buffer lines, and `visual-wrap.el' processes them independently: it has
|
||||||
|
no notion of \"these lines belong to one logical bullet\". Under the
|
||||||
|
old code the step was an estimated ~17 pixels (column-rounded
|
||||||
|
`min-width' on ` * ' line, no processing on ` ' lines). The
|
||||||
|
redesign (bug#81039) drops it to an estimated ~2 pixels (natural pixel
|
||||||
|
width of ` * ' is slightly larger than that of ` ' in a proportional
|
||||||
|
font), small enough to look aligned.
|
||||||
|
" prefix)))
|
||||||
|
(visual-wrap-test--show buf)))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-004a ()
|
||||||
|
"Variable-pitch narrow prefix `;;; '. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(visual-wrap-test--variable-pitch
|
||||||
|
"*visual-wrap-test-004a*" ";;; " "narrower"))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-004b ()
|
||||||
|
"Variable-pitch wide prefix `%%% '. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(visual-wrap-test--variable-pitch
|
||||||
|
"*visual-wrap-test-004b*" "%%% " "wider"))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-005 ()
|
||||||
|
"Non-zero `visual-wrap-extra-indent'. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buf (visual-wrap-test--prepare "*visual-wrap-test-005*")))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(setq-local visual-wrap-extra-indent 4)
|
||||||
|
(insert "\
|
||||||
|
visual-wrap-test-005 — non-zero `visual-wrap-extra-indent'
|
||||||
|
=========================================================
|
||||||
|
|
||||||
|
Mode: `text-mode'. `visual-wrap-extra-indent' is set buffer-local
|
||||||
|
to 4.
|
||||||
|
|
||||||
|
`visual-wrap--adjust-prefix' must now convert four canonical-char
|
||||||
|
columns to pixels before adding them to the prefix's pixel width
|
||||||
|
(since `visual-wrap--content-prefix' returns a pixel count under
|
||||||
|
the redesign (bug#81039)).
|
||||||
|
|
||||||
|
`visual-wrap-prefix-mode' is enabled. Narrow the window so the
|
||||||
|
paragraph wraps.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
* Continuation visual lines start four canonical-character
|
||||||
|
columns to the right of the `> ' prefix on line 1.
|
||||||
|
* Line 1 itself is not shifted.
|
||||||
|
|
||||||
|
If continuations land at zero columns past the prefix end, the
|
||||||
|
column-to-pixel conversion in `visual-wrap--adjust-prefix' is not
|
||||||
|
firing. If they land somewhere fractional or wrong, the unit
|
||||||
|
conversion is wrong.
|
||||||
|
|
||||||
|
Sample line:
|
||||||
|
|
||||||
|
> ")
|
||||||
|
(insert visual-wrap-test--long "\n"))
|
||||||
|
(visual-wrap-test--show buf)))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-006 ()
|
||||||
|
"markdown-ts-mode + `markdown-ts-hide-markup'. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((have-mode (fboundp 'markdown-ts-mode))
|
||||||
|
(buf (get-buffer-create "*visual-wrap-test-006*")))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(read-only-mode -1)
|
||||||
|
(erase-buffer)
|
||||||
|
(kill-all-local-variables)
|
||||||
|
(unless have-mode
|
||||||
|
(insert "\
|
||||||
|
WARNING: this Emacs build does not expose `markdown-ts-mode`.
|
||||||
|
Test 006 cannot run. Use Emacs 31 or newer.
|
||||||
|
|
||||||
|
"))
|
||||||
|
(insert "\
|
||||||
|
visual-wrap-test-006 — markdown-ts-mode + hide-markup (real-world repro)
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Mode: `markdown-ts-mode` with `markdown-ts-hide-markup` enabled. This
|
||||||
|
is the case that originally exposed the bug reported in bug#81039. The
|
||||||
|
ATX heading marker `### ` carries `invisible markdown-ts--markup`, which
|
||||||
|
is in `buffer-invisibility-spec` while hide-markup is on.
|
||||||
|
|
||||||
|
Default `adaptive-fill-regexp` matches `### ` as a paragraph
|
||||||
|
prefix. Under the old code, hidden hashes were still counted by
|
||||||
|
`string-width`, so the heading text shifted right by four columns
|
||||||
|
the moment `visual-wrap-prefix-mode` came on — visible even
|
||||||
|
without any wrapping happening.
|
||||||
|
|
||||||
|
Two cases are demonstrated below: a short heading (no wrap
|
||||||
|
needed, but the shift was visible) and a long heading (wraps,
|
||||||
|
and the continuation must align with the visible heading text).
|
||||||
|
|
||||||
|
Expected with the redesign (bug#81039):
|
||||||
|
* Short heading: not shifted; reads as `A short heading`.
|
||||||
|
* Long heading: line 1 not shifted; continuation visual lines
|
||||||
|
align with the start of the visible heading text.
|
||||||
|
|
||||||
|
To compare, revert the patch (bug#81039), restart, and re-run; you
|
||||||
|
should see the heading text on line 1 shift right by four columns.
|
||||||
|
|
||||||
|
Case 1 — short heading (no wrap; the shift was visible without
|
||||||
|
wrapping under the old code):
|
||||||
|
|
||||||
|
### A short heading
|
||||||
|
|
||||||
|
Case 2 — long heading (wraps; continuation visual lines must
|
||||||
|
align with the start of the visible heading text):
|
||||||
|
|
||||||
|
### ")
|
||||||
|
(insert visual-wrap-test--long "\n")
|
||||||
|
(when have-mode
|
||||||
|
(markdown-ts-mode)
|
||||||
|
;; Enable hide-markup directly. `markdown-ts-toggle-hide-markup'
|
||||||
|
;; is not autoloaded, so it is not yet bound at the time the
|
||||||
|
;; enclosing `let' captures `fboundp' on entry.
|
||||||
|
(setq markdown-ts-hide-markup t)
|
||||||
|
(add-to-invisibility-spec 'markdown-ts--markup)
|
||||||
|
(font-lock-flush))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(visual-wrap-prefix-mode 1))
|
||||||
|
(switch-to-buffer buf)))
|
||||||
|
|
||||||
|
(defun visual-wrap-test-007 ()
|
||||||
|
"Org-table-style `|' prefix. See banner in the test buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buf (visual-wrap-test--prepare "*visual-wrap-test-007*")))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "\
|
||||||
|
visual-wrap-test-007 — org-table-style `|' prefix (bug#73882 regression)
|
||||||
|
========================================================================
|
||||||
|
|
||||||
|
Mode: `text-mode'. The buffer below contains a pre-aligned org-style
|
||||||
|
table. `|' is in the default `adaptive-fill-regexp', so each table
|
||||||
|
row is treated as a logical line with `| ' as its first-line prefix.
|
||||||
|
|
||||||
|
Original bug: with `global-visual-wrap-prefix-mode' enabled, the table
|
||||||
|
cells in the first column got misaligned because `min-width' from a
|
||||||
|
prior fontification of the same `|' character accumulated on each
|
||||||
|
pass, inflating the width past one space. Reporter:
|
||||||
|
Arthur Elsenaar, 2024-10-19. Fixed by Jim Porter as 81a5beb8af0
|
||||||
|
\(strip prior `min-width' before measuring the prefix).
|
||||||
|
|
||||||
|
The redesign (bug#81039) supersedes that fix at a lower level: no
|
||||||
|
`min-width' display property is installed at all, so there is nothing
|
||||||
|
that can accumulate across fontification passes.
|
||||||
|
|
||||||
|
`visual-wrap-prefix-mode' is enabled in this buffer.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
* The table cells stay aligned. Each `|' character in every column
|
||||||
|
sits at the same horizontal position from row to row.
|
||||||
|
* No `min-width' property appears anywhere on the table text.
|
||||||
|
|
||||||
|
To compare against the pre-Jim-Porter behavior, you would need to
|
||||||
|
revert his commit and ours; this is purely a regression check today.
|
||||||
|
|
||||||
|
You may also want to run `M-x org-mode' in this buffer and verify
|
||||||
|
that the table remains properly aligned.
|
||||||
|
|
||||||
|
Sample table:
|
||||||
|
|
||||||
|
| head | 1 | 2 | 3 | 4 |
|
||||||
|
|--------+---+---+---+---|
|
||||||
|
| apple | | | | |
|
||||||
|
| orange | | | | |
|
||||||
|
| pear | | | | |
|
||||||
|
| banana | | | | |
|
||||||
|
"))
|
||||||
|
(visual-wrap-test--show buf)))
|
||||||
|
|
||||||
|
(provide 'visual-wrap-test)
|
||||||
|
|
||||||
|
;;; visual-wrap-test.el ends here
|
||||||
Loading…
Reference in a new issue