362 lines
12 KiB
EmacsLisp

;;; outline-toc.el --- Sidebar showing a "table of contents".
;; Copyright (C) 2017 Austin Bingham
;; Author: Austin Bingham <austin.bingham@gmail.com>
;; Keywords: convenience outlines
;; URL: https://github.com/abingham/outline-toc.el
;; Version: 0.1
;; This file is not part of GNU Emacs.
;;; License:
;;
;; 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 provides a sidebar buffer which shows a "table of
;; contents" for an associated outline-mode buffer. Basically,
;; this shows you the sections of the outline-mode buffer, but
;; not the bodies. This is to help you remember where you are in
;; a large document.
;; Simply use M-x outline-toc-mode to toggle activation of the
;; outline-toc. Use 'M-x customize-group RET outline-toc RET' to
;; adapt outline-toc to your needs.
;; Much of this was originally adapated from David Engster's
;; excellent minimap.el (https://github.com/dengste/minimap).
;;; Code:
(require 'outline)
(defgroup outline-toc nil
"A outline-toc sidebar."
:group 'convenience)
(defface outline-toc-font-face nil
"Face used for text in outline-toc buffer, notably the font family and height.
This height should be really small. You probably want to use a
TrueType font for this. After changing this, you should
recreate the outline-toc to avoid problems with recentering."
:group 'outline-toc)
(defface outline-toc-current-section
'((t (:inherit highlight)))
"Face for the current line in the TOC."
:group 'outline-toc)
(defcustom outline-toc-width-fraction 0.15
"Fraction of width which should be used for outline-toc sidebar."
:type 'number
:group 'outline-toc)
(defcustom outline-toc-minimum-width 30
"Minimum width of outline-toc in characters (default size).
Use nil to disable."
:type 'number
:group 'outline-toc)
(defcustom outline-toc-window-location 'left
"Location of the outline-toc window.
Can be either the symbol `left' or `right'."
:type '(choice (const :tag "Left" left)
(const :tag "Right" right))
:group 'outline-toc)
(defcustom outline-toc-buffer-name " *OUTLINE-TOC*"
"Buffer name of outline-toc sidebar."
:type 'string
:group 'outline-toc)
(defcustom outline-toc-update-delay 0.1
"Delay in seconds after which sidebar gets updated.
Setting this to 0 will let the outline-toc react immediately, but
this will slow down scrolling."
:type 'number
:set (lambda (sym value)
(set sym value)
(when (and (boundp 'outline-toc--timer-object)
outline-toc--timer-object)
(cancel-timer outline-toc--timer-object)
(setq outline-toc--timer-object
(run-with-idle-timer
outline-toc-update-delay t 'outline-toc--update))))
:group 'outline-toc)
(defcustom outline-toc-hide-scroll-bar t
"Whether the outline-toc should hide the vertical scrollbar."
:type 'boolean
:group 'outline-toc)
(defcustom outline-toc-hide-fringes nil
"Whether the outline-toc should hide the fringes."
:type 'boolean
:group 'outline-toc)
(defcustom outline-toc-dedicated-window t
"Whether the outline-toc should create a dedicated window."
:type 'boolean
:group 'outline-toc)
;; TODO: How do we specify "for all outline-mode" docs? Outline-mode is minor, I think...
(defcustom outline-toc-major-modes '(markdown-mode org-mode outline-mode rst-mode)
"Major modes for which a outline-toc should be created.
This can also be a parent mode like 'prog-mode.
If nil, a outline-toc must be explicitly created for each buffer."
:type '(repeat symbol)
:group 'outline-toc)
(defcustom outline-toc-recreate-window t
"Whether the outline-toc window should be automatically re-created.
If this is non-nil, the side window for the outline-toc will be
automatically re-created as soon as you kill it."
:type 'boolean
:group 'outline-toc)
(defcustom outline-toc-automatically-delete-window t
"Whether the outline-toc window should be automatically deleted.
Setting this to non-nil will delete the minibuffer side window
when you enter a buffer which is not derived from
`outline-toc-major-modes' (excluding the minibuffer)."
:type 'boolean
:group 'outline-toc)
(defcustom outline-toc-highlight-line t
"Whether the outline-toc should highlight the current line."
:type 'boolean
:group 'outline-toc)
;;; Internal variables
(defvar outline-toc--active-buffer nil
"The buffer currently displayed in the outline-toc.")
;; Window start/end from the base buffer
(defvar outline-toc--start nil)
(defvar outline-toc--end nil)
;; General overlay for the outline-toc
(defvar outline-toc--base-overlay nil)
;; Timer
(defvar outline-toc--timer-object nil)
;; Lines the outline-toc can display
(defvar outline-toc--numlines nil)
;; Line overlay
(defvar outline-toc--line-overlay nil)
;;; Helpers
(defun outline-toc-active-current-buffer-p ()
"Whether the current buffer is displayed in the outline-toc."
(and (eq (current-buffer) outline-toc--active-buffer)
(get-buffer outline-toc-buffer-name)
(with-current-buffer outline-toc-buffer-name
(eq outline-toc--active-buffer (buffer-base-buffer)))))
(defsubst outline-toc--get-window ()
"Get current outline-toc window."
(when (get-buffer outline-toc-buffer-name)
(get-buffer-window outline-toc-buffer-name)))
(defsubst outline-toc-kill-buffer ()
"Kill the outline-toc buffer."
(when (get-buffer outline-toc-buffer-name)
(kill-buffer outline-toc-buffer-name)))
(defun outline-toc-create-window ()
"Create TOC sidebare window."
(let ((width (round (* (window-width)
outline-toc-width-fraction))))
(when (< width outline-toc-minimum-width)
(setq width outline-toc-minimum-width))
(if (eq outline-toc-window-location 'left)
(split-window-horizontally width)
(split-window-horizontally
(* -1 width))
(other-window 1))
;; Set up the outline-toc window:
;; You should not be able to enter the outline-toc window.
(set-window-parameter nil 'no-other-window t)
;; Hide things.
(when outline-toc-hide-scroll-bar
(setq vertical-scroll-bar nil))
(when outline-toc-hide-fringes
(set-window-fringes nil 0 0))
;; Switch to buffer.
(switch-to-buffer
(get-buffer-create outline-toc-buffer-name) t t)
;; Do not fold lines in the outline-toc.
(setq truncate-lines t)
;; Make it dedicated.
(when outline-toc-dedicated-window
(set-window-dedicated-p nil t))
(prog1
(selected-window)
(other-window 1))))
;;; Outline-Toc creation / killing
;;;###autoload
(define-minor-mode outline-toc-mode
"Toggle outline-toc mode."
:global t
:group 'outline-toc
:lighter " OToc"
(if outline-toc-mode
(progn
(when (and outline-toc-major-modes
(apply 'derived-mode-p outline-toc-major-modes))
(unless (outline-toc--get-window)
(outline-toc-create-window))
;; Create outline-toc.
(outline-toc-new-outline-toc))
;; Create timer.
(setq outline-toc--timer-object
(run-with-idle-timer outline-toc-update-delay t 'outline-toc--update)))
;; Turn it off
(outline-toc-kill)
(outline-toc-setup-hooks t)))
(defun outline-toc-create ()
"Create a outline-toc sidebar."
(interactive)
(outline-toc-mode 1))
(defun outline-toc-new-outline-toc ()
"Create new outline-toc BUFNAME for current buffer and window.
Re-use already existing outline-toc window if possible."
(interactive)
(let ((currentbuffer (current-buffer))
(maj-mode major-mode)
(win (outline-toc--get-window))
(indbuf (make-indirect-buffer (current-buffer)
(concat outline-toc-buffer-name "_temp")))
(edges (window-pixel-edges)))
;; Remember the active buffer currently displayed in the outline-toc.
(setq outline-toc--active-buffer (current-buffer))
(with-selected-window win
;; Now set up the outline-toc:
(when (window-dedicated-p)
(set-window-dedicated-p nil nil))
(switch-to-buffer indbuf t t)
(outline-toc-kill-buffer)
(rename-buffer outline-toc-buffer-name)
;; Do not fold lines in the outline-toc.
;; (setq truncate-lines t)
(when outline-toc-dedicated-window
(set-window-dedicated-p nil t))
;; Set up the base overlay
(setq outline-toc--base-overlay (make-overlay (point-min) (point-max) nil t t))
(overlay-put outline-toc--base-overlay 'face 'outline-toc-font-face)
(overlay-put outline-toc--base-overlay 'priority 1)
;; (outline-toc-sb-mode 1)
;; (when (and (boundp 'linum-mode)
;; linum-mode)
;; (linum-mode 0))
(funcall maj-mode)
(outline-hide-body)
(setq buffer-read-only t)
;; Calculate the actual number of lines displayable with the outline-toc face.
(setq outline-toc--numlines
(floor
(/
(- (nth 3 edges) (nth 1 edges))
(car (progn (redisplay t) (window-line-height)))))))
;; (outline-toc-sync-overlays)
))
(defun outline-toc-kill ()
"Kill outline-toc."
(interactive)
(when (outline-toc--get-window)
(delete-window (outline-toc--get-window)))
(cancel-timer outline-toc--timer-object))
;;; Outline-Toc update
(defun outline-toc--update (&optional force)
"Update outline-toc sidebar if necessary.
This is meant to be called from the idle-timer or the post command hook.
When FORCE, enforce update of the active region."
(interactive)
;; If we are in the minibuffer, do nothing.
(unless (active-minibuffer-window)
(when (outline-toc-active-current-buffer-p)
;; Recreate toc window if necessary
(when (null (outline-toc--get-window))
(outline-toc-create-window))
;; Update our position in the TOC window
(let ((win (outline-toc--get-window))
(pt (point)))
(with-selected-window win
(outline-show-all)
(goto-char pt)
(outline-previous-heading)
(outline-hide-body)
(recenter)
(unless outline-toc--line-overlay
(setq outline-toc--line-overlay (make-overlay (point) (1+ (point)) nil t))
(overlay-put outline-toc--line-overlay 'face 'outline-toc-current-section)
(overlay-put outline-toc--line-overlay 'priority 6))
(move-overlay outline-toc--line-overlay (point) (line-beginning-position 2)))))))
(defun outline-toc--line-to-pos (line)
"Return point position of line number LINE."
(save-excursion
(goto-char 1)
(if (eq selective-display t)
(re-search-forward "[\n\C-m]" nil 'end (1- line))
(forward-line (1- line)))
(point)))
;;; Outline-Toc minor mode
(defvar outline-toc-sb-mode-map (make-sparse-keymap)
"Keymap used by function `outline-toc-sb-mode'.")
(define-minor-mode outline-toc-sb-mode
"Minor mode for outline-toc sidebar."
nil "outline-toc" outline-toc-sb-mode-map)
(provide 'outline-toc)
;;; outline-toc.el ends here