1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
|
;;; org-margin.el --- Org-mode notes for arbitrary documents -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Simon Parri
;; Author: Simon Parri <simonparri@ganzeria.com>
;; 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 <https://www.gnu.org/licenses/>.
;;; 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-doc-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
|