Merge branch 'maint'
[org-mode.git] / contrib / lisp / org-toc.el
blob1c14a4f36cc7577e27a1b19ea0500f0d5ef442fe
1 ;;; org-toc.el --- Table of contents for Org-mode buffer
3 ;; Copyright 2007-2017 Free Software Foundation, Inc.
4 ;;
5 ;; Author: Bastien Guerry <bzg@gnu.org>
6 ;; Keywords: Org table of contents
7 ;; Homepage: http://www.cognition.ens.fr/~guerry/u/org-toc.el
8 ;; Version: 0.8
10 ;; This file is not part of GNU Emacs.
12 ;; This program is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation; either version 3, or (at your option)
15 ;; any later version.
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
25 ;;; Commentary:
27 ;; This library implements a browsable table of contents for Org files.
29 ;; Put this file into your load-path and the following into your ~/.emacs:
30 ;; (require 'org-toc)
32 ;;; Code:
34 (provide 'org-toc)
35 (eval-when-compile
36 (require 'cl))
38 ;;; Custom variables:
39 (defvar org-toc-base-buffer nil)
40 (defvar org-toc-columns-shown nil)
41 (defvar org-toc-odd-levels-only nil)
42 (defvar org-toc-config-alist nil)
43 (defvar org-toc-cycle-global-status nil)
44 (defalias 'org-show-table-of-contents 'org-toc-show)
46 (defgroup org-toc nil
47 "Options concerning the browsable table of contents of Org-mode."
48 :tag "Org TOC"
49 :group 'org)
51 (defcustom org-toc-default-depth 1
52 "Default depth when invoking `org-toc-show' without argument."
53 :group 'org-toc
54 :type '(choice
55 (const :tag "same as base buffer" nil)
56 (integer :tag "level")))
58 (defcustom org-toc-follow-mode nil
59 "Non-nil means navigating through the table of contents will
60 move the point in the Org buffer accordingly."
61 :group 'org-toc
62 :type 'boolean)
64 (defcustom org-toc-info-mode nil
65 "Non-nil means navigating through the table of contents will
66 show the properties for the current headline in the echo-area."
67 :group 'org-toc
68 :type 'boolean)
70 (defcustom org-toc-show-subtree-mode nil
71 "Non-nil means show subtree when going to headline or following
72 it while browsing the table of contents."
73 :group 'org-toc
74 :type '(choice
75 (const :tag "show subtree" t)
76 (const :tag "show entry" nil)))
78 (defcustom org-toc-recenter-mode t
79 "Non-nil means recenter the Org buffer when following the
80 headlines in the TOC buffer."
81 :group 'org-toc
82 :type 'boolean)
84 (defcustom org-toc-recenter 0
85 "Where to recenter the Org buffer when unfolding a subtree.
86 This variable is only used when `org-toc-recenter-mode' is set to
87 'custom. A value >=1000 will call recenter with no arg."
88 :group 'org-toc
89 :type 'integer)
91 (defcustom org-toc-info-exclude '("ALLTAGS")
92 "A list of excluded properties when displaying info in the
93 echo-area. The COLUMNS property is always exluded."
94 :group 'org-toc
95 :type 'lits)
97 ;;; Org TOC mode:
98 (defvar org-toc-mode-map (make-sparse-keymap)
99 "Keymap for `org-toc-mode'.")
101 (defun org-toc-mode ()
102 "A major mode for browsing the table of contents of an Org buffer.
104 \\{org-toc-mode-map}"
105 (interactive)
106 (kill-all-local-variables)
107 (use-local-map org-toc-mode-map)
108 (setq mode-name "Org TOC")
109 (setq major-mode 'org-toc-mode))
111 ;; toggle modes
112 (define-key org-toc-mode-map "F" 'org-toc-follow-mode)
113 (define-key org-toc-mode-map "S" 'org-toc-show-subtree-mode)
114 (define-key org-toc-mode-map "s" 'org-toc-store-config)
115 (define-key org-toc-mode-map "g" 'org-toc-restore-config)
116 (define-key org-toc-mode-map "i" 'org-toc-info-mode)
117 (define-key org-toc-mode-map "r" 'org-toc-recenter-mode)
119 ;; navigation keys
120 (define-key org-toc-mode-map "p" 'org-toc-previous)
121 (define-key org-toc-mode-map "n" 'org-toc-next)
122 (define-key org-toc-mode-map "f" 'org-toc-forward)
123 (define-key org-toc-mode-map "b" 'org-toc-back)
124 (define-key org-toc-mode-map [(left)] 'org-toc-back)
125 (define-key org-toc-mode-map [(right)] 'org-toc-forward)
126 (define-key org-toc-mode-map [(up)] 'org-toc-previous)
127 (define-key org-toc-mode-map [(down)] 'org-toc-next)
128 (define-key org-toc-mode-map "1" (lambda() (interactive) (org-toc-show 1 (point))))
129 (define-key org-toc-mode-map "2" (lambda() (interactive) (org-toc-show 2 (point))))
130 (define-key org-toc-mode-map "3" (lambda() (interactive) (org-toc-show 3 (point))))
131 (define-key org-toc-mode-map "4" (lambda() (interactive) (org-toc-show 4 (point))))
132 (define-key org-toc-mode-map " " 'org-toc-goto)
133 (define-key org-toc-mode-map "q" 'org-toc-quit)
134 (define-key org-toc-mode-map "x" 'org-toc-quit)
135 ;; go to the location and stay in the base buffer
136 (define-key org-toc-mode-map [(tab)] 'org-toc-jump)
137 (define-key org-toc-mode-map "v" 'org-toc-jump)
138 ;; go to the location and delete other windows
139 (define-key org-toc-mode-map [(return)]
140 (lambda() (interactive) (org-toc-jump t)))
142 ;; special keys
143 (define-key org-toc-mode-map "c" 'org-toc-columns)
144 (define-key org-toc-mode-map "?" 'org-toc-help)
145 (define-key org-toc-mode-map ":" 'org-toc-cycle-subtree)
146 (define-key org-toc-mode-map "\C-c\C-o" 'org-open-at-point)
147 ;; global cycling in the base buffer
148 (define-key org-toc-mode-map (kbd "C-S-<iso-lefttab>")
149 'org-toc-cycle-base-buffer)
150 ;; subtree cycling in the base buffer
151 (define-key org-toc-mode-map [(control tab)]
152 (lambda() (interactive) (org-toc-goto nil t)))
154 ;;; Toggle functions:
155 (defun org-toc-follow-mode ()
156 "Toggle follow mode in a `org-toc-mode' buffer."
157 (interactive)
158 (setq org-toc-follow-mode (not org-toc-follow-mode))
159 (message "Follow mode is %s"
160 (if org-toc-follow-mode "on" "off")))
162 (defun org-toc-info-mode ()
163 "Toggle info mode in a `org-toc-mode' buffer."
164 (interactive)
165 (setq org-toc-info-mode (not org-toc-info-mode))
166 (message "Info mode is %s"
167 (if org-toc-info-mode "on" "off")))
169 (defun org-toc-show-subtree-mode ()
170 "Toggle show subtree mode in a `org-toc-mode' buffer."
171 (interactive)
172 (setq org-toc-show-subtree-mode (not org-toc-show-subtree-mode))
173 (message "Show subtree mode is %s"
174 (if org-toc-show-subtree-mode "on" "off")))
176 (defun org-toc-recenter-mode (&optional line)
177 "Toggle recenter mode in a `org-toc-mode' buffer. If LINE is
178 specified, then make `org-toc-recenter' use this value."
179 (interactive "P")
180 (setq org-toc-recenter-mode (not org-toc-recenter-mode))
181 (when (numberp line)
182 (setq org-toc-recenter-mode t)
183 (setq org-toc-recenter line))
184 (message "Recenter mode is %s"
185 (if org-toc-recenter-mode
186 (format "on, line %d" org-toc-recenter) "off")))
188 (defun org-toc-cycle-subtree ()
189 "Locally cycle a headline through two states: 'children and
190 'folded"
191 (interactive)
192 (let ((beg (point))
193 (end (save-excursion (end-of-line) (point)))
194 (ov (car (overlays-at (point))))
195 status)
196 (if ov (setq status (overlay-get ov 'status))
197 (setq ov (make-overlay beg end)))
198 ;; change the folding status of this headline
199 (cond ((or (null status) (eq status 'folded))
200 (org-show-children)
201 (message "CHILDREN")
202 (overlay-put ov 'status 'children))
203 ((eq status 'children)
204 (show-branches)
205 (message "BRANCHES")
206 (overlay-put ov 'status 'branches))
207 (t (hide-subtree)
208 (message "FOLDED")
209 (overlay-put ov 'status 'folded)))))
211 ;;; Main show function:
212 ;; FIXME name this org-before-first-heading-p?
213 (defun org-toc-before-first-heading-p ()
214 "Before first heading?"
215 (save-excursion
216 (null (re-search-backward org-outline-regexp-bol nil t))))
218 ;;;###autoload
219 (defun org-toc-show (&optional depth position)
220 "Show the table of contents of the current Org-mode buffer."
221 (interactive "P")
222 (if (eq major-mode 'org-mode)
223 (progn (setq org-toc-base-buffer (current-buffer))
224 (setq org-toc-odd-levels-only org-odd-levels-only))
225 (if (eq major-mode 'org-toc-mode)
226 (org-pop-to-buffer-same-window org-toc-base-buffer)
227 (error "Not in an Org buffer")))
228 ;; create the new window display
229 (let ((pos (or position
230 (save-excursion
231 (if (org-toc-before-first-heading-p)
232 (progn (re-search-forward org-outline-regexp-bol nil t)
233 (match-beginning 0))
234 (point))))))
235 (setq org-toc-cycle-global-status org-cycle-global-status)
236 (delete-other-windows)
237 (and (get-buffer "*org-toc*") (kill-buffer "*org-toc*"))
238 (switch-to-buffer-other-window
239 (make-indirect-buffer org-toc-base-buffer "*org-toc*"))
240 ;; make content before 1st headline invisible
241 (goto-char (point-min))
242 (let* ((beg (point-min))
243 (end (and (re-search-forward "^\\*" nil t)
244 (1- (match-beginning 0))))
245 (ov (make-overlay beg end))
246 (help (format "Table of contents for %s (press ? for a quick help):\n"
247 (buffer-name org-toc-base-buffer))))
248 (overlay-put ov 'invisible t)
249 (overlay-put ov 'before-string help))
250 ;; build the browsable TOC
251 (cond (depth
252 (let* ((dpth (if org-toc-odd-levels-only
253 (1- (* depth 2)) depth)))
254 (org-content dpth)
255 (setq org-toc-cycle-global-status
256 `(org-content ,dpth))))
257 ((null org-toc-default-depth)
258 (if (eq org-toc-cycle-global-status 'overview)
259 (progn (org-overview)
260 (setq org-cycle-global-status 'overview)
261 (run-hook-with-args 'org-cycle-hook 'overview))
262 (progn (org-overview)
263 ;; FIXME org-content to show only headlines?
264 (org-content)
265 (setq org-cycle-global-status 'contents)
266 (run-hook-with-args 'org-cycle-hook 'contents))))
267 (t (let* ((dpth0 org-toc-default-depth)
268 (dpth (if org-toc-odd-levels-only
269 (1- (* dpth0 2)) dpth0)))
270 (org-content dpth)
271 (setq org-toc-cycle-global-status
272 `(org-content ,dpth)))))
273 (goto-char pos))
274 (move-beginning-of-line nil)
275 (org-toc-mode)
276 (shrink-window-if-larger-than-buffer)
277 (setq buffer-read-only t))
279 ;;; Navigation functions:
280 (defun org-toc-goto (&optional jump cycle)
281 "From Org TOC buffer, follow the targeted subtree in the Org window.
282 If JUMP is non-nil, go to the base buffer.
283 If JUMP is 'delete, go to the base buffer and delete other windows.
284 If CYCLE is non-nil, cycle the targeted subtree in the Org window."
285 (interactive)
286 (let ((pos (point))
287 (toc-buf (current-buffer)))
288 (switch-to-buffer-other-window org-toc-base-buffer)
289 (goto-char pos)
290 (if cycle (org-cycle)
291 (progn (org-overview)
292 (if org-toc-show-subtree-mode
293 (org-show-subtree)
294 (org-show-entry))
295 (org-show-context)))
296 (if org-toc-recenter-mode
297 (if (>= org-toc-recenter 1000) (recenter)
298 (recenter org-toc-recenter)))
299 (cond ((null jump)
300 (switch-to-buffer-other-window toc-buf))
301 ((eq jump 'delete)
302 (delete-other-windows)))))
304 (defun org-toc-cycle-base-buffer ()
305 "Call `org-cycle' with a prefix argument in the base buffer."
306 (interactive)
307 (switch-to-buffer-other-window org-toc-base-buffer)
308 (org-cycle t)
309 (other-window 1))
311 (defun org-toc-jump (&optional delete)
312 "From Org TOC buffer, jump to the targeted subtree in the Org window.
313 If DELETE is non-nil, delete other windows when in the Org buffer."
314 (interactive "P")
315 (if delete (org-toc-goto 'delete)
316 (org-toc-goto t)))
318 (defun org-toc-previous ()
319 "Go to the previous headline of the TOC."
320 (interactive)
321 (if (save-excursion
322 (beginning-of-line)
323 (re-search-backward "^\\*" nil t))
324 (outline-previous-visible-heading 1)
325 (message "No previous heading"))
326 (if org-toc-info-mode (org-toc-info))
327 (if org-toc-follow-mode (org-toc-goto)))
329 (defun org-toc-next ()
330 "Go to the next headline of the TOC."
331 (interactive)
332 (outline-next-visible-heading 1)
333 (if org-toc-info-mode (org-toc-info))
334 (if org-toc-follow-mode (org-toc-goto)))
336 (defun org-toc-forward ()
337 "Go to the next headline at the same level in the TOC."
338 (interactive)
339 (condition-case nil
340 (outline-forward-same-level 1)
341 (error (message "No next headline at this level")))
342 (if org-toc-info-mode (org-toc-info))
343 (if org-toc-follow-mode (org-toc-goto)))
345 (defun org-toc-back ()
346 "Go to the previous headline at the same level in the TOC."
347 (interactive)
348 (condition-case nil
349 (outline-backward-same-level 1)
350 (error (message "No previous headline at this level")))
351 (if org-toc-info-mode (org-toc-info))
352 (if org-toc-follow-mode (org-toc-goto)))
354 (defun org-toc-quit ()
355 "Quit the current Org TOC buffer."
356 (interactive)
357 (kill-buffer)
358 (other-window 1)
359 (delete-other-windows))
361 ;;; Special functions:
362 (defun org-toc-columns ()
363 "Toggle columns view in the Org buffer from Org TOC."
364 (interactive)
365 (let ((indirect-buffer (current-buffer)))
366 (org-pop-to-buffer-same-window org-toc-base-buffer)
367 (if (not org-toc-columns-shown)
368 (progn (org-columns)
369 (setq org-toc-columns-shown t))
370 (progn (org-columns-remove-overlays)
371 (setq org-toc-columns-shown nil)))
372 (org-pop-to-buffer-same-window indirect-buffer)))
374 (defun org-toc-info ()
375 "Show properties of current subtree in the echo-area."
376 (interactive)
377 (let ((pos (point))
378 (indirect-buffer (current-buffer))
379 props prop msg)
380 (org-pop-to-buffer-same-window org-toc-base-buffer)
381 (goto-char pos)
382 (setq props (org-entry-properties))
383 (while (setq prop (pop props))
384 (unless (or (equal (car prop) "COLUMNS")
385 (member (car prop) org-toc-info-exclude))
386 (let ((p (car prop))
387 (v (cdr prop)))
388 (if (equal p "TAGS")
389 (setq v (mapconcat 'identity (split-string v ":" t) " ")))
390 (setq p (concat p ":"))
391 (add-text-properties 0 (length p) '(face org-special-keyword) p)
392 (setq msg (concat msg p " " v " ")))))
393 (org-pop-to-buffer-same-window indirect-buffer)
394 (message msg)))
396 ;;; Store and restore TOC configuration:
397 (defun org-toc-store-config ()
398 "Store the current status of the tables of contents in
399 `org-toc-config-alist'."
400 (interactive)
401 (let ((file (buffer-file-name org-toc-base-buffer))
402 (pos (point))
403 (hlcfg (org-toc-get-headlines-status)))
404 (setq org-toc-config-alist
405 (delete (assoc file org-toc-config-alist)
406 org-toc-config-alist))
407 (add-to-list 'org-toc-config-alist
408 `(,file ,pos ,org-toc-cycle-global-status ,hlcfg))
409 (message "TOC configuration saved: (%s)"
410 (if (listp org-toc-cycle-global-status)
411 (concat "org-content "
412 (number-to-string
413 (cadr org-toc-cycle-global-status)))
414 (symbol-name org-toc-cycle-global-status)))))
416 (defun org-toc-restore-config ()
417 "Get the stored status in `org-toc-config-alist' and set the
418 current table of contents to it."
419 (interactive)
420 (let* ((file (buffer-file-name org-toc-base-buffer))
421 (conf (cdr (assoc file org-toc-config-alist)))
422 (pos (car conf))
423 (status (cadr conf))
424 (hlcfg (caddr conf)) hlcfg0 ov)
425 (cond ((listp status)
426 (org-toc-show (cadr status) (point)))
427 ((eq status 'overview)
428 (org-overview)
429 (setq org-cycle-global-status 'overview)
430 (run-hook-with-args 'org-cycle-hook 'overview))
432 (org-overview)
433 (org-content)
434 (setq org-cycle-global-status 'contents)
435 (run-hook-with-args 'org-cycle-hook 'contents)))
436 (while (setq hlcfg0 (pop hlcfg))
437 (save-excursion
438 (goto-char (point-min))
439 (when (search-forward (car hlcfg0) nil t)
440 (unless (overlays-at (match-beginning 0))
441 (setq ov (make-overlay (match-beginning 0)
442 (match-end 0))))
443 (cond ((eq (cdr hlcfg0) 'children)
444 (org-show-children)
445 (message "CHILDREN")
446 (overlay-put ov 'status 'children))
447 ((eq (cdr hlcfg0) 'branches)
448 (show-branches)
449 (message "BRANCHES")
450 (overlay-put ov 'status 'branches))))))
451 (goto-char pos)
452 (if org-toc-follow-mode (org-toc-goto))
453 (message "Last TOC configuration restored")
454 (sit-for 1)
455 (if org-toc-info-mode (org-toc-info))))
457 (defun org-toc-get-headlines-status ()
458 "Return an alist of headlines and their associated folding
459 status."
460 (let (output ovs)
461 (save-excursion
462 (goto-char (point-min))
463 (while (and (not (eobp))
464 (goto-char (next-overlay-change (point))))
465 (when (looking-at org-outline-regexp-bol)
466 (add-to-list
467 'output
468 (cons (buffer-substring-no-properties
469 (match-beginning 0)
470 (save-excursion
471 (end-of-line) (point)))
472 (overlay-get
473 (car (overlays-at (point))) 'status))))))
474 ;; return an alist like (("* Headline" . 'status))
475 output))
477 ;; In Org TOC buffer, hide headlines below the first level.
478 (defun org-toc-help ()
479 "Display a quick help message in the echo-area for `org-toc-mode'."
480 (interactive)
481 (let ((st-start 0)
482 (help-message
483 "\[space\] show heading \[1-4\] hide headlines below this level
484 \[TAB\] jump to heading \[F\] toggle follow mode (currently %s)
485 \[return\] jump and delete others windows \[i\] toggle info mode (currently %s)
486 \[S-TAB\] cycle subtree (in Org) \[S\] toggle show subtree mode (currently %s)
487 \[C-S-TAB\] global cycle (in Org) \[r\] toggle recenter mode (currently %s)
488 \[:\] cycle subtree (in TOC) \[c\] toggle column view (currently %s)
489 \[n/p\] next/previous heading \[s\] save TOC configuration
490 \[f/b\] next/previous heading of same level
491 \[q\] quit the TOC \[g\] restore last TOC configuration"))
492 (while (string-match "\\[[^]]+\\]" help-message st-start)
493 (add-text-properties (match-beginning 0)
494 (match-end 0) '(face bold) help-message)
495 (setq st-start (match-end 0)))
496 (message help-message
497 (if org-toc-follow-mode "on" "off")
498 (if org-toc-info-mode "on" "off")
499 (if org-toc-show-subtree-mode "on" "off")
500 (if org-toc-recenter-mode (format "on, line %s" org-toc-recenter) "off")
501 (if org-toc-columns-shown "on" "off"))))
504 ;;;;##########################################################################
505 ;;;; User Options, Variables
506 ;;;;##########################################################################
508 ;;; org-toc.el ends here