commit | author | age
|
76bbd0
|
1 |
;;; org-indent.el --- Dynamic indentation for Org -*- lexical-binding: t; -*- |
C |
2 |
|
|
3 |
;; Copyright (C) 2009-2018 Free Software Foundation, Inc. |
|
4 |
;; |
|
5 |
;; Author: Carsten Dominik <carsten at orgmode dot org> |
|
6 |
;; Keywords: outlines, hypermedia, calendar, wp |
|
7 |
;; Homepage: https://orgmode.org |
|
8 |
;; |
|
9 |
;; This file is part of GNU Emacs. |
|
10 |
;; |
|
11 |
;; GNU Emacs is free software: you can redistribute it and/or modify |
|
12 |
;; it under the terms of the GNU General Public License as published by |
|
13 |
;; the Free Software Foundation, either version 3 of the License, or |
|
14 |
;; (at your option) any later version. |
|
15 |
;; |
|
16 |
;; GNU Emacs is distributed in the hope that it will be useful, |
|
17 |
;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
;; GNU General Public License for more details. |
|
20 |
;; |
|
21 |
;; You should have received a copy of the GNU General Public License |
|
22 |
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
;; |
|
24 |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
|
25 |
;; |
|
26 |
;;; Commentary: |
|
27 |
|
|
28 |
;; This is an implementation of dynamic virtual indentation. It works |
|
29 |
;; by adding text properties to a buffer to make sure lines are |
|
30 |
;; indented according to outline structure. |
|
31 |
;; |
|
32 |
;; The process is synchronous, toggled at every buffer modification. |
|
33 |
;; Though, the initialization (indentation of text already in the |
|
34 |
;; buffer), which can take a few seconds in large buffers, happens on |
|
35 |
;; idle time. |
|
36 |
;; |
|
37 |
;;; Code: |
|
38 |
|
|
39 |
(require 'org-macs) |
|
40 |
(require 'org-compat) |
|
41 |
(require 'org) |
|
42 |
|
|
43 |
(require 'cl-lib) |
|
44 |
|
|
45 |
(declare-function org-inlinetask-get-task-level "org-inlinetask" ()) |
|
46 |
(declare-function org-inlinetask-in-task-p "org-inlinetask" ()) |
|
47 |
(declare-function org-list-item-body-column "org-list" (item)) |
|
48 |
(defvar org-inlinetask-show-first-star) |
|
49 |
|
|
50 |
(defgroup org-indent nil |
|
51 |
"Options concerning dynamic virtual outline indentation." |
|
52 |
:tag "Org Indent" |
|
53 |
:group 'org) |
|
54 |
|
|
55 |
(defvar org-indent-inlinetask-first-star (org-add-props "*" '(face org-warning)) |
|
56 |
"First star of inline tasks, with correct face.") |
|
57 |
(defvar org-indent-agent-timer nil |
|
58 |
"Timer running the initialize agent.") |
|
59 |
(defvar org-indent-agentized-buffers nil |
|
60 |
"List of buffers watched by the initialize agent.") |
|
61 |
(defvar org-indent-agent-resume-timer nil |
|
62 |
"Timer to reschedule agent after switching to other idle processes.") |
|
63 |
(defvar org-indent-agent-active-delay '(0 2 0) |
|
64 |
"Time to run agent before switching to other idle processes. |
|
65 |
Delay used when the buffer to initialize is current.") |
|
66 |
(defvar org-indent-agent-passive-delay '(0 0 400000) |
|
67 |
"Time to run agent before switching to other idle processes. |
|
68 |
Delay used when the buffer to initialize isn't current.") |
|
69 |
(defvar org-indent-agent-resume-delay '(0 0 100000) |
|
70 |
"Minimal time for other idle processes before switching back to agent.") |
|
71 |
(defvar org-indent--initial-marker nil |
|
72 |
"Position of initialization before interrupt. |
|
73 |
This is used locally in each buffer being initialized.") |
|
74 |
(defvar org-hide-leading-stars-before-indent-mode nil |
|
75 |
"Used locally.") |
|
76 |
(defvar org-indent-modified-headline-flag nil |
|
77 |
"Non-nil means the last deletion operated on a headline. |
|
78 |
It is modified by `org-indent-notify-modified-headline'.") |
|
79 |
|
|
80 |
|
|
81 |
(defcustom org-indent-boundary-char ?\s |
|
82 |
"The end of the virtual indentation strings, a single-character string. |
|
83 |
The default is just a space, but if you wish, you can use \"|\" or so. |
|
84 |
This can be useful on a terminal window - under a windowing system, |
|
85 |
it may be prettier to customize the `org-indent' face." |
|
86 |
:group 'org-indent |
|
87 |
:type 'character) |
|
88 |
|
|
89 |
(defcustom org-indent-mode-turns-off-org-adapt-indentation t |
|
90 |
"Non-nil means setting the variable `org-indent-mode' will \ |
|
91 |
turn off indentation adaptation. |
|
92 |
For details see the variable `org-adapt-indentation'." |
|
93 |
:group 'org-indent |
|
94 |
:type 'boolean) |
|
95 |
|
|
96 |
(defcustom org-indent-mode-turns-on-hiding-stars t |
|
97 |
"Non-nil means setting the variable `org-indent-mode' will \ |
|
98 |
turn on `org-hide-leading-stars'." |
|
99 |
:group 'org-indent |
|
100 |
:type 'boolean) |
|
101 |
|
|
102 |
(defcustom org-indent-indentation-per-level 2 |
|
103 |
"Indentation per level in number of characters." |
|
104 |
:group 'org-indent |
|
105 |
:type 'integer) |
|
106 |
|
|
107 |
(defface org-indent '((t (:inherit org-hide))) |
|
108 |
"Face for outline indentation. |
|
109 |
The default is to make it look like whitespace. But you may find it |
|
110 |
useful to make it ever so slightly different." |
|
111 |
:group 'org-faces) |
|
112 |
|
|
113 |
(defvar org-indent--text-line-prefixes nil |
|
114 |
"Vector containing line prefixes strings for regular text.") |
|
115 |
|
|
116 |
(defvar org-indent--heading-line-prefixes nil |
|
117 |
"Vector containing line prefix strings for headlines.") |
|
118 |
|
|
119 |
(defvar org-indent--inlinetask-line-prefixes nil |
|
120 |
"Vector containing line prefix strings for inline tasks.") |
|
121 |
|
|
122 |
(defconst org-indent--deepest-level 50 |
|
123 |
"Maximum theoretical headline depth.") |
|
124 |
|
|
125 |
(defun org-indent--compute-prefixes () |
|
126 |
"Compute prefix strings for regular text and headlines." |
|
127 |
(setq org-indent--heading-line-prefixes |
|
128 |
(make-vector org-indent--deepest-level nil)) |
|
129 |
(setq org-indent--inlinetask-line-prefixes |
|
130 |
(make-vector org-indent--deepest-level nil)) |
|
131 |
(setq org-indent--text-line-prefixes |
|
132 |
(make-vector org-indent--deepest-level nil)) |
|
133 |
(dotimes (n org-indent--deepest-level) |
|
134 |
(let ((indentation (if (<= n 1) 0 |
|
135 |
(* (1- org-indent-indentation-per-level) |
|
136 |
(1- n))))) |
|
137 |
;; Headlines line prefixes. |
|
138 |
(let ((heading-prefix (make-string indentation ?*))) |
|
139 |
(aset org-indent--heading-line-prefixes |
|
140 |
n |
|
141 |
(org-add-props heading-prefix nil 'face 'org-indent)) |
|
142 |
;; Inline tasks line prefixes |
|
143 |
(aset org-indent--inlinetask-line-prefixes |
|
144 |
n |
|
145 |
(cond ((<= n 1) "") |
|
146 |
((bound-and-true-p org-inlinetask-show-first-star) |
|
147 |
(concat org-indent-inlinetask-first-star |
|
148 |
(substring heading-prefix 1))) |
|
149 |
(t (org-add-props heading-prefix nil 'face 'org-indent))))) |
|
150 |
;; Text line prefixes. |
|
151 |
(aset org-indent--text-line-prefixes |
|
152 |
n |
|
153 |
(concat (org-add-props (make-string (+ n indentation) ?\s) |
|
154 |
nil 'face 'org-indent) |
|
155 |
(and (> n 0) |
|
156 |
(char-to-string org-indent-boundary-char))))))) |
|
157 |
|
|
158 |
(defsubst org-indent-remove-properties (beg end) |
|
159 |
"Remove indentations between BEG and END." |
|
160 |
(org-with-silent-modifications |
|
161 |
(remove-text-properties beg end '(line-prefix nil wrap-prefix nil)))) |
|
162 |
|
|
163 |
;;;###autoload |
|
164 |
(define-minor-mode org-indent-mode |
|
165 |
"When active, indent text according to outline structure. |
|
166 |
|
|
167 |
Internally this works by adding `line-prefix' and `wrap-prefix' |
|
168 |
properties, after each buffer modification, on the modified zone. |
|
169 |
|
|
170 |
The process is synchronous. Though, initial indentation of |
|
171 |
buffer, which can take a few seconds on large buffers, is done |
|
172 |
during idle time." |
|
173 |
nil " Ind" nil |
|
174 |
(cond |
|
175 |
(org-indent-mode |
|
176 |
;; mode was turned on. |
|
177 |
(setq-local indent-tabs-mode nil) |
|
178 |
(setq-local org-indent--initial-marker (copy-marker 1)) |
|
179 |
(when org-indent-mode-turns-off-org-adapt-indentation |
|
180 |
(setq-local org-adapt-indentation nil)) |
|
181 |
(when org-indent-mode-turns-on-hiding-stars |
|
182 |
(setq-local org-hide-leading-stars-before-indent-mode |
|
183 |
org-hide-leading-stars) |
|
184 |
(setq-local org-hide-leading-stars t)) |
|
185 |
(org-indent--compute-prefixes) |
|
186 |
(add-hook 'filter-buffer-substring-functions |
|
187 |
(lambda (fun start end delete) |
|
188 |
(org-indent-remove-properties-from-string |
|
189 |
(funcall fun start end delete))) |
|
190 |
nil t) |
|
191 |
(add-hook 'after-change-functions 'org-indent-refresh-maybe nil 'local) |
|
192 |
(add-hook 'before-change-functions |
|
193 |
'org-indent-notify-modified-headline nil 'local) |
|
194 |
(and font-lock-mode (org-restart-font-lock)) |
|
195 |
(org-indent-remove-properties (point-min) (point-max)) |
|
196 |
;; Submit current buffer to initialize agent. If it's the first |
|
197 |
;; buffer submitted, also start the agent. Current buffer is |
|
198 |
;; pushed in both cases to avoid a race condition. |
|
199 |
(if org-indent-agentized-buffers |
|
200 |
(push (current-buffer) org-indent-agentized-buffers) |
|
201 |
(push (current-buffer) org-indent-agentized-buffers) |
|
202 |
(setq org-indent-agent-timer |
|
203 |
(run-with-idle-timer 0.2 t #'org-indent-initialize-agent)))) |
|
204 |
(t |
|
205 |
;; mode was turned off (or we refused to turn it on) |
|
206 |
(kill-local-variable 'org-adapt-indentation) |
|
207 |
(setq org-indent-agentized-buffers |
|
208 |
(delq (current-buffer) org-indent-agentized-buffers)) |
|
209 |
(when (markerp org-indent--initial-marker) |
|
210 |
(set-marker org-indent--initial-marker nil)) |
|
211 |
(when (boundp 'org-hide-leading-stars-before-indent-mode) |
|
212 |
(setq-local org-hide-leading-stars |
|
213 |
org-hide-leading-stars-before-indent-mode)) |
|
214 |
(remove-hook 'filter-buffer-substring-functions |
|
215 |
(lambda (fun start end delete) |
|
216 |
(org-indent-remove-properties-from-string |
|
217 |
(funcall fun start end delete)))) |
|
218 |
(remove-hook 'after-change-functions 'org-indent-refresh-maybe 'local) |
|
219 |
(remove-hook 'before-change-functions |
|
220 |
'org-indent-notify-modified-headline 'local) |
|
221 |
(org-with-wide-buffer |
|
222 |
(org-indent-remove-properties (point-min) (point-max))) |
|
223 |
(and font-lock-mode (org-restart-font-lock)) |
|
224 |
(redraw-display)))) |
|
225 |
|
|
226 |
(defun org-indent-indent-buffer () |
|
227 |
"Add indentation properties to the accessible part of the buffer." |
|
228 |
(interactive) |
|
229 |
(if (not (derived-mode-p 'org-mode)) |
|
230 |
(error "Not in Org mode") |
|
231 |
(message "Setting buffer indentation. It may take a few seconds...") |
|
232 |
(org-indent-remove-properties (point-min) (point-max)) |
|
233 |
(org-indent-add-properties (point-min) (point-max)) |
|
234 |
(message "Indentation of buffer set."))) |
|
235 |
|
|
236 |
(defun org-indent-remove-properties-from-string (string) |
|
237 |
"Remove indentation properties from STRING." |
|
238 |
(remove-text-properties 0 (length string) |
|
239 |
'(line-prefix nil wrap-prefix nil) string) |
|
240 |
string) |
|
241 |
|
|
242 |
(defun org-indent-initialize-agent () |
|
243 |
"Start or resume current buffer initialization. |
|
244 |
Only buffers in `org-indent-agentized-buffers' trigger an action. |
|
245 |
When no more buffer is being watched, the agent suppress itself." |
|
246 |
(when org-indent-agent-resume-timer |
|
247 |
(cancel-timer org-indent-agent-resume-timer)) |
|
248 |
(setq org-indent-agentized-buffers |
|
249 |
(cl-remove-if-not #'buffer-live-p org-indent-agentized-buffers)) |
|
250 |
(cond |
|
251 |
;; Job done: kill agent. |
|
252 |
((not org-indent-agentized-buffers) (cancel-timer org-indent-agent-timer)) |
|
253 |
;; Current buffer is agentized: start/resume initialization |
|
254 |
;; somewhat aggressively. |
|
255 |
((memq (current-buffer) org-indent-agentized-buffers) |
|
256 |
(org-indent-initialize-buffer (current-buffer) |
|
257 |
org-indent-agent-active-delay)) |
|
258 |
;; Else, start/resume initialization of the last agentized buffer, |
|
259 |
;; softly. |
|
260 |
(t (org-indent-initialize-buffer (car org-indent-agentized-buffers) |
|
261 |
org-indent-agent-passive-delay)))) |
|
262 |
|
|
263 |
(defun org-indent-initialize-buffer (buffer delay) |
|
264 |
"Set virtual indentation for the buffer BUFFER, asynchronously. |
|
265 |
Give hand to other idle processes if it takes longer than DELAY, |
|
266 |
a time value." |
|
267 |
(with-current-buffer buffer |
|
268 |
(when org-indent-mode |
|
269 |
(org-with-wide-buffer |
|
270 |
(let ((interruptp |
|
271 |
;; Always nil unless interrupted. |
|
272 |
(catch 'interrupt |
|
273 |
(and org-indent--initial-marker |
|
274 |
(marker-position org-indent--initial-marker) |
|
275 |
(equal (marker-buffer org-indent--initial-marker) |
|
276 |
buffer) |
|
277 |
(org-indent-add-properties org-indent--initial-marker |
|
278 |
(point-max) |
|
279 |
delay) |
|
280 |
nil)))) |
|
281 |
(move-marker org-indent--initial-marker interruptp) |
|
282 |
;; Job is complete: un-agentize buffer. |
|
283 |
(unless interruptp |
|
284 |
(setq org-indent-agentized-buffers |
|
285 |
(delq buffer org-indent-agentized-buffers)))))))) |
|
286 |
|
|
287 |
(defun org-indent-set-line-properties (level indentation &optional heading) |
|
288 |
"Set prefix properties on current line an move to next one. |
|
289 |
|
|
290 |
LEVEL is the current level of heading. INDENTATION is the |
|
291 |
expected indentation when wrapping line. |
|
292 |
|
|
293 |
When optional argument HEADING is non-nil, assume line is at |
|
294 |
a heading. Moreover, if is is `inlinetask', the first star will |
|
295 |
have `org-warning' face." |
|
296 |
(let* ((line (aref (pcase heading |
|
297 |
(`nil org-indent--text-line-prefixes) |
|
298 |
(`inlinetask org-indent--inlinetask-line-prefixes) |
|
299 |
(_ org-indent--heading-line-prefixes)) |
|
300 |
level)) |
|
301 |
(wrap |
|
302 |
(org-add-props |
|
303 |
(concat line |
|
304 |
(if heading (concat (make-string level ?*) " ") |
|
305 |
(make-string indentation ?\s))) |
|
306 |
nil 'face 'org-indent))) |
|
307 |
;; Add properties down to the next line to indent empty lines. |
|
308 |
(add-text-properties (line-beginning-position) (line-beginning-position 2) |
|
309 |
`(line-prefix ,line wrap-prefix ,wrap))) |
|
310 |
(forward-line)) |
|
311 |
|
|
312 |
(defun org-indent-add-properties (beg end &optional delay) |
|
313 |
"Add indentation properties between BEG and END. |
|
314 |
|
|
315 |
When DELAY is non-nil, it must be a time value. In that case, |
|
316 |
the process is asynchronous and can be interrupted, either by |
|
317 |
user request, or after DELAY. This is done by throwing the |
|
318 |
`interrupt' tag along with the buffer position where the process |
|
319 |
stopped." |
|
320 |
(save-match-data |
|
321 |
(org-with-wide-buffer |
|
322 |
(goto-char beg) |
|
323 |
(beginning-of-line) |
|
324 |
;; Initialize prefix at BEG, according to current entry's level. |
|
325 |
(let* ((case-fold-search t) |
|
326 |
(limited-re (org-get-limited-outline-regexp)) |
|
327 |
(level (or (org-current-level) 0)) |
|
328 |
(time-limit (and delay (time-add (current-time) delay)))) |
|
329 |
;; For each line, set `line-prefix' and `wrap-prefix' |
|
330 |
;; properties depending on the type of line (headline, inline |
|
331 |
;; task, item or other). |
|
332 |
(org-with-silent-modifications |
|
333 |
(while (and (<= (point) end) (not (eobp))) |
|
334 |
(cond |
|
335 |
;; When in asynchronous mode, check if interrupt is |
|
336 |
;; required. |
|
337 |
((and delay (input-pending-p)) (throw 'interrupt (point))) |
|
338 |
;; In asynchronous mode, take a break of |
|
339 |
;; `org-indent-agent-resume-delay' every DELAY to avoid |
|
340 |
;; blocking any other idle timer or process output. |
|
341 |
((and delay (time-less-p time-limit (current-time))) |
|
342 |
(setq org-indent-agent-resume-timer |
|
343 |
(run-with-idle-timer |
|
344 |
(time-add (current-idle-time) org-indent-agent-resume-delay) |
|
345 |
nil #'org-indent-initialize-agent)) |
|
346 |
(throw 'interrupt (point))) |
|
347 |
;; Headline or inline task. |
|
348 |
((looking-at org-outline-regexp) |
|
349 |
(let* ((nstars (- (match-end 0) (match-beginning 0) 1)) |
|
350 |
(type (or (looking-at-p limited-re) 'inlinetask))) |
|
351 |
(org-indent-set-line-properties nstars 0 type) |
|
352 |
;; At an headline, define new value for LEVEL. |
|
353 |
(unless (eq type 'inlinetask) (setq level nstars)))) |
|
354 |
;; List item: `wrap-prefix' is set where body starts. |
|
355 |
((org-at-item-p) |
|
356 |
(org-indent-set-line-properties |
|
357 |
level (org-list-item-body-column (point)))) |
|
358 |
;; Regular line. |
|
359 |
(t |
|
360 |
(org-indent-set-line-properties level (org-get-indentation)))))))))) |
|
361 |
|
|
362 |
(defun org-indent-notify-modified-headline (beg end) |
|
363 |
"Set `org-indent-modified-headline-flag' depending on context. |
|
364 |
|
|
365 |
BEG and END are the positions of the beginning and end of the |
|
366 |
range of deleted text. |
|
367 |
|
|
368 |
This function is meant to be called by `before-change-functions'. |
|
369 |
Flag will be non-nil if command is going to modify or delete an |
|
370 |
headline." |
|
371 |
(when org-indent-mode |
|
372 |
(setq org-indent-modified-headline-flag |
|
373 |
(org-with-wide-buffer |
|
374 |
(goto-char beg) |
|
375 |
(save-match-data |
|
376 |
(or (and (org-at-heading-p) (< beg (match-end 0))) |
|
377 |
(re-search-forward |
|
378 |
(org-with-limited-levels org-outline-regexp-bol) end t))))))) |
|
379 |
|
|
380 |
(defun org-indent-refresh-maybe (beg end _) |
|
381 |
"Refresh indentation properties in an adequate portion of buffer. |
|
382 |
BEG and END are the positions of the beginning and end of the |
|
383 |
range of inserted text. DUMMY is an unused argument. |
|
384 |
|
|
385 |
This function is meant to be called by `after-change-functions'." |
|
386 |
(when org-indent-mode |
|
387 |
(save-match-data |
|
388 |
;; If a headline was modified or inserted, set properties until |
|
389 |
;; next headline. |
|
390 |
(org-with-wide-buffer |
|
391 |
(if (or org-indent-modified-headline-flag |
|
392 |
(save-excursion |
|
393 |
(goto-char beg) |
|
394 |
(beginning-of-line) |
|
395 |
(re-search-forward |
|
396 |
(org-with-limited-levels org-outline-regexp-bol) end t))) |
|
397 |
(let ((end (save-excursion |
|
398 |
(goto-char end) |
|
399 |
(org-with-limited-levels (outline-next-heading)) |
|
400 |
(point)))) |
|
401 |
(setq org-indent-modified-headline-flag nil) |
|
402 |
(org-indent-add-properties beg end)) |
|
403 |
;; Otherwise, only set properties on modified area. |
|
404 |
(org-indent-add-properties beg end)))))) |
|
405 |
|
|
406 |
(provide 'org-indent) |
|
407 |
|
|
408 |
;; Local variables: |
|
409 |
;; generated-autoload-file: "org-loaddefs.el" |
|
410 |
;; End: |
|
411 |
|
|
412 |
;;; org-indent.el ends here |