-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathorg-indent-pixel.el
More file actions
195 lines (174 loc) · 8.43 KB
/
org-indent-pixel.el
File metadata and controls
195 lines (174 loc) · 8.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
;;; org-indent-pixel.el --- Pixel-accurate wrap-prefix for variable-pitch Org buffers -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Pablo Stafforini
;; Author: Pablo Stafforini <pablo@stafforini.com>
;; Maintainer: Pablo Stafforini <pablo@stafforini.com>
;; URL: https://github.com/benthamite/org-indent-pixel
;; Version: 0.1.1
;; Package-Requires: ((emacs "29.1") (org "9.6"))
;; Keywords: outlines, faces
;; This file is NOT part of GNU Emacs.
;; This program 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.
;;
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Fix misaligned wrapped lines in variable-pitch Org buffers.
;;
;; When `org-indent-mode' and `buffer-face-mode' (with a variable-pitch
;; face) are both active, list item continuation lines become progressively
;; misaligned at deeper nesting levels. This happens because `org-indent'
;; sets `wrap-prefix' using fixed-width space characters, but
;; variable-pitch fonts have different space widths.
;;
;; `org-indent-pixel-mode' fixes this by advising
;; `org-indent-set-line-properties' to replace the space-based
;; `wrap-prefix' with a pixel-accurate specification. It measures the
;; actual rendered width of the line-prefix and buffer content (including
;; display properties from packages like `org-modern') in the
;; variable-pitch font, then sets `wrap-prefix' to an exact pixel value.
;;; Code:
(require 'org)
(require 'org-indent)
(defgroup org-indent-pixel nil
"Pixel-accurate wrap-prefix for variable-pitch Org buffers."
:group 'org-indent
:prefix "org-indent-pixel-")
(defvar org-indent-pixel--buffer-count 0
"Number of buffers with `org-indent-pixel-mode' currently active.
Used to manage the global advice on `org-indent-set-line-properties'.")
(defconst org-indent-pixel--work-buffer-name " *org-indent-pixel*"
"Name of the hidden work buffer used for pixel width measurement.")
(defun org-indent-pixel--string-pixel-width (string)
"Like `string-pixel-width' but with the calling buffer's face remapping.
`string-pixel-width' uses an internal work buffer that lacks the
calling buffer's `face-remapping-alist', so faces that inherit from
`default' resolve to the monospace font instead of the variable-pitch
font of `buffer-face-mode'."
(if (zerop (length string))
0
(let ((remapping face-remapping-alist))
(with-current-buffer (get-buffer-create org-indent-pixel--work-buffer-name)
(setq-local face-remapping-alist remapping)
(setq line-prefix nil
wrap-prefix nil)
(setq-local display-line-numbers nil)
(delete-region (point-min) (point-max))
(insert (propertize string 'line-prefix nil 'wrap-prefix nil))
(car (buffer-text-pixel-size nil nil t))))))
(defun org-indent-pixel--kill-work-buffer ()
"Kill the hidden work buffer if it exists."
(when-let ((buf (get-buffer org-indent-pixel--work-buffer-name)))
(kill-buffer buf)))
(defun org-indent-pixel--fix-line (_level indentation &optional heading)
"Fix `wrap-prefix' on the current line.
Replace the space-based `wrap-prefix' set by
`org-indent-set-line-properties' with a pixel-accurate specification.
_LEVEL is ignored. INDENTATION is the target column for the body text.
When HEADING is non-nil the line is a heading and is skipped."
(when (and (bound-and-true-p org-indent-pixel-mode)
(bound-and-true-p buffer-face-mode)
(not heading)
(> indentation 0))
(save-excursion
;; `org-indent-set-line-properties' calls `forward-line' at the end,
;; so point is now at the beginning of the NEXT line.
(forward-line -1)
(let ((lp (get-text-property (point) 'line-prefix))
(wp (get-text-property (point) 'wrap-prefix)))
(when (and (stringp wp) (stringp lp)
(> (length wp) (length lp)))
(let* ((bol (line-beginning-position))
(next-bol (line-beginning-position 2))
(body-start (save-excursion
(goto-char bol)
(move-to-column indentation)
(point)))
(reached-col (save-excursion
(goto-char body-start)
(current-column))))
;; Only proceed if `move-to-column' actually reached the target
;; column. If the line is shorter than INDENTATION, the
;; measurement would be for the wrong amount of text.
(when (= reached-col indentation)
(let* ((s (buffer-substring bol body-start))
(len (length s)))
(when (> len 0)
(remove-text-properties
0 len '(line-prefix nil wrap-prefix nil fontified nil) s)
(let* ((text-px (org-indent-pixel--string-pixel-width s))
(lp-px (org-indent-pixel--string-pixel-width lp))
(total-px (+ lp-px text-px)))
(when (> total-px 0)
(put-text-property
bol (min next-bol (point-max))
'wrap-prefix
(propertize " " 'display
`(space :width (,total-px)))))))))))))))
;;;###autoload
(define-minor-mode org-indent-pixel-mode
"Pixel-accurate wrap-prefix for variable-pitch Org buffers.
When enabled, replace the space-based `wrap-prefix' from
`org-indent-mode' with pixel specifications so that list item
continuation lines align correctly in variable-pitch fonts."
:lighter nil
(if org-indent-pixel-mode
(if (not (display-graphic-p))
(progn
(setq org-indent-pixel-mode nil)
(message "org-indent-pixel-mode requires a graphical display"))
(cl-incf org-indent-pixel--buffer-count)
(advice-add 'org-indent-set-line-properties
:after #'org-indent-pixel--fix-line)
(when org-indent-mode
(org-indent-add-properties (point-min) (point-max))))
(cl-decf org-indent-pixel--buffer-count)
(when (<= org-indent-pixel--buffer-count 0)
(setq org-indent-pixel--buffer-count 0)
(advice-remove 'org-indent-set-line-properties
#'org-indent-pixel--fix-line)
(org-indent-pixel--kill-work-buffer))
(when org-indent-mode
(org-indent-add-properties (point-min) (point-max)))))
(defun org-indent-pixel--maybe-activate ()
"Activate or deactivate `org-indent-pixel-mode' when conditions change.
Intended for use in `org-mode-hook' and `buffer-face-mode-hook'.
Activates the mode when `org-indent-mode', `buffer-face-mode', and
`org-mode' are all active. Deactivates the mode when `buffer-face-mode'
is turned off."
(cond
((and (derived-mode-p 'org-mode)
(bound-and-true-p org-indent-mode)
(bound-and-true-p buffer-face-mode)
(not (bound-and-true-p org-indent-pixel-mode)))
(org-indent-pixel-mode 1))
((and (bound-and-true-p org-indent-pixel-mode)
(not (bound-and-true-p buffer-face-mode)))
(org-indent-pixel-mode -1))))
;;;###autoload
(defun org-indent-pixel-setup ()
"Set up hooks to activate `org-indent-pixel-mode' automatically.
Call this once in your init file to enable `org-indent-pixel-mode'
in all Org buffers that use both `org-indent-mode' and
`buffer-face-mode'."
(when (display-graphic-p)
(add-hook 'org-mode-hook #'org-indent-pixel--maybe-activate 90)
(add-hook 'buffer-face-mode-hook #'org-indent-pixel--maybe-activate)))
(defun org-indent-pixel-teardown ()
"Remove hooks and disable `org-indent-pixel-mode' in all buffers.
Reverses the effect of `org-indent-pixel-setup'."
(remove-hook 'org-mode-hook #'org-indent-pixel--maybe-activate)
(remove-hook 'buffer-face-mode-hook #'org-indent-pixel--maybe-activate)
(dolist (buf (buffer-list))
(with-current-buffer buf
(when (bound-and-true-p org-indent-pixel-mode)
(org-indent-pixel-mode -1)))))
(provide 'org-indent-pixel)
;;; org-indent-pixel.el ends here