-
Notifications
You must be signed in to change notification settings - Fork 22
/
org-remark.el
1954 lines (1663 loc) · 81.4 KB
/
org-remark.el
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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; org-remark.el --- Highlight & annotate text, Info, EPUB, EWW -*- lexical-binding: t; -*-
;; Copyright (C) 2020-2024 Free Software Foundation, Inc.
;; 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 <http://www.gnu.org/licenses/>.
;; Author: Noboru Ota <[email protected]>
;; Created: 22 December 2020
;; Last modified: 02 November 2024
;; URL: https://github.com/nobiot/org-remark
;; Keywords: org-mode, annotation, note-taking, marginal-notes, wp,
;; Version: 1.2.2
;; Package-Requires: ((emacs "27.1") (org "9.4"))
;; This file is not part of GNU Emacs.
;;; Commentary:
;; This package lets you highlight and annotate any text file with using
;; Org mode. For usage, refer to the user manual available as an Info
;; node by evaluating (info "org remark")
;;; Code:
;;;; Requirements
(require 'org)
(require 'org-id)
(require 'org-remark-global-tracking)
(declare-function org-remark-convert-legacy-data "org-remark-convert-legacy")
;;;; Customization
(defgroup org-remark nil
"Highlight and annotate any text files with using Org mode."
:group 'org
:prefix "org-remark-"
:link '(url-link :tag "GitHub" "https://github.com/nobiot/org-remark"))
(defface org-remark-highlighter
'((((class color) (min-colors 88) (background light))
:underline "#aecf90" :background "#ecf7ed")
(((class color) (min-colors 88) (background dark))
:underline "#00422a" :background "#1d3c25")
(t
:inherit highlight))
"Face for the default highlighter pen.")
(defface org-remark-highlighter-warning
'((((class color) (min-colors 88) (background light))
:foreground "#604000" :background "#fff29a")
(((class color) (min-colors 88) (background dark))
:foreground: "#e2d980" :background "#693200")
(t
:inherit warning))
"Face for highlighter warning.
For example, it is used by the characters that indicate the
location of the highlight has been automatically adjusted from its
original.")
(defcustom org-remark-create-default-pen-set t
"When non-nil, Org-remark creates default pen set.
Set to nil if you prefer for it not to."
:type 'boolean)
(defcustom org-remark-notes-display-buffer-action
`((display-buffer-in-side-window)
(side . left)
(slot . 1))
"Buffer display action that Org-remark uses to open marginal notes buffer.
The default is to use a side-window on the left.
Org-remark uses `pop-to-buffer', which passes this display action
list to `display-buffer'. Refer to its documentation for more
detail and expected elements of the list."
:type display-buffer--action-custom-type)
(defcustom org-remark-notes-buffer-name "*marginal notes*"
"Buffer name of the marginal notes buffer.
`org-remark-open' and `org-remark-visit' create an indirect clone
buffer with this name."
:type 'string)
(defcustom org-remark-notes-auto-delete nil
"How Org-remark removes entries when user deletes highlights.
This controls the behavior of Org-remark when the user deletes
the highlight in the source (for example, when deleting a whole
line including a highlight). The default behavior is to remove
only the properties in the notes buffer and keeping the headline
title and any notes in the entry (with using
`org-remark-remove').
If this behavior leads to cluttering the marginal notes org file,
you can set this customizing variable to \\=':auto-delete\\='.
With this option, Org-remark will delete the entire entry when it
contains no notes without a prompt asking for confirmation. This
is the same behavior as passing a single `universal-argument'
\(\\[universal-argument]) to`org-remark-delete' or double `universal-argument'
\(\\[universal-argument] \\[universal-argument]) to `org-remark-remove'."
:type '(radio
(const :tag "Keep entries (default)" nil)
(const :tag "Delete entries automatically when no notes exist"
:auto-delete)))
(defvaralias
'org-remark-source-path-function 'org-remark-source-file-name)
(make-obsolete-variable
'org-remark-source-path-function 'org-remark-source-file-name "0.2.0")
(defcustom org-remark-source-file-name #'file-relative-name
"Function that returns the file name to point back at the source file.
The function is called with a single argument: the absolute file
name of source file. The `default-directory' is temporarily set
to the directory where the marginal notes file resides.
This means that when the \"Relative file name\" option is
selected, the source file name recorded in the marginal notes
file will be relative to it."
:type '(choice
(const :tag "Relative file name" file-relative-name)
(const :tag "Abbreviated absolute file name" abbreviate-file-name)
(function :tag "Other function")))
(defcustom org-remark-use-org-id nil
"When non-nil, Org-remark adds an Org-ID link to marginal notes.
The link points at the relevant Org-ID in the source file.
Org-remark does not create this ID, which needs to be added
manually or some other function to either the headline or file."
:type 'boolean)
(defcustom org-remark-report-no-highlights t
"When non-nil, Org-remark reports that there are no highlights in the buffer.
If Org-remark finds no highlights or annotations in a buffer it is
enabled in, then a non-nil value means a message indicating this will be
shown. Otherwise, do not show such a message."
:type 'boolean)
(defcustom org-remark-open-hook nil
"Hook run when a note buffer is opened/visited.
The current buffer is the note buffer."
:type 'hook)
(defcustom org-remark-highlights-after-load-functions
'(org-remark-highlights-adjust-positions)
"Abnormal hook run after `org-remark-highlights-load'.
It is run with OVERLAYS and NOTES-BUF as arguments. OVERLAYS are
highlights. It is run with the source buffer as current buffer."
:type 'hook)
;;;; Variables
(defvar org-remark-default-features '(org-remark-icon org-remark-line)
"Extension features to be enabled by default.
It is suggested to keep them as the default, but you can choose to disable them")
(defvar org-remark-default-feature-modes '()
"Extension minor modes to be enabled together with `org-remark-mode'.
These minor modes should be registered to this variable by the
respective feature where required. As an example, see
`org-remark-line'.")
(defvar org-remark-find-dwim-functions nil
"Functions to find the highlight overlays.
These functions should be registered to this variable by the
respective feature where required. As an example, see
`org-remark-line'.")
(defvar org-remark-last-notes-buffer nil
"The cloned indirect buffer visiting the notes file.
It is meant to exist only one of these in each Emacs session.")
(defvar org-remark-available-pens (list #'org-remark-mark)
"A list of pens available.
Each pen is a function. Users can create a new custom pen with
using `org-remark-create', which automatically add a new pen
function this list. It is used by `org-remark-change' as a
selection list.")
(defvar-local org-remark-highlights '()
"All the highlights in current source buffer.
It is a local variable and is a list of overlays. Each overlay
represents a highlighted text region.
On `save-buffer' each highlight will be saved in the notes file
returned by `org-remark-notes-get-file-name'.")
(defvar-local org-remark-highlights-hidden nil
"Hidden/shown state of the highlights in current source buffer.")
(defvar-local org-remark-notes-source-buffers '()
"List of source buffers that have loaded current notes buffer.
Each notes buffer locally keeps track of the source buffers that
have loaded notes from itself. This list is used when automatic
sync is triggered in `after-save-buffer' of the notes buffer, as
not all the sources may be open. Buffers in this list may be
killed so that this needs to be checked with `buffer-live-p'.")
(defvar-local org-remark-source-setup-done nil
"Local indicator that sync with notes buffer is set up.")
(defvar-local org-remark-highlights-toggle-hide-functions nil
"Functions to be called when toggling to hide highlights.
Each function is called with one argument HIGHLIGHT, which is an
overlay that shows the highlight. It also stores properties to
control visibility such as \\=':face\\='.
This variable is an abnormal hook and is intended to be used to
add additional controls for the overlay properties.")
(defvar-local org-remark-highlights-toggle-show-functions nil
"Functions to be called when toggling to show highlights.
Each function is called with one argument HIGHLIGHT, which is an
overlay that shows the highlight. It also stores properties to
control visibility such as \\=':face\\='.
This variable is an abnormal hook and is intended to be used to
add additional controls for the overlay properties")
;; Const for the names of properties in Org Mode
(defconst org-remark-prop-id "org-remark-id")
(defconst org-remark-prop-source-file "org-remark-file")
(defconst org-remark-prop-source-beg "org-remark-beg")
(defconst org-remark-prop-source-end "org-remark-end")
;;;; Macros to create user-defined highlighter pen functions
(defmacro org-remark-create (label &optional face properties)
"Create and register new highlighter pen functions.
The newly created pen function will be registered to variable
`org-remark-available-pens'. It is used by `org-remark-change'
as a selection list.
LABEL is the name of the highlighter and mandatory. The function
will be named `org-remark-mark-LABEL'.
The highlighter pen function will apply FACE to the selected region.
FACE can be an anonymous face. When FACE is nil, this macro uses
the default face `org-remark-highlighter'.
PROPERTIES is a plist of pairs of a symbol and value. Each
highlighted text region will have a corresponding Org headline in
the notes file, and it can have additional properties in the
property drawer from the highlighter pen. To do this, prefix
property names with \"org-remark-\" or use \"CATEGORY\"."
(if (or (not label) (stringp label)
(user-error "org-remark-create: Label is missing or not string"))
(let ((org-remark-type
`(quote ,(plist-get (eval properties) 'org-remark-type))))
`(progn
;; Define custom pen function
(defun ,(intern (format "org-remark-mark-%s" label))
(beg end &optional id mode)
,(format "Apply the following face to the region selected by BEG and END.
%s
Following overlay properties will be added to the highlighted
text region:
%S
Return the overlay.
When this function is used interactively, it will generate a new
ID, always assuming it is working on a new highlighted text
region, and Org-remark will start tracking the highlight's
location in the current buffer.
When this function is called from Elisp, ID can be optionally
passed, indicating to Org-remark that it is an existing
highlight. In this case, no new ID gets generated.
When the pen itself defines the help-echo property, it will have
the priority over the excerpt of the marginal notes."
(or face "`org-remark-highlighter'") properties)
(interactive (org-remark-beg-end ,org-remark-type))
(org-remark-highlight-mark beg end id mode ,label ,face ,properties))
;; Register to `org-remark-available-pens'
(add-to-list 'org-remark-available-pens
(intern (format "org-remark-mark-%s" ,label)))
;; Add function prop This is for `org-remark-change' to show
;; only the pens of the same type
(when 'org-remark-type (function-put
(intern (format "org-remark-mark-%s" ,label))
'org-remark-type
,org-remark-type))
;; Add the custom pen function to the minor-mode menu
(define-key-after org-remark-pen-map
[,(intern (format "org-remark-mark-%s" label))]
'(menu-item ,(format "%s pen" label) ,(intern (format "org-remark-mark-%s" label))))
;; Add the custom pen change function to the minor-mode menu
(define-key-after org-remark-change-pen-map
[,(intern (format "org-remark-change-to-%s" label))]
'(menu-item ,(format "%s pen" label)
(lambda ()
(interactive)
(org-remark-change
#',(intern (format "org-remark-mark-%s" label))))
:enable (org-remark-pen-same-type-at-point-p ,org-remark-type)))))))
;;;; Minor mode
;;;###autoload
(define-minor-mode org-remark-mode
"Highlight and annotate any text file with using Org mode.
This is a local minor-mode.
On activation, it loads your saved highlights from the notes file
and enables automatic saving of highlights thereafter.
The automatic saving is achieved via function
`org-remark-save' added to `after-save-hook'.
On deactivation, it removes all the overlays and stops tracking
the highlights in this buffer by setting variable
`org-remark-highlights' to nil. Be careful of behavior, if
you still wish to retain the locations of highlights.
It is recommended to use `org-remark-toggle' if you wish to
temporarily hide highlights in the current buffer. It keeps
`org-remark-highlights' unchanged.
While the tracking of highlights is stopped,
editing the buffer will likely result in mismatch between the
saved highlights' locations and the current buffer's text
content.
Highlights tracked by variable `org-remark-highlights' cannot
persist when you kill the buffer or quit Emacs. When you
re-launch Emacs and visit the same file, ensure to turn on
`org-remark-mode' to load the highlights from the marginalia
file. `org-remark-global-tracking-mode' automates this. It is
recommended to turn it on as part of Emacs initialization.
\\{org-remark-mode-map}"
:init-value nil
:lighter " ormk"
:global nil
:keymap (let ((map (make-sparse-keymap)))
map)
(cond
(org-remark-mode
;; Activate
(dolist (feature org-remark-default-features)
(unless (featurep feature) (require feature nil 'noerror)))
(dolist (feature-mode org-remark-default-feature-modes)
(when (functionp feature-mode) (funcall feature-mode +1)))
(org-remark-highlights-load)
(add-hook 'org-remark-find-dwim-functions
#'org-remark-find-overlay-at-point nil :local)
(add-hook 'after-save-hook #'org-remark-save nil :local)
(add-hook 'org-remark-highlight-link-to-source-functions
#'org-remark-highlight-link-to-source-default 80)
(add-hook 'after-revert-hook #'org-remark-highlights-load 80 :local)
(add-hook 'clone-buffer-hook #'org-remark-highlights-load 80 :local))
(t
;; Deactivate
(when org-remark-highlights
(dolist (highlight org-remark-highlights)
(delete-overlay highlight)))
(setq org-remark-highlights nil)
(dolist (feature-mode org-remark-default-feature-modes)
(funcall feature-mode -1))
(remove-hook 'org-remark-find-dwim-functions
#'org-remark-find-overlay-at-point :local)
(remove-hook 'after-save-hook #'org-remark-save t)
(remove-hook 'org-remark-highlight-link-to-source-functions
#'org-remark-highlight-link-to-source-default)
(remove-hook 'after-revert-hook #'org-remark-highlights-load :local)
(remove-hook 'clone-buffer-hook #'org-remark-highlights-load :local))))
;;;; Org-remark Menu
(defvar org-remark-menu-map
(make-sparse-keymap "Org-remark"))
(define-key-after org-remark-menu-map
[org-remark-open]
'(menu-item "Open" org-remark-open
:help "Display and move to marginal notes for highlight at point"
:enable (org-remark-find-dwim)))
(define-key-after org-remark-menu-map
[org-remark-view]
'(menu-item "View" org-remark-view
:help "Display marginal notes for highlight at point; stay in current buffer"
:enable (org-remark-find-dwim)))
(define-key-after org-remark-menu-map
[org-remark-view-next]
'(menu-item "View next" org-remark-view-next
:enable org-remark-highlights))
(define-key-after org-remark-menu-map
[org-remark-view-prev]
'(menu-item "View previous" org-remark-view-prev
:enable org-remark-highlights))
(define-key-after org-remark-menu-map
[org-remark-toggle]
'(menu-item "Toggle" org-remark-toggle
:help "Toggle showing/hiding of highlights in current buffer"
:enable org-remark-highlights))
(define-key-after org-remark-menu-map
[org-remark-remove]
'(menu-item "Remove" org-remark-remove
:help "Remove highlight at point, keeping the marginal notes entry"
:enable (org-remark-find-dwim)))
(define-key-after org-remark-menu-map
[org-remark-delete]
'(menu-item "Delete" org-remark-delete
:help "Delete highlight at point and the marginal notes entry"
:enable (org-remark-find-dwim)))
;; Make pen functions menu
(defvar org-remark-pen-map
(make-sparse-keymap "Org-remark-mark"))
(define-key-after org-remark-pen-map
[org-remark-mark]
'(menu-item "default pen" org-remark-mark))
;; Make change pen menu
(defvar org-remark-change-pen-map
(make-sparse-keymap "Org-remark-change"))
(define-key-after org-remark-change-pen-map
[org-remark-change]
'(menu-item "default pen"
(lambda ()
(interactive)
(org-remark-change #'org-remark-mark))
:enable (org-remark-pen-same-type-at-point-p nil)))
;; Add change menu to the parent menu
(define-key-after org-remark-menu-map
[org-remark-change-pens]
`(menu-item "Change to..." ,org-remark-change-pen-map
:enable (org-remark-find-dwim)
'org-remark-toggle))
;; Add pen menu to the parent menu
(define-key org-remark-menu-map
[org-remark-pens]
(list 'menu-item "Highlight with..." org-remark-pen-map))
;; Add all to the main menu
(define-key org-remark-mode-map
[menu-bar org-remark]
(list 'menu-item "Org-remark" org-remark-menu-map))
(defun org-remark-pen-same-type-at-point-p (org-remark-type)
"Return t if the highlight's type is the same as ORG-REMARK-TYPE."
(eql org-remark-type
(overlay-get (org-remark-find-dwim (point)) 'org-remark-type)))
;;;; Commands
;;;###autoload
(defun org-remark-mark (beg end &optional id mode)
"Apply face `org-remark-highlighter' to the region between BEG and END.
When this function is used interactively, it will generate a new
ID, always assuming it is working on a new highlighted text
region.
Return the highlight overlay.
A Org headline entry for the highlight will be created in the
marginal notes file specified by
`org-remark-notes-get-file-name'. If the file does not exist
yet, it will be created.
When this function is called from Elisp, ID can be
optionally passed, indicating to Org-remark that it is to load an
existing highlight. In this case, no new ID gets generated and
the highlight saved again, avoiding the unnecessary round-trip
back to the database.
MODE is also an argument which can be passed from Elisp. It
determines whether or not highlight is to be saved in the
marginal notes file. The expected values are nil, :load and
:change."
(interactive (org-remark-beg-end nil)) ;; passing org-remark-type nil
;; FIXME
;; Adding "nil" is different to removing a prop
;; This will do for now
(org-remark-highlight-mark beg end id mode
nil nil
(list 'org-remark-label "nil")))
(when org-remark-create-default-pen-set
;; Create default pen set.
(org-remark-create "red-line"
`(:underline (:color "dark red" :style wave))
`(CATEGORY "review" help-echo "Review this"))
(org-remark-create "yellow"
`(:underline "gold2")
`(CATEGORY "important")))
(defun org-remark-save ()
"Save all the highlights tracked in current buffer to notes buffer.
This function is automatically called when you save the current
buffer via `after-save-hook'.
`org-remark-highlights' is the local variable that tracks every highlight
in the current buffer. Each highlight is an overlay."
(interactive)
(org-remark-highlights-housekeep)
(org-remark-highlights-sort)
(let ((notes-buf (find-file-noselect (org-remark-notes-get-file-name)))
(source-buf (or (buffer-base-buffer) (current-buffer))))
(dolist (h org-remark-highlights)
(org-remark-highlight-add h source-buf notes-buf))
;;; Avoid saving the notes buffer if it is the same as the source buffer
(if (eq source-buf notes-buf)
(set-buffer-modified-p nil)
(with-current-buffer notes-buf
(save-buffer)))))
(defun org-remark-open (point &optional view-only)
"Open marginal notes file for highlight at POINT.
The marginal notes will be narrowed to the relevant headline to
show only the highlight at point.
This function creates a cloned indirect buffer for the marginal
notes file. You can edit it as a normal Org buffer. Once you have
done editing, you can simply save and kill the buffer or keep it
around. Org-remark ensures that there is only one cloned buffer
for notes file by tracking it.
The marginal notes file gets displayed by the action defined by
`org-remark-notes-display-buffer-action' (by default in a left
side window of the current frame), narrowed to the relevant
headline.
You can customize the name of the marginal notes buffer with
`org-remark-notes-buffer-name'.
By default, the cursor will go to the marginal notes buffer for
further editing. When VIEW-ONLY is \\=':view-only\\=' \(e.g.
Elisp program to pass the value), you can view the marginal notes
buffer with the cursor remaining in the current buffer.
You can open the marginal notes buffer associated with the
current buffer with `find-file' when the notes file exist and if
there is no highlight at point. Passing a single universal
argument with \\[universal-argument]\) also lets you open the
notes buffer in this way.
If you pass any other values to VIEW-ONLY, this function behaves
in the way as passing \\=':view-only\\=' to it and simply let you
view the marginal notes in a cloned indirect buffer in the
side-window (as defined by user option
`org-remark-notes-display-buffer-action')."
(interactive "d\nP")
(let ((ov (org-remark-find-dwim point)))
;; If C-u is used or the cursor is not on a highlight, we don't want
;; to open in a normal way but open the margnal notes buffer with
;; find-file.
(if (or (eql (prefix-numeric-value current-prefix-arg) 4)
;; :view-only should not open the marginal notes buffer
(and (null ov) (not (eql view-only :view-only))))
(let ((notes-file (org-remark-notes-get-file-name)))
(when (file-exists-p notes-file) (find-file notes-file)))
;; Open marginal notes normally as an indirect buffer in a side
;; window.
(when-let*
((ov ov) ;; OV must be present here.
(id (overlay-get ov 'org-remark-id))
(ibuf (org-remark-notes-buffer-get-or-create))
(cbuf (current-buffer)))
(pop-to-buffer ibuf org-remark-notes-display-buffer-action)
(widen)
(when-let (p (org-find-property org-remark-prop-id id))
;; Somehow recenter is needed when a highlight is deleted and move to a
;; previous highlight. Otherwise, the cursor is too low to show the
;; entire entry. It looks like there is no entry.
(goto-char p) (org-narrow-to-subtree) (org-end-of-meta-data t) (recenter))
;; Run hook with the current-buffer being the note's buffer
(run-hooks 'org-remark-open-hook)
;; Avoid error when buffer-action is set to display a new frame
(when view-only
(select-window (get-buffer-window cbuf)))))))
(defun org-remark-view (point)
"View marginal notes for highlight at POINT.
The marginal notes file gets displayed by the action defined by
`org-remark-notes-display-buffer-action' (by default in a side
window in the left of the current frame), narrowed to the
relevant headline. The cursor remains in the current buffer.
Also see the documentation of `org-remark-open'."
(interactive "d")
(org-remark-open point :view-only))
(defun org-remark-next ()
"Move to the next highlight, if any.
If there is none below the point but there is a highlight in the
buffer, cycle back to the first one.
After the point has moved to the next highlight, this command
lets you move further by re-entering only the last letter like
this example:
C-n \] \] \] \] \] \(assuming this command is bound to C-n \]\)
This is achieved by transient map with `set-transient-map'.
If you have the same prefix for `org-remark-prev', you can combine it in
the sequence like so:
C-n \] \] \] \[ \["
(interactive)
(org-remark-next-or-prev :next))
(defun org-remark-prev ()
"Move to the previous highlight, if any.
If there is none above the point, but there is a highlight in the
buffer, cycle back to the last one.
After the point has moved to the previous highlight, this command
lets you move further by re-entering only the last letter like
this example:
C-n \[ \[ \[ \[ \[ \(assuming this command is bound to C-n \[\)
This is achieved by transient map with `set-transient-map'.
If you have the same prefix for `org-remark-next', you can combine it in
the sequence like so:
C-n \] \] \] \[ \["
(interactive)
(org-remark-next-or-prev))
(defun org-remark-view-next ()
"Move the cursor to the next highlight and view its marginal notes."
(interactive)
(org-remark-next)(org-remark-view (point)))
(defun org-remark-view-prev ()
"Move the cursor to the previous highlight and view its marginal notes."
(interactive)
(org-remark-prev)(org-remark-view (point)))
(defun org-remark-toggle ()
"Toggle showing/hiding of highlights in current buffer.
If you would like to hide/show the highlights in the current
buffer, it is recommended to use this command instead of
`org-remark-mode'. This command only affects the display of the
highlights and their locations are still kept tracked. Toggling
off `org-remark-mode' stops this tracking completely, which will
likely result in inconsistency between the marginal notes file
and the current source buffer."
(interactive)
(if org-remark-highlights-hidden
(org-remark-highlights-show)
(org-remark-highlights-hide))
t)
(defun org-remark-change (&optional pen)
"Change the highlight at point to PEN.
This function will show you a list of available pens to choose
from."
(interactive)
(if-let* ((ov (org-remark-find-dwim))
(id (overlay-get ov 'org-remark-id))
(beg (overlay-start ov))
(end (overlay-end ov)))
(let* ((available-pens (seq-filter
(lambda (pen-fn)
(let ((type (overlay-get ov 'org-remark-type)))
(eql type (function-get pen-fn 'org-remark-type))))
org-remark-available-pens))
(new-pen
(if pen pen
(intern
;; To guard against minibuffer quit error when
;; the user quit without selecting any pen.
(unwind-protect
(completing-read "Which pen?:"
available-pens))))))
(org-remark-highlight-clear ov)
(funcall new-pen beg end id :change))
;; if ov or any other variables are not found
(message "No highlight here.")))
(defun org-remark-remove (point &optional delete)
"Remove the highlight at POINT.
By default, it will remove the highlight from the source buffer
and the properties of entry from the marginal notes buffer, but
will keep the headline title and any notes in it. This is to
ensure to keep any notes you might have written intact.
Optionally, you can let this command delete the entire heading
subtree for the highlight along with the notes you have written,
by passing universal argument in DELETE. For deletion, this
command differentiates a single or double universal arguments as
follows:
- \\[universal-argument]
This is the same behavior as function `org-remark-delete'.
Look for notes in the entry. If there is any, the side-window
will show them and a prompt will ask the user for confirmation.
The function will delete the entry only when the user confirms
with \\='y\\='. When \\='n\\=', it will only remove the entry's
properties.
If there are no notes in the entry, the command will delete the
entry without the prompt.
- \\[universal-argument] \\[universal-argument]
This is automatic deletion. This command will delete the entry
without asking the user when there is no notes in the entry. If
there are any notes, only the entry's properties will be
removed. This is the same behavior as passing a single
`universal-argument' to function `org-remark-delete'.
If you have removed or deleted a highlight by error, you can
still `undo' it in the marginal notes buffer and not in the
current buffer. This is because adding and removing overlays are
not part of the undo tree. You can undo the deletion in the
marginal notes buffer and then save it to sync the highlight back
in the source."
(interactive "d\nP")
(and-let* ((ov (org-remark-find-dwim point))
(id (overlay-get ov 'org-remark-id)))
;; Remove the highlight overlay and id. If there is more than one,
;; remove only one. It should be last-in-first-out in general but
;; overlays functions don't guarantee it (when delete
;; (org-remark-open point :view-only))
(org-remark-highlight-clear ov)
;; Update the notes file accordingly
(org-remark-notes-remove id delete)
(org-remark-highlights-housekeep)
(org-remark-highlights-sort)
t))
(defun org-remark-delete (point &optional arg)
"Delete the highlight at POINT and marginal notes for it.
This function will prompt for confirmation if there is any notes
present in the highlight's entry in the marginal notes buffer.
When it is not displayed in the current frame, it will be
temporarily displayed together with the prompt for the user to
see the notes to help with confirmation.
If there are no notes, this function will not prompt for
confirmation and will remove the highlight in the source buffer
and delete the entry in the marginal notes buffer.
This is the same behavior as passing a single `universal-argument'
to function `org-remark-remove'.
Optionally, you can pass `universal-argument' to this function
with ARG and it will behave as follows.
- \\[universal-argument]
This is automatic deletion. Delete the entry without asking the
user when there is no notes in the entry. If there are any
notes, remove the entry's properties only. This is the same
behavior as passing double universal-arguments to function
`org-remark-remove'.
If you have removed or deleted a highlight by error, you can
still `undo' it in the marginal notes buffer and not in the
current buffer. This is because adding and removing overlays are
not part of the undo tree. You can undo the deletion in the
marginal notes buffer and then save it to sync the highlight
back in the source."
(interactive "d\nP")
(let ((delete (if (eql 4 (prefix-numeric-value arg))
'(16) ;; make it universal-arg x 2
:delete)))
(org-remark-remove point delete)))
;;;; Private Functions
;;;;; org-remark-find
;; Find a highlight (e.g. next/prev or overlay)
(defun org-remark-next-or-prev (&optional next)
"Move cursor to the next or previous highlight if any.
When NEXT is non-nil, move to the next; for nil, to the previous.
This function is internal only and meant to be used by interactive
commands such as `org-remark-next' and `org-remark-prev'.
Return t if the cursor has moved to next/prev.
Return nil if not and outputs a message in the echo."
(org-remark-highlights-housekeep)
(if (not org-remark-highlights)
(progn (message "No highlights present in the buffer") nil)
(let ((p (if next (org-remark-find-next-highlight)
(org-remark-find-prev-highlight))))
(if p (progn
(goto-char p)
;; Setup the overriding keymap.
(unless overriding-terminal-local-map
(let ((prefix-keys (substring (this-single-command-keys) 0 -1))
(map (cdr org-remark-mode-map)))
(when (< 0 (length prefix-keys))
(mapc (lambda (k) (setq map (assq k map))) prefix-keys)
(setq map (cdr-safe map))
(when (keymapp map) (set-transient-map map t)))))
t)
(message "No visible highlights present in the buffer")
nil))))
(defun org-remark-find-next-highlight ()
"Return the beg point of the next highlight.
Look through `org-remark-highlights' list."
(when-let ((points (org-remark-highlights-get-positions)))
;; Find the first occurrence of p > (point). If none, this means all the
;; points occur before the current point. Take the first one. Assume
;; `org-remark-highlights' is sorted in the ascending order (it is).
(seq-find (lambda (p) (> p (point))) points (nth 0 points))))
(defun org-remark-find-prev-highlight ()
"Return the beg point of the previous highlight.
Look through `org-remark-highlights' list (in descending order)."
(when-let ((points (org-remark-highlights-get-positions 'reverse)))
;; Find the first occurrence of p < (point). If none, this means all the
;; points occur before the current point. Take the first one. Assume
;; `org-remark-highlights' is sorted in the descending order .
(seq-find (lambda (p) (< p (point))) points (nth 0 points))))
(defun org-remark-find-dwim (&optional point)
"Return one highlight overlay for the context.
It is a generic wrapper function to get and return as what the
context requires. This is achieved via abnormal hook that passed
the POINT as a single argument.
The highligh to be returned can be the range-highlight at point.
POINT is optional and if not passed, the current point is used.
It can also be a line-highlight for the line, which is a zero
length overlay put to the beginning of the line. For the latter,
the user's point can be anywhere."
(or (run-hook-with-args-until-success
'org-remark-find-dwim-functions point)
;; Fallback
(org-remark-find-overlay-at-point point)))
(defun org-remark-find-overlay-at-point (&optional point)
"Return one org-remark overlay at POINT.
When point is nil, use the current point.
If there are more than one, return CAR of the list."
(let* ((pt (or point (point)))
(overlays (overlays-at pt))
found)
(while overlays
(let ((overlay (car overlays)))
(if (overlay-get overlay 'org-remark-id)
(setq found (cons overlay found))))
(setq overlays (cdr overlays)))
(car found)))
(defun org-remark-find-overlay-in (beg end &optional id)
"Return one org-remark overlay between BEG and END.
If there are more than one, return CAR of the list.
Optionally ID can be passed to find the exact ID match."
(let* ((overlays (overlays-in beg end))
found)
(while overlays
(let ((overlay (car overlays)))
(if (overlay-get overlay 'org-remark-id)
(setq found (cons overlay found))))
(setq overlays (cdr overlays)))
(when id (setq found
(seq-filter
(lambda (ov)
(equal (overlay-get ov 'org-remark-id) id))
found)))
(car found)))
;;;;; org-remark-highlight
;; Private functions that work on a single highlight. A highlight is an
;; overlay placed on a a part of text. With using an analogy of pens
;; and books, a highlight is the mark you make over a part of a book
;; with a highlighter pen or marker.
;;
;; As highlights are overlays placed on the source buffer, the
;; functions here mostly assume the current buffer is the source
;; buffer.
(cl-defgeneric org-remark-highlight-make-overlay (_beg _end _face _org-remark-type)
"Make overlay and return it.
Put FACE and other necessary properties to the highlight OV"
(ignore))
(cl-defmethod org-remark-highlight-make-overlay (beg end face
(_org-remark-type (eql nil)))
"Make overlay BEG END and add FACE to it.
If FACE is nil, this function uses default face `org-remark-highlighter'.
This is a method for highlights of default ORG-REMARK-TYPE, that
is for a character range."
(let ((ov (make-overlay beg end nil :front-advance)))
(overlay-put ov 'face (if face face 'org-remark-highlighter))
ov))
(defun org-remark-highlight-mark
(beg end &optional id mode label face properties)
"Apply the FACE to the region selected by BEG and END.
This function will apply FACE to the selected region. When it is
nil, this function will use the default face `org-remark-highlighter'
This function will add LABEL and PROPERTIES as overlay
properties. PROPERTIES is a plist of pairs of a symbol and value.
Return the highlight overlay.
When this function is used interactively, it will generate a new
ID, always assuming it is working on a new highlighted text
region, and Org-remark will start tracking the highlight's
location in the current buffer.
MODE determines whether or not highlight is to be saved in the
marginal notes file. The expected values are nil, :load and
:change.
An Org headline entry for the highlight will be created in the
marginal notes file specified by `org-remark-notes-get-file-name'.
If the file does not exist yet, it will be created.
When this function is called from Elisp, ID can be optionally
passed, indicating to Org-remark that it is to load an existing
highlight. In this case, no new ID gets generated and the
highlight will not be saved again, avoiding the unnecessary
round-trip back to the notes file."
;; Ensure to turn on the local minor mode
(unless org-remark-mode (org-remark-mode +1))
;; When highlights are toggled hidden, only the new one gets highlighted in
;; the wrong toggle state.
(when org-remark-highlights-hidden (org-remark-highlights-show))
(org-with-wide-buffer
(let* ((org-remark-type (plist-get properties 'org-remark-type))
;;(make-overlay beg end nil :front-advance))
;; UUID is too long; does not have to be the full length
(id (if id id (substring (org-id-uuid) 0 8)))
(filename (org-remark-source-find-file-name))
;; Add highlight overlay only when filename is assigned.
(ov (when filename
(org-remark-highlight-make-overlay
beg end face org-remark-type))))
(if (not filename)
(message (format "org-remark: Highlights not saved.\
This buffer (%s) is not supported" (symbol-name major-mode)))
;; OV may not be created for line-highlights when the user opens
;; the buffer for the first time, as the window may not have been
;; created to display the buffer yet. This is necessary for the
;; margin width to be calculated.
(when ov
(while properties
(let ((prop (pop properties))
(val (pop properties)))
(overlay-put ov prop val)))
(when label (overlay-put ov 'org-remark-label label))
(overlay-put ov 'org-remark-id id)
;; Keep track of the overlay in a local variable. It's a list that is
;; guaranteed to contain only org-remark overlays as opposed to the one
;; returned by `overlay-lists' that lists all overlays.
(push ov org-remark-highlights)