-
Notifications
You must be signed in to change notification settings - Fork 14
/
org-fragtog.el
254 lines (221 loc) · 10.8 KB
/
org-fragtog.el
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
;;; org-fragtog.el --- Auto-toggle Org LaTeX fragments -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Benjamin Levy - MIT/X11 License
;; Author: Benjamin Levy <blevy@protonmail.com>
;; Version: 0.4.2
;; Description: Automatically toggle Org mode LaTeX fragment previews as the cursor enters and exits them
;; Homepage: https://github.com/io12/org-fragtog
;; Package-Requires: ((emacs "27.1"))
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;;
;; The above copyright notice and this permission notice shall be included in all
;; copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
;;; Commentary:
;; This package automates toggling Org mode LaTeX fragment
;; previews. Fragment previews are disabled for editing when
;; your cursor steps onto them, and re-enabled when the cursor
;; leaves.
;;; Code:
(require 'org)
(require 'org-element)
(defgroup org-fragtog nil
"Auto-toggle Org LaTeX fragments."
:group 'org-latex)
(defcustom org-fragtog-ignore-predicates nil
"List of predicates to determine whether to ignore a fragment.
For example, adding `org-at-table-p' will ignore fragments inside tables."
:group 'org-fragtog
:type 'hook
:options '(org-at-table-p
org-at-table.el-p
org-at-block-p
org-at-heading-p))
(defcustom org-fragtog-preview-delay 0.0
"Seconds of delay before LaTeX preview."
:group 'org-fragtog
:type 'number)
;;;###autoload
(define-minor-mode org-fragtog-mode
"A minor mode that automatically toggles Org mode LaTeX fragment previews.
Fragment previews are disabled for editing when your cursor steps onto them,
and re-enabled when the cursor leaves."
:init-value nil
;; Fix nil error in `org-element-context'
;; when using `org-fragtog' without Org mode.
(setq org-complex-heading-regexp (or org-complex-heading-regexp ""))
(if org-fragtog-mode
(add-hook 'post-command-hook #'org-fragtog--post-cmd nil t)
(remove-hook 'post-command-hook #'org-fragtog--post-cmd t)))
(defvar-local org-fragtog--prev-frag nil
"Previous fragment that surrounded the cursor.
If the cursor was not on a fragment, this variable is nil. This is used to
track when the cursor leaves a fragment.")
(defvar-local org-fragtog--prev-point nil
"Value of point from before the most recent command.")
(defvar-local org-fragtog--timer nil
"Current active timer.")
(defun org-fragtog--post-cmd ()
"This function is executed by `post-command-hook' in `org-fragtog-mode'.
It handles toggling fragments depending on whether the cursor entered or exited
them."
(let*
;; Previous fragment
((prev-frag (or org-fragtog--prev-frag
;; If there is no previous fragment,
;; try to use a fragment at the previous cursor position.
;; This case matters when the cursor constructs a fragment
;; without ever being inside of it while it's constructed.
;; For example, if the user types "$foo$" in sequential order,
;; entering the final "$" creates a fragment without
;; the cursor ever being inside of it.
(if org-fragtog--prev-point
(save-excursion
(goto-char org-fragtog--prev-point)
(org-fragtog--cursor-frag)))))
;; Previous fragment's start position
(prev-frag-start-pos (org-fragtog--frag-start prev-frag))
;; Current fragment
(cursor-frag (org-fragtog--cursor-frag))
;; The current fragment didn't change
(frag-same (equal
;; Fragments are considered the same if they have the same
;; start position
(org-fragtog--frag-start cursor-frag)
prev-frag-start-pos))
;; The current fragment changed
(frag-changed (not frag-same))
;; The fragment at position of previous fragment.
;; This can be nil when for example $foo$ is edited to become $foo $.
(frag-at-prev-pos (and prev-frag-start-pos
(save-excursion
(goto-char prev-frag-start-pos)
(org-fragtog--cursor-frag)))))
;; Only do anything if the current fragment changed
(when frag-changed
;; Current fragment is the new previous
(setq org-fragtog--prev-frag cursor-frag)
;; Enable fragment if cursor left it after a timed disable
;; and the fragment still exists
(when (and frag-at-prev-pos
(not (org-fragtog--frag-enabled frag-at-prev-pos)))
(org-fragtog--enable-frag frag-at-prev-pos))
;; Cancel and expire timer
(when org-fragtog--timer
(cancel-timer org-fragtog--timer)
(setq org-fragtog--timer nil))
;; Disable fragment if cursor entered it
(when cursor-frag
(if (> org-fragtog-preview-delay 0)
(setq org-fragtog--timer (run-with-idle-timer org-fragtog-preview-delay
nil
#'org-fragtog--disable-frag
cursor-frag
t))
(org-fragtog--disable-frag cursor-frag))))
(setq org-fragtog--prev-point (point))))
(defun org-fragtog--frag-enabled (frag)
"Return whether FRAG is enabled.
A fragment is enabled when it has a preview image overlay in the buffer."
(org-fragtog--overlay-in-p (org-fragtog--frag-start frag)
(org-fragtog--frag-end frag)))
(defun org-fragtog--overlay-in-p (start-pos end-pos)
"Return whether there is a fragment overlay between START-POS and END-POS."
(seq-find (lambda (overlay)
(equal (overlay-get overlay 'org-overlay-type)
'org-latex-overlay))
(overlays-in start-pos end-pos)))
(defun org-fragtog--frag-start (frag)
"Return the position of the beginning of FRAG."
(org-element-property :begin frag))
(defun org-fragtog--frag-end (frag)
"Return the position of the end of FRAG."
;; Normally org-mode considers whitespace after an element as part of the
;; element. Avoid this behavior and consider trailing whitespace as outside
;; the fragment.
(- (org-element-property :end frag)
(org-element-property :post-blank frag)))
(defun org-fragtog--cursor-frag ()
"Return the fragment currently surrounding the cursor.
If there is none, return nil.
If the fragment is ignored from rules in `org-fragtog-ignore-predicates',
return nil."
(let*
;; Element surrounding the cursor
((elem (org-element-context))
;; A LaTeX fragment or environment is surrounding the cursor
(elem-is-latex (and (member (org-element-type elem)
'(latex-fragment latex-environment))
(< (point) (org-fragtog--frag-end elem))))
;; Whether the fragment should be ignored
(should-ignore (run-hook-with-args-until-success
'org-fragtog-ignore-predicates)))
(if (and elem-is-latex (not should-ignore))
elem
nil)))
(defun org-fragtog--enable-frag (frag)
"Enable the Org LaTeX fragment preview for the fragment FRAG."
;; The fragment must be disabled before `org-latex-preview', since
;; `org-latex-preview' only toggles, leaving no guarantee that it's enabled
;; afterwards.
(save-excursion
(org-fragtog--disable-frag frag))
;; Move to fragment and enable
(save-excursion
(goto-char (org-fragtog--frag-start frag))
;; Org's "\begin ... \end" style LaTeX fragments consider whitespace
;; before the fragment as part of the fragment.
;; Some users overload `org-latex-preview' to functions with similar functionality,
;; such as `math-preview-at-point' from the `math-preview' package on MELPA.
;; These alternatives might get confused if they're asked to enable a fragment at
;; point when point is on whitespace before the fragment.
;; So, advance to the nearest non-whitespace character before enabling.
(re-search-forward "[^ \t]")
(ignore-errors (org-latex-preview))))
(defun org-fragtog--set-point-after-disable-frag (frag)
"Set point to where it should be after FRAG was disabled.
If point decreases and enters a fragment from the end, disabling it, then point
should move to the end of the fragment. If point moved up one line, its column
should be maintained."
(when (and ;; There has to be a prev-point.
org-fragtog--prev-point
;; Only move to the end of the fragment if it's closer to the
;; prev-point location than the start of the fragment is.
(< (abs (- org-fragtog--prev-point (org-fragtog--frag-end frag)))
(abs (- org-fragtog--prev-point (org-fragtog--frag-start frag)))))
(let ((prev-point-column (save-excursion
(goto-char org-fragtog--prev-point)
(current-column))))
;; Move to the line where the fragment ends while preserving the point
;; column.
(goto-char (1- (org-fragtog--frag-end frag)))
(when (/= (line-number-at-pos org-fragtog--prev-point)
(line-number-at-pos))
(move-to-column prev-point-column)))))
(defun org-fragtog--disable-frag (frag &optional renew)
"Disable the Org LaTeX fragment preview for the fragment FRAG.
If RENEW is non-nil, renew the fragment at point."
;; Renew frag at point in case point was adjusted
;; See Emacs Lisp manual, 21.6 Adjusting Point After Commands
(when renew
(setq frag (org-fragtog--cursor-frag))
(setq org-fragtog--prev-frag frag)
(setq org-fragtog--timer nil))
;; There may be nothing at the adjusted point
(when frag
(org-clear-latex-preview (org-fragtog--frag-start frag)
(org-fragtog--frag-end frag))
(org-fragtog--set-point-after-disable-frag frag)))
(provide 'org-fragtog)
;;; org-fragtog.el ends here