Add Org Num minor mode
* doc/org-manual.org (Headlines): Refer to new section. (Dynamic Headline Numbering): New section. * lisp/org-num.el: * testing/lisp/test-org-num.el: New files.
This commit is contained in:
parent
43fbf8a540
commit
69c300bbf6
|
@ -427,6 +427,9 @@ Some people find the many stars too noisy and would prefer an outline
|
|||
that has whitespace followed by a single star as headline starters.
|
||||
See [[*A Cleaner Outline View]].
|
||||
|
||||
Headline are not numbered. However, you may want to dynamically
|
||||
number some, or all, of them. See [[*Dynamic Headline Numbering]].
|
||||
|
||||
#+vindex: org-cycle-separator-lines
|
||||
An empty line after the end of a subtree is considered part of it and
|
||||
is hidden when the subtree is folded. However, if you leave at least
|
||||
|
@ -19025,6 +19028,36 @@ headings as shown in examples below.
|
|||
org-convert-to-odd-levels)}}} and {{{kbd(M-x
|
||||
org-convert-to-oddeven-levels)}}}.
|
||||
|
||||
** Dynamic Headline Numbering
|
||||
:PROPERTIES:
|
||||
:DESCRIPTION: Display and update outline numbering.
|
||||
:END:
|
||||
|
||||
#+cindex: Org Num mode
|
||||
#+cindex: number headlines
|
||||
The Org Num minor mode, toggled with {{{kbd(M-x org-num-mode)}}},
|
||||
displays on top of headlines. It also updates numbering automatically
|
||||
upon changes to the structure of the document.
|
||||
|
||||
#+vindex: org-num-max-level
|
||||
#+vindex: org-num-skip-tags
|
||||
#+vindex: org-num-skip-commented
|
||||
#+vindex: org-num-skip-unnumbered
|
||||
By default, all headlines are numbered. You can limit numbering to
|
||||
specific headlines according to their level, tags, =COMMENT= keyword,
|
||||
or =UNNUMBERED= property. Set ~org-num-max-level~,
|
||||
~org-num-skip-tags~, ~org-num-skip-commented~,
|
||||
~org-num-skip-unnumbered~, or ~org-num-skip-footnotes~ accordingly.
|
||||
|
||||
#+vindex: org-num-skip-footnotes
|
||||
If ~org-num-skip-footnotes~ is non-~nil~, footnotes sections (see
|
||||
[[*Creating Footnotes]]) are not numbered either.
|
||||
|
||||
#+vindex: org-num-face
|
||||
#+vindex: org-num-format-function
|
||||
You can control how the numbering is displayed by setting
|
||||
~org-num-face~ and ~org-num-format-function~.
|
||||
|
||||
** Using Org on a TTY
|
||||
:PROPERTIES:
|
||||
:DESCRIPTION: Using Org on a tty.
|
||||
|
|
|
@ -42,6 +42,12 @@ See [[git:3367ac9457]] for details.
|
|||
** New features
|
||||
*** Babel
|
||||
**** Add LaTeX output support in PlantUML
|
||||
*** New minor mode to display headline numbering
|
||||
|
||||
Use =<M-x org-num-mode>= to get a visual indication of the numbering
|
||||
in the outline. The numbering is also automatically updated upon
|
||||
changes in the buffer.
|
||||
|
||||
*** New property =HTML_HEADLINE_CLASS= in HTML export
|
||||
The new property =HTML_HEADLINE_CLASS= assigns a class attribute to
|
||||
a headline.
|
||||
|
|
|
@ -0,0 +1,455 @@
|
|||
;;; org-num.el --- Dynamic Headlines Numbering -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2018-2019 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
|
||||
;; Keywords: outlines, hypermedia, calendar, wp
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; This library provides dynamic numbering for Org headlines. Use
|
||||
;;
|
||||
;; <M-x org-num-mode>
|
||||
;;
|
||||
;; to toggle it.
|
||||
;;
|
||||
;; You can select what is numbered according to level, tags, COMMENT
|
||||
;; keyword, or UNNUMBERED property. You can also skip footnotes
|
||||
;; sections. See `org-num-max-level', `org-num-skip-tags',
|
||||
;; `org-num-skip-commented', `org-num-skip-unnumbered', and
|
||||
;; `org-num-skip-footnotes' for details.
|
||||
;;
|
||||
;; You can also control how the numbering is displayed by setting
|
||||
;;`org-num-face' and `org-num-format-function'.
|
||||
;;
|
||||
;; Internally, the library handles an ordered list, per buffer
|
||||
;; position, of overlays in `org-num--overlays'. These overlays are
|
||||
;; marked with the `org-num' property set to a non-nil value.
|
||||
;;
|
||||
;; Overlays store the level of the headline in the `level' property,
|
||||
;; and the face used for the numbering in `numbering-face'.
|
||||
;;
|
||||
;; The `skip' property is set to t when the corresponding headline has
|
||||
;; some characteristic -- e.g., a node property, or a tag -- that
|
||||
;; prevents it from being numbered.
|
||||
;;
|
||||
;; An overlay with `org-num' property set to `invalid' is called an
|
||||
;; invalid overlay. Modified overlays automatically become invalid
|
||||
;; and set `org-num--invalid-flag' to a non-nil value. After
|
||||
;; a change, `org-num--invalid-flag' indicates numbering needs to be
|
||||
;; updated and invalid overlays indicate where the buffer needs to be
|
||||
;; parsed. So does `org-num--missing-overlay' variable. See
|
||||
;; `org-num--verify' function for details.
|
||||
;;
|
||||
;; Numbering display is done through the `after-string' property.
|
||||
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'org-macs)
|
||||
|
||||
(defvar org-comment-string)
|
||||
(defvar org-complex-heading-regexp)
|
||||
(defvar org-cycle-level-faces)
|
||||
(defvar org-n-level-faces)
|
||||
(defvar org-odd-levels-only)
|
||||
(defvar org-level-faces)
|
||||
|
||||
(declare-function org-back-to-heading "org" (&optional invisible-ok))
|
||||
(declare-function org-entry-get "org" (pom property &optional inherit literal-nil))
|
||||
(declare-function org-reduced-level "org" (l))
|
||||
|
||||
|
||||
;;; Customization
|
||||
|
||||
(defcustom org-num-face nil
|
||||
"Face to use for numbering.
|
||||
When nil, use the same face as the headline. This value is
|
||||
ignored if `org-num-format-function' specifies a face for its
|
||||
output."
|
||||
:group 'org-appearance
|
||||
:type '(choice (const :tag "Like the headline" nil)
|
||||
(face :tag "Use face"))
|
||||
:safe (lambda (val) (or (null val) (facep val))))
|
||||
|
||||
(defcustom org-num-format-function 'org-num-default-format
|
||||
"Function used to display numbering.
|
||||
It is called with one argument, a list of numbers, and should
|
||||
return a string, or nil. When nil, no numbering is displayed.
|
||||
Any `face' text property on the returned string overrides
|
||||
`org-num-face'."
|
||||
:group 'org-appearance
|
||||
:type 'function
|
||||
:safe nil)
|
||||
|
||||
(defcustom org-num-max-level nil
|
||||
"Level below which headlines are not numbered.
|
||||
When set to nil, all headlines are numbered."
|
||||
:group 'org-appearance
|
||||
:type '(choice (const :tag "Number everything" nil)
|
||||
(integer :tag "Stop numbering at level"))
|
||||
:safe (lambda (val) (or (null val) (wholenump val))))
|
||||
|
||||
(defcustom org-num-skip-commented nil
|
||||
"Non-nil means commented sub-trees are not numbered."
|
||||
:group 'org-appearance
|
||||
:type 'boolean
|
||||
:safe #'booleanp)
|
||||
|
||||
(defcustom org-num-skip-footnotes nil
|
||||
"Non-nil means footnotes sections are not numbered."
|
||||
:group 'org-appearance
|
||||
:type 'boolean
|
||||
:safe #'booleanp)
|
||||
|
||||
(defcustom org-num-skip-tags nil
|
||||
"List of tags preventing the numbering of sub-trees.
|
||||
|
||||
For example, add \"ARCHIVE\" to this list to avoid numbering
|
||||
archived sub-trees.
|
||||
|
||||
Tag in this list prevent numbering the whole sub-tree,
|
||||
irrespective to `org-use-tags-inheritance', or other means to
|
||||
control tag inheritance."
|
||||
:group 'org-appearance
|
||||
:type '(repeat (string :tag "Tag"))
|
||||
:safe (lambda (val) (and (listp val) (cl-every #'stringp val))))
|
||||
|
||||
(defcustom org-num-skip-unnumbered nil
|
||||
"Non-nil means numbering obeys to UNNUMBERED property."
|
||||
:group 'org-appearance
|
||||
:type 'boolean
|
||||
:safe #'booleanp)
|
||||
|
||||
|
||||
;;; Internal Variables
|
||||
|
||||
(defconst org-num--comment-re (format "\\`%s\\(?: \\|$\\)" org-comment-string)
|
||||
"Regexp matching a COMMENT keyword at headline beginning.")
|
||||
|
||||
(defvar-local org-num--overlays nil
|
||||
"Ordered list of overlays used for numbering outlines.")
|
||||
|
||||
(defvar-local org-num--skip-level nil
|
||||
"Level below which headlines from current tree are not numbered.
|
||||
When nil, all headlines are numbered. It is used to handle
|
||||
inheritance of no-numbering attributes.")
|
||||
|
||||
(defvar-local org-num--numbering nil
|
||||
"Current headline numbering.
|
||||
A numbering is a list of integers, in reverse order. So numbering
|
||||
for headline \"1.2.3\" is (3 2 1).")
|
||||
|
||||
(defvar-local org-num--missing-overlay nil
|
||||
"Buffer position signaling a headline without an overlay.")
|
||||
|
||||
(defvar-local org-num--invalid-flag nil
|
||||
"Non-nil means an overlay became invalid since last update.")
|
||||
|
||||
|
||||
;;; Internal Functions
|
||||
|
||||
(defsubst org-num--headline-regexp ()
|
||||
"Return regexp matching a numbered headline."
|
||||
(if (null org-num-max-level) (org-with-limited-levels org-outline-regexp-bol)
|
||||
(format "^\\*\\{1,%d\\} "
|
||||
(if org-odd-levels-only (1- (* 2 org-num-max-level))
|
||||
org-num-max-level))))
|
||||
|
||||
(defsubst org-num--overlay-p (o)
|
||||
"Non-nil if overlay O is a numbering overlay."
|
||||
(overlay-get o 'org-num))
|
||||
|
||||
(defsubst org-num--valid-overlay-p (o)
|
||||
"Non-nil if overlay O is still active in the buffer."
|
||||
(not (eq 'invalid (overlay-get o 'org-num))))
|
||||
|
||||
(defsubst org-num--invalidate-overlay (o)
|
||||
"Mark overlay O as invalid.
|
||||
Update `org-num--invalid-flag' accordingly."
|
||||
(overlay-put o 'org-num 'invalid)
|
||||
(setq org-num--invalid-flag t))
|
||||
|
||||
(defun org-num--make-overlay (numbering level skip)
|
||||
"Return overlay for numbering headline at point.
|
||||
|
||||
NUMBERING is the numbering to use, as a list of integers, or nil
|
||||
if nothing should be displayed. LEVEL is the level of the
|
||||
headline. SKIP is its skip value.
|
||||
|
||||
Assume point is at a headline."
|
||||
(let ((after-edit-functions
|
||||
(list (lambda (o &rest _) (org-num--invalidate-overlay o))))
|
||||
(o (save-excursion
|
||||
(beginning-of-line)
|
||||
(skip-chars-forward "*")
|
||||
(make-overlay (line-beginning-position) (1+ (point))))))
|
||||
(overlay-put o 'org-num t)
|
||||
(overlay-put o 'skip skip)
|
||||
(overlay-put o 'level level)
|
||||
(overlay-put o 'numbering-face
|
||||
(or org-num-face
|
||||
;; Compute face that would be used at the
|
||||
;; headline. We cannot extract it from the
|
||||
;; buffer: at the time the overlay is created,
|
||||
;; Font Lock has not proceeded yet.
|
||||
(nth (if org-cycle-level-faces
|
||||
(% (1- level) org-n-level-faces)
|
||||
(1- (min level org-n-level-faces)))
|
||||
org-level-faces)))
|
||||
(overlay-put o 'modification-hooks after-edit-functions)
|
||||
(overlay-put o 'insert-in-front-hooks after-edit-functions)
|
||||
(org-num--refresh-display o numbering)
|
||||
o))
|
||||
|
||||
(defun org-num--refresh-display (overlay numbering)
|
||||
"Refresh OVERLAY's display.
|
||||
NUMBERING specifies the new numbering, as a list of integers, or
|
||||
nil if nothing should be displayed. Assume OVERLAY is valid."
|
||||
(let ((display (and numbering
|
||||
(funcall org-num-format-function (reverse numbering)))))
|
||||
(when (and display (not (get-text-property 0 'face display)))
|
||||
(org-add-props display `(face ,(overlay-get overlay 'numbering-face))))
|
||||
(overlay-put overlay 'after-string display)))
|
||||
|
||||
(defun org-num--skip-value ()
|
||||
"Return skip value for headline at point.
|
||||
Value is t when headline should not be numbered, and nil
|
||||
otherwise."
|
||||
(org-match-line org-complex-heading-regexp)
|
||||
(let ((title (match-string 4))
|
||||
(tags (and org-num-skip-tags
|
||||
(match-end 5)
|
||||
(org-split-string (match-string 5) ":"))))
|
||||
(or (and org-num-skip-footnotes
|
||||
org-footnote-section
|
||||
(equal title org-footnote-section))
|
||||
(and org-num-skip-commented
|
||||
(let ((case-fold-search nil))
|
||||
(string-match org-num--comment-re title))
|
||||
t)
|
||||
(and org-num-skip-tags
|
||||
(cl-some (lambda (tag) (member tag org-num-skip-tags))
|
||||
tags)
|
||||
t)
|
||||
(and org-num-skip-unnumbered
|
||||
(org-entry-get (point) "UNNUMBERED")
|
||||
t))))
|
||||
|
||||
(defun org-num--current-numbering (level skip)
|
||||
"Return numbering for current headline.
|
||||
LEVEL is headline's level, and SKIP its skip value. Return nil
|
||||
if headline should be skipped."
|
||||
(cond
|
||||
;; Skipped by inheritance.
|
||||
((and org-num--skip-level (> level org-num--skip-level)) nil)
|
||||
;; Skipped by a non-nil skip value; set `org-num--skip-level'
|
||||
;; to skip the whole sub-tree later on.
|
||||
(skip (setq org-num--skip-level level) nil)
|
||||
(t
|
||||
(setq org-num--skip-level nil)
|
||||
;; Compute next numbering, and update `org-num--numbering'.
|
||||
(let ((last-level (length org-num--numbering)))
|
||||
(setq org-num--numbering
|
||||
(cond
|
||||
;; First headline : nil => (1), or (1 0)...
|
||||
((null org-num--numbering) (cons 1 (make-list (1- level) 0)))
|
||||
;; Sibling: (1 1) => (2 1).
|
||||
((= level last-level)
|
||||
(cons (1+ (car org-num--numbering)) (cdr org-num--numbering)))
|
||||
;; Parent: (1 1 1) => (2 1), or (2).
|
||||
((< level last-level)
|
||||
(let ((suffix (nthcdr (- last-level level) org-num--numbering)))
|
||||
(cons (1+ (car suffix)) (cdr suffix))))
|
||||
;; Child: (1 1) => (1 1 1), or (1 0 1 1)...
|
||||
(t
|
||||
(append (cons 1 (make-list (- level last-level 1) 0))
|
||||
org-num--numbering))))))))
|
||||
|
||||
(defun org-num--number-region (start end)
|
||||
"Add numbering overlays between START and END positions.
|
||||
When START or END are nil, use buffer boundaries. Narrowing, if
|
||||
any, is ignored. Return the list of created overlays, newest
|
||||
first."
|
||||
(org-with-point-at (or start 1)
|
||||
;; Do not match headline starting at START.
|
||||
(when start (end-of-line))
|
||||
(let ((regexp (org-num--headline-regexp))
|
||||
(new nil))
|
||||
(while (re-search-forward regexp end t)
|
||||
(let* ((level (org-reduced-level
|
||||
(- (match-end 0) (match-beginning 0) 1)))
|
||||
(skip (org-num--skip-value))
|
||||
(numbering (org-num--current-numbering level skip)))
|
||||
;; Apply numbering to current headline. Store overlay for
|
||||
;; the return value.
|
||||
(push (org-num--make-overlay numbering level skip)
|
||||
new)))
|
||||
new)))
|
||||
|
||||
(defun org-num--update ()
|
||||
"Update buffer's numbering.
|
||||
This function removes invalid overlays and refreshes numbering
|
||||
for the valid ones in the numbering overlays list. It also adds
|
||||
missing overlays to that list."
|
||||
(setq org-num--skip-level nil)
|
||||
(setq org-num--numbering nil)
|
||||
(let ((new-overlays nil)
|
||||
(overlay nil))
|
||||
(while (setq overlay (pop org-num--overlays))
|
||||
(cond
|
||||
;; Valid overlay.
|
||||
;;
|
||||
;; First handle possible missing overlays OVERLAY. If missing
|
||||
;; overlay marker is pointing before next overlay and after the
|
||||
;; last known overlay, make sure to parse the buffer between
|
||||
;; these two overlays.
|
||||
((org-num--valid-overlay-p overlay)
|
||||
(let ((next (overlay-start overlay))
|
||||
(last (and new-overlays (overlay-start (car new-overlays)))))
|
||||
(cond
|
||||
((null org-num--missing-overlay))
|
||||
((> org-num--missing-overlay next))
|
||||
((or (null last) (> org-num--missing-overlay last))
|
||||
(setq org-num--missing-overlay nil)
|
||||
(setq new-overlays (nconc (org-num--number-region last next)
|
||||
new-overlays)))
|
||||
;; If it is already after the last known overlay, reset it:
|
||||
;; some previous invalid overlay already triggered the
|
||||
;; necessary parsing.
|
||||
(t
|
||||
(setq org-num--missing-overlay nil))))
|
||||
;; Update OVERLAY's numbering.
|
||||
(let* ((level (overlay-get overlay 'level))
|
||||
(skip (overlay-get overlay 'skip))
|
||||
(numbering (org-num--current-numbering level skip)))
|
||||
(org-num--refresh-display overlay numbering)
|
||||
(push overlay new-overlays)))
|
||||
;; Invalid overlay. It indicates that the buffer needs to be
|
||||
;; parsed again between the two surrounding valid overlays or
|
||||
;; buffer boundaries.
|
||||
(t
|
||||
;; Delete all consecutive invalid overlays: we re-create all
|
||||
;; overlays between last valid overlay and the next one.
|
||||
(delete-overlay overlay)
|
||||
(while (and org-num--overlays
|
||||
(not (org-num--valid-overlay-p (car org-num--overlays))))
|
||||
(delete-overlay (pop org-num--overlays)))
|
||||
;; Create and register new overlays.
|
||||
(let ((last (and new-overlays (overlay-start (car new-overlays))))
|
||||
(next (and org-num--overlays
|
||||
(overlay-start (car org-num--overlays)))))
|
||||
(setq new-overlays (nconc (org-num--number-region last next)
|
||||
new-overlays))))))
|
||||
;; If invalid position hasn't been handled yet, it must be located
|
||||
;; between last valid overlay and end of the buffer. Parse that
|
||||
;; area before returning.
|
||||
(when org-num--missing-overlay
|
||||
(let ((last (and new-overlays (overlay-start (car new-overlays)))))
|
||||
(setq new-overlays (nconc (org-num--number-region last nil)
|
||||
new-overlays))))
|
||||
;; Numbering is now up-to-date. Reset invalid flag. Also return
|
||||
;; `org-num--overlays' in a sorted fashion.
|
||||
(setq org-num--invalid-flag nil)
|
||||
(setq org-num--overlays (nreverse new-overlays))))
|
||||
|
||||
(defun org-num--verify (beg end _)
|
||||
"Check numbering integrity; update it if necessary.
|
||||
This function is meant to be used in `after-change-functions'.
|
||||
See this variable for the meaning of BEG and END."
|
||||
(setq org-num--missing-overlay nil)
|
||||
(save-match-data
|
||||
(org-with-point-at beg
|
||||
(let ((regexp (org-num--headline-regexp)))
|
||||
;; At this point, directly altered overlays between BEG and
|
||||
;; END are marked as invalid and will trigger a full update.
|
||||
;; However, there are still two cases to handle.
|
||||
;;
|
||||
;; First, some valid overlays may need to be invalidated, due
|
||||
;; to an indirect change. That happens when the skip value --
|
||||
;; see `org-num--skip-value' -- of the heading BEG belongs to
|
||||
;; is altered, or when deleting the newline character right
|
||||
;; before the next headline.
|
||||
(save-excursion
|
||||
;; Bail out if we're before first headline or within
|
||||
;; a headline too deep to be numbered.
|
||||
(when (and (org-with-limited-levels
|
||||
(ignore-errors (org-back-to-heading t)))
|
||||
(looking-at regexp))
|
||||
(pcase (get-char-property-and-overlay (point) 'org-num)
|
||||
(`(nil)
|
||||
;; At a headline, without a numbering overlay: change
|
||||
;; just created one. Mark it for parsing.
|
||||
(setq org-num--missing-overlay (point)))
|
||||
(`(t . ,o)
|
||||
;; Check if skip value changed. Invalidate overlay
|
||||
;; accordingly.
|
||||
(unless (eq (org-num--skip-value) (overlay-get o 'skip))
|
||||
(org-num--invalidate-overlay o)))
|
||||
(_ nil))))
|
||||
;; Deleting the newline character before a numbering overlay
|
||||
;; doesn't invalidate it, even though it could land in the
|
||||
;; middle of a line. Be sure to catch this case.
|
||||
(when (and (= beg end) (not (bolp)))
|
||||
(pcase (get-char-property-and-overlay (point) 'org-num)
|
||||
(`(t . ,o) (org-num--invalidate-overlay o))
|
||||
(_ nil)))
|
||||
;; Second, if nothing is marked as invalid, and therefore if
|
||||
;; no full update is due so far, changes may still have
|
||||
;; created new headlines, at BEG -- which is actually handled
|
||||
;; by the previous phase --, or, in case of a multi-line
|
||||
;; insertion, at END, or in-between.
|
||||
(unless (or org-num--invalid-flag
|
||||
org-num--missing-overlay
|
||||
(<= end (line-end-position))) ;single line change
|
||||
(forward-line)
|
||||
(when (or (re-search-forward regexp end 'move)
|
||||
;; Check if change created a headline after END.
|
||||
(progn (skip-chars-backward "*") (looking-at regexp)))
|
||||
(setq org-num--missing-overlay (line-beginning-position))))))
|
||||
;; Update numbering only if a headline was altered or created.
|
||||
(when (or org-num--missing-overlay org-num--invalid-flag)
|
||||
(org-num--update))))
|
||||
|
||||
|
||||
;;; Public Functions
|
||||
|
||||
;;;###autoload
|
||||
(defun org-num-default-format (numbering)
|
||||
"Default numbering display function.
|
||||
NUMBERING is a list of numbers."
|
||||
(concat (mapconcat #'number-to-string numbering ".") " "))
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode org-num-mode
|
||||
"Dynamic numbering of headlines in an Org buffer."
|
||||
:lighter " o#"
|
||||
(cond
|
||||
(org-num-mode
|
||||
(unless (derived-mode-p 'org-mode)
|
||||
(user-error "Cannot activate headline numbering outside Org mode"))
|
||||
(setq org-num--numbering nil)
|
||||
(setq org-num--overlays (nreverse (org-num--number-region nil nil)))
|
||||
(add-hook 'after-change-functions #'org-num--verify nil t))
|
||||
(t
|
||||
(mapc #'delete-overlay org-num--overlays)
|
||||
(setq org-num--overlays nil)
|
||||
(remove-hook 'after-change-functions #'org-num--verify t))))
|
||||
|
||||
|
||||
(provide 'org-num)
|
||||
;;; org-num.el ends here
|
|
@ -0,0 +1,261 @@
|
|||
;;; test-org-num.el --- Tests for Org Num library -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2018 Nicolas Goaziou
|
||||
|
||||
;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
|
||||
|
||||
;; This file is not part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Code:
|
||||
|
||||
;; FIXME: this test fails in batch mode.
|
||||
;;
|
||||
;; (ert-deftest test-org-num/face ()
|
||||
;; "Test `org-num-face' parameter."
|
||||
;; (should
|
||||
;; (equal
|
||||
;; '(foo)
|
||||
;; (org-test-with-temp-text "* H1"
|
||||
;; (let ((org-num-face 'foo)) (org-num-mode 1))
|
||||
;; (mapcar (lambda (o)
|
||||
;; (get-text-property 0 'face (overlay-get o 'after-string)))
|
||||
;; (overlays-in (point-min) (point-max)))))))
|
||||
|
||||
(ert-deftest test-org-num/format-function ()
|
||||
"Test `org-num-format-function' parameter."
|
||||
(should
|
||||
(equal '("foo" "foo")
|
||||
(org-test-with-temp-text "* H1\n** H2"
|
||||
(let ((org-num-format-function (lambda (_) "foo")))
|
||||
(org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Preserve face, when set.
|
||||
(should
|
||||
(equal-including-properties
|
||||
'(#("foo" 0 3 (face bar)))
|
||||
(org-test-with-temp-text "* H1"
|
||||
(let ((org-num-format-function
|
||||
(lambda (_) (org-add-props "foo" nil 'face 'bar))))
|
||||
(org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Set face override `org-num-face'.
|
||||
(should
|
||||
(equal-including-properties
|
||||
'(#("foo" 0 3 (face bar)))
|
||||
(org-test-with-temp-text "* H1"
|
||||
(let ((org-num-face 'baz)
|
||||
(org-num-format-function
|
||||
(lambda (_) (org-add-props "foo" nil 'face 'bar))))
|
||||
(org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max)))))))
|
||||
|
||||
(ert-deftest test-org-num/max-level ()
|
||||
"Test `org-num-max-level' option."
|
||||
(should
|
||||
(equal '("1.1 " "1 ")
|
||||
(org-test-with-temp-text "* H1\n** H2\n*** H3"
|
||||
(let ((org-num-max-level 2)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max)))))))
|
||||
|
||||
(ert-deftest test-org-num/skip-numbering ()
|
||||
"Test various skip numbering parameters."
|
||||
;; Skip commented headlines.
|
||||
(should
|
||||
(equal '(nil "1 ")
|
||||
(org-test-with-temp-text "* H1\n* COMMENT H2"
|
||||
(let ((org-num-skip-commented t)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '("2 " "1 ")
|
||||
(org-test-with-temp-text "* H1\n* COMMENT H2"
|
||||
(let ((org-num-skip-commented nil)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Skip commented sub-trees.
|
||||
(should
|
||||
(equal '(nil nil)
|
||||
(org-test-with-temp-text "* COMMENT H1\n** H2"
|
||||
(let ((org-num-skip-commented t)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Skip footnotes sections.
|
||||
(should
|
||||
(equal '(nil "1 ")
|
||||
(org-test-with-temp-text "* H1\n* FN"
|
||||
(let ((org-num-skip-footnotes t)
|
||||
(org-footnote-section "FN"))
|
||||
(org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '("2 " "1 ")
|
||||
(org-test-with-temp-text "* H1\n* FN"
|
||||
(let ((org-num-skip-footnotes nil)
|
||||
(org-footnote-section "FN"))
|
||||
(org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Skip tags, recursively.
|
||||
(should
|
||||
(equal '(nil "1 ")
|
||||
(org-test-with-temp-text "* H1\n* H2 :foo:"
|
||||
(let ((org-num-skip-tags '("foo"))) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '(nil nil)
|
||||
(org-test-with-temp-text "* H1 :foo:\n** H2"
|
||||
(let ((org-num-skip-tags '("foo"))) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Skip unnumbered sections.
|
||||
(should
|
||||
(equal '(nil "1 ")
|
||||
(org-test-with-temp-text
|
||||
"* H1\n* H2\n:PROPERTIES:\n:UNNUMBERED: t\n:END:"
|
||||
(let ((org-num-skip-unnumbered t)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '("2 " "1 ")
|
||||
(org-test-with-temp-text
|
||||
"* H1\n* H2\n:PROPERTIES:\n:UNNUMBERED: t\n:END:"
|
||||
(let ((org-num-skip-unnumbered nil)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '("2 " "1 ")
|
||||
(org-test-with-temp-text
|
||||
"* H1\n* H2\n:PROPERTIES:\n:UNNUMBERED: nil\n:END:"
|
||||
(let ((org-num-skip-unnumbered t)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Skip unnumbered sub-trees.
|
||||
(should
|
||||
(equal '(nil nil)
|
||||
(org-test-with-temp-text
|
||||
"* H1\n:PROPERTIES:\n:UNNUMBERED: t\n:END:\n** H2"
|
||||
(let ((org-num-skip-unnumbered t)) (org-num-mode 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max)))))))
|
||||
|
||||
(ert-deftest test-org-num/update ()
|
||||
"Test numbering update after a buffer modification."
|
||||
;; Headlines created at BEG.
|
||||
(should
|
||||
(equal "1 "
|
||||
(org-test-with-temp-text "X* H"
|
||||
(org-num-mode 1)
|
||||
(delete-char 1)
|
||||
(overlay-get (car (overlays-at (line-beginning-position)))
|
||||
'after-string))))
|
||||
(should
|
||||
(equal "1 "
|
||||
(org-test-with-temp-text "*<point>\n H"
|
||||
(org-num-mode 1)
|
||||
(delete-char 1)
|
||||
(overlay-get (car (overlays-at (line-beginning-position)))
|
||||
'after-string))))
|
||||
(should
|
||||
(equal "1 "
|
||||
(org-test-with-temp-text "*<point>bold*"
|
||||
(org-num-mode 1)
|
||||
(insert " ")
|
||||
(overlay-get (car (overlays-at (line-beginning-position)))
|
||||
'after-string))))
|
||||
;; Headlines created at END.
|
||||
(should
|
||||
(equal '("1 ")
|
||||
(org-test-with-temp-text "X<point> H"
|
||||
(org-num-mode 1)
|
||||
(insert "\n*")
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '("1 ")
|
||||
(org-test-with-temp-text "X<point>* H"
|
||||
(org-num-mode 1)
|
||||
(insert "\n")
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Headlines created between BEG and END.
|
||||
(should
|
||||
(equal '("1.1 " "1 ")
|
||||
(org-test-with-temp-text ""
|
||||
(org-num-mode 1)
|
||||
(insert "\n* H\n** H2")
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Change level of a headline.
|
||||
(should
|
||||
(equal '("0.1 ")
|
||||
(org-test-with-temp-text "* H"
|
||||
(org-num-mode 1)
|
||||
(insert "*")
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '("1 ")
|
||||
(org-test-with-temp-text "*<point>* H"
|
||||
(org-num-mode 1)
|
||||
(delete-char 1)
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Alter skip state.
|
||||
(should
|
||||
(equal '("1 ")
|
||||
(org-test-with-temp-text "* H :fo<point>o:"
|
||||
(let ((org-num-skip-tags '("foo")))
|
||||
(org-num-mode 1)
|
||||
(delete-char 1))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
(should
|
||||
(equal '(nil)
|
||||
(org-test-with-temp-text "* H :fo<point>:"
|
||||
(let ((org-num-skip-tags '("foo")))
|
||||
(org-num-mode 1)
|
||||
(insert "o"))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))
|
||||
;; Invalidate an overlay and insert new headlines.
|
||||
(should
|
||||
(equal '("1.2 " "1.1 " "1 ")
|
||||
(org-test-with-temp-text
|
||||
"* H\n:PROPERTIES:\n:UNNUMBE<point>RED: t\n:END:"
|
||||
(let ((org-num-skip-unnumbered t))
|
||||
(org-num-mode 1)
|
||||
(insert "\n** H2\n** H3\n")
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max)))))))
|
||||
;; Invalidate two overlays: current headline and next one.
|
||||
(should
|
||||
(equal '("1 ")
|
||||
(org-test-with-temp-text
|
||||
"* H\n:PROPERTIES:\n:UNNUMBE<point>RED: t\n:END:\n** H2"
|
||||
(let ((org-num-skip-unnumbered t))
|
||||
(org-num-mode 1)
|
||||
(delete-region (point) (line-beginning-position 3))
|
||||
(mapcar (lambda (o) (overlay-get o 'after-string))
|
||||
(overlays-in (point-min) (point-max))))))))
|
||||
|
||||
(provide 'test-org-num)
|
||||
;;; org-test-num.el ends here
|
Loading…
Reference in New Issue