;;; org-fragtog.el --- Auto-toggle Org LaTeX fragments -*- lexical-binding: t; -*- ;; Copyright (C) 2020 Benjamin Levy - MIT/X11 License ;; Author: Benjamin Levy ;; Version: 0.4.0 ;; Package-Version: 20220106.758 ;; Package-Commit: 5b346068c346c4164f5e48e81d1e1bb285da8fd5 ;; Description: Automatically toggle Org mode LaTeX fragment previews as the cursor enters and exits them ;; Homepage: https://github.com/io12/org-fragtog ;; Package-Requires: ((emacs "27.1")) ;; 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 automates toggling Org mode LaTeX fragment ;; previews. Fragment previews are disabled for editing when ;; your cursor steps onto them, and re-enabled when the cursor ;; leaves. ;;; Code: (require 'org) (defgroup org-fragtog nil "Auto-toggle Org LaTeX fragments" :group 'org-latex) (defcustom org-fragtog-ignore-predicates nil "List of predicates to determine whether to ignore a fragment. For example, adding `org-at-table-p' will ignore fragments inside tables." :group 'org-fragtog :type 'hook :options '(org-at-table-p org-at-table.el-p org-at-block-p org-at-heading-p)) (defcustom org-fragtog-preview-delay 0.0 "Seconds of delay before LaTeX preview." :group 'org-fragtog :type 'number) ;;;###autoload (define-minor-mode org-fragtog-mode "A minor mode that automatically toggles Org mode LaTeX fragment previews. Fragment previews are disabled for editing when your cursor steps onto them, and re-enabled when the cursor leaves." nil nil nil ;; Fix nil error in `org-element-context' ;; when using `org-fragtog' without Org mode. (setq org-complex-heading-regexp (or org-complex-heading-regexp "")) (if org-fragtog-mode (add-hook 'post-command-hook #'org-fragtog--post-cmd nil t) (remove-hook 'post-command-hook #'org-fragtog--post-cmd t))) (defvar-local org-fragtog--prev-frag nil "Previous fragment that surrounded the cursor, or nil if the cursor was not on a fragment. This is used to track when the cursor leaves a fragment.") (defvar-local org-fragtog--prev-point nil "Value of point from before the most recent command.") (defvar-local org-fragtog--timer nil "Current active timer.") (defun org-fragtog--post-cmd () "This function is executed by `post-command-hook' in `org-fragtog-mode'. It handles toggling fragments depending on whether the cursor entered or exited them." (let* ;; Previous fragment ((prev-frag (or org-fragtog--prev-frag ;; If there is no previous fragment, ;; try to use a fragment at the previous cursor position. ;; This case matters when the cursor constructs a fragment ;; without ever being inside of it while it's constructed. ;; For example, if the user types "$foo$" in sequential order, ;; entering the final "$" creates a fragment without ;; the cursor ever being inside of it. (if org-fragtog--prev-point (save-excursion (goto-char org-fragtog--prev-point) (org-fragtog--cursor-frag))))) ;; Previous fragment's start position (prev-frag-start-pos (car (org-fragtog--frag-pos prev-frag))) ;; Current fragment (cursor-frag (org-fragtog--cursor-frag)) ;; The current fragment didn't change (frag-same (equal ;; Fragments are considered the same if they have the same ;; start position (car (org-fragtog--frag-pos cursor-frag)) prev-frag-start-pos)) ;; The current fragment changed (frag-changed (not frag-same)) ;; The fragment at position of previous fragment. ;; This can be nil when for example $foo$ is edited to become $foo $. (frag-at-prev-pos (and prev-frag-start-pos (save-excursion (goto-char prev-frag-start-pos) (org-fragtog--cursor-frag))))) ;; Only do anything if the current fragment changed (when frag-changed ;; Current fragment is the new previous (setq org-fragtog--prev-frag cursor-frag) ;; Enable fragment if cursor left it after a timed disable ;; and the fragment still exists (when (and frag-at-prev-pos (not (org-fragtog--overlay-in-p (org-fragtog--frag-pos frag-at-prev-pos)))) (org-fragtog--enable-frag frag-at-prev-pos)) ;; Cancel and expire timer (when org-fragtog--timer (cancel-timer org-fragtog--timer) (setq org-fragtog--timer nil)) ;; Disable fragment if cursor entered it (when cursor-frag (if (> org-fragtog-preview-delay 0) (setq org-fragtog--timer (run-with-idle-timer org-fragtog-preview-delay nil #'org-fragtog--disable-frag cursor-frag t)) (org-fragtog--disable-frag cursor-frag)))) (setq org-fragtog--prev-point (point)))) (defun org-fragtog--overlay-in-p (range) "Return whether there is a fragment overlay in RANGE. The RANGE parameter is a cons of start and end positions." (seq-find (lambda (overlay) (equal (overlay-get overlay 'org-overlay-type) 'org-latex-overlay)) (overlays-in (car range) (cdr range)))) (defun org-fragtog--cursor-frag () "Return the fragment currently surrounding the cursor. If there is none, return nil. If the fragment is ignored from rules in `org-fragtog-ignore-predicates', return nil." (let* ;; Element surrounding the cursor ((elem (org-element-context)) ;; Type of element surrounding the cursor (elem-type (nth 0 elem)) ;; List of fragment's properties (elem-plist (nth 1 elem)) ;; A LaTeX fragment or environment is surrounding the cursor (elem-is-latex (and (member elem-type '(latex-fragment latex-environment)) ;; Normally org-mode considers whitespace after an ;; element as part of the element. ;; Avoid this behavior and consider trailing ;; whitespace as outside the fragment. (< (point) (- (plist-get elem-plist :end) (plist-get elem-plist :post-blank))))) ;; Whether the fragment should be ignored (should-ignore (run-hook-with-args-until-success 'org-fragtog-ignore-predicates))) (if (and elem-is-latex (not should-ignore)) elem nil))) (defun org-fragtog--enable-frag (frag) "Enable the Org LaTeX fragment preview for the fragment FRAG." ;; The fragment must be disabled before `org-latex-preview', since ;; `org-latex-preview' only toggles, leaving no guarantee that it's enabled ;; afterwards. (org-fragtog--disable-frag frag) ;; Move to fragment and enable (save-excursion (goto-char (car (org-fragtog--frag-pos frag))) (ignore-errors (org-latex-preview)))) (defun org-fragtog--disable-frag (frag &optional renew) "Disable the Org LaTeX fragment preview for the fragment FRAG. If RENEW is non-nil, renew the fragment at point." ;; Renew frag at point in case point was adjusted ;; See Emacs Lisp manual, 21.6 Adjusting Point After Commands (when renew (setq frag (org-fragtog--cursor-frag)) (setq org-fragtog--prev-frag frag) (setq org-fragtog--timer nil)) ;; There may be nothing at the adjusted point (when frag (let ((pos (org-fragtog--frag-pos frag))) (org-clear-latex-preview (car pos) (cdr pos))))) (defun org-fragtog--frag-pos (frag) "Get the position of the fragment FRAG. Return a cons of the begin and end positions." (cons (org-element-property :begin frag) (org-element-property :end frag))) (provide 'org-fragtog) ;;; org-fragtog.el ends here