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