;;; org-margin.el --- Org-mode notes for arbitrary documents -*- lexical-binding: t; -*- ;; Copyright (C) 2025 Simon Parri ;; Author: Simon Parri ;; Keywords: multimedia, hypermedia, outlines, docs ;; Package-Requires: ((emacs "24.3") (org "9.6") (compat "29.1")) ;; Version: 0.50 ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; This package allows users to associate headings in an Org buffer (known as ;; the "notes" buffer) with links to an arbitrary document (using Org's ;; linking facilities). It also provides commands to make linking between the ;; document and the notes easy (`org-margin-doc-set-link' and ;; `org-margin-org-set-link'), and to navigate between headings with margin ;; links (`org-margin-org-next-heading' and `org-margin-org-previous-heading'). ;; See these commands, as well as the Customization group, for more ;; information on how to use this package. ;; For users familiar with the `org-noter' package, this package essentially ;; does the same thing, but is more generalized, since it uses arbitrary Org ;; links instead of expecting the medium to be paged. ;; Some uses for this package could be: ;; + To take notes on code using Org file: links (which remmeber the line as ;; well as the file) ;; + To take notes on a PDF or EPUB file (using `nov.el' to read EPUBs, and my ;; package `ol-file-paged' for per-page links to DocView and `pdf-tools' ;; buffers) ;;; Code: (require 'org) ;; Custom variables (defgroup org-margin () "Org-mode notes for arbitrary documents." :group 'org) (defcustom org-margin-find-org-buffer-functions '(org-margin-find-org-buffer-fallback) "List of functions to be used to find an appropriate Org notes buffer. They will be called in order with no arguments and the value returned by the first one to return non-nil will be used as the notes buffer." :type 'hook :options '(org-margin-find-org-buffer-fallback org-margin-find-other-buffer)) (defcustom org-margin-doc-modes '(doc-view-mode pdf-view-mode nov-mode) "List of modes to be considered as document modes." :type '(set symbol)) (defcustom org-margin-find-doc-buffer-functions '(org-margin-find-doc-buffer-fallback org-margin-find-other-buffer) "List of functions to be used to find an appropriate document buffer. They will be called in order with no arguments and the value returned by the first one to return non-nil will be used as the document buffer." :type 'hook :options '(org-margin-find-doc-buffer-fallback org-margin-find-other-buffer)) (defcustom org-margin-org-fold-others t "When t, org-margin-org command fold other headings after moving. When this is nil, `org-margin-org-next-heading' and `org-margin-org-previous-heading' will leave the folding as-is after moving." :type 'boolean) ;; Buffer-finding code (defun org-margin--find (list) "Search LIST of functions for the first one that returns non-nil. Returns the value returned by the function. Each function is called with no arguments." (cl-loop for f in list do (when-let ((it (funcall f))) (cl-return it)))) (defun org-margin-find-other-buffer () "Get the buffer from the \"other\" window. Other window is determined by `other-window-for-scrolling'." (with-selected-window (other-window-for-scrolling) (current-buffer))) (defun org-margin-find-org-buffer-fallback () "Try to find an Org-mode buffer that is currently displayed. First tries the \"other\" window (as determined by `other-window-for-scrolling'), then tries any other displayed window. Returns nil if no appropriate buffer is found." (or (with-selected-window (other-window-for-scrolling) (when (derived-mode-p 'org-mode) (current-buffer))) (when-let ((w (cl-find-if (lambda (x) (with-selected-window x (derived-mode-p 'org-mode))) (window-list)))) (with-selected-window w (current-buffer))))) (defun org-margin-find-org-buffer () "Find an appropriate Org buffer for the current document buffer. Uses `org-margin-find-org-buffer-functions', which see for how to customize its behavior." (org-margin--find org-margin-find-org-buffer-functions)) (defun org-margin-find-doc-buffer-fallback () "Find a relevant buffer in one of `org-margin-doc-modes'. Goes through all visible windows and returns the first displayed buffer that is in one of the modes listed in `org-margin-doc-modes'." (when-let ((w (cl-find-if (lambda (x) (with-selected-window x (derived-mode-p org-margin-doc-modes))) (window-list)))) (with-selected-window w (current-buffer)))) (defun org-margin-find-doc-buffer () "Find an appropriate Org buffer for the current document buffer. Uses `org-margin-find-doc-buffer-functions', which see for how to customize its behavior." (org-margin--find org-margin-find-org-buffer-functions)) (cl-defmacro org-margin--with-buffer ((buffer &optional error) &rest body) "Evaluate BODY in BUFFER, raising ERROR if BUFFER is nil. If ERROR is not given, a generic \"No buffer found\" error is raised instead." (declare (indent 1)) (let ((b (gensym 'b))) `(if-let ((,b ,buffer)) (with-current-buffer ,b ,@body) (user-error ,(or error "No buffer found."))))) ;; Margin-doc mode code ;;;###autoload (defun org-margin-doc-set-link () "Link the current heading in the notes buffer to the current view. Uses `org-store-link' to get the most appropriate link for the current buffer and position, and stores it in the MARGIN_LINK property of the current heading in the notes buffer (as found by `org-margin-find-org-buffer')." (interactive nil org-margin-doc-mode) (let ((link (let (org-stored-links) (org-store-link nil)))) (org-margin--with-buffer ((org-margin-find-org-buffer) "No Org buffer found.") (org-entry-put nil "MARGIN_LINK" link)))) (defvar-keymap org-margin-doc-mode-map "C-M-." 'org-margin-doc-set-link) ;;;###autoload (define-minor-mode org-margin-doc-mode "Minor mode for Org-Margin in the document buffer." :lighter " Margin-Doc" :interactive t) ;; Margin-org mode code ;;;###autoload (defun org-margin-org-set-link () "Link the current heading to the current view of the document buffer. See `org-margin-find-doc-buffer' for how the document buffer is determined and `org-margin-doc-set-link' for how the link is determined and set." (interactive nil org-mode) (org-margin--with-buffer ((org-margin-find-doc-buffer) "No other window.") (org-margin-doc-set-link))) (defun org-margin-org-get-link () "Get the margin link of the current heading." (when (derived-mode-p 'org-mode) (org-entry-get nil "MARGIN_LINK"))) ;;;###autoload (defun org-margin-org-open-link () "Open the margin link of the current heading." (interactive nil org-mode) (let ((w (selected-window))) (org-link-open-from-string (or (org-margin-org-get-link) (user-error "No link for this heading."))) (select-window w t))) (defun org-margin-org--goto-heading-with-property (dir) "Go to the nearest heading with a MARGIN_LINK property in direction DIR. If DIR is greater than 0, look forwards, otherwise look backwards. Returns the value of the heading's MARGIN_LINK property." (org-back-to-heading) (if (> dir 0) (outline-next-heading) (outline-previous-heading)) (while (not (or (org-entry-get nil "MARGIN_LINK") (if (> dir 0) (eobp) (bobp)))) (if (> dir 0) (outline-next-heading) (outline-previous-heading))) (org-entry-get nil "MARGIN_LINK")) ;;;###autoload (defun org-margin-org-next-heading (&optional arg) "Move to the next heading with a margin link and follow the link. With ARG repeats, or moves backward if negative." (interactive "p" org-mode) (unless arg (setq arg 1)) (let ((last nil) (point (point))) (when org-margin-org-fold-others (save-excursion (org-back-to-heading-or-point-min) (org-fold-hide-entry))) (dotimes (_ (abs arg)) (setq last (org-margin-org--goto-heading-with-property arg))) (org-reveal t) (when org-margin-org-fold-others (org-fold-show-entry t)) (if last (org-margin-org-open-link) (goto-char point) (user-error "No other heading with margin link")))) ;;;###autoload (defun org-margin-org-previous-heading (&optional arg) "Move to the previous heading with a margin link and follow the link. With ARG repeats, or moves forward if negative." (interactive "p" org-mode) (org-margin-org-next-heading (- arg))) (defvar-keymap org-margin-org-mode-map "C-M-." 'org-margin-org-set-link "C-M-," 'org-margin-org-open-link "C-M-n" 'org-margin-org-next-heading "C-M-p" 'org-margin-org-previous-heading) ;;;###autoload (define-minor-mode org-margin-org-mode "Minor mode for Org-Margin in the Org mode buffer." :lighter " Margin-Org") (provide 'org-margin) ;;; org-margin.el ends here