-
Notifications
You must be signed in to change notification settings - Fork 3
/
lister.el
1837 lines (1594 loc) · 76.9 KB
/
lister.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
;;; lister.el --- Yet another list printer -*- lexical-binding: t; -*-
;; Copyright (C) 2018-2021, 2023
;; Author: <joerg@joergvolbers.de>
;; Version: 0.9.6
;; Package-Requires: ((emacs "26.1"))
;; Keywords: lisp
;; URL: https://github.com/publicimageltd/lister
;; 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:
;; A library for creating interactive list buffers.
;;; Code:
(require 'cl-lib)
(require 'ewoc)
;;; * Global Variables
(defcustom lister-mark-face-or-property
'(:background "darkorange3"
:foreground "white")
"Text properties to be added when highlighting marked items.
Possible values are either a plist of face attributes or the name
of a face."
:group 'lister
:type '(choice (face :tag "Name of a face")
(plist :tag "Plist of face attributes")))
;;; * Buffer Local Variables
(defvar-local lister-local-left-margin 2
"Default margin (integer length) for every item.")
(defvar-local lister-local-padding-string " "
"Padding string for indentation.
Each item is inserted padded with this string, n times for
indentation level n.")
(defvar-local lister-local-mapper nil
"Buffer local mapper function for printing lister list items.
The mapper function converts a data object (any kind of list
object) to a list of strings, which will then be inserted as a
representation of that data.")
(defvar-local lister-local-ewoc nil
"Buffer local store of the ewoc object.
This is useful for interactive functions called in the Lister
buffer.")
(defvar-local lister-local-filter nil
"Buffer local filter predicate for Lister lists.
Do not set this directly; use `lister-set-filter' instead.")
(defvar-local lister-local-marking-predicate nil
"Buffer local marking predicate.
Do not set this directly; use `lister-set-marking-predicate' instead.")
(defvar-local lister-local-modified nil
"Buffer local modified flag.
This will be non-nil once an item has been deleted or inserted.")
;;; * Data Types
(cl-defstruct (lister--item (:constructor lister--item-create))
"The list item plus some additional extra informations.
The slot DATA contains the 'real' data, which is printed using
the mapper."
;; do not add slots 'visible' and 'with-new-level', they
;; would interfere with the functions defined below
level marked invisible beg end data)
(defun lister--item-visible (item-struct)
"For ITEM-STRUCT, get negated value of of `lister--item-invisible'."
(not (lister--item-invisible item-struct)))
;;; * Functions used as item accessors or for creating list items
(defun lister--item-with-new-level (item level)
"Set the LEVEL of ITEM and return ITEM."
(setf (lister--item-level item) level)
item)
(defun lister--new-item-from-data (data &optional level)
"Create a new lister item storing DATA and LEVEL."
(lister--item-create :data data :level level))
(defun lister--cleaned-item (item)
"Return ITEM cleaned of position and visibility information.
Returns ITEM cleaned of all information which will be overwritten
anyways when re-inserting the item."
(setf (lister--item-beg item) nil
(lister--item-end item) nil
(lister--item-invisible item) nil)
item)
;;; * Helper
;; use this instead of flatten-tree for emacsen < 27.1:
(defun lister--flatten (l)
"Flatten the list L, removing any null values.
This is a simple copy of dash's `-flatten' using `seq'."
(if (and (listp l) (listp (cdr l)))
(seq-mapcat #'lister--flatten l)
(list l)))
(defmacro lister-with-node (ewoc pos var &rest body)
"Bind VAR to the node at POS and execute BODY if node exists.
EWOC is an ewoc structure."
(declare (indent 3) (debug (sexp sexp symbolp body)))
`(when-let ((,var (lister--parse-position ,ewoc ,pos)))
,@body))
(defmacro lister-with-node-or-error (ewoc pos var error &rest body)
"Bind VAR to the node at POS and execute BODY if node exists.
Else, execute ERROR. EWOC is an ewoc structure."
(declare (indent 3))
`(let ((,var (lister--parse-position ,ewoc ,pos)))
(if (not ,var)
,error
,@body)))
(defmacro lister-save-current-node (ewoc &rest body)
"In EWOC, save point, execute BODY and restore point.
Keep track of the position using the node's data object. It is
safe to delete and reinsert the node in BODY. Do not restore
point if the node's data object is invalid or not re-inserted
after BODY exits."
(declare (indent 2) (debug (sexp body)))
(let ((pos-var (gensym "--pos--"))
(item-var (gensym "--item--"))
(footerp-var (gensym "--footer-p--"))
(eobp-var (gensym "--eobp--")))
`(let* ((,footerp-var (lister-eolp))
(,eobp-var (eobp))
(,item-var (unless ,footerp-var
(ewoc-data (lister--parse-position ,ewoc :point)))))
,@body
(if ,footerp-var
(if ,eobp-var
(goto-char (point-max))
(ewoc-goto-node ,ewoc (ewoc--footer ,ewoc)))
;; this works because our custom ewoc printer always keeps the item
;; slots "beg" and "end" up to date:
(when-let ((,pos-var (and ,item-var (lister--item-beg ,item-var))))
(with-current-buffer (marker-buffer ,pos-var)
(goto-char ,pos-var)))))))
;;; * Low-level printing / insertion:
;;;
;; Insert the item strings:
(defun lister--padding-string (margin level s)
"Return string for padding.
Return a string which adds on a new string of MARGIN spaces LEVEL
times the string S. If LEVEL is nil, use 0 instead."
(concat
(make-string margin ? )
(string-join (make-list (or level 0) s))))
;; TODO Write tests
(defun lister--get-prop (string prop &optional start end)
"Get all occurences of PROP between START and END of STRING."
(let* (acc next-pos
(start (or start 0))
(end (or end (length string)))
(pos start))
(while (and pos (setq pos (if (get-text-property pos prop string)
pos
(next-single-property-change pos prop string)))
(< pos end))
(setq next-pos (next-single-property-change pos prop string))
(push `(,pos ,(or next-pos end)) acc)
(setq pos next-pos))
(nreverse acc)))
;;; TODO Write tests
(defun lister--make-string-intangible (string)
"Make STRING intangible except where it has certain properties.
Do not make those parts of STRING intangible where the properties
`field' or `button' are non-nil."
(add-text-properties 0 (length string) '(cursor-intangible t) string)
(cl-dolist (prop '(field button))
(pcase-dolist (`(,from ,to) (lister--get-prop string prop))
(add-text-properties
;; The first field character is not tangible, even though
;; `describe-text-properties' says it has `cursor-intangible'
;; set to nil. So we correct that. I don't understand why this
;; works, however. Something with stickiness, I think.
(max 0 (1- from))
to
'(cursor-intangible nil) ;; maybe add a special field face?
string)))
string)
(defun lister--insert-intangible (strings padding-level)
"In current buffer, insert all STRINGS with text property \\='intangible'.
Insert a newline after each single string in STRINGS. Pad all
strings according to PADDING-LEVEL and the buffer local value of
`lister-local-left-margin'."
(when strings
(let* ((padding-string (lister--padding-string lister-local-left-margin
padding-level
lister-local-padding-string))
(strings (mapcar (apply-partially #'concat padding-string)
strings)))
;; Assumes rear-stickiness.
(insert (lister--make-string-intangible
(string-join strings "\n"))
"\n")))) ;; <- this leaves the "tangible" gap for the next item!
;; Insert header / footer
;; The ewoc pretty printer does not know if it prints a header or a
;; footer, but we need to handle them differently for setting text
;; properties. So we wrap our own functions around.
;;; TODO Write tests
(defun lister--get-hf-strings (hf-data)
"Use HF-DATA to get strings for a header or footer.
If HF-DATA is a function, call it with the current ewoc buffer
active and use the result. Return HF-DATA if it is nil or a
list. If HF-DATA is a string, wrap it into a list. In any case,
flatten the list and return nil values. If HF-DATA is not a
function, nor a string, nor a list, nor nil, throw an error."
(when (functionp hf-data)
(setq hf-data (funcall hf-data)))
(cl-etypecase hf-data
(string (list hf-data))
;; this also catches nil:
(list (lister--flatten hf-data))))
(defun lister--insert-as-hf (hf-data)
"In current Lister buffer, insert HF-DATA as header or footer.
HF-DATA can be either a function, a string or a list of strings."
(pcase-let ((`(,type ,data) hf-data))
(when data
(let* ((strings (lister--get-hf-strings data))
(beg (point))
(lister-local-left-margin 0))
;; Propertize footer for eolp recognition:
(when (and strings (eq type 'footer))
(setq strings (mapcar (lambda (s) (propertize s 'footer t)) strings)))
;; insert...
(lister--insert-intangible strings 0)
;; ..and close cursor gap in the header:
(when (eq type 'header)
(put-text-property beg (1+ beg) 'front-sticky t))))))
(defun lister--get-hf (ewoc)
"Get header and footer of EWOC."
(pcase-let ((`(,header . ,footer) (ewoc-get-hf ewoc)))
;; ewoc-create initializes with "" if header arg is nil
(list (if (eq header "") (list 'header "") header)
(if (eq footer "") (list 'footer "") footer))))
(defun lister-refresh-header-footer (ewoc)
"Redisplay the header and the footer of EWOC.
Mostly makes sense if one them has a function as its data."
(apply #'ewoc-set-hf ewoc (lister--get-hf ewoc)))
(defun lister-set-header (ewoc strings-or-fn)
"Set STRINGS-OR-FN as a list header in EWOC.
Use STRINGS-OR-FN to determine the header. It can be a function,
a string or a list of strings."
(pcase-let ((`(_ ,footer) (lister--get-hf ewoc)))
(ewoc-set-hf ewoc (list 'header strings-or-fn) footer)))
(defun lister-set-footer (ewoc strings-or-fn)
"Set STRINGS-OR-FN as a list footer in EWOC.
Use STRINGS-OR-FN to determine the footer. It can be a function,
a string or a list of strings."
(pcase-let ((`(,header _) (lister--get-hf ewoc)))
(ewoc-set-hf ewoc header (list 'footer strings-or-fn))))
;; Make the item invisible / visible:
(defun lister--invisibilize-item (item value)
"For ITEM in current buffer, set the property \\='invisible' to VALUE.
Respect the cursor gap so that the user cannot navigate to
invisible items. The VALUE t hides the item, nil makes it
visible."
(let* ((inhibit-read-only t)
(beg (lister--item-beg item))
(end (lister--item-end item))
;; we don't want to store any values, just t or nil:
(value (not (not value))))
;;
(put-text-property beg end 'invisible value)
;; this opens or closes the gap for the marker:
(put-text-property beg (1+ beg) 'front-sticky value)
;; store the status in the lister item:
(setf (lister--item-invisible item) value)))
(defun lister--invisibilize-node (node value)
"For NODE in current buffer, set the property \\='invisible' to VALUE.
Respect the cursor gap so that the user cannot navigate to
invisible items. The VALUE t hides the item, nil makes it
visible."
(lister--invisibilize-item (ewoc-data node) value))
;; Mark / unmark the item:
(defun lister--add-face-property (beg end value &optional append)
"Add VALUE to the face property between BEG and END."
(add-face-text-property beg end value append))
(defun lister--remove-face-property (beg end value)
"Remove VALUE from the face property from BEG to END.
This is a slightly modified copy of `font-lock--remove-face-from-text-property'."
(let ((beg (text-property-not-all beg end 'face nil))
next prev)
(while beg
(setq next (next-single-property-change beg 'face nil end)
prev (get-text-property beg 'face))
(cond ((or (atom prev)
(keywordp (car prev))
(eq (car prev) 'foreground-color)
(eq (car prev) 'background-color))
(when (eq value prev)
(remove-list-of-text-properties beg next (list 'face))))
((memq value prev) ;Assume prev is not dotted.
(let ((new (remq value prev)))
(cond ((null new)
(remove-list-of-text-properties beg next (list 'face)))
((= (length new) 1)
(put-text-property beg next 'face (car new)))
(t
(put-text-property beg next 'face new))))))
(setq beg (text-property-not-all next end 'face nil)))))
(defun lister--update-mark-state (item)
"Modify ITEM according to its marked state."
(let* ((inhibit-read-only t)
(beg (lister--item-beg item))
(end (lister--item-end item)))
(if (lister--item-marked item)
(lister--add-face-property beg end lister-mark-face-or-property)
(lister--remove-face-property beg end lister-mark-face-or-property))))
;; The actual printer for the ewoc:
;; NOTE Does storing three markers in each node
;; (ewoc--node-start-marker,beg and end) lead to a performance
;; problem?
(defun lister--ewoc-printer (item)
"Insert pretty printed ITEM in the current buffer.
ITEM is a `lister--item', that is, the data hold in the local
Ewoc's node. Note that the `lister--item' is thus not just the
data to be printed, but rather a structure with additional
information which also holds the actual data. Build the item by
passing this data object to the buffer local mapper function,
which must return a string or a list of strings."
(unless lister-local-mapper
(error "No buffer local mapper function defined"))
(let* ((inhibit-read-only t)
(strings (funcall lister-local-mapper (lister--item-data item)))
;; make sure STRINGS is a list:
(strings (if (listp strings) strings (list strings)))
;; flatten it and remove nil values:
(strings (lister--flatten strings))
(beg (point-marker)))
;; FIXME If insertion happens before (!) on invisible overlay,
;; e.g. if refreshing the top node of a hidden sublist, the
;; ellipsis is not added at the end of this item, but rather
;; displayed on a separate line. Could not find a simple fix for
;; this.
;; actual printing: insert the strings at point
(lister--insert-intangible strings (lister--item-level item))
;; store positions for post-insertion modifications
(set-marker-insertion-type beg t)
(setf (lister--item-beg item) beg
(lister--item-end item) (point-marker))
;; maybe display marker:
(when (lister--item-marked item)
(lister--update-mark-state item))
;; maybe hide item:
(when (and lister-local-filter
(funcall lister-local-filter (lister--item-data item))
(not (lister--item-invisible item)))
(lister--invisibilize-item item t))))
(defun lister--parse-position (ewoc pos)
"Return node according to POS in EWOC.
POS can be one of the symbols `:point', `:first', `:next',
`:prev', `:last', or an integer indicating the nth node counting
from 0. All positions refer to the actual list; any filtering is
ignored.
If POS is a node, pass it through.
If POS is neither a node, nor an integer, nor one of the symbols
above, throw an error."
(unless ewoc
(error "%s is not a valid ewoc object" ewoc))
;; ewoc-locate uses (point) without setting the current buffer,
;; so we do it instead:
(with-current-buffer (ewoc-buffer ewoc)
(pcase pos
(:first (ewoc-nth ewoc 0))
(:last (ewoc-nth ewoc -1))
(:point (ewoc-locate ewoc))
(:next (ewoc-next ewoc (ewoc-locate ewoc)))
(:prev (ewoc-prev ewoc (ewoc-locate ewoc)))
;; FIXME Bypass header/footer: (ewoc--node-nth (ewoc--dll ewoc) n)
((pred integerp) (or (ewoc-nth ewoc pos)
(error "Index out of bounds: %d" pos)))
;; I couldn't find any predicate ewoc--node-p or alike, even
;; though ewoc--node is defined as a cl struct. So we do some
;; very minimal testing only:
((pred vectorp) pos)
(_ (error "Unkown position argument: %s" pos)))))
;; TODO Write tests
(defun lister-eolp (&optional buf)
"Return non-nil if point is after the last item in BUF.
Use the current buffer or BUF."
(with-current-buffer (or buf (current-buffer))
(or (eobp)
(get-text-property (point) 'footer))))
(defun lister--determine-level (prev-level new-level)
"Return the level for new items relative to PREV-LEVEL.
If PREV-LEVEL is nil, assume the new item to be a top item.
NEW-LEVEL can be nil or an integer requesting a level. Return
the level value which is appropriate considering the value of
PREV-LEVEL."
(max 0
(cond
((null prev-level) 0) ;; top item: always indent with 0
((null new-level) prev-level)
((> new-level prev-level) (1+ prev-level))
(t new-level))))
(defun lister--debug-nodes (&rest nodes)
"Return only the items contained in NODES."
(mapcar #'ewoc-data (lister--flatten nodes)))
;;; * Low level list movement basics
(cl-defun lister--next-node-matching (ewoc node pred-fn
&optional
(move-fn #'ewoc-next)
limit)
"Starting from NODE, find the next node matching PRED-FN via MOVE-FN.
Return the next matching node or nil if there is none.
Optionally stop search unconditionally when reaching the node
LIMIT, returning nil. To move backwards, set MOVE-FN to
`ewoc-prev'. EWOC is an Ewoc object."
(while (and node
(setq node (funcall move-fn ewoc node))
(or (not (eq limit node))
(setq node nil))
(not (funcall pred-fn node))))
node)
(cl-defun lister--next-or-this-node-matching (ewoc node pred-fn
&optional (move-fn #'ewoc-next)
limit)
"In EWOC, check NODE against PRED-FN or find next match.
Return the matching node found by iterating MOVE-FN, or nil if
there is none. Optionally stop search unconditionally when
reaching the node LIMIT, returning nil. To move backwards, set
MOVE-FN to `ewoc-prev'."
(while (and node
(or (not (eq node limit))
(setq node nil))
(not (funcall pred-fn node)))
(setq node (funcall move-fn ewoc node)))
node)
(defun lister--next-node-same-level (ewoc pos move-fn)
"Find next node with POS's level, skipping items with bigger indentation.
In EWOC, use MOVE-FN to find the next node with the same level as
POS, skipping nodes with bigger indentation. Return nil if no
next node is found."
(lister-with-node ewoc pos node
(let ((level (lister-get-level-at ewoc node)))
;; this is actually a copy of lister--next-node-matching
;; but hey, it seems so awkward to define a lambda for that!
(while (and node
(setq node (funcall move-fn ewoc node))
(> (lister--item-level (ewoc-data node)) level)))
;; now node is either nil or <= level
(and node
(and (= (lister--item-level (ewoc-data node)) level)
node)))))
(cl-defun lister--next-visible-node (ewoc node &optional
(move-fn #'ewoc-next))
"In EWOC, move from NODE to next visible node via MOVE-FN.
Return the next node or nil if there is none. To move backwards,
set MOVE-FN to `ewoc-prev'."
(lister--next-node-matching ewoc node #'lister-node-visible-p move-fn))
(cl-defun lister--prev-visible-node (ewoc node)
"In EWOC, move from NODE to previous visible node.
Return the next node or nil if there is none."
(lister--next-node-matching ewoc node #'lister-node-visible-p #'ewoc-prev))
(defun lister--first-visible-node (ewoc &optional node)
"Find the first visible node in EWOC, beginning with NODE.
If NODE is nil, return the first visible node of the EWOC."
(lister--next-or-this-node-matching ewoc (or node (ewoc-nth ewoc 0))
#'lister-node-visible-p))
(defun lister--last-visible-node (ewoc &optional node)
"Find the last visible node in EWOC, searching backwards from NODE.
If NODE is nil, return the last visible node of the EWOC."
(lister--next-or-this-node-matching ewoc (or node (ewoc-nth ewoc -1))
#'lister-node-visible-p
#'ewoc-prev))
;;; * Public API
(defun lister-get-ewoc (buf)
"Get ewoc object associated with BUF."
;; see https://github.com/alphapapa/emacs-package-dev-handbook#accessing-buffer-local-variables
(buffer-local-value 'lister-local-ewoc buf))
(defun lister-modified-p (ewoc)
"Return non-nil if items in EWOC have been modified."
(buffer-local-value 'lister-local-modified (ewoc-buffer ewoc)))
(defun lister-set-modified-p (ewoc &optional flag)
"Mark EWOC as modified according to FLAG."
(with-current-buffer (ewoc-buffer ewoc)
(setq-local lister-local-modified flag)))
(defmacro lister-with-boundaries (ewoc beg-var end-var &rest body)
"In EWOC, do BODY binding BEG-VAR and END-VAR to list nodes.
BEG and END have to be variable names. When executing BODY, bind
BEG and END to the nodes indicated by the current value of these
variables. All values understood by `lister--parse-position' are
accepted. If either variable is nil at runtime or yet unbound,
bind it to very first or to the very last node, respectively.
Do nothing if list is empty."
(declare (indent 3) (debug (sexp symbolp symbolp body)))
(unless (and (symbolp beg-var) (not (keywordp beg-var)))
(signal 'wrong-type-argument (list 'symbolp beg-var)))
(unless (and (symbolp end-var) (not (keywordp end-var)))
(signal 'wrong-type-argument (list 'symbolp end-var)))
`(when-let ((,beg-var (lister--parse-position ,ewoc (or ,beg-var :first)))
(,end-var (lister--parse-position ,ewoc (or ,end-var :last))))
,@body))
;; * Basic Loop Macros
(cl-defmacro lister-dolist-nodes ((ewoc var &optional beg end) &rest body)
"In EWOC, execute BODY looping over a list of nodes.
The first argument is a list with the following arguments:
EWOC - An ewoc object.
VAR - A variable which is bound to the current node in the loop.
[BEG] - Optionally a beginning node, or a position which can be
parsed by `lister--parse-position'.
[END] - Optionally a final node, or a position.
If BEG or END are not provided or nil, use the first or the last
item of the list, respectively.
The node bound to VAR can be safely destroyed without quitting
the loop.
BODY is wrapped in an implicit `cl-block'. To quit it
immediately, use (cl-return).
\(fn (EWOC VAR [BEG] [END]) BODY...)"
(declare (indent 1) (debug ((sexp symbolp &optional sexp sexp)
body)))
(let ((temp-node (gensym "--temp-node--"))
(last-var (gensym "--last-node--")))
`(let ((,var (lister--parse-position ,ewoc (or ,beg :first)))
(,last-var (lister--parse-position ,ewoc (or ,end :last))))
(cl-block nil
(while ,var
;; get the next node before BODY, since BODY might delete
;; the current node pointer:
(let ((,temp-node (ewoc-next ,ewoc ,var)))
,@body
(setq ,var (unless (eq ,var ,last-var)
,temp-node))))))))
(cl-defmacro lister-dolist ((ewoc var &optional beg end node-var)
&rest body)
"In EWOC, execute BODY looping over a list of item data.
The first argument is a list with the following arguments:
EWOC - An ewoc object.
VAR - A variable which is bound to the current data in the loop.
[BEG] - Optionally a beginning node, or a position which can be
parsed by `lister--parse-position'.
[END] - Optionally a final node, or a position.
[NODE-VAR] - Optionally bind the data's node.
If BEG or END are not provided or nil, use the first or the last
item of the list, respectively.
BODY is wrapped in an implicit `cl-block'. To quit it
immediately, use (cl-return).
\(fn (EWOC VAR [BEG] [END] [NODE-VAR]) BODY...)"
(declare (indent 1) (debug ((sexp symbolp &optional sexp sexp symbolp)
body)))
(let ((node-sym (or node-var (gensym "--node--"))))
`(lister-dolist-nodes (,ewoc ,node-sym ,beg ,end)
(let ((,var (lister--item-data (ewoc-data ,node-sym))))
,@body))))
;; * More Specific Loop Functions
(defun lister-collect-list (ewoc &optional beg end pred-fn map-fn)
"In EWOC, collect and optionally transform all data between BEG and END.
BEG and END are positions understood by `lister--parse-position'
and point to the first or the last node to be considered. If
nil, use the first or the last node of the complete list instead.
If PRED-FN is set, only consider those nodes for which PRED-FN,
when called with the node's data, returns true. If MAP-FN is
set, call MAP-FN on the node's data before collecting it."
(let (acc)
(lister-dolist (ewoc data beg end)
(when (or (not pred-fn)
(funcall pred-fn data))
(push (funcall (or map-fn #'identity) data) acc)))
(nreverse acc)))
(defun lister-collect-nodes (ewoc &optional beg end pred-fn map-fn)
"In EWOC, collect and optionally transform all nodes between BEG and END.
Return all nodes between BEG and END. BEG and END are positions
understood by `lister--parse-position' and point to the first or
the last node to be considered. If nil, use the first or the
last node of the complete list instead. When PRED-FN is set,
only return those nodes for which PRED-FN, called with the node,
returns true. If MAP-FN is set, call MAP-FN on the node before
collecting it."
(let (acc)
(lister-dolist-nodes (ewoc node beg end)
(when (or (not pred-fn)
(funcall pred-fn node))
(push (funcall (or map-fn #'identity) node) acc)))
(nreverse acc)))
(defun lister-update-list (ewoc action-fn &optional beg end pred-fn)
"Apply ACTION-FN on each item's data and redisplay it.
In EWOC, call ACTION-FN on each item data within the node
positions BEG and END. If BEG or END is nil, use the first or
the last node instead.
ACTION-FN is called with one argument, the item's data. Update
the item if ACTION-FN returns a non-nil value. If PRED-FN is
set, restrict action only to matching nodes."
(let (new-data)
(lister-dolist (ewoc data beg end node)
(when (and (or (not pred-fn)
(funcall pred-fn data))
(setq new-data (funcall action-fn (cl-copy-seq data))))
(lister-replace-at ewoc node new-data)))))
(defun lister-walk-nodes (ewoc action-fn &optional beg end pred-fn)
"Apply ACTION-FN on each item's node.
In EWOC, call ACTION-FN on each item's node within the node
positions BEG and END. If BEG or END is nil, use the first or
the last node instead.
ACTION-FN is called with two arguments: the EWOC and the node.
It is up to ACTION-FN to redisplay the node. If PRED-FN is set,
restrict action only to matching nodes.
Return the number of processe nodes."
(let ((n-counter 0))
(lister-dolist-nodes (ewoc node beg end)
(when (or (not pred-fn)
(funcall pred-fn node))
(funcall action-fn ewoc node)
(cl-incf n-counter)))
n-counter))
;; * Some stuff which does not fit anywhere else
(defun lister-empty-p (ewoc)
"Return t if EWOC has no list."
(null (ewoc-nth ewoc 0)))
(defun lister-node-in-region-p (node node-beg node-end)
"Check if NODE is part of the list from nodes NODE-BEG to NODE-END.
Do only check the positions of the nodes, not their content."
(and (>= (ewoc-location node) (ewoc-location node-beg))
(<= (ewoc-location node) (ewoc-location node-end))))
;; * Goto Nodes
(defun lister-goto (ewoc pos)
"In EWOC, move point to POS.
POS can be either an ewoc node, an index position, or one of the
symbols `:first', `:last', `:point' (sic!), `:next' or `:prev'.
Do nothing if position does not exist; throw an error if position
is invalid (i.e. index is out of bounds) or invisible."
(lister-with-node ewoc pos node
(if (lister--item-visible (ewoc-data node))
(ewoc-goto-node ewoc node)
(error "Cannot go to invisible item %s" pos))))
;; * Inspect Nodes
(defalias 'lister-get-node-at 'lister--parse-position)
(defun lister-node-get-level (node)
"Get the indentation level of NODE."
(lister--item-level (ewoc-data node)))
(defun lister-set-node-level (ewoc node level)
"In EWOC, set indentation of NODE to LEVEL, refreshing it."
(setf (lister--item-level (ewoc-data node)) level)
(ewoc-invalidate ewoc node))
(defun lister-get-level-at (ewoc pos)
"In EWOC, get the indentation level of the item at POS.
Do nothing if POS is nil."
(lister-with-node ewoc pos node
(lister-node-get-level node)))
(defun lister-set-level-at (ewoc pos level)
"In EWOC, set indentation of node at POS to LEVEL, refreshing it.
Do nothing if POS is nil."
(lister-with-node ewoc pos node
(lister-set-node-level ewoc node level)))
(defun lister-node-get-data (node)
"Get the item data stored in NODE."
(lister--item-data (ewoc-data node)))
(defun lister-get-data-at (ewoc pos)
"In EWOC, return the data of the lister node at POS.
POS can be either an ewoc node, an index position, or one of the
symbols `:first', `:last', `:point', `:next' or `:prev'."
(lister-with-node-or-error ewoc pos node
(error "No node or lister item at position %s" pos)
(lister-node-get-data node)))
(defun lister-set-data-at (ewoc pos data)
"In EWOC, replace the data at POS with DATA."
(lister-with-node-or-error ewoc pos node
(error "No node or lister item at position %s" pos)
(lister-set-modified-p ewoc t)
(setf (lister--item-data (ewoc-data node)) data)
(ewoc-invalidate ewoc node)))
(defalias 'lister-replace-at 'lister-set-data-at)
(defun lister-node-visible-p (node)
"In EWOC, test if NODE is visible."
(lister--item-visible (ewoc-data node)))
(defalias 'lister-item-visible-p 'lister--item-visible)
(defun lister-node-marked-p (node)
"In EWOC, test if NODE is in a marked state."
(lister--item-marked (ewoc-data node)))
(defalias 'lister-item-marked-p 'lister--item-marked)
(defun lister-node-marked-and-visible-p (node)
"In EWOC, test if NODE is marked and visible."
(let ((item (ewoc-data node)))
(and (lister--item-marked item)
(lister--item-visible item))))
;; * Delete Items
(defun lister-delete-at (ewoc pos)
"In EWOC, delete node specified by POS."
(lister-set-modified-p ewoc t)
(let* ((node (lister--parse-position ewoc pos))
(inhibit-read-only t)
(item (ewoc-data node)))
;; deleting the marker saves memory and makes sure that
;; `lister-save-current-node' works well
(setf (lister--item-beg item) nil
(lister--item-end item) nil)
(ewoc-delete ewoc node)))
(defun lister-delete-list (ewoc beg end)
"In EWOC, delete all nodes from BEG up to END.
BEG and END is a node or a position, or nil standing for the
first or the last node, respectively."
(lister-dolist-nodes (ewoc node beg end)
(lister-delete-at ewoc node)))
(defun lister-delete-all (ewoc)
"Delete all items in EWOC."
(lister-delete-list ewoc :first :last))
;; * Redisplay items
(defun lister-refresh-at (ewoc pos-or-node)
"In EWOC, redisplay the node at POS-OR-NODE."
(lister-with-node ewoc pos-or-node node
(ewoc-invalidate ewoc node)))
(defun lister-refresh-list (ewoc &optional beg end)
"In EWOC, redisplay the nodes from BEG to END."
(when-let ((nodes (lister-collect-nodes ewoc beg end)))
(apply #'ewoc-invalidate ewoc nodes)))
;; * Marking
(defun lister--markable-p (ewoc node)
"Check if NODE is markable in EWOC.
Return t if either there is no markable predicate set or if the
predicate returns t when called with the node's data."
(with-current-buffer (ewoc-buffer ewoc)
(or (not lister-local-marking-predicate)
(funcall lister-local-marking-predicate (lister-node-get-data node)))))
(defun lister-set-marking-predicate (ewoc pred)
"Set PRED as a marking predicate in EWOC.
All items whose data matches PRED are markable. Delete existing
marks if they do not match PRED. If the PRED set to nil, all
items are markable."
(with-current-buffer (ewoc-buffer ewoc)
(setq-local lister-local-marking-predicate pred)
(when pred
(lister-walk-marked-nodes ewoc
(lambda (ewoc node)
(lister-mark-unmark-at ewoc node
(lister--markable-p ewoc node)))))))
(defun lister-count-marked-items (ewoc &optional beg end)
"Count all marked items in EWOC.
Use BEG and END to restrict the items checked."
(let ((n 0))
(lister-dolist-nodes (ewoc node beg end)
(when (lister-node-marked-p node)
(cl-incf n)))
n))
(defun lister-marked-at-p (ewoc pos)
"Check if EWOC item at POS is marked."
(lister-with-node ewoc pos node
(lister-node-marked-p node)))
(defun lister-items-marked-p (ewoc &optional beg end)
"Return non-nil if there are marked items in EWOC.
Use BEG and END to restrict the items checked."
(lister-dolist-nodes (ewoc node beg end)
(when (lister-node-marked-p node)
(cl-return t))))
(defun lister-mark-unmark-at (ewoc pos state)
"In EWOC, mark or unmark node at POS using boolean STATE."
(lister-with-node ewoc pos node
(let* ((item (ewoc-data node))
(old-state (lister--item-marked item)))
(when (not (eq old-state state))
(with-current-buffer (ewoc-buffer ewoc)
;; Can unmark after modifying `lister-local-marking-predicate'
(when (or (lister--markable-p ewoc node)
old-state)
(setf (lister--item-marked item) state)
(lister--update-mark-state item)))))))
(defun lister-mark-unmark-list (ewoc beg end state)
"In EWOC, mark or unmark the list between BEG and END.
If boolean STATE is true, the node are taken to be \\='marked'. BEG
and END can be nodes, positions such as `:first', `:point' or
`:last', or indices."
(lister-dolist-nodes (ewoc node beg end)
(lister-mark-unmark-at ewoc node state)))
(defun lister-get-marked-list (ewoc &optional beg end marker-pred-fn do-not-flatten-list)
"In EWOC, get all items which are marked and visible.
BEG and END refer to the first and last node to be checked,
defaulting to the first and last node of the list. Return a flat
list of all marked items unless DO-NOT-FLATTEN-LIST is non-nil.
Per default, return those items which are marked and visible.
Alternative node predicates can be passed to MARKER-PRED-FN."
(let ((l (lister-get-list ewoc beg end 0
(or marker-pred-fn #'lister-node-marked-and-visible-p))))
(if (not do-not-flatten-list)
(lister--flatten l)
l)))
(defun lister-walk-marked-nodes (ewoc action-fn &optional beg end marker-pred-fn)
"In EWOC, call ACTION-FN on each node which is marked and visible.
BEG and END refer to the first and last node to be checked,
defaulting to the first and last node of the list.
Call ACTION-FN with the EWOC as its first and the current node as
the second argument. Return the number of calls.
Per default, only consider those items which are marked and
visible. Alternative predicates can be passed to MARKER-PRED-FN."
(lister-walk-nodes ewoc action-fn beg end
(or marker-pred-fn #'lister-node-marked-and-visible-p)))
(defun lister-delete-marked-list (ewoc &optional beg end marker-pred-fn)
"In EWOC, delete marked and visible items between BEG and END.
BEG and END refer to the first and last node to be checked,
defaulting to the first and last node of the list.
Per default, only consider those items which are marked and
visible. Alternative predicates can be passed to MARKER-PRED-FN."
(let* ((inhibit-read-only t)
(pred-fn (or marker-pred-fn #'lister-node-marked-and-visible-p))
(nodes (lister-collect-nodes ewoc beg end pred-fn)))
(lister-set-modified-p ewoc t)
(apply #'ewoc-delete ewoc nodes)))
;; * Insert Items
;; All higher level insertion function come in two variants: a private
;; function which inserts items, preserving marking state, and a
;; public function which inserts data, creating new unmarked items on
;; the fly. The function lister--insert-nested is the abstract core of
;; both.
(defun lister---walk-insert (ewoc tail level node item-fn)
"In EWOC, recursively insert all elements of the list TAIL.
Insert each element of TAIL with indentation LEVEL. If there are
lists in TAIL, recurse into them one level deeper. Create
each inserted item by calling ITEM-FN with two arguments, the
current head of TAIL and the current level. The items are
inserted from bottom to top, that is, they are \\='stacked' either
on NODE or, if NODE is nil, on the bottom of the list."
(let (item)
(while tail
(setq item (car tail)
tail (cdr tail))
(if (listp item)
(lister---walk-insert ewoc item (1+ level) node item-fn)
(setq item (funcall item-fn item level))
(if node
(ewoc-enter-before ewoc node item)
(ewoc-enter-last ewoc item))))))
(defun lister--insert-nested (ewoc pos l level insert-after item-fn)
"In EWOC, insert L at POS.
POS can be either an ewoc node, an index position, or one of the
symbols `:first', `:last', `:point', `:next' or `:prev'. If
LEVEL is nil, align the new data item's level with its
predecessor. If LEVEL is an integer value, indent the item LEVEL
times, but never more then one level than the previous item.
Items inserted at top always have level 0. Create each inserted
item by calling ITEM-FN with two arguments, the current element
of the list and its assigned level. Insert L before (or
visually \\='above') the node at POS, unless INSERT-AFTER is set."
(lister-set-modified-p ewoc t)
(let* (;; determine the level:
(node (lister--parse-position ewoc pos))
(prev (if insert-after node (ewoc-prev ewoc node)))
(prev-level (if prev (lister--item-level (ewoc-data prev)) 0))
(this-level (if prev (lister--determine-level prev-level level) 0)))
;; Per default, lister---walk-insert inserts before NODE. Adjust
;; the other cases to that scheme:
(cond
((and (null node) insert-after) ;; insert at top?
(setq node (ewoc-nth ewoc 0))) ;; => insert before the first item
((and node insert-after) ;; insert after NODE?
(setq node (ewoc-next ewoc node)))) ;; => insert before its next node
;;
(lister---walk-insert ewoc l this-level node item-fn)))
(defun lister--insert-items (ewoc pos item-list level insert-after)
"In EWOC, insert ITEM-LIST at POS.
POS can be either an ewoc node, an index position, or one of the
symbols `:first', `:last', `:point', `:next' or `:prev'. If
LEVEL is nil, align the new data item's level with its
predecessor. If LEVEL is an integer value, indent the item LEVEL
times, but never more then one level than the previous item.
Items inserted at top always have level 0. Insert L before (or
visually \\='above') the node at POS, unless INSERT-AFTER is set.
ITEM-LIST must be a lists of `lister--item' objects."
(lister--insert-nested ewoc pos item-list level insert-after
#'lister--item-with-new-level))
(defun lister-insert-list (ewoc pos data-list &optional level insert-after)
"In EWOC, insert DATA-LIST at POS.
POS can be either an ewoc node, an index position, or one of the
symbols `:first', `:last', `:point', `:next' or `:prev'. If
LEVEL is nil, align the new data item's level with its