- Constants
- README.org
- org-visibility.el
- org-visibility-test.el
- Makefile
- .elpaignore
- .gitignore
- LICENSE
Kyle W T Sherman
<kylewsherman@gmail.com>
Author: <<name>> <<email>>
(format-time-string "%Y" nil t)
Copyright (C) 2021-<<year()>> <<name>>
URL: https://github.com/nullman/emacs-org-visibility
1.1.11
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 2 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, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301 USA.
* Org Visibility
:BADGE:
[[https://www.gnu.org/software/emacs/][https://img.shields.io/badge/Emacs-27-8e44bd.svg]]
[[http://www.gnu.org/licenses/gpl-2.0.txt][https://img.shields.io/badge/license-GPL_2-green.svg]]
[[https://melpa.org/#/org-visibility][file:https://melpa.org/packages/org-visibility-badge.svg]]
[[https://stable.melpa.org/#/org-visibility][file:https://stable.melpa.org/packages/org-visibility-badge.svg]]
:END:
Org Visibility is an Emacs package that adds the ability to persist (save and
load) the state of the visible sections of =org-mode= files. The state is
automatically saved when the file is saved or killed, and restored when the
file is loaded.
Example Configuration:
#+BEGIN_SRC emacs-lisp
(use-package org-visibility
:after (org)
:demand t
:bind* (:map org-visibility-mode-map
("C-x C-v" . org-visibility-force-save) ; defaults to `find-alternative-file'
("C-x M-v" . org-visibility-remove)) ; defaults to undefined
:custom
;; list of directories and files to automatically persist and restore visibility state of
(org-visibility-include-paths `(,(file-truename "~/.emacs.d/init-emacs.org")
,(file-truename "~/org")))
;; list of directories and files to not persist and restore visibility state of
(org-visibility-exclude-paths `(,(file-truename "~/org/old")
,(file-truename "~/org/test")))
:config
(org-visibility-mode 1))
#+END_SRC
Of course you will need to edit the above paths to match your setup.
See the =Installation= and =Usage= parts of the =Commentary= section in
[[file:org-visibility.el][org-visibility.el]] for more information and a full list of customizations.
Emacs version 27.1 or later is required.
Build with =make=.
Clean with =make clean=.
Run tests with =make test=.
All files are generated from [[file:org-visibility.org][org-visibility.org]] using Emacs' org-mode literate
programming system to "tangle" them.
<<copyright>>
License: [[file:LICENSE][GNU General Public License 2]]
;;; org-visibility.el --- Persistent org tree visibility -*- lexical-binding: t; -*-
;;
;;; <<copyright>>
;;
;; <<author>>
;; <<url>>
;; Created: 2021-07-17
;; Version: <<version>>
;; Keywords: outlines convenience
;; Package-Requires: ((emacs "27.1"))
;;
;; This file is not part of GNU Emacs.
;;
;; <<license-header>>
;;
;;; Commentary:
;;
;; Org Visibility is an Emacs package that adds the ability to persist (save
;; and load) the state of the visible sections of `org-mode' files. The state
;; is saved when the file is saved or killed, and restored when the file is
;; loaded.
;;
;; Hooks are used to persist and restore org tree visibility upon loading and
;; saving org files. Whether or not a given buffer's file will have its
;; visibility persisted is determined by the following logic:
;;
;; Qualification Rules:
;;
;; Files are only considered if their buffer is an `org-mode' buffer and they
;; meet one of the following requirements:
;;
;; - File has buffer local variable `org-visibility' set to t
;;
;; - File is contained within one of the directories listed in
;; `org-visibility-include-paths'
;;
;; - File path matches one of the regular expressions listed in
;; `org-visibility-include-regexps'
;;
;; Files are removed from consideration if they meet one of the following
;; requirements (overriding the above include logic):
;;
;; - File has buffer local variable `org-visibility' set to 'never
;;
;; - File is contained within one of the directories listed in
;; `org-visibility-exclude-paths'
;;
;; - File matches one of the regular expressions listed in
;; `org-visibility-exclude-regexps'.
;;
;; Provides the following interactive functions:
;;
;; `org-visibility-save' - Save visibility state for current buffer
;; `org-visibility-force-save' - Save even if buffer has not been modified
;; `org-visibility-save-all-buffers' - Save all buffers that qualify
;; `org-visibility-load' - Load a file and restore its visibility state
;; `org-visibility-remove' - Remove current buffer from `org-visibility-state-file'
;; `org-visibility-clean' - Cleanup `org-visibility-state-file'
;; `org-visibility-enable-hooks' - Enable all hooks
;; `org-visibility-disable-hooks' - Disable all hooks
;;
;; Installation:
;;
;; Put `org-visibility.el' where you keep your elisp files and add something
;; like the following to your .emacs file:
;;
;; ;; optionally change the location of the state file
;; ;;(setq org-visibility-state-file `,(expand-file-name "/some/path/.org-visibility"))
;;
;; ;; list of directories and files to persist and restore visibility state of
;; (setq org-visibility-include-paths `(,(file-truename "~/.emacs.d/init-emacs.org")
;; ,(file-truename "~/org"))
;; ;; persist all org files regardless of location
;; ;;(setq org-visibility-include-regexps '("\\.org\\'"))
;;
;; ;; list of directories and files to not persist and restore visibility state of
;; ;;(setq org-visibility-exclude-paths `(,(file-truename "~/org/old")))
;;
;; ;; optionally set maximum number of files to keep track of
;; ;; oldest files will be removed from the state file first
;; ;;(setq org-visibility-maximum-tracked-files 100)
;;
;; ;; optionally set maximum number of days (since saved) to keep track of
;; ;; files older than this number of days will be removed from the state file
;; ;;(setq org-visibility-maximum-tracked-days 180)
;;
;; ;; optionally turn off visibility state change messages
;; ;;(setq org-visibility-display-messages nil)
;;
;; (require 'org-visibility)
;;
;; ;; enable org-visibility-mode
;; (org-visibility-mode 1)
;;
;; ;; optionally set a keybinding to force save
;; (bind-keys* :map org-visibility-mode-map
;; ("C-x C-v" . org-visibility-force-save) ; defaults to `find-alternative-file'
;; ("C-x M-v" . org-visibility-remove)) ; defaults to undefined
;;
;; Or, if using `use-package', add something like this instead:
;;
;; (use-package org-visibility
;; :after (org)
;; :demand t
;; :bind* (:map org-visibility-mode-map
;; ("C-x C-v" . org-visibility-force-save) ; defaults to `find-alternative-file'
;; ("C-x M-v" . org-visibility-remove)) ; defaults to undefined
;; :custom
;; ;; optionally change the location of the state file
;; ;;(org-visibility-state-file `,(expand-file-name "/some/path/.org-visibility"))
;; ;; list of directories and files to persist and restore visibility state of
;; (org-visibility-include-paths `(,(file-truename "~/.emacs.d/init-emacs.org")
;; ,(file-truename "~/org")))
;; ;; persist all org files regardless of location
;; ;;(org-visibility-include-regexps '("\\.org\\'"))
;; ;; list of directories and files to not persist and restore visibility state of
;; ;;(org-visibility-exclude-paths `(,(file-truename "~/org/old")))
;; ;; optionally set maximum number of files to keep track of
;; ;; oldest files will be removed from the state file first
;; ;;(org-visibility-maximum-tracked-files 100)
;; ;; optionally set maximum number of days (since saved) to keep track of
;; ;; files older than this number of days will be removed from the state file
;; ;;(org-visibility-maximum-tracked-days 180)
;; ;; optionally turn off visibility state change messages
;; ;;(org-visibility-display-messages nil)
;; :config
;; (org-visibility-mode 1))
;;
;; Usage:
;;
;; As long as `org-visibility-mode' is enabled, visibility state is
;; automatically persisted on file save or kill, and restored when loaded. No
;; user intervention is needed. The user can, however, call
;; `org-visibility-force-save' to save the current visibility state of a
;; buffer before a file save or kill would automatically trigger it next.
;;
;; Interactive commands:
;;
;; The `org-visibility-mode' function toggles the minor mode on and off. For
;; normal use, turn it on when `org-mode' is enabled.
;;
;; The `org-visibility-save' function saves the current buffer's file
;; visibility state if it has been modified or had an `org-cycle' change, and
;; matches the above Qualification Rules.
;;
;; The `org-visibility-force-save' function saves the current buffer's file
;; visibility state if it matches the above Qualification Rules, regardless of
;; whether the file has been modified.
;;
;; The `org-visibility-save-all-buffers' function saves the visibility state
;; for any modified buffer files that match the above Qualification Rules.
;;
;; The `org-visibility-load' function loads a file and restores its visibility
;; state if it matches the above Qualification Rules.
;;
;; The `org-visibility-remove' function removes a given file (or the current
;; buffer's file) from `org-visibility-state-file'.
;;
;; The `org-visibility-clean' function removes all missing or untracked files
;; from `org-visibility-state-file'.
;;; Code:
(require 'cl-lib)
(require 'outline)
(require 'org)
(require 'org-macs)
(defgroup org-visibility nil
"Persistent org tree visibility."
:group 'org
:prefix "org-visibility-")
(defcustom org-visibility-display-messages t
"Whether or not to display messages when visibility states are changed."
:type 'boolean
:group 'org-visibility)
(defcustom org-visibility-state-file
`,(expand-file-name ".org-visibility" user-emacs-directory)
"File used to store org visibility state."
:type 'string
:group 'org-visibility)
(defcustom org-visibility-include-paths '()
"List of directories and files that will persist visibility.
These directories and files will persist their visibility state
when saved and loaded."
:type '(repeat (choice string))
:group 'org-visibility)
(defcustom org-visibility-exclude-paths '()
"List of directories and files that will not persist visibility.
These directories and files will not persist their visibility
state.
Overrides `org-visibility-include-paths' and
`org-visibility-include-regexps'."
:type '(repeat (choice string))
:group 'org-visibility)
(defcustom org-visibility-include-regexps '()
"List of regular expressions that will persist visibility.
The directories and files that match these regular expressions
will persist their visibility state when saved and loaded."
:type '(repeat (choice regexp))
:group 'org-visibility)
(defcustom org-visibility-exclude-regexps '()
"List of regular expressions that will not persist visibility.
The directories and files that match these regular expressions
will not persist their visibility state.
Overrides `org-visibility-include-paths' and
`org-visibility-include-regexps'."
:type '(repeat (choice regexp))
:group 'org-visibility)
(defcustom org-visibility-maximum-tracked-files nil
"Maximum number of files to track the visibility state of.
When non-nil and persisting the state of a new org file causes
this number to be exceeded, the oldest tracked file will be
removed from the state file."
:type 'number
:group 'org-visibility)
(defcustom org-visibility-maximum-tracked-days nil
"Maximum number of days to track file visibility state.
When non-nil, file states in the state file that have not been
modified for this number of days will have their state
information removed."
:type 'number
:group 'org-visibility)
(defvar-local org-visibility
nil
"File local variable to determine visibility persistence.
If nil, this setting has no effect on determining buffer file
visibility state persistence.
If t, buffer file should have its visibility state persisted and
restored.
If 'never, buffer file should never have its visibility state
persisted and restored.
Overrides `org-visibility-include-paths',
`org-visibility-exclude-paths', `org-visibility-include-regexps',
and `org-visibility-exclude-regexps'.)")
(defvar-local org-visibility-dirty
nil
"Non-nil if buffer has been modified since last visibility save.")
(defun org-visibility-version (&optional insert)
"Display the version of Org Visibility that is running in this session.
If INSERT is non-nil, insert the Emacs version string at point
instead of displaying it."
(interactive)
(let ((version-string "Org Visibility <<version>>"))
(if insert
(insert version-string)
(if (called-interactively-p 'interactive)
(message "%s" version-string)
version-string))))
(defun org-visibility--timestamp ()
"Return timestamp in ISO 8601 format (YYYY-mm-ddTHH:MM:SSZ)."
(format-time-string "%FT%T%Z"))
(defun org-visibility--timestamp-to-epoch (timestamp)
"Return epoch (seconds since 1970-01-01) from TIMESTAMP."
(truncate (float-time (date-to-time timestamp))))
(defun org-visibility--buffer-checksum (&optional buffer)
"Return checksum for BUFFER."
(secure-hash 'md5 (or buffer (current-buffer))))
(defun org-visibility--remove-over-maximum-tracked-files (data)
"Remove oldest files over maximum file count from DATA.
Does nothing unless `org-visibility-maximum-tracked-files' is
non-nil and exceeded."
(when (and org-visibility-maximum-tracked-files
(cl-plusp org-visibility-maximum-tracked-files))
(while (> (length data) org-visibility-maximum-tracked-files)
(setq data (nreverse (cdr (nreverse data))))))
data)
(defun org-visibility--remove-over-maximum-tracked-days (data)
"Remove all files over maximum day count from DATA.
Does notthing unless `org-visibility-maximum-tracked-days' is
non-nil and exceeded."
(if (and org-visibility-maximum-tracked-days
(cl-plusp org-visibility-maximum-tracked-days))
(cl-do ((day (- (time-to-days (current-time)) org-visibility-maximum-tracked-days))
(d data (cdr d))
(n 0 (1+ n)))
((< (time-to-days (date-to-time (cadar d))) day)
(cl-subseq data 0 n)))
data))
(defun org-visibility--set (buffer visible)
"Set visibility state.
Set visibility state record for BUFFER to VISIBLE and update
`org-visibility-state-file' with new state."
(let ((print-length nil)
(data (and (file-exists-p org-visibility-state-file)
(ignore-errors
(with-temp-buffer
(insert-file-contents org-visibility-state-file)
(read (buffer-substring-no-properties (point-min) (point-max)))))))
(file-name (buffer-file-name buffer))
(date (org-visibility--timestamp))
(checksum (org-visibility--buffer-checksum buffer)))
(when file-name
(setq data (delq (assoc file-name data) data)) ; remove previous value
(setq data (append (list (list file-name date checksum visible)) data)) ; add new value
(setq data (org-visibility--remove-over-maximum-tracked-files data)) ; remove old files over maximum count
(setq data (org-visibility--remove-over-maximum-tracked-days data)) ; remove old files over maximum days
(with-temp-file org-visibility-state-file
(insert (format "%S\n" data)))
(when org-visibility-display-messages
(message "Set visibility state for %s" file-name)))))
(defun org-visibility--get (buffer)
"Get visibility state.
Return visibility state for BUFFER if found in
`org-visibility-state-file'."
(let ((data (and (file-exists-p org-visibility-state-file)
(ignore-errors
(with-temp-buffer
(insert-file-contents org-visibility-state-file)
(read (buffer-substring-no-properties (point-min) (point-max)))))))
(file-name (buffer-file-name buffer))
(checksum (org-visibility--buffer-checksum buffer)))
(when file-name
(let ((state (assoc file-name data)))
(when (string= (caddr state) checksum)
(cadddr state))))))
(defun org-visibility--save-internal (&optional buffer noerror force)
"Save visibility snapshot of org BUFFER.
If NOERROR is non-nil, do not throw errors.
If FORCE is non-nil, save even if file is not marked as dirty."
(let ((buffer (or buffer (current-buffer)))
(file-name (buffer-file-name buffer))
(visible '()))
(with-current-buffer buffer
(if (not (derived-mode-p 'org-mode))
(unless noerror
(user-error "Not an Org buffer"))
(if (not file-name)
(unless noerror
(user-error "No file associated with this buffer: %S" buffer))
(when (or force org-visibility-dirty)
(save-mark-and-excursion
(goto-char (point-min))
(while (not (eobp))
(when (not (invisible-p (point)))
(push (point) visible))
(forward-visible-line 1)))
(org-visibility--set buffer (nreverse visible))
(setq org-visibility-dirty nil)))))))
(defun org-visibility--load-internal (&optional buffer noerror)
"Load visibility snapshot of org BUFFER.
If NOERROR is non-nil, do not throw errors."
(let ((buffer (or buffer (current-buffer)))
(file-name (buffer-file-name buffer)))
(with-current-buffer buffer
(if (not (derived-mode-p 'org-mode))
(unless noerror
(user-error "Not an Org buffer"))
(if (not (buffer-file-name buffer))
(unless noerror
(user-error "No file associated with this buffer: %S" buffer))
(let ((visible (org-visibility--get buffer)))
(when visible
(save-mark-and-excursion
(outline-hide-sublevels 1)
(dolist (x visible)
(ignore-errors
(when (> x 1)
(goto-char x)
(when (invisible-p (1- (point)))
(org-flag-region (1- (point-at-bol)) (point-at-eol) nil 'outline))))))
(when org-visibility-display-messages
(message "Restored visibility state for %s" file-name)))
(setq org-visibility-dirty nil)))))))
(defun org-visibility--check-file-path (file-name paths)
"Return whether FILE-NAME is in one of the PATHS."
(let ((file-name (file-truename file-name)))
(cl-do ((paths paths (cdr paths))
(match nil))
((or (null paths) match) match)
(let ((path (car paths)))
(when (>= (length file-name) (length path))
(let ((part (substring file-name 0 (length path))))
(when (string= part path)
(setq match t))))))))
(defun org-visibility--check-file-regexp (file-name regexps)
"Return whether FILE-NAME is a match for one of the REGEXPS."
(let ((file-name (file-truename file-name)))
(cl-do ((regexps regexps (cdr regexps))
(match nil))
((or (null regexps) match) match)
(let ((regexp (car regexps)))
(when (string-match regexp file-name)
(setq match t))))))
(defun org-visibility--check-file-include-exclude-paths-and-regexps (file-name)
"Return whether FILE-NAME should have its visibility state persisted.
Return whether FILE-NAME is in one of the paths listed in
`org-visibility-include-paths' or matches a regular expression
listed in `org-visibility-include-regexps', and FILE-NAME is not
in one of the paths listed in `org-visibility-exclude-paths' or
matches a regular expression listed in
`org-visibility-exclude-regexps'."
(and (or (org-visibility--check-file-path file-name org-visibility-include-paths)
(org-visibility--check-file-regexp file-name org-visibility-include-regexps))
(not (or (org-visibility--check-file-path file-name org-visibility-exclude-paths)
(org-visibility--check-file-regexp file-name org-visibility-exclude-regexps)))))
(defun org-visibility--check-buffer-file-persistence (buffer)
"Return whether BUFFER should have its visibility state persisted.
Return whether BUFFER's file is in one of the paths listed in
`org-visibility-include-paths' or matches a regular expression
listed in `org-visibility-include-regexps', and BUFFER's file is
not in one of the paths listed in `org-visibility-exclude-paths'
or matches a regular expression listed in
`org-visibility-exclude-regexps'."
(with-current-buffer buffer
(cl-case (if (boundp 'org-visibility) org-visibility nil)
('nil (let ((file-name (buffer-file-name buffer)))
(if file-name
(org-visibility--check-file-include-exclude-paths-and-regexps file-name)
nil)))
('never nil)
(t t))))
;;;###autoload
(defun org-visibility-remove (&optional file-name)
"Remove visibility state of FILE-NAME or `current-buffer'."
(interactive)
(let ((print-length nil)
(file-name (or file-name (buffer-file-name (current-buffer)))))
(when file-name
(let ((data
(cl-remove-if
(lambda (x) (string-equal (car x) file-name))
(and (file-exists-p org-visibility-state-file)
(with-temp-buffer
(insert-file-contents org-visibility-state-file)
(read (buffer-substring-no-properties (point-min) (point-max))))))))
(with-temp-file org-visibility-state-file
(insert (format "%S\n" data)))
(when org-visibility-display-messages
(message "Removed visibility state of %s" file-name))))))
;;;###autoload
(defun org-visibility-clean ()
"Remove any missing files from `org-visibility-state-file'."
(interactive)
(let ((print-length nil)
(data
(cl-remove-if-not
(lambda (x)
(let ((file-name (car x)))
(and (file-exists-p file-name)
(org-visibility--check-file-include-exclude-paths-and-regexps file-name))))
(and (file-exists-p org-visibility-state-file)
(with-temp-buffer
(insert-file-contents org-visibility-state-file)
(read (buffer-substring-no-properties (point-min) (point-max))))))))
(with-temp-file org-visibility-state-file
(insert (format "%S\n" data)))
(when org-visibility-display-messages
(message "Visibility state file has been cleaned"))))
;;;###autoload
(defun org-visibility-save (&optional noerror force)
"Save visibility state if buffer has been modified.
If NOERROR is non-nil, do not throw errors.
If FORCE is non-nil, save even if file is not marked as dirty."
(interactive)
(when (org-visibility--check-buffer-file-persistence (current-buffer))
(org-visibility--save-internal (current-buffer) noerror force)))
(defun org-visibility-save-noerror ()
"Save visibility state if buffer has been modified, ignoring errors."
(org-visibility-save :noerror))
;;;###autoload
(defun org-visibility-force-save ()
"Save visibility state even if buffer has not been modified."
(interactive)
(org-visibility-save nil :force))
;;;###autoload
(defun org-visibility-save-all-buffers (&optional force)
"Save visibility state for any modified buffers, ignoring errors.
If FORCE is non-nil, save even if files are not marked as dirty."
(interactive)
(dolist (buffer (buffer-list))
(when (org-visibility--check-buffer-file-persistence buffer)
(org-visibility--save-internal buffer :noerror force))))
;;;###autoload
(defun org-visibility-load (&optional file)
"Load FILE or `current-buffer' and restore its visibility state, ignoring errors."
(interactive)
(let ((buffer (if file (get-file-buffer file) (current-buffer))))
(when (and buffer (org-visibility--check-buffer-file-persistence buffer))
(org-visibility--load-internal buffer :noerror))))
(defun org-visibility-dirty ()
"Set visibility dirty flag."
(when (and (eq major-mode 'org-mode)
(not org-visibility-dirty)
(org-visibility--check-buffer-file-persistence (current-buffer)))
(setq org-visibility-dirty t)))
(defun org-visibility-dirty-org-cycle (state)
"Set visibility dirty flag when `org-cycle' is called.
Unless STATE is 'INVALID-STATE."
;; dummy check to prevent compiler warning
(when (not (eq state 'INVALID-STATE))
(org-visibility-dirty)))
(defun org-visibility-enable-hooks ()
"Helper function to enable all `org-visibility' hooks."
(add-hook 'after-save-hook #'org-visibility-save-noerror :append)
(add-hook 'kill-buffer-hook #'org-visibility-save-noerror :append)
(add-hook 'kill-emacs-hook #'org-visibility-save-all-buffers :append)
(add-hook 'find-file-hook #'org-visibility-load :append)
(add-hook 'first-change-hook #'org-visibility-dirty :append)
(add-hook 'org-cycle-hook #'org-visibility-dirty-org-cycle :append))
(defun org-visibility-disable-hooks ()
"Helper function to disable all `org-visibility' hooks."
(remove-hook 'after-save-hook #'org-visibility-save-noerror)
(remove-hook 'kill-buffer-hook #'org-visibility-save-noerror)
(remove-hook 'kill-emacs-hook #'org-visibility-save-all-buffers)
(remove-hook 'find-file-hook #'org-visibility-load)
(remove-hook 'first-change-hook #'org-visibility-dirty)
(remove-hook 'org-cycle-hook #'org-visibility-dirty-org-cycle))
;;;###autoload
(define-minor-mode org-visibility-mode
"Minor mode for toggling `org-visibility' hooks on and off.
This minor mode will persist (save and load) the state of the
visible sections of `org-mode' files. The state is saved when the
file is saved or killed, and restored when the file is loaded.
\\{org-visibility-mode-map}"
:lighter " vis"
:keymap (make-sparse-keymap)
:global t
(if org-visibility-mode
(org-visibility-enable-hooks)
(org-visibility-disable-hooks)))
(provide 'org-visibility)
;;; org-visibility.el ends here
;;; org-visibility-test.el --- Test org-visibility.el -*- lexical-binding: t; no-byte-compile: t; -*-
;;
;;; <<copyright>>
;;
;; <<author>>
;; Created: 2021-07-17
;;
;; This file is not part of GNU Emacs.
;;
;; <<license-header>>
;;
;;; Commentary:
;;
;; Run all tests interactively:
;;
;; (ert-run-tests-interactively '(tag org-visibility))
;;
;; Run all tests in batch mode:
;;
;; (ert-run-tests-batch '(tag org-visibility))
;;; Code:
(require 'org)
(require 'org-visibility)
(require 'ert)
(defun org-visibility-test-run-test (test files)
"Setup test environment, run TEST using FILES, then restore environment."
(let ((org-startup-folded 'showeverything)
(org-odd-levels-only t)
(enable-local-variables :all)
(enable-local-eval t)
(state-file org-visibility-state-file)
(include-paths org-visibility-include-paths)
(exclude-paths org-visibility-exclude-paths)
(temp-state-file (make-temp-file "org-visibility-test-state-file-"))
(is-enabled org-visibility-mode)
errors)
(setq org-visibility-state-file temp-state-file
org-visibility-include-paths '()
org-visibility-exclude-paths '())
(org-visibility-mode 1)
(unwind-protect
(setq errors (remove nil (apply #'append (nreverse (funcall test)))))
(progn
(mapc
(lambda (x) (when (file-exists-p x) (delete-file x)))
files)
(delete-file temp-state-file)
(setq org-visibility-state-file state-file
org-visibility-include-paths include-paths
org-visibility-exclude-paths exclude-paths)
(if is-enabled
(org-visibility-mode 1)
(org-visibility-mode -1))))
(should (not errors))))
(defun org-visibility-test-create-org-file (&optional local-var-visbility)
"Create temporary `org-mode' file to test with.
If LOCAL-VAR-VISBILITY is non-nil, set local variable
`org-visibility' to LOCAL-VAR-VISBILITY."
(let ((file (make-temp-file "org-visibility-test-" nil ".org")))
(with-temp-file file
(insert "* Heading 1")
(newline)
(insert "*** Heading 1.2")
(newline)
(insert "Body text 1.2")
(newline)
(insert "And some more")
(newline)
(insert "* Heading 2")
(newline)
(insert "*** Heading 2.1")
(newline)
(insert "***** Heading 2.1.1")
(newline)
(insert "Body text 2.1.1")
(newline)
(insert "*** Heading 2.2")
(newline)
(insert "Body text 2.2")
(newline)
(insert "* Heading 3")
(newline)
(insert "Body text 3")
(newline)
(when local-var-visbility
(newline)
;; concat is used to prevent emacs from trying to set local variables on this file
(insert (concat ";; Local " "Variables:"))
(newline)
(insert (format ";; org-visibility: %s" local-var-visbility))
(newline)
(insert ";; End:")
(newline)))
file))
(defun org-visibility-test-cycle-outline ()
"Hide all sublevels then cycle Heading 2."
(outline-hide-sublevels 1)
(goto-char (point-min))
(forward-line 4)
(org-cycle))
(defun org-visibility-test-check-visible-lines (lines)
"Test that all LINES are visible, and no others, in current buffer.
Return list of errors, or nil, if none."
(let (errors)
(goto-char (point-min))
(while (not (eobp))
(let ((line (buffer-substring-no-properties (point-at-bol) (point-at-eol))))
(if (member (line-number-at-pos) lines)
(unless (not (invisible-p (point)))
(push (format "Line not visible: %s" line) errors))
(unless (invisible-p (point))
(push (format "Line visible: %s" line) errors)))
(forward-line 1)))
(nreverse errors)))
(defun org-visibility-test-check-mode (enabled)
"Test that `org-visibility-mode' is ENABLED.
Return error, if not ENABLED, otherwise nil."
(if (or (and org-visibility-mode enabled)
(and (not org-visibility-mode) (not enabled)))
nil
(list (format "Mode not: %s" enabled))))
(defun org-visibility-test-check-state-file-entries (count)
"Test that `org-visibility-state-file' has COUNT entries.
Return a list of one error, or nil, if correct."
(with-temp-buffer
(insert-file-contents org-visibility-state-file)
(let ((entries (condition-case nil (length (read (buffer-string))) ('error 0))))
(if (= entries count)
nil
(list (format "State file entry count: %s (expected %s)" entries count))))))
(defun org-visibility-test-decrease-state-file-timestamps (days)
"Subtract DAYS from all timestamps in `org-visibility-state-file'."
(let ((data (and (file-exists-p org-visibility-state-file)
(ignore-errors
(with-temp-buffer
(insert-file-contents org-visibility-state-file)
(read (buffer-substring-no-properties (point-min) (point-max))))))))
(when data
(dolist (x data)
(let ((ts (format-time-string
"%FT%T%Z"
(encode-time
(decoded-time-add (decode-time (date-to-time (cadr x)))
(make-decoded-time :day (- days)))))))
(setcdr x (cons ts (cddr x)))))
(with-temp-file org-visibility-state-file
(insert (format "%S\n" data))))))
(defun org-visibility-test-check-dirty-status (is-dirty)
"Test that `org-visibility-dirty' is IS-DIRTY.
Return a list of one error, or nil, if correct."
(if (eq org-visibility-dirty is-dirty)
nil
(list (format "Dirty flag: %s (expect %s)" org-visibility-dirty is-dirty))))
;;; Tests
(ert-deftest org-visibility-test-test-no-persistence ()
"Test no visibility persistence."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12)) errors)
(kill-buffer (current-buffer))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-local-var-nil ()
"Test no visibility persistence using local var `org-visibility' set to nil."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file "nil"))
errors)
(org-visibility-test-run-test
(lambda ()
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-local-var-never ()
"Test no visibility persistence using local var `org-visibility' set to never."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file "never"))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-persistence-with-local-var-t ()
"Test visibility persistence using local var `org-visibility' set to t."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file "t"))
errors)
(org-visibility-test-run-test
(lambda ()
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
errors)
(list file))))
(ert-deftest org-visibility-test-test-persistence-with-include-paths ()
"Test visibility persistence using include paths."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-persistence-with-include-regexps ()
"Test visibility persistence using include regular expressions."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-regexps (list "\\.org\\'")))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-include-exclude-paths ()
"Test no visibility persistence using include and exclude paths."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list (file-name-directory file)))
(org-visibility-exclude-paths (list file)))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-include-exclude-regexps ()
"Test no visibility persistence using include and exclude regular expressions."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-regexps (list "\\.org\\'"))
(org-visibility-exclude-regexps (list "\\.org\\'")))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-fundamental-mode-and-local-var-t ()
"Test no visibility persistence using `fundamental-mode' and local var `org-visibility' set to t."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file "t"))
errors)
(org-visibility-test-run-test
(lambda ()
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(fundamental-mode)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-fundamental-mode-and-include-paths ()
"Test no visibility persistence using `fundamental-mode' and include paths."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(fundamental-mode)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-fundamental-mode-and-include-regexps ()
"Test no visibility persistence using `fundamental-mode' and include regular expressions."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-regexps (list "\\.org\\'")))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(fundamental-mode)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-no-persistence-with-mode-disabled ()
"Test no visibility persistence with mode disabled."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(org-visibility-mode -1)
(find-file file)
(org-visibility-test-check-mode nil)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file)
(org-visibility-test-check-mode nil)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-persistence-with-mode-enabled ()
"Test visibility persistence with mode enabled."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(org-visibility-mode -1)
(find-file file)
(org-visibility-test-check-mode nil)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(org-visibility-mode 1)
(find-file file)
(org-visibility-test-check-mode nil)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(org-visibility-test-check-mode t)
(kill-buffer (current-buffer))
(find-file file)
(org-visibility-test-check-mode t)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-persistence-with-startup-keyword-settings ()
"Test visibility persistence when STARTUP keyword settings are present."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(goto-char (point-min))
(insert "#+STARTUP: overview\n\n")
(push (org-visibility-test-check-visible-lines '(1 2 3 7 8 11 13)) errors)
(save-buffer)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 2 3 7 8 11 13)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-maximum-tracked-files ()
"Test visibility persistence expires when `org-visibility-maximum-tracked-files' is exceeded."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-maximum-tracked-files 1)
(org-visibility-include-paths (list file1 file2)))
(find-file file1)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file2)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file2)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file1)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file1 file2))))
(ert-deftest org-visibility-test-test-maximum-tracked-files-2 ()
"Test visibility persistence expires when `org-visibility-maximum-tracked-files' is exceeded."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
(file3 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-maximum-tracked-files 2)
(org-visibility-include-paths (list file1 file2 file3)))
(find-file file1)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file2)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(find-file file3)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(find-file file3)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file2)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(org-visibility-force-save)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(find-file file1)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(find-file file1)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file2)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file3)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file1 file2 file3))))
(ert-deftest org-visibility-test-test-maximum-tracked-days ()
"Test visibility persistence expires when `org-visibility-maximum-tracked-days' is exceeded."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-maximum-tracked-days 3)
(org-visibility-include-paths (list file1 file2)))
(find-file file1)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(org-visibility-test-decrease-state-file-timestamps 2)
(find-file file2)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(org-visibility-test-decrease-state-file-timestamps 2)
(find-file file2)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(org-visibility-force-save)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file1)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file1 file2))))
(ert-deftest org-visibility-test-test-maximum-tracked-days-2 ()
"Test visibility persistence expires when `org-visibility-maximum-tracked-days' is exceeded."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
(file3 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-maximum-tracked-days 5)
(org-visibility-include-paths (list file1 file2 file3)))
(find-file file1)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(org-visibility-test-decrease-state-file-timestamps 2)
(find-file file2)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(org-visibility-test-decrease-state-file-timestamps 2)
(find-file file3)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 3) errors)
(org-visibility-test-decrease-state-file-timestamps 2)
(find-file file3)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file2)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(org-visibility-force-save)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(find-file file1)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 3) errors)
(org-visibility-test-decrease-state-file-timestamps 4)
(find-file file1)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(org-visibility-force-save)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(find-file file2)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file3)
(push (org-visibility-test-check-visible-lines '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18)) errors)
(kill-buffer (current-buffer)))
errors)
(list file1 file2 file3))))
(ert-deftest org-visibility-test-test-remove ()
"Test `org-visibility-remove'."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file1 file2)))
(find-file file1)
(org-visibility-test-cycle-outline)
(kill-buffer (current-buffer))
(find-file file2)
(org-visibility-test-cycle-outline)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(org-visibility-remove file1)
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file2)
(org-visibility-remove)
(push (org-visibility-test-check-state-file-entries 0) errors))
errors)
(list file1 file2))))
(ert-deftest org-visibility-test-test-clean-remove-file ()
"Test `org-visibility-clean'."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file1 file2)))
(find-file file1)
(org-visibility-test-cycle-outline)
(kill-buffer (current-buffer))
(find-file file2)
(org-visibility-test-cycle-outline)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(delete-file file1)
(org-visibility-clean)
(push (org-visibility-test-check-state-file-entries 1) errors)
(delete-file file2))
errors)
(list file1 file2))))
(ert-deftest org-visibility-test-test-clean-remove-include-path ()
"Test `org-visibility-clean'."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file1 file2)))
(find-file file1)
(org-visibility-test-cycle-outline)
(kill-buffer (current-buffer))
(find-file file2)
(org-visibility-test-cycle-outline)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 2) errors)
(setq org-visibility-include-paths (list file1))
(org-visibility-clean)
(push (org-visibility-test-check-state-file-entries 1) errors))
errors)
(list file1 file2))))
(ert-deftest org-visibility-test-test-force-save ()
"Test `org-visibility-force-save'."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file1 file2)))
(find-file file1)
(org-visibility-test-cycle-outline)
(kill-buffer (current-buffer))
(push (org-visibility-test-check-state-file-entries 1) errors)
(find-file file2)
(org-visibility-force-save)
(push (org-visibility-test-check-state-file-entries 2) errors)
(kill-buffer (current-buffer)))
errors)
(list file1 file2))))
(ert-deftest org-visibility-test-test-save-all-buffers ()
"Test `org-visibility-save-all-buffers'."
:tags '(org-visibility)
(let ((file1 (org-visibility-test-create-org-file))
(file2 (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file1 file2)))
(find-file file1)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(find-file file2)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(org-visibility-save-all-buffers)
(push (org-visibility-test-check-state-file-entries 2) errors)
(find-file file1)
(kill-buffer (current-buffer))
(find-file file2)
(kill-buffer (current-buffer))
(find-file file1)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer))
(find-file file2)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer)))
errors)
(list file1 file2))))
(ert-deftest org-visibility-test-test-save-error-with-fundamental-mode-and-include-paths ()
"Test save error thrown using `fundamental-mode' and include paths"
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file))
(error '("Expected `org-visibility-save' to throw an error")))
(find-file file)
(org-visibility-test-cycle-outline)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(fundamental-mode)
(condition-case err
(org-visibility-force-save)
('error
(setq error nil)))
(kill-buffer (current-buffer))
(if error
(nreverse (push error errors))
errors)))
(list file))))
(ert-deftest org-visibility-test-test-dirty ()
"Test file dirty state from change."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(find-file file)
(push (org-visibility-test-check-dirty-status nil) errors)
(goto-char (point-at-eol))
(push (org-visibility-test-check-dirty-status nil) errors)
(insert "A")
(push (org-visibility-test-check-dirty-status t) errors)
(save-buffer)
(kill-buffer (current-buffer)))
errors)
(list file))))
(ert-deftest org-visibility-test-test-dirty-org-cycle ()
"Test file dirty state from `org-cycle'."
:tags '(org-visibility)
(let ((file (org-visibility-test-create-org-file))
errors)
(org-visibility-test-run-test
(lambda ()
(let ((org-visibility-include-paths (list file)))
(find-file file)
(push (org-visibility-test-check-dirty-status nil) errors)
(outline-hide-sublevels 1)
(push (org-visibility-test-check-dirty-status nil) errors)
(forward-line 4)
(push (org-visibility-test-check-dirty-status nil) errors)
(org-cycle)
(push (org-visibility-test-check-dirty-status t) errors)
(save-buffer)
(kill-buffer (current-buffer))
(find-file file)
(push (org-visibility-test-check-visible-lines '(1 5 6 9 11)) errors)
(kill-buffer (current-buffer)))
errors)
(list file))))
;;; org-visibility-test.el ends here
.RECIPEPREFIX = >
.PHONY: all clean test
EMACS = emacs
ELCFILES = $(addsuffix .elc, $(basename $(wildcard *.el)))
all: $(ELCFILES)
%.elc: %.el
> @echo Compiling $<
> @${EMACS} -batch -q -no-site-file -L . -f batch-byte-compile $<
clean:
> @rm -f *.elc
test: all
> @${EMACS} -batch -L . -l *-test.el -f ert-run-tests-batch-and-exit
Run make clean, all, and tests.
make clean all test 2>&1 | tail -n 2 | head -n 1
.gitignore
Makefile
*-test.el
*.elc
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public License is intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users. This General Public License applies to
most of the Free Software Foundation's software and to any other program whose
authors commit to using it. (Some other Free Software Foundation software is
covered by the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom to
distribute copies of free software (and charge for this service if you wish),
that you receive source code or can get it if you want it, that you can change
the software or use pieces of it in new free programs; and that you know you
can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These
restrictions translate to certain responsibilities for you if you distribute
copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for
a fee, you must give the recipients all the rights that you have. You must
make sure that they, too, receive or can get the source code. And you must
show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2)
offer you this license which gives you legal permission to copy, distribute
and/or modify the software.
Also, for each author's protection and ours, we want to make certain that
everyone understands that there is no warranty for this free software. If the
software is modified by someone else and passed on, we want its recipients to
know that what they have is not the original, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that redistributors of a free program will
individually obtain patent licenses, in effect making the program proprietary.
To prevent this, we have made it clear that any patent must be licensed for
everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification
follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice
placed by the copyright holder saying it may be distributed under the terms of
this General Public License. The "Program", below, refers to any such program
or work, and a "work based on the Program" means either the Program or any
derivative work under copyright law: that is to say, a work containing the
Program or a portion of it, either verbatim or with modifications and/or
translated into another language. (Hereinafter, translation is included
without limitation in the term "modification".) Each licensee is addressed as
"you".
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running the Program is
not restricted, and the output from the Program is covered only if its
contents constitute a work based on the Program (independent of having been
made by running the Program). Whether that is true depends on what the Program
does.
1. You may copy and distribute verbatim copies of the Program's source code as
you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and
disclaimer of warranty; keep intact all the notices that refer to this License
and to the absence of any warranty; and give any other recipients of the
Program a copy of this License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may
at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it,
thus forming a work based on the Program, and copy and distribute such
modifications or work under the terms of Section 1 above, provided that you
also meet all of these conditions:
a) You must cause the modified files to carry prominent notices stating
that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole
or in part contains or is derived from the Program or any part thereof, to
be licensed as a whole at no charge to all third parties under the terms
of this License.
c) If the modified program normally reads commands interactively when run,
you must cause it, when started running for such interactive use in the
most ordinary way, to print or display an announcement including an
appropriate copyright notice and a notice that there is no warranty (or
else, saying that you provide a warranty) and that users may redistribute
the program under these conditions, and telling the user how to view a
copy of this License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on the
Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Program, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Program, the distribution of the whole must be on
the terms of this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise the
right to control the distribution of derivative or collective works based on
the Program.
In addition, mere aggregation of another work not based on the Program with
the Program (or with a work based on the Program) on a volume of a storage or
distribution medium does not bring the other work under the scope of this
License.
3. You may copy and distribute the Program (or a work based on it, under
Section 2) in object code or executable form under the terms of Sections 1 and
2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source
code, which must be distributed under the terms of Sections 1 and 2 above
on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to
give any third party, for a charge no more than your cost of physically
performing source distribution, a complete machine-readable copy of the
corresponding source code, to be distributed under the terms of Sections 1
and 2 above on a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to
distribute corresponding source code. (This alternative is allowed only
for noncommercial distribution and only if you received the program in
object code or executable form with such an offer, in accord with
Subsection b above.)
The source code for a work means the preferred form of the work for making
modifications to it. For an executable work, complete source code means all
the source code for all modules it contains, plus any associated interface
definition files, plus the scripts used to control compilation and
installation of the executable. However, as a special exception, the source
code distributed need not include anything that is normally distributed (in
either source or binary form) with the major components (compiler, kernel, and
so on) of the operating system on which the executable runs, unless that
component itself accompanies the executable.
If distribution of executable or object code is made by offering access to
copy from a designated place, then offering equivalent access to copy the
source code from the same place counts as distribution of the source code,
even though third parties are not compelled to copy the source along with the
object code.
4. You may not copy, modify, sublicense, or distribute the Program except as
expressly provided under this License. Any attempt otherwise to copy, modify,
sublicense or distribute the Program is void, and will automatically terminate
your rights under this License. However, parties who have received copies, or
rights, from you under this License will not have their licenses terminated so
long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed it.
However, nothing else grants you permission to modify or distribute the
Program or its derivative works. These actions are prohibited by law if you do
not accept this License. Therefore, by modifying or distributing the Program
(or any work based on the Program), you indicate your acceptance of this
License to do so, and all its terms and conditions for copying, distributing
or modifying the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program),
the recipient automatically receives a license from the original licensor to
copy, distribute or modify the Program subject to these terms and conditions.
You may not impose any further restrictions on the recipients' exercise of the
rights granted herein. You are not responsible for enforcing compliance by
third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or otherwise)
that contradict the conditions of this License, they do not excuse you from
the conditions of this License. If you cannot distribute so as to satisfy
simultaneously your obligations under this License and any other pertinent
obligations, then as a consequence you may not distribute the Program at all.
For example, if a patent license would not permit royalty-free redistribution
of the Program by all those who receive copies directly or indirectly through
you, then the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply and
the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or
other property right claims or to contest validity of any such claims; this
section has the sole purpose of protecting the integrity of the free software
distribution system, which is implemented by public license practices. Many
people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose that
choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain
countries either by patents or by copyrighted interfaces, the original
copyright holder who places the Program under this License may add an explicit
geographical distribution limitation excluding those countries, so that
distribution is permitted only in or among countries not thus excluded. In
such case, this License incorporates the limitation as if written in the body
of this License.
9. The Free Software Foundation may publish revised and/or new versions of the
General Public License from time to time. Such new versions will be similar in
spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any later
version", you have the option of following the terms and conditions either of
that version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of this License,
you may choose any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs
whose distribution conditions are different, write to the author to ask for
permission. For software which is copyrighted by the Free Software Foundation,
write to the Free Software Foundation; we sometimes make exceptions for this.
Our decision will be guided by the two goals of preserving the free status of
all derivatives of our free software and of promoting the sharing and reuse of
software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE
PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR
DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR
A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively convey the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
one line to give the program's name and an idea of what it does.
Copyright (C) yyyy name of author
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 2 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, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when it
starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software,
and you are welcome to redistribute it under certain conditions; type `show c'
for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may be
called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General Public
License instead of this License.