diff --git a/lisp/org-appear.el b/lisp/org-appear.el new file mode 100644 index 00000000..39e67964 --- /dev/null +++ b/lisp/org-appear.el @@ -0,0 +1,322 @@ +;;; org-appear.el --- Auto-toggle Org elements -*- lexical-binding: t; -*- + +;; Portions of code in this file are taken from org-fragtog https://github.com/io12/org-fragtog +;; org-fragtog Copyright (C) 2020 Benjamin Levy - MIT/X11 License +;; org-appear Copyright (C) 2021 Alice Istleyeva - MIT License +;; Author: Alice Istleyeva +;; Version: 0.2.4 +;; Package-Version: 20211202.604 +;; Package-Commit: a4d10fc346ba14f487eb7aa95761b9295089ba55 +;; Description: Toggle Org mode element visibility upon entering and leaving +;; Homepage: https://github.com/awth13/org-appear +;; Package-Requires: ((emacs "25.1") (org "9.3")) + +;; Permission is hereby granted, free of charge, to any person obtaining a copy +;; of this software and associated documentation files (the "Software"), to deal +;; in the Software without restriction, including without limitation the rights +;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +;; copies of the Software, and to permit persons to whom the Software is +;; furnished to do so, subject to the following conditions: +;; +;; The above copyright notice and this permission notice shall be included in all +;; copies or substantial portions of the Software. +;; +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +;; SOFTWARE. + +;;; Commentary: + +;; This package enables automatic visibility toggling of various Org elements depending on cursor position. +;; It supports automatic toggling of emphasis markers, links, subscripts and +;; superscripts, entities, and keywords. By default, toggling is instantaneous +;; and only affects emphasis markers. If Org mode custom variables that control +;; visibility of elements are configured to show hidden parts, the respective +;; `org-appear' settings do not have an effect. + +;;; Code: + +(require 'org) +(require 'org-element) +(require 'subr-x) ; Compatibility + +(defgroup org-appear nil + "Auto-toggle Org elements." + :group 'org) + +(defcustom org-appear-autoemphasis t + "Non-nil enables automatic toggling of emphasised and verbatim markers. +Does not have an effect if `org-hide-emphasis-markers' is nil." + :type 'boolean + :group 'org-appear) + +(defcustom org-appear-autosubmarkers nil + "Non-nil enables automatic toggling of subscript and superscript markers. +Does not have an effect if `org-pretty-entities' is nil." + :type 'boolean + :group 'org-appear) + +(defcustom org-appear-autoentities nil + "Non-nil enables automatic toggling of org entities. +Does not have an effect if `org-pretty-entities' is nil." + :type 'boolean + :group 'org-appear) + +(defcustom org-appear-autolinks nil + "Non-nil enables automatic toggling of links. +Does not have an effect if `org-link-descriptive' is nil." + :type 'boolean + :group 'org-appear) + +(defcustom org-appear-autokeywords nil + "Non-nil enables automatic toggling of keywords. +Does not have an effect if `org-hidden-keywords' is nil." + :type 'boolean + :group 'org-appear) + +(defcustom org-appear-delay 0.0 + "Seconds of delay before toggling an element." + :type 'number + :group 'org-appear) + +(defvar-local org-appear--timer nil + "Current active timer.") + +;;;###autoload +(define-minor-mode org-appear-mode + "A minor mode that automatically toggles elements in Org mode." + :init-value nil + :lighter nil + :keymap nil + + (cond + (org-appear-mode + (org-appear--set-elements) + (add-hook 'post-command-hook #'org-appear--post-cmd nil t) + (add-hook 'pre-command-hook #'org-appear--pre-cmd nil t)) + (t + ;; Clean up current element when disabling the mode + (when-let ((current-elem (org-appear--current-elem))) + (org-appear--hide-invisible current-elem) + (when org-appear--timer + (cancel-timer org-appear--timer) + (setq org-appear--timer nil))) + (remove-hook 'post-command-hook #'org-appear--post-cmd t) + (remove-hook 'pre-command-hook #'org-appear--pre-cmd t)))) + +(defvar org-appear-elements nil + "List of Org elements to toggle.") + +(defvar-local org-appear--prev-elem nil + "Previous element that surrounded the cursor. +nil if the cursor was not on an element.") + +(defun org-appear--set-elements () + "Add elements to toggle to `org-appear-elements'." + (let ((emphasis-elements '(bold + italic + underline + strike-through + verbatim + code)) + (script-elements '(subscript + superscript)) + (entity-elements '(entity)) + (link-elements '(link)) + (keyword-elements '(keyword))) + + ;; HACK: is there a better way to do this? + (setq-local org-appear--prev-elem nil) + (setq org-appear-elements nil) ; reset + (when (and org-hide-emphasis-markers org-appear-autoemphasis) + (setq org-appear-elements (append org-appear-elements emphasis-elements))) + (when (and org-pretty-entities org-appear-autosubmarkers) + (setq org-appear-elements (append org-appear-elements script-elements))) + (when (and org-pretty-entities org-appear-autoentities) + (setq org-appear-elements (append org-appear-elements entity-elements))) + (when (and org-link-descriptive org-appear-autolinks) + (setq org-appear-elements (append org-appear-elements link-elements))) + (when (and org-hidden-keywords org-appear-autokeywords) + (setq org-appear-elements (append org-appear-elements keyword-elements))))) + +(defun org-appear--post-cmd () + "This function is executed by `post-command-hook' in `org-appear-mode'. +It handles toggling elements depending on whether the cursor entered or exited them." + (let* ((prev-elem org-appear--prev-elem) + (prev-elem-start (org-element-property :begin prev-elem)) + (current-elem (org-appear--current-elem)) + (current-elem-start (org-element-property :begin current-elem))) + + ;; After leaving an element + (when (and prev-elem + (not (equal prev-elem-start current-elem-start))) + + ;; If timer for prev-elem fired and was expired + (if (not org-appear--timer) + (save-excursion + (goto-char prev-elem-start) + ;; Reevaluate `org-element-context' in case the bounds + ;; of the previous element changed + (org-appear--hide-invisible (org-element-context))) + (cancel-timer org-appear--timer) + (setq org-appear--timer nil))) + + ;; Inside an element + (when current-elem + + ;; New element, delay first unhiding + (when (and (> org-appear-delay 0) + (not (eq prev-elem-start current-elem-start))) + (setq org-appear--timer (run-with-idle-timer org-appear-delay + nil + #'org-appear--show-with-lock + current-elem + t))) + + ;; Not a new element + (when (not org-appear--timer) + (org-appear--show-with-lock current-elem))) + + ;; Remember current element as the last visited element + (setq org-appear--prev-elem current-elem))) + +(defun org-appear--pre-cmd () + "This function is executed by `pre-command-hook' in `org-appear-mode'. +It hides elements before commands that modify the buffer based on column width." + (when (memq this-command '(org-fill-paragraph + org-ctrl-c-ctrl-c)) + (when-let ((current-elem (org-appear--current-elem))) + (org-appear--hide-invisible current-elem)))) + +(defun org-appear--current-elem () + "Return element at point. +Return nil if element is not supported by `org-appear-mode'." + (when-let ((elem (org-element-context))) + (let* ((elem-type (car elem)) + (elem-end (- (org-element-property :end elem) + (1- (org-element-property :post-blank elem)))) + (link-ignore-p (and (eq elem-type 'link) + (or (string-match-p "[Cc]ite" + (org-element-property :type elem)) + (eq 'plain + (org-element-property :format elem))))) + (key-ignore-p (and (eq elem-type 'keyword) + (not (memq (intern (downcase + (org-element-property :key elem))) + org-hidden-keywords)))) + (script-ignore-p (and (or (eq elem-type 'subscript) + (eq elem-type 'superscript)) + (not (org-element-property :use-brackets-p elem)) + (not (eq org-use-sub-superscripts t))))) + (if (and (memq elem-type org-appear-elements) + (< (point) elem-end) ; Ignore post-element whitespace + (not link-ignore-p) ; Ignore plain and org-ref links + (not key-ignore-p) ; Ignore unhidden keywords + (not script-ignore-p)) ; Ignore sub/supercripts ignored by Org + elem + nil)))) + +(defun org-appear--parse-elem (elem) + "Return bounds of element ELEM. +Return nil if element cannot be parsed." + (let* ((elem-type (car elem)) + (elem-tag (cond ((memq elem-type '(bold + italic + underline + strike-through + verbatim + code)) + 'emph) + ((memq elem-type '(subscript + superscript)) + 'script) + ((eq elem-type 'entity) + 'entity) + ((eq elem-type 'link) + 'link) + ((eq elem-type 'keyword) + 'keyword) + (t nil))) + (elem-start (org-element-property :begin elem)) + (elem-end (org-element-property :end elem)) + (elem-content-start (org-element-property :contents-begin elem)) + (elem-content-end (org-element-property :contents-end elem)) + ;; Some elements have extra spaces at the end + ;; The number of spaces is stored in the post-blank property + (post-elem-spaces (org-element-property :post-blank elem)) + (elem-end-real (- elem-end post-elem-spaces))) + ;; Only sub/superscript elements are guaranteed to have + ;; contents-begin and contents-end properties + (when elem-tag + `(:start ,elem-start + :end ,elem-end-real + :visible-start ,(pcase elem-tag + ('emph (1+ elem-start)) + ('script elem-content-start) + ('link (or elem-content-start (+ elem-start 2)))) + :visible-end ,(pcase elem-tag + ('emph (1- elem-end-real)) + ('script elem-content-end) + ('link (or elem-content-end (- elem-end-real 2)))))))) + +(defun org-appear--show-invisible (elem) + "Silently remove invisible property from invisible parts of element ELEM." + (let* ((elem-at-point (org-appear--parse-elem elem)) + (elem-type (car elem)) + (start (plist-get elem-at-point :start)) + (end (plist-get elem-at-point :end)) + (visible-start (plist-get elem-at-point :visible-start)) + (visible-end (plist-get elem-at-point :visible-end))) + (with-silent-modifications + (cond ((eq elem-type 'entity) + (decompose-region start end)) + ((eq elem-type 'keyword) + (remove-text-properties start end '(invisible org-link))) + (t + (remove-text-properties start visible-start '(invisible org-link)) + (remove-text-properties visible-end end '(invisible org-link))))))) + +(defun org-appear--show-with-lock (elem &optional renew) + "Show invisible parts of element ELEM. +When RENEW is non-nil, obtain element at point instead." + ;; When called with timer, element might be different upon arrival + (when renew + (setq elem (org-appear--current-elem)) + (setq org-appear--prev-elem elem) + (setq org-appear--timer nil)) + + (when-let ((elem-start (org-element-property :begin elem)) + (elem-end (org-element-property :end elem))) + ;; Call `font-lock-ensure' before unhiding to prevent `jit-lock-mode' + ;; from refontifying the element region after changes in buffer + (font-lock-ensure elem-start elem-end) + (org-appear--show-invisible elem))) + +(defun org-appear--hide-invisible (elem) + "Silently add invisible property to invisible parts of element ELEM." + (let* ((elem-at-point (org-appear--parse-elem elem)) + (elem-type (car elem)) + (start (plist-get elem-at-point :start)) + (end (plist-get elem-at-point :end)) + (visible-start (plist-get elem-at-point :visible-start)) + (visible-end (plist-get elem-at-point :visible-end))) + (when elem-at-point + (with-silent-modifications + (cond ((eq elem-type 'entity) + (compose-region start end (org-element-property :utf-8 elem))) + ((eq elem-type 'keyword) + (font-lock-flush start end)) + (t + (put-text-property start visible-start 'invisible 'org-link) + (put-text-property visible-end end 'invisible 'org-link)))) + ;; (font-lock-flush start end) + ;; Call `font-lock-ensure' after flushing to prevent `jit-lock-mode' + ;; from refontifying the next element entered + (font-lock-ensure start end)))) + +(provide 'org-appear) +;;; org-appear.el ends here diff --git a/lisp/versions b/lisp/versions index 021cc46f..3ef4391a 100644 --- a/lisp/versions +++ b/lisp/versions @@ -62,8 +62,9 @@ | multiple-cursors | [[https://melpa.org/#/multiple-cursors][melpa]] | 1.4.0 | 20211112.2223 | 1.4.0 | 20201215.1559 | | | ob-async.el | [[https://melpa.org/#/ob-async][melpa]] | 0.1 | 20210428.2052 | 0.1 | 20190916.1537 | | | org | [[https://elpa.gnu.org/packages/org.html][elpa]] | 9.5.2 | - | 9.4.4 | - | | -| org-contrib | [[https://elpa.nongnu.org/nongnu/org-contrib.html][elpa]] | 0.3 | - | | | | +| org-appear.el | [[https://melpa.org/#/org-appear][melpa]] | 0.2.4 | 20211202.604 | | | | | org-brain.el | [[https://melpa.org/#/org-brain][melpa]] | 0.94 | 20210706.1519 | 0.94 | 20201214.822 | | +| org-contrib | [[https://elpa.nongnu.org/nongnu/org-contrib.html][elpa]] | 0.3 | - | | | | | org-cliplink | [[https://melpa.org/#/org-cliplink][melpa]] | 0.2 | 20201126.1020 | | 20190608.2134 | | | org-drill | [[https://melpa.org/#/org-drill][melpa]] | 2.7.0 | 20210427.2003 | 2.7.0 | 20200412.1812 | (alternatives anki-mode, anki-editor) | | org-fancy-priorities.el | [[https://melpa.org/#/org-fancy-priorities][melpa]] | 1.1 | 20210830.1657 | 1.1 | 20180328.2331 | | diff --git a/settings/general-settings.el b/settings/general-settings.el index 07bb7016..43c1beca 100644 --- a/settings/general-settings.el +++ b/settings/general-settings.el @@ -756,7 +756,9 @@ Version 2016-07-13" )) (easy-menu-add my-org-export-menu nil) (easy-menu-add-item nil '("My") my-org-export-menu) - (easy-menu-add-item nil '("My") '["Org Link Description Update" my-org-link-description-update :visible (eq major-mode 'org-mode)]) + (easy-menu-add-item nil '("My") '["org-toggle-pretty-entities" org-toggle-pretty-entities :visible (eq major-mode 'org-mode)]) + (easy-menu-add-item nil '("My") '["org-toggle-link-display" org-toggle-link-display :visible (eq major-mode 'org-mode)]) + (easy-menu-add-item nil '("My") '["Org Link Description Update" my-org-link-description-update :visible (eq major-mode 'org-mode) :help "my-org-link-description-update"]) ) (with-eval-after-load 'notmuch diff --git a/settings/org-settings.el b/settings/org-settings.el index 32667714..3ed68f33 100644 --- a/settings/org-settings.el +++ b/settings/org-settings.el @@ -242,9 +242,13 @@ Example defines :init (setq org-export-backends '(ascii html icalendar latex md odt reveal tufte)) :config + (setq org-hidden-keywords '(author date title email subtitle)) ;; see also `org-appear' + (setq org-hide-emphasis-markers t) ;; see also `org-appear' (setq org-startup-folded nil) ;; nil = showall = #+STARTUP: showall (setq org-startup-with-inline-images t) ;; #+STARTUP: inlineimages|noinlineimages, show inline images when loading a new Org file (setq org-use-sub-superscripts '{}) ;; braces are *required* in order to trigger interpretations as sub/superscript, see also `org-export-with-sub-superscripts'. C-c C-x \ (`org-toggle-pretty-entities') + (setq org-pretty-entities t) ;; see also `org-appear' and [C-c C-x \] (`org-toggle-pretty-entities') + (setq org-pretty-entities-include-sub-superscripts t) ;; if `org-pretty-entities' is active include also sub-superscripts. (setq org-image-actual-width '(600)) ;; image width displayed in org (setq org-tag-persistent-alist '(("ignore" . ?i) ("noexport" . ?n))) ;; todo see also org-todo-keyword-faces and org-superstar-todo-bullet-alist @@ -293,6 +297,17 @@ Example defines (add-to-list 'image-file-name-extensions "eps" t)) ;; TODO: eps not needed? +;; Make invisible parts of Org elements appear visible. +(use-package org-appear + :after (org) + :hook (org-mode . org-appear-mode) + :config + (setq org-appear-autoentities t) ;; toogle entities: \alpha -> ɑ, needs `org-pretty-entries' active + (setq org-appear-autoemphasis t) ;; toggle emphasis markers, t by default, needs `org-hide-emphasis-markers' active + (setq org-appear-autolinks t) ;; toogle links, needs `org-link-descriptive' active + (setq org-appear-autosubmarkers t) ;; toogle subscripts and superscripts: ^3 -> ³, needs `org-pretty-entries' active + (setq org-appear-autokeywords t)) ;; toogle keywords: #+TITLE: foo -> foo, needs `org-hidden-keywords' active + (use-package org-contrib :load-path (lambda () (list (concat config-dir "lisp/org-contrib"))))