-
Notifications
You must be signed in to change notification settings - Fork 3
/
taskpaper-mode.el
5648 lines (5110 loc) · 216 KB
/
taskpaper-mode.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
;;; taskpaper-mode.el --- Major mode for working with TaskPaper files
;; Copyright 2016-2024 Dmitry Safronov
;; Author: Dmitry Safronov <saf.dmitry@gmail.com>
;; Maintainer: Dmitry Safronov <saf.dmitry@gmail.com>
;; URL: <https://github.com/saf-dmitry/taskpaper-mode>
;; Keywords: outlines, notetaking, task management, productivity, taskpaper
;; Package-Requires: ((emacs "25.1"))
;; This file is not part of GNU Emacs.
;; 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, 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/>.
;;; Commentary:
;; TaskPaper mode is a major mode for working with files in TaskPaper
;; format. The format was invented by Jesse Grosjean and named after his
;; TaskPaper macOS app <https://www.taskpaper.com>, which is a system
;; for organizing your outlines and tasks in a text file.
;;
;; TaskPaper mode is implemented on top of Outline mode. Visibility
;; cycling and structure editing help to work with the outline
;; structure. Special commands also provided for outline-aware filtering,
;; tags manipulation, sorting, refiling, and archiving of items.
;;; Code:
;;;; Features
(require 'outline)
(require 'font-lock)
(require 'easymenu)
(require 'calendar)
(require 'parse-time)
(require 'cal-iso)
(require 'overlay)
(require 'cl-lib)
;;;; Variables
(defconst taskpaper-mode-version "1.0"
"TaskPaper mode version number.")
(defconst taskpaper-mode-manual-uri
"https://github.com/saf-dmitry/taskpaper-mode/blob/master/manual.md"
"URI for TaskPaper mode manual.")
(defvar taskpaper-mode-map (make-keymap)
"Keymap for TaskPaper mode.")
(defvar taskpaper-mode-syntax-table
(make-syntax-table text-mode-syntax-table)
"Syntax table for TaskPaper mode.")
(defvar taskpaper-read-date-history nil
"History list for date prompt.")
(defvar taskpaper-query-history nil
"History list for query prompt.")
;;;; Custom variables
(defgroup taskpaper nil
"Major mode for editing and querying files in TaskPaper format."
:prefix "taskpaper-"
:group 'wp
:group 'text
:group 'applications)
(defcustom taskpaper-faces-easy-properties :foreground
"The property changes by easy faces.
The value can be `:foreground' or `:background'. A color string
for specific tags will then be interpreted as either foreground
or background color. For more details see custom variable
`taskpaper-tag-faces'."
:group 'taskpaper
:type '(choice (const :foreground)
(const :background)))
(defcustom taskpaper-tag-faces nil
"Faces for specific tags.
This is a list of cons cells, with tag names in the car and faces
in the cdr. The tag name can contain letters, digits, hyphens,
underscores, and dots. The face can be a symbol corresponding to
a name of an existing face, a color (in which case it will be
interpreted as either foreground or background color according to
the variable `taskpaper-faces-easy-properties' and the rest is
inherited from the face `taskpaper-tag') or a property list of
face attributes."
:group 'taskpaper
:type '(repeat
(cons (string :tag "Tag name")
(choice :tag "Face"
(string :tag "Color")
(sexp :tag "Face")))))
(defcustom taskpaper-tag-alist nil
"List of tags for fast selection.
This is a list of cons cells, with tag strings in the car and
selection characters in the cdr."
:group 'taskpaper
:type '(repeat
(cons (string :tag "Tag")
(character :tag "Key"))))
(defcustom taskpaper-tags-exclude-from-inheritance nil
"List of tags that should not be inherited."
:group 'taskpaper
:type '(repeat (string :tag "Tag name")))
(defcustom taskpaper-tags-to-remove-when-done nil
"List of tags to remove when completing item."
:group 'taskpaper
:type '(repeat (string :tag "Tag name")))
(defcustom taskpaper-complete-save-date 'date
"Non-nil means, include date when completing item.
When `date', include current date. When `time', include date and
time."
:group 'taskpaper
:type '(choice
(const :tag "No date" nil)
(const :tag "Date" date)
(const :tag "Date and time" time)))
(defcustom taskpaper-blocker-hook nil
"Hook for functions to block completion of item.
The value of this hook may be nil, a function, or a list of
functions. Functions in this hook should not modify the buffer.
Each function gets as its single argument a buffer position at
the beginning of item. If any of the functions in this hook
returns nil, the completion is blocked."
:group 'taskpaper
:type 'hook)
(defcustom taskpaper-after-completion-hook nil
"Hook run after completing item."
:group 'taskpaper
:type 'hook)
(defcustom taskpaper-read-date-popup-calendar t
"Non-nil means, pop up a calendar when prompting for a date."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-read-date-display-live t
"Non-nil means, display the date prompt interpretation live."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-startup-folded nil
"Non-nil means, switch to Overview when entering TaskPaper mode."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-startup-with-inline-images nil
"Non-nil means, show inline images when entering TaskPaper mode."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-max-image-size nil
"Maximum width and height for displayed inline images.
This variable may be nil or a cons cell with maximum width in the
car and maximum height in the cdr, in pixels. When nil, use the
actual size. Otherwise, use ImageMagick to resize larger images.
This requires Emacs to be built with ImageMagick support."
:group 'taskpaper
:type '(choice
(const :tag "Actual size" nil)
(cons (choice (sexp :tag "Maximum width")
(const :tag "No maximum width" nil))
(choice (sexp :tag "Maximum height")
(const :tag "No maximum height" nil)))))
(defcustom taskpaper-after-sorting-items-hook nil
"Hook run after sorting of items.
When children are sorted, the cursor is in the parent line when
this hook gets called."
:group 'taskpaper
:type 'hook)
(defcustom taskpaper-archive-location "%s_archive.taskpaper::"
"The location where subtrees should be archived.
The value of this variable is a string, consisting of two parts,
separated by a double-colon. The first part is a file name and
the second part is a heading.
When the file name is omitted, archiving happens in the same
file. A %s formatter in the file name will be replaced by the
current file name without the directory part and file extension.
The archived subtrees will be filed as children of the specified
heading. When the heading is omitted, the subtrees will be filed
at the end of the file. Also in the heading you can use %s to
represent the file name."
:group 'taskpaper
:type 'string)
(defcustom taskpaper-archive-save-context nil
"Non-nil means, add context information when archiving."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-archive-hook nil
"Hook run after successfull archiving of a subtree.
Hook functions are called with point on the subtree in the
original location. At this stage, the subtree has been added to
the archive location, but not yet deleted from the original
location."
:group 'taskpaper
:type 'hook)
(defcustom taskpaper-reverse-note-order nil
"Non-nil means, put new subitems at the beginning of item."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-file-apps
'((directory . emacs)
(remote . emacs)
(auto-mode . emacs))
"External applications for opening files.
The entries in this list are cons cells where the car identifies
files and the cdr the corresponding command.
Possible values for the file identifier are:
string Files with this extension
`directory' Directories
`remote' Remote files
`auto-mode' Files that are matched by any entry in `auto-mode-alist'
`system' System command to open files
t Files not matched by any of the other options
Possible values for the command are:
`emacs' Current Emacs process
`default' Default application for this file type
`system' System command for opening files
`mailcap' Command specified in the mailcaps
string A command to be executed by a shell;
%s will be replaced by the quoted file path
function Lisp function to be called with one argument:
the file path
See also variable `taskpaper-open-non-existing-files'."
:group 'taskpaper
:type '(repeat
(cons
(choice :value ""
(string :tag "Files with extension")
(const :tag "Directories" directory)
(const :tag "Remote files" remote)
(const :tag "Files that have Emacs modes" auto-mode)
(const :tag "System command to open files" system)
(const :tag "Other files" t))
(choice :value ""
(const :tag "Emacs" emacs)
(const :tag "Default application" default)
(const :tag "System command" system)
(const :tag "Mailcap command" mailcap)
(string :tag "Shell command")
(function :tag "Lisp function")))))
(defcustom taskpaper-open-non-existing-files nil
"Non-nil means, open non-existing files in file links.
When nil, an error will be generated. This variable applies only
to external applications because they might choke on non-existing
files. If the link is to a file that will be opened in Emacs, the
variable is ignored."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-open-uri-hook nil
"Hook for functions to open links with an URI scheme.
The value of this hook may be nil, a function, or a list of
functions. The functions will be called for links with an URI
scheme like \"http:\". Each function must take a single argument,
the link URI. When the function does handle the URI, it must
return a non-nil value. If it decides that it is not responsible
for this URI, it must return nil to indicate that TaskPaper mode
can continue to resolve the URI with other options."
:group 'taskpaper
:type 'hook)
(defcustom taskpaper-mark-ring-length 4
"Number of positions to be recorded in the mark ring.
Changing this option requires a restart of Emacs."
:group 'taskpaper
:type 'integer)
(defcustom taskpaper-custom-queries nil
"List of custom queries for fast selection.
Each element in the list should be a list where the first element
is a selection character, the second element is a description
string, and the third element is a query string. If the first
element is a string, it will be used as block separator."
:group 'taskpaper
:type '(repeat
(choice (list (character :tag "Key")
(string :tag "Description")
(string :tag "Query string"))
(list (string :tag "Block separator")))))
(defcustom taskpaper-iquery-default nil
"Non-nil means, querying commands will use `taskpaper-iquery'
instead of default `taskpaper-query'."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-iquery-delay 0.5
"Incremental query evaluation delay in seconds."
:group 'taskpaper
:type 'number)
(defcustom taskpaper-pretty-task-marks t
"Non-nil means, enable the composition display of task marks.
This does not change the underlying buffer content, but it
overlays the UTF-8 character for display purposes only."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-bullet ?\u2013
"Display character for task mark.
See `taskpaper-pretty-task-marks' variable for details."
:group 'taskpaper
:type 'character)
(defcustom taskpaper-bullet-done ?\u2013
"Display character for done task mark.
See `taskpaper-pretty-task-marks' variable for details."
:group 'taskpaper
:type 'character)
(defcustom taskpaper-fontify-done-items t
"Non-nil means, fontify completed items."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-hide-markup nil
"Non-nil means, hide inline markup characters."
:group 'taskpaper
:type 'boolean)
(make-variable-buffer-local 'taskpaper-hide-markup)
(defcustom taskpaper-use-inline-emphasis t
"Non-nil means, interpret emphasis delimiters.
This will interpret \"*\" and \"_\" characters as inline emphasis
delimiters for strong and emphasis markup similar to Markdown."
:group 'taskpaper
:type 'boolean)
(defcustom taskpaper-mode-hook nil
"Hook run when entering `taskpaper-mode'."
:group 'taskpaper
:type 'hook)
;;;; Compatibility code for older Emacsen
;;;; General utility functions
(defun taskpaper-mode-version ()
"Show TaskPaper mode version."
(interactive)
(message "TaskPaper mode version %s" taskpaper-mode-version))
(defun taskpaper-mode-browse-manual ()
"Browse TaskPaper mode manual."
(interactive)
(browse-url taskpaper-mode-manual-uri))
(defun taskpaper-overlay-display (overlay text &optional face evap)
"Make OVERLAY display TEXT with face FACE.
When EVAP is non-nil, set the `evaporate' property to t."
(overlay-put overlay 'display text)
(when face (overlay-put overlay 'face face))
(when evap (overlay-put overlay 'evaporate t)))
(defun taskpaper-new-marker (&optional pos)
"Return a new marker at POS.
If POS is omitted or nil, the value of point is used by default."
(let ((marker (copy-marker (or pos (point)) t))) marker))
(defsubst taskpaper-get-at-bol (prop)
"Get text property PROP at the beginning of line."
(get-text-property (point-at-bol) prop))
(defun taskpaper-release-buffers (blist)
"Release all buffers in list BLIST.
When a buffer is modified, prompt the user to save it first."
(let (file)
(dolist (buf blist)
(setq file (buffer-file-name buf))
(when (and (buffer-modified-p buf) file
(y-or-n-p (format "Save file %s? " file)))
(with-current-buffer buf (save-buffer)))
(kill-buffer buf))))
(defun taskpaper-find-base-buffer-visiting (file)
"Return the base buffer visiting FILE."
(let ((buf (or (get-file-buffer file)
(find-buffer-visiting file))))
(if buf (or (buffer-base-buffer buf) buf) nil)))
(defun taskpaper-in-regexp-p (regexp &optional pos)
"Return non-nil if POS is in a match for REGEXP.
Set the match data. If POS is omitted or nil, the value of point
is used by default. Only current line is checked."
(catch 'exit
(let ((pos (or pos (point))))
(save-excursion
(goto-char pos) (beginning-of-line)
(while (re-search-forward regexp (line-end-position) t)
(when (<= (match-beginning 0) pos (match-end 0))
(throw 'exit t)))))))
(defsubst taskpaper-uniquify (list)
"Non-destructively remove duplicate elements from LIST."
(let ((res (copy-sequence list))) (delete-dups res)))
(defsubst taskpaper-sort (list)
"Non-destructively sort elements of LIST as strings."
(let ((res (copy-sequence list))) (sort res #'string<)))
(defun taskpaper-unlogged-message (&rest args)
"Display a message without logging."
(let ((message-log-max nil)) (apply #'message args)))
(defun taskpaper-escape-double-quotes (str)
"Escape double quotation marks in STR."
(when (stringp str)
(setq str (replace-regexp-in-string "\"" "\\\\\"" str)))
str)
(defun taskpaper-unescape-double-quotes (str)
"Unescape double quotation marks in STR."
(when (stringp str)
(setq str (replace-regexp-in-string "\\\\\"" "\"" str)))
str)
(defun taskpaper-file-path-escape (path)
"Escape special characters in PATH."
(when (stringp path)
(setq path (replace-regexp-in-string " " "\\\\ " path)))
path)
(defun taskpaper-file-path-unescape (path)
"Unescape special characters in PATH."
(when (stringp path)
(setq path (replace-regexp-in-string "\\\\ " " " path)))
path)
(defun taskpaper-file-missing-p (file)
"Test if local FILE exists.
Return non-nil if local FILE does not exist. For performance
reasons remote files are not checked."
(if (and (not (file-remote-p file)) (not (file-exists-p file)))
t
nil))
(defun taskpaper-file-image-p (file)
"Return non-nil if FILE is an image file."
(string-match-p (image-file-name-regexp) file))
(defsubst taskpaper-rear-nonsticky-at (pos)
"Add nonsticky text properties at POS."
(add-text-properties
(1- pos) pos
(list 'rear-nonsticky
'(face mouse-face keymap help-echo display invisible intangible))))
(defconst taskpaper-markup-properties
'(face taskpaper-markup taskpaper-syntax markup invisible taskpaper-markup)
"Properties to apply to inline markup.")
(defun taskpaper-range-property-any (begin end prop vals)
"Check property PROP from BEGIN to END.
Return non-nil if at least one character between BEGIN and END
has a property PROP whose value is one of the given values VALS."
(cl-some (lambda (val) (text-property-any begin end prop val)) vals))
(defun taskpaper-remove-markup-chars (s)
"Remove markup characters from propertized string S."
(let (b)
(while (setq b (text-property-any
0 (length s)
'invisible 'taskpaper-markup s))
(setq s (concat
(substring s 0 b)
(substring s (or (next-single-property-change
b 'invisible s)
(length s)))))))
s)
(defun taskpaper-remove-flyspell-overlays-in (begin end)
"Remove Flyspell overlays in region between BEGIN and END."
(and (bound-and-true-p flyspell-mode)
(fboundp #'flyspell-delete-region-overlays)
(flyspell-delete-region-overlays begin end)))
(defun taskpaper-remap (map &rest commands)
"In keymap MAP, remap the functions given in COMMANDS.
COMMANDS is a list of alternating OLDDEF NEWDEF command names."
(let (olddef newdef)
(while commands
(setq olddef (pop commands) newdef (pop commands))
(define-key map (vector 'remap olddef) newdef))))
(defun taskpaper-add-tag-prefix (name)
"Add tag prefix to NAME.
NAME should be a string or a list of strings."
(cond
((stringp name)
(if (string-prefix-p "@" name) name (concat "@" name)))
((and (listp name) (cl-every #'stringp name))
(mapcar (lambda (x) (if (string-prefix-p "@" x) x (concat "@" x))) name))
(t (error "Argument should be a string or a list of strings."))))
(defun taskpaper-remove-tag-prefix (name)
"Remove tag prefix from NAME.
NAME should be a string or a list of strings."
(cond
((stringp name)
(string-remove-prefix "@" name))
((and (listp name) (cl-every #'stringp name))
(mapcar (lambda (x) (string-remove-prefix "@" x)) name))
(t (error "Argument should be a string or a list of strings."))))
(defun taskpaper-kill-is-subtree-p (&optional text)
"Check if the current kill is a valid subtree.
Return non-nil if the current kill is a valid subtree or a set of
subtrees. If optional TEXT string is given, check it instead of
the current kill."
(save-match-data
(let* ((kill (or text (and kill-ring (current-kill 0)) ""))
(start-level (and (string-match "\\`\\([\t]*[^\t\f\n]\\)" kill)
(- (match-end 1) (match-beginning 1))))
(start (1+ (or (match-beginning 1) -1))))
(if (not start-level) nil
(catch 'exit
(while (setq start (string-match
"^\\([\t]*[^\t\f\n]\\)" kill (1+ start)))
(when (< (- (match-end 1) (match-beginning 1)) start-level)
(throw 'exit nil)))
t)))))
;;;; Re-usable regexps
(defconst taskpaper-tag-name-char-regexp
(concat
"[-a-zA-Z0-9._\u00b7\u0300-\u036f\u203f-\u2040"
"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff\u0370-\u037d"
"\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef"
"\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]")
"Regular expression matching valid tag name character.")
(defconst taskpaper-tag-name-regexp
(format "%s+" taskpaper-tag-name-char-regexp)
"Regular expression matching tag name.")
(defconst taskpaper-tag-value-regexp
"\\(?:\\\\(\\|\\\\)\\|[^()\n]\\)*"
"Regular expression matching tag value.")
(defconst taskpaper-tag-regexp
(format "\\(?:^\\|\\s-+\\)\\(@\\(%s\\)\\(?:(\\(%s\\))\\)?\\)"
taskpaper-tag-name-regexp
taskpaper-tag-value-regexp)
"Regular expression matching tag.
Group 1 matches the whole tag expression.
Group 2 matches the tag name.
Group 3 matches the optional tag value.")
(defconst taskpaper-consec-tags-regexp
(format "\\(?:%s\\)+" taskpaper-tag-regexp)
"Regular expression matching multiple consecutive tags.")
(defconst taskpaper-email-regexp
(concat
"\\("
"\\(?:\\<mailto:\\)?"
"[[:alnum:]!#$%&'*+./=?^_`{|}~-]+@"
"[[:alnum:]]\\(?:[[:alnum:]-]\\{0,61\\}[[:alnum:]]\\)?"
"\\(?:[.][[:alnum:]]\\(?:[[:alnum:]-]\\{0,61\\}[[:alnum:]]\\)?\\)*"
"\\)")
"Regular expression matching plain email link.")
(defconst taskpaper-file-path-regexp
(concat
"\\("
"\\(?:~\\|[.][.]?\\|[a-zA-Z][:]\\)?[/]\\(?:\\\\ \\|[^ \0\n]\\)+"
"\\)")
"Regular expression matching file path.")
(defconst taskpaper-file-link-regexp
(concat "\\(?:^\\|\\s-\\)" taskpaper-file-path-regexp)
"Regular expression matching plain file link.")
(defconst taskpaper-uri-regexp
(concat
"\\<\\("
"\\(?:"
"[a-zA-Z][-a-zA-Z0-9.+]\\{1,31\\}[:]"
"\\(?:[/]\\{1,3\\}\\|[[:alnum:]%]\\)"
"\\|"
"www[0-9]\\{0,3\\}[.]"
"\\)"
"\\(?:"
"[^[:space:]()<>]"
"\\|"
"(\\(?:[^[:space:]()<>]+\\|([^[:space:]()<>]+)\\)*)"
"\\)+"
"\\(?:"
"(\\(?:[^[:space:]()<>]+\\|([^[:space:]()<>]+)\\)*)"
"\\|"
"[^[:space:][:punct:]]"
"\\|"
"[/]"
"\\)"
"\\)")
"Regular expression matching generic URI.")
(defconst taskpaper-markdown-link-regexp
(concat
"\\("
"\\(\\[\\)"
"\\([^][\n]+\\)"
"\\(\\]\\)"
"\\((\\)"
"\\("
"\\(?:"
"\\\\ "
"\\|"
"[^[:space:]()]"
"\\|"
"(\\(?:[^[:space:]()]+\\|([^[:space:]()]+)\\)*)"
"\\)+"
"\\)"
"\\()\\)"
"\\)")
"Regular expression matching Markdown link.
Group 1 matches the entire link expression.
Group 2 matches the opening square bracket.
Group 3 matches the link description.
Group 4 matches the closing square bracket.
Group 5 matches the opening parenthesis.
Group 6 matches the link destination.
Group 7 matches the closing parenthesis.")
(defconst taskpaper-any-link-regexp
(format "\\(%s\\)\\|\\(%s\\)\\|\\(%s\\)\\|\\(%s\\)"
taskpaper-uri-regexp
taskpaper-email-regexp
taskpaper-file-link-regexp
taskpaper-markdown-link-regexp)
"Regular expression matching any link.")
;;;; Font Lock regexps
(defconst taskpaper-task-regexp
"^[ \t]*\\(\\(-\\) +\\([^\n]*\\)\\)$"
"Regular expression matching task.
Group 1 matches the whole task expression.
Group 2 matches the task mark.
Group 3 matches the task name.")
(defconst taskpaper-project-regexp
(format
"^[ \t]*\\(\\([^\n]*\\)\\(:\\)\\(%s\\)?\\)$"
taskpaper-consec-tags-regexp)
"Regular expression matching project.
Group 1 matches the whole project expression.
Group 2 matches the project name.
Group 3 matches the project mark.
Group 4 matches optional trailing tags.")
(defconst taskpaper-note-regexp
"^[ \t]*\\(.*\\S-.*\\)$"
"Regular expression matching note.
Group 1 matches the whole note expression.")
(defconst taskpaper-emphasis-prefix-regexp
"\\(?:^\\|[^\n*_\\]\\)"
"Regular expression matching emphasis prefix.")
(defconst taskpaper-emphasis-suffix-regexp
"\\(?:[^\n*_]\\|$\\)"
"Regular expression matching emphasis suffix.")
(defconst taskpaper-emphasis-text-regexp
(concat
"\\(?:"
"\\(?:\\\\.\\|[^[:space:]*_\\]\\)"
"\\|"
"[^[:space:]*_][^\n]*?\\(?:\\\\.\\|[^[:space:]*_\\]\\)"
"\\)")
"Regular expression matching emphasis text.")
(defconst taskpaper-strong-regexp
(format "%s\\(\\(\\*\\*\\|__\\)\\(%s\\)\\(\\2\\)\\)%s"
taskpaper-emphasis-prefix-regexp
taskpaper-emphasis-text-regexp
taskpaper-emphasis-suffix-regexp)
"Regular expression matching strong inline emphasis.
Group 1 matches the entire expression.
Group 2 matches the opening delimiters.
Group 3 matches the text inside the delimiters.
Group 4 matches the closing delimiters.")
(defconst taskpaper-emphasis-regexp
(format "%s\\(\\(\\*\\|_\\)\\(%s\\)\\(\\2\\)\\)%s"
taskpaper-emphasis-prefix-regexp
taskpaper-emphasis-text-regexp
taskpaper-emphasis-suffix-regexp)
"Regular expression matching inline emphasis.
Group 1 matches the entire expression.
Group 2 matches the opening delimiters.
Group 3 matches the text inside the delimiters.
Group 4 matches the closing delimiters.")
;;;; Faces
(defgroup taskpaper-faces nil
"Faces used in TaskPaper mode."
:group 'taskpaper
:group 'faces)
(defface taskpaper-project-name
'((t :inherit font-lock-function-name-face))
"Face for project names."
:group 'taskpaper-faces)
(defface taskpaper-project-mark
'((t :inherit taskpaper-project-name))
"Face for project marks."
:group 'taskpaper-faces)
(defface taskpaper-task
'((t :inherit default))
"Face for tasks."
:group 'taskpaper-faces)
(defface taskpaper-task-undone-mark
'((t :inherit taskpaper-task))
"Face for undone task marks."
:group 'taskpaper-faces)
(defface taskpaper-task-done-mark
'((t :inherit taskpaper-task))
"Face for done task marks."
:group 'taskpaper-faces)
(defface taskpaper-done-item
`((t :strike-through ,(face-attribute 'shadow :foreground)))
"Face for items marked as complete."
:group 'taskpaper-faces)
(defface taskpaper-note
'((t :inherit font-lock-comment-face))
"Face for notes."
:group 'taskpaper-faces)
(defface taskpaper-tag
'((t :inherit shadow))
"Face for tags."
:group 'taskpaper-faces)
(defface taskpaper-link
'((t :inherit link))
"Face for links."
:group 'taskpaper-faces)
(defface taskpaper-missing-link
'((t :foreground "red" :inherit link))
"Face for file links to non-existing files."
:group 'taskpaper-faces)
(defface taskpaper-emphasis
'((t (:inherit italic)))
"Face for inline emphasis."
:group 'taskpaper-faces)
(defface taskpaper-strong
'((t (:inherit bold)))
"Face for strong inline emphasis."
:group 'taskpaper-faces)
(defface taskpaper-markup
'((t (:slant normal :weight normal :inherit shadow)))
"Face for markup elements."
:group 'taskpaper-faces)
(defface taskpaper-query-error
'((t :foreground "red" :inherit default))
"Face for malformed query string."
:group 'taskpaper-faces)
(defface taskpaper-query-secondary-text
'((t :inherit shadow))
"Face for secondary text in query string."
:group 'taskpaper-faces)
(defface taskpaper-fast-select-key
'((t :weight bold :inherit default))
"Face for selection keys in fast selection dialogs."
:group 'taskpaper-faces)
;;;; Font Lock
(defun taskpaper-face-from-face-or-color (inherit face-or-color)
"Create a face list that set the color and inherits INHERIT.
When FACE-OR-COLOR is not a string, just return it."
(if (stringp face-or-color)
(list :inherit inherit
taskpaper-faces-easy-properties face-or-color)
face-or-color))
(defun taskpaper-get-tag-face (tag)
"Get the right face for TAG.
If TAG is a number, get the corresponding match group."
(let ((tag (if (wholenump tag) (match-string tag) tag)))
(or (taskpaper-face-from-face-or-color
'taskpaper-tag (cdr (assoc tag taskpaper-tag-faces)))
'taskpaper-tag)))
(defvar taskpaper-mouse-map-tag
(let ((map (make-sparse-keymap)))
(define-key map [mouse-1] #'taskpaper-query-tag-at-point)
map)
"Mouse events for tags.")
(defun taskpaper-font-lock-tags (limit)
"Fontify tags from point to LIMIT."
(when (re-search-forward taskpaper-tag-regexp limit t)
(if (not (taskpaper-in-tag-p))
;; Move forward and recursively search again
(progn
(goto-char (min (1+ (match-beginning 1)) limit))
(when (< (point) limit)
(taskpaper-font-lock-tags limit)))
;; Fontify
(taskpaper-remove-flyspell-overlays-in
(match-beginning 1) (match-end 1))
(add-text-properties
(match-beginning 1) (match-end 1)
(list 'taskpaper-syntax 'tag
'face (taskpaper-get-tag-face 2)
'mouse-face 'highlight
'keymap taskpaper-mouse-map-tag))
(taskpaper-rear-nonsticky-at (match-end 1)))
t))
(defun taskpaper-get-link-type (link)
"Return type of LINK as symbol."
(let* ((fmt "\\`%s\\'")
(re-file (format fmt taskpaper-file-path-regexp))
(re-email (format fmt taskpaper-email-regexp))
(re-uri (format fmt taskpaper-uri-regexp)))
(cond ((string-match-p re-file link) 'file)
((string-match-p re-email link) 'email)
((string-match-p re-uri link) 'uri)
(t nil))))
(defun taskpaper-get-link-face (link)
"Get the right face for LINK."
(if (and (eq (taskpaper-get-link-type link) 'file)
(taskpaper-file-missing-p
(expand-file-name (taskpaper-file-path-unescape link))))
'taskpaper-missing-link
'taskpaper-link))
(defvar taskpaper-mouse-map-link
(let ((map (make-sparse-keymap)))
(define-key map [mouse-1] #'taskpaper-open-link-at-point)
map)
"Mouse events for links.")
(defun taskpaper-font-lock-markdown-links (limit)
"Fontify Markdown links from point to LIMIT."
(when (re-search-forward taskpaper-markdown-link-regexp limit t)
(taskpaper-remove-flyspell-overlays-in
(match-beginning 1) (match-end 1))
(let ((link (match-string-no-properties 6)))
(add-text-properties
(match-beginning 3) (match-end 3)
(list 'taskpaper-syntax 'markdown-link
'face (taskpaper-get-link-face link)
'mouse-face 'highlight
'keymap taskpaper-mouse-map-link
'help-echo link)))
(add-text-properties
(match-beginning 2) (match-end 2) taskpaper-markup-properties)
(add-text-properties
(match-beginning 4) (match-end 7) taskpaper-markup-properties)
(taskpaper-rear-nonsticky-at (match-end 1))
t))
(defun taskpaper-font-lock-email-links (limit)
"Fontify plain email links from point to LIMIT."
(when (re-search-forward taskpaper-email-regexp limit t)
(if (taskpaper-range-property-any
(match-beginning 1) (match-end 1)
'taskpaper-syntax '(markup))
;; Move forward and recursively search again
(progn
(goto-char (min (1+ (match-beginning 1)) limit))
(when (< (point) limit)
(taskpaper-font-lock-email-links limit)))
;; Fontify
(taskpaper-remove-flyspell-overlays-in
(match-beginning 1) (match-end 1))
(let ((link (match-string-no-properties 1)))
(add-text-properties
(match-beginning 1) (match-end 1)
(list 'taskpaper-syntax 'plain-link
'face (taskpaper-get-link-face link)
'mouse-face 'highlight
'keymap taskpaper-mouse-map-link
'help-echo link)))
(taskpaper-rear-nonsticky-at (match-end 1))
t)))
(defun taskpaper-font-lock-uri-links (limit)
"Fontify plain URI links from point to LIMIT."
(when (re-search-forward taskpaper-uri-regexp limit t)
(if (taskpaper-range-property-any
(match-beginning 1) (match-end 1)
'taskpaper-syntax '(markup))
;; Move forward and recursively search again
(progn
(goto-char (min (1+ (match-beginning 1)) limit))
(when (< (point) limit)
(taskpaper-font-lock-uri-links limit)))
;; Fontify
(taskpaper-remove-flyspell-overlays-in
(match-beginning 1) (match-end 1))
(let ((link (match-string-no-properties 1)))
(add-text-properties
(match-beginning 1) (match-end 1)
(list 'taskpaper-syntax 'plain-link
'face (taskpaper-get-link-face link)
'mouse-face 'highlight
'keymap taskpaper-mouse-map-link
'help-echo link)))
(taskpaper-rear-nonsticky-at (match-end 1))
t)))
(defun taskpaper-font-lock-file-links (limit)
"Fontify plain file links from point to LIMIT."
(when (re-search-forward taskpaper-file-link-regexp limit t)
(if (taskpaper-range-property-any
(match-beginning 1) (match-end 1)
'taskpaper-syntax '(markup))
;; Move forward and recursively search again
(progn
(goto-char (min (1+ (match-beginning 1)) limit))
(when (< (point) limit)
(taskpaper-font-lock-file-links limit)))
;; Fontify
(taskpaper-remove-flyspell-overlays-in
(match-beginning 1) (match-end 1))
(let ((link (match-string-no-properties 1)))
(add-text-properties
(match-beginning 1) (match-end 1)
(list 'taskpaper-syntax 'plain-link
'face (taskpaper-get-link-face link)
'mouse-face 'highlight
'keymap taskpaper-mouse-map-link
'help-echo link)))
(taskpaper-rear-nonsticky-at (match-end 1))
t)))
(defun taskpaper-font-lock-done-tasks (limit)
"Fontify completed tasks from point to LIMIT."
(when (re-search-forward taskpaper-task-regexp limit t)
(when (save-excursion
(save-match-data
(taskpaper-item-has-attribute "done")))
(font-lock-prepend-text-property
(match-beginning 2) (match-end 2)
'face 'taskpaper-task-done-mark)
(font-lock-prepend-text-property
(match-beginning 3) (match-end 3)
'face 'taskpaper-done-item))
t))
(defun taskpaper-font-lock-done-projects (limit)
"Fontify completed projects from point to LIMIT."
(when (re-search-forward taskpaper-project-regexp limit t)