-
Notifications
You must be signed in to change notification settings - Fork 0
/
jumar.el
1435 lines (1234 loc) · 57.3 KB
/
jumar.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
;;; jumar.el --- Jump and marker like in Vim, with Helm interface.
;; Copyright (C) 2014 Ken Okada
;; Author: Ken Okada <[email protected]>
;; Maintainer: Ken Okada <[email protected]>
;; Keywords: tools, convenience, emulations
;; URL: https://github.com/kenoss/jumar
;; Package-Requires: ((emacs "24") (cl-lib "0.3") (erfi "0.1"))
;; Apache License, Version 2.0
;;; Commentary:
;; Jump and marker like in Vim, a tool to climb source code mountain.
;;
;; This package provides another marker enviroment. Emacs's default mark(er) is
;; very restricted; a lot of commands modify marks without asking and there's no
;; option to control their behavior; controlling marks is only available with
;; mark-ring and only non user frendly commands are provided; also `pop-tag-mark'
;; does not allow bi-directional movement; global mark-ring discards inaccessible
;; markers and one cannot jump with reopening that file; one cannot view marks
;; while Vim provides `:jumps' command.
;;
;; A solution jumar proposes is as the following:
;; - A new marker object `jumar:jumarker': It allows one to jump to points
;; in other files. Accessibility of buffers is automatically managed.
;; When file is reopened, observer try to revive jumarkers in that file.
;; One can jump to jumarker in killed file buffer.
;; - By default, one can use tree and list for set of jumarkers. Simultaneous
;; use is allowed. Tree is like `undo-tree'; List is like markers in Vim.
;; By default, some use cases are provided with DWIM commands.
;; - For default visualizer UI, Helm is used: User can look over sets of
;; jumarkers select target and action; peep, jump and delete. For tree,
;; one can change branches easily. (Use of visualiver is optional.)
;;
;; Jumar does not alter "mark" in Emacs, which is set many commands automatically.
;; Jumar is an environment to manage "marker", which users can use freely;
;; jumar only helps one to save and manage points manually. If automatic saving
;; is necessary, one can use functions provided by jumar for hooks.
;;
;; For more details, see ./README.md .
;; Namespaces:
;; `jumar:', `helm-jumar:' : Jumar internal.
;; `jumar-', `helm-jumar-' : Public APIs, user commands and custom variables.
;; `jumar-default-' : Default functions for custom variables.
;; `jumar:*variables*' : Internal variables danger to touch.
;;; Code:
(eval-when-compile
(require 'cl)
(require 'cl-lib) ; for `jumar:d-let*'
(require 'erfi-macros)
(erfi:use-short-macro-name))
(require 'erfi-srfi-1)
(require 'erfi-gauche)
(require 'erfi-misc)
;;;
;;; Custom variables
;;;
(defgroup jumar nil
"Jump and marker like in Vim."
:group 'emacs)
(defcustom jumar-jump-current-if-no-further-marker t
"If non-nil, jump jumarker of current node when there is no further jumarker."
:group 'jumar
:type 'boolean)
(defcustom jumar-message-function 'message
"A function used in `jumar:message' to print messages."
:group 'jumar
:type 'function)
(defcustom jumar-reopen-file-function 'jumar-default-reopen-file
"A function used to reopen file. It is called in functions like
`jumar-jump-forward', when target jumarker's state is 'unavailable.
It must return opened buffer or nil.
In this function, `jumar:jumarker-hook-find-file' must be called
to revive managed jumarkers. (Usually it is called from `fnid-file'
with `find-file-hook'.)"
:group 'jumar
:type 'function)
(defcustom jumar-restore-window-start t
"If t, `jumar:jumarker-goto' try to restore window start.
Sometimes it fails, for example, window size was changed, or jumarker was set
by emacs lisp code."
:group 'jumar
:type 'boolean)
(defcustom jumar-revive-marker-function 'jumar-default-revive-marker
"A function called to revive markers in killed or reverted buffers.
It must return a newly created marker or symbol 'broken. It is called with
two arguments: buffer, and an alist that `jumar:jumarker->saved-datum' returns.
The default value is `jumar-default-revive-marker'."
:group 'jumar
:type 'function)
(defcustom jumar-y-or-n-p 'y-or-n-p
"`y-or-n-p' used in helm-jumar session.
If function, it must be like `y-or-n-p'.
If t (resp. nil), it is equivalent to returning always t (resp. nil)."
:group 'jumar
:type '(choice (const :tag "Always yes" t)
(const :tag "Always no" nil)
function))
(defcustom helm-jumar-candidate-number-limit '(25 24)
"Nil or integer or list (Int Int). Integer n is equivalent to (n n).
If (a b), maximum number of candidates of helm-jumar is a+b+1.
a (resp. b) is max of number of candidates before (resp. after) current jumarker.
Nil means no limit.
After setting this variable, one must execute `jumar-init'.
Real value is set to `*helm-jumar-candidate-number-limit*'."
:group 'jumar)
(defcustom jumar-preview-text-length-limit 100
"Max length of text that in previewed in helm-jumar buffer.
It must be nil or an integer.
If nil, no preview is showed. If an integer, buffer contents of the line
at marker, form beginnig of line to the end of line or this limit, is previewed.
If one want to propertize previewed text,
use `jumar-preview-text-process-function'."
:group 'jumar
:type '(choice (const :tag "No preview" nil)
integer))
(defcustom jumar-preview-text-process-function (lambda (text pt) text)
"A function to process preview text for helm-jumar.
This function is called with two arguments: string TEXT and integer POINT;
helm-jumar preview line text where jumarker exists. TEXT is line content
with properties and POINT indicating place in TEXT where the marker is.
In the case that `jmuar-preview-text-length-limit' is smaller than length
of line content, length of TEXT can be than POINT, too.
This function must return a string; it is used in actual preview.
To make disable preview, use the variable `jumar-preview-text-length-limit'."
:group 'jumar
:type 'function)
(defvar jumar-tree-quota-default '(100 150))
;; These values are customizable, but ordinary user shouldn't touch.
;; If one use it, see "Quota" section of source code carefully.
(defvar jumar-reduce-tree-size-function 'jumar-default-reduce-tree-size)
(defvar jumar-reduce-tree-size-algorithm 'jumar-default-reduce-tree-size-algorithm)
;;;
;;; Auxiliary macro
;;;
(defmacro jumar:d-let* (bindings &rest body)
"Successive `destructing-bind'. Interface alikes `match-let*' in Gauche."
(declare (indent 1))
(cond ((null bindings)
`(progn ,@body))
((and (listp bindings)
(= 2 (length (car bindings))))
`(cl-destructuring-bind ,@(car bindings)
(jumar:d-let* ,(cdr bindings) ,@body)))
(t
(lwarn 'jumar :error "Malformed `jumar:d-let*'."))))
;;;
;;; Data structures: tree and node
;;;
(defstruct (jumar:tree
(:constructor nil)
(:constructor jumar:make-tree
(&optional
(content-destructor nil)
(quota jumar-tree-quota-default)
(reduce-size-function nil)
&aux
(root nil)
(current nil)
(size 0))))
root current size content-destructor quota reduce-size-function)
;; Types: Tree a (In this package, a is `jumar:jumarker'.)
;; root: Maybe jumar:node a
;; current: Maybe jumar:node a
;; size: Integer
;; content-destructor: Maybe Function ; destructor of contents of nodes
;; quota: (Integer Integer)
;; reduce-size-function: Maybe Function
;;
;; Rules:
;; Trees do not share nodes.
;; (jumar:tree-size obj) equals number of jumar:node under the ROOT of obj.
;; (jumar:node<= ROOT CURRENT) is t.
;; (memq (jumar:tree-current tree) (jumar:tree-breadcrumbs tree)) is non-nil.
;; (and (<= 2 (car QUOTA)) (<= (car QUOTA) (cadr QUOTA))) is t.
;; If non-nil, REDUCE-SIZE-FUNCITON behaves like `jumar-default-reduce-tree-size'.
;; Note that it is not guaranteed that SIZE is less than or equal to (cadr QUOTA) .
(defstruct (jumar:node
(:constructor nil)
(:constructor jumar:make-node/parent
(content parent
&aux
(child nil)
(lchildren '())
(rchildren '())
(time (current-time))))
(:constructor jumar:make-node/child
(content child
&aux
(parent nil)
(lchildren '())
(rchildren '())
(time (current-time)))))
parent lchildren child rchildren content)
;; Types: Node a (In this package, a is `jumar:jumarker'.)
;; parent: Maybe jumar:node a
;; child: Maybe jumar:node a
;; lchildren: List jumar:node a
;; rchildren: List jumar:node a
;; content: a
;;
;; Rules:
;; (eq node (jumar:node-parent (jumar:node-child node))) is t unless node is leaf.
;; A node is root if and only if its PARENT is nil.
;;; Predicates
(defsubst jumar:tree-empty? (tree)
(not (jumar:tree-root tree)))
(defsubst jumar:node-leaf? (node)
(null (jumar:node-child node)))
(defsubst jumar:node-root? (node)
(null (jumar:node-parent node)))
(defsubst jumar:tree-root? (tree node)
(eq node (jumar:tree-root tree)))
;;; Partial order
(defun jumar:node< (n1 n2)
"Return non-nil if and only if N1 is an ancestor of N2."
(erfi:let lp ((node (jumar:node-parent n2)))
(if (not node)
nil
(or (eq n1 node) (lp (jumar:node-parent node))))))
(defsubst jumar:node<= (n1 n2)
"Return non-nil if and only if N1 is equal to N2 or an ancestor of N2."
(or (eq n1 n2) (jumar:node< n1 n2)))
(defun jumar:tree-has-node? (tree node)
(and (not (jumar:tree-empty? tree))
(jumar:node<= (jumar:tree-root tree) node)))
;; Is it necessary?
(defun jumar:common-ancestor (n1 n2)
"Return common ancestor if exists, nil otherwise."
(erfi:let lp1 ((n1 n1) (n2 n2) (memory '()))
(cond ((not n1)
(erfi:let lp2 ((node n2) (memory memory))
(and node (or (car-safe (member node memory))
(lp2 (jumar:node-parent node) (cons node memory))))))
((member n1 memory) ; This is not the best but enough.
n1)
(t
(lp1 n2 (jumar:node-parent n1) (cons n1 memory))))))
;;; Constructors
(defun jumar:tree-set-first-node! (tree content)
(let1 new (jumar:make-node/parent content nil)
(setf (jumar:tree-root tree) new)
(setf (jumar:tree-current tree) new)
(incf (jumar:tree-size tree))
new))
(defun jumar:tree-enhance-upward! (tree content)
"Make a new node that has CONTENT and renew root of TREE. Return the new node.
This function does not run `jumar:tree-keep-size-limit!'."
(if (jumar:tree-empty? tree)
(jumar:tree-set-first-node! tree content)
(let* ((root (jumar:tree-root tree))
(new (jumar:make-node/child content root)))
(setf (jumar:node-parent root) new)
(setf (jumar:tree-root tree) new)
(incf (jumar:tree-size tree))
new)))
(defun jumar:tree-enhance-downward! (tree content &optional node update-child-flag)
"Make a new node that has CONTENT and add to the children of NODE.
NODE defaults to current node of TREE. Return the new node."
(if (jumar:tree-empty? tree)
(jumar:tree-set-first-node! tree content)
(let* ((node (or node (jumar:tree-current tree)))
(new (jumar:make-node/parent content node)))
(cond ((not (jumar:node-child node))
(setf (jumar:node-child node) new))
(update-child-flag
(setf (jumar:node-lchildren node)
(erfi:append-reverse (jumar:node-rchildren node)
(cons (jumar:node-child node) (jumar:node-lchildren node))))
(setf (jumar:node-child node) new)
(setf (jumar:node-rchildren node) '()))
(t
(setf (jumar:node-rchildren node) (append (jumar:node-rchildren node) (list new)))))
(incf (jumar:tree-size tree))
(jumar:tree-keep-size-limit! tree)
new)))
(defun jumar:tree-enhance-downward!/descend (tree content)
"Make a new node that has CONTENT and add to the children of NODE.
Return the new node."
(rlet1 new (jumar:tree-enhance-downward! tree content nil t)
(setf (jumar:tree-current tree) new)))
(defun jumar:tree-enhance-last! (tree content &optional update-current-flag)
"Make a new node that has CONTENT and add to the last of TREE,
regarded as list by `jumar:tree-breadcrumbs'. Return the new node.
If UPDATE-CURRENT-FLAG is t, set the newly created node to the current node of
TREE."
(if (jumar:tree-empty? tree)
(jumar:tree-set-first-node! tree content)
(erfi:let lp ((current (jumar:tree-current tree)))
(if (not (jumar:node-leaf? current))
(lp (jumar:node-child current))
(rlet1 new (jumar:tree-enhance-downward! tree content current)
(when update-current-flag
(setf (jumar:tree-current tree) new)))))))
(defun jumar:tree-add!/child-replaced (tree content &optional node update-current-flag)
"Make a new node that has CONTENT and replace the child of NODE with it.
Old child is deleted.
NODE defaults to current node of TREE. Return the new node.
If UPDATE-CURRENT-FLAG is non-nil, set the new node to the current node of TREE."
(if (jumar:tree-empty? tree)
(jumar:tree-set-first-node! tree content)
(let* ((node (or node (jumar:tree-current tree)))
(new (jumar:make-node/parent content node))
(old (jumar:node-child node)))
(setf (jumar:node-child node) new)
(setf (jumar:tree-size tree) (+ 1 (jumar:tree-size tree)
(if old
(- (jumar:node-count-nodes old))
0)))
(if update-current-flag
(setf (jumar:tree-current tree) new)
(when (and old (jumar:node<= old (jumar:tree-current tree)))
(setf (jumar:tree-current tree) node)))
(when old
(if-let1 d (jumar:tree-content-destructor tree)
(jumar:tree-for-each old d))))))
;;; Modifiers
(defun jumar:tree-descend-current! (tree &optional n)
;; MORE DOCUMENT
(let1 n (or n 1)
(cond ((< n 0)
(jumar:tree-ascend-current! tree (- n)))
((= n 0)
nil)
(t
(if-let1 current (jumar:tree-current tree)
(erfi:let lp ((current current) (n (or n 1)))
(let1 next (jumar:node-child current)
(cond ((not next)
nil)
((= 1 n)
(setf (jumar:tree-current tree) next))
(t
(lp next (- n 1)))))))))))
(defun jumar:tree-ascend-current! (tree &optional n)
;; MORE DOCUMENT
(let1 n (or n 1)
(cond ((< n 0)
(jumar:tree-descend-current! tree (- n)))
((= n 0)
nil)
(t
(if-let1 current (jumar:tree-current tree)
(erfi:let lp ((current current) (n n))
(let1 next (jumar:node-parent current)
(cond ((not next)
nil)
((= 1 n)
(setf (jumar:tree-current tree) next))
(t
(lp next (- n 1)))))))))))
(defun jumar:node-forward-branch! (node-or-tree &optional n)
"Change chosen child of the node to the next N th child.
N defaults to 1. If NODE-OR-TREE is a tree, use the current node.
If N is out of range, does not change anything and return nil.
If N is valid, return non-nil."
(let ((n (or n 1))
(node (cond ((jumar:node-p node-or-tree) node-or-tree)
((jumar:tree-p node-or-tree) (jumar:tree-current node-or-tree))
(t (lwarn 'jumar 'erorr "Wrong argument type: node-or-tree: %s" node-or-tree)))))
(when node
(erfi:let lp ((n n)
(l (jumar:node-lchildren node))
(c (jumar:node-child node))
(r (jumar:node-rchildren node)))
(cond ((< 0 n)
(if (null r)
nil
(lp (- n 1) (cons c l) (car r) (cdr r))))
((< n 0)
(if (null l)
nil
(lp (+ n 1) (cdr l) (car l) (cons c r))))
(t ; i.e., (= n 0)
(setf (jumar:node-lchildren node) l)
(setf (jumar:node-child node) c)
(setf (jumar:node-rchildren node) r)
c))))))
;;; Deletion
(defun jumar:tree-delete-node! (tree &optional node delete-below-flag)
"Delete NODE from TREE. NODE defaults to current node of TREE.
If DELETE-BELOW-FLAG is t, delete children of NODE together. Otherwise,
new child of parent of NODE is child of NODE. (lchildren and rchildren of NODE
are deleted. This means, \"delete single line in *helm jumar* buffer.\")
If new child of parent would be nil and other children exist, they will be
\"shifted\".
NODE must be in TREE. Hence, TREE must not be empty."
;; Test
(unless (jumar:tree-has-node? tree node)
(lwarn 'jmuar :error "`jumar:tree-delete-node!': argument out of range.")
(error "argument out of range."))
(let1 node (or node (jumar:tree-current tree))
(when node
(let* ((root? (jumar:tree-root? tree node))
(child (if delete-below-flag nil (jumar:node-child node)))
(parent (if root? nil (jumar:node-parent node))))
;; Remove node from tree.
(if root?
(prog1 t
;; Current node will be set at last.
(setf (jumar:tree-root tree) child)
(when child
(setf (jumar:node-parent child) nil)))
;; Link parent and child each other. (Here parent is non-nil.)
(prog1 t
(when child
(setf (jumar:node-parent child) parent))
;; Here we can't use `jumar:node-remove-child!' because node maybe in l(r)children.
(cond ((eq node (jumar:node-child parent))
(if child
(setf (jumar:node-child parent) child)
(jumar:node-remove-child! parent)))
;; For ordinary use, these two cases below will not occur.
((memq node (jumar:node-lchildren parent))
(setf (jumar:node-lchildren parent)
(destructuring-bind (a b) (erfi:break (cut eq node <>)
(jumar:node-lchildren parent))
(erfi:append! a (and child (list child)) (cdr b)))))
((memq node (jumar:node-rchildren parent))
(setf (jumar:node-rchildren parent)
(destructuring-bind (a b) (erfi:break (cut eq node <>)
(jumar:node-rchildren parent))
(erfi:append! a (and child (list child)) (cdr b)))))
(t (lwarn 'jumar :error "Violation of the rule")))))
;; Then apply destructor to the removed nodes.
(when (not delete-below-flag)
(setf (jumar:node-child node) nil))
(setf (jumar:tree-size tree) (- (jumar:tree-size tree) (jumar:node-count-nodes node)))
(if-let1 d (jumar:tree-content-destructor tree)
(jumar:tree-for-each node d))
;; Set new current node if old one was deleted.
(when (jumar:node<= node (jumar:tree-current tree))
(setf (jumar:tree-current tree) (or child parent)))))))
(defun jumar:node-remove-child! (node-or-tree &optional prefered-direction)
"Remove child of node and set the next one.
PREFERED-DIRECTION must be 'left or 'right, defaults to 'right.
If NODE-OR-TREE is tree, use current node.
Return t if and only if new child set.
This function does not take care of destructor."
(let* ((pdirection (or prefered-direction 'right))
(node (cond ((jumar:node-p node-or-tree) node-or-tree)
((jumar:tree-p node-or-tree) (jumar:tree-current node-or-tree))
(t (lwarn 'jumar 'erorr "Wrong type of argument: node-or-tree: %s" node-or-tree))))
(l (jumar:node-lchildren node))
(r (jumar:node-rchildren node)))
(if (and (null l) (null r))
(prog1 nil
(setf (jumar:node-child node) nil))
(prog1 t
(erfi:case (or prefered-direction 'right)
((left) (if (not (null l))
(progn
(setf (jumar:node-lchildren node) (cdr l))
(setf (jumar:node-child node) (car l)))
(progn
(setf (jumar:node-rchildren node) (cdr r))
(setf (jumar:node-child node) (car r)))))
((right) (if (not (null r))
(progn
(setf (jumar:node-rchildren node) (cdr r))
(setf (jumar:node-child node) (car r)))
(progn
(setf (jumar:node-lchildren node) (cdr l))
(setf (jumar:node-child node) (car l))))))))))
;; For quota
(defun jumar:tree-set-new-root! (tree &optional node)
"Delete nodes above NODE and set NODE to the new root of TREE.
This alikes `jumar:tree-delete-node!' with delete-below-flag t."
(if (jumar:tree-root? tree node)
nil
(prog1 t
(let1 old-root (jumar:tree-root tree)
(unless (jumar:node<= old-root node)
(lwarn 'jumar :error "`jumar:tree-set-new-root!': argument out of range")
(error "`jumar:tree-set-new-root!': argument out of range"))
(setf (jumar:tree-root tree) node)
(setf (jumar:node-child (jumar:node-parent node)) nil)
(setf (jumar:tree-size tree) (- (jumar:tree-size tree) (jumar:node-count-nodes old-root)))
(if-let1 d (jumar:tree-content-destructor tree)
(jumar:tree-for-each old-root d))
(when (jumar:node<= old-root (jumar:tree-current tree))
(setf (jumar:tree-current tree) (jumar:tree-root tree)))))))
;;; Auxiliary function: counting nodes
(defvar jumar:*node-count-nodes:counter* nil)
(defun jumar:node-count-nodes (node)
(dynamic-let ((jumar:*node-count-nodes:counter* 0))
(jumar:tree-for-each node (lambda (_) (incf jumar:*node-count-nodes:counter*)))
jumar:*node-count-nodes:counter*))
;;; Iterator
(defun jumar:tree-for-each (tree-or-node f)
"Apply F to the contents of TREE-OR-NODE.
If NODE-OR-TREE is node, F is applyed to subtree whose root is that node.
Oerder of application is undefined (for performance)."
;; Exception of the rules:
;; lchildren and rchildren are processed even if child is nil.
;; This case is used in `jumar:tree-delete-node!'.
(let1 node (cond ((jumar:node-p tree-or-node) tree-or-node)
((jumar:tree-p tree-or-node) (jumar:tree-root tree-or-node))
(t (lwarn 'jumar 'erorr "Wrong argument type: tree-or-node: %s" tree-or-node)))
(jumar:tree-for-each:aux node f)))
(defun jumar:tree-for-each:aux (node f)
;; Since "list of jumarkers" is represented by tree without lchildren and rchildren,
;; simplest implementation may cause stack overflow. Here we use TCO for child,
;; explicit stack for lchildren and rchildren.
(erfi:let lp ((node node) (s '()) (stack '()))
(cond (node
(funcall f (jumar:node-content node))
(lp (jumar:node-child node)
s
`(,(jumar:node-lchildren node) ,(jumar:node-rchildren node) ,@stack)))
((not (null s))
(lp (car s) (cdr s) stack))
((not (null stack))
(lp nil (car stack) (cdr stack)))
(t
t))))
;;; Conversion
(defun jumar:tree-breadcrumbs (tree &optional f)
"Return a breadcrumb list of TREE from root to leaf along directions.
The type of return value is \"list of node-content\"."
(let ((f (or f 'jumar:node-content))
(root (jumar:tree-root tree)))
(if (not root)
'()
(erfi:let lp ((node (jumar:tree-root tree))
(res '()))
(if (jumar:node-leaf? node)
(nreverse (cons (jumar:node-content node) res))
(lp (jumar:node-child node) (cons (funcall f node) res)))))))
(defun jumar:tree-breadcrumbs+index (tree &optional f)
"Return a breadcrumb list and the index of current node.
(eq (jumar:node-content (jumar:tree-currnet tree))
(apply 'nth (reverse (jumar:tree-breadcrums/index tree))))"
(let ((f (or f 'jumar:node-content))
(current (jumar:tree-current tree))
(root (jumar:tree-root tree)))
(if (not root)
'(() nil)
(erfi:let lp ((node (jumar:tree-root tree))
(res '())
(index 0)
(res-index nil))
(if (jumar:node-leaf? node)
`(,(nreverse (cons (funcall f node) res)) ,(or res-index index))
(lp (jumar:node-child node) (cons (funcall f node) res)
(+ index 1) (or res-index (and (eq node current) index))))))))
(defun jumar:node->branch-string (node)
(let ((l (jumar:node-lchildren node))
(c (jumar:node-child node))
(r (jumar:node-rchildren node)))
(concat (if l "-" " ")
(if (or l r) "+" "|")
(if r "-" " "))))
;;; Quota
(defun jumar:tree-keep-size-limit! (tree)
"If size is less than or equal to cadr of quota of TREE, this has no effect.
If not, reduce size by delegating to reduce-size-function of TREE or
`jumar-reduce-tree-size-function'."
(if-let1 quota (jumar:tree-quota tree)
(when (< (cadr quota) (jumar:tree-size tree))
(funcall (or (jumar:tree-reduce-size-function tree) jumar-reduce-tree-size-function) tree))))
(defun jumar-default-reduce-tree-size (tree)
"Default function of `jumar-reduce-tree-size-function'.
List up candidates by `jumar-reduce-tree-size-algorithm' and delete them.
This function has to reduce size of TREE.
Goal size is (car (jumar:tree-quota TREE)) ."
(destructuring-bind (new-root node-list-1 node-list-2)
(funcall jumar-reduce-tree-size-algorithm
(jumar:tree-root tree) (car (jumar:tree-quota tree)))
(jumar:tree-set-new-root! tree new-root)
(dolist (node node-list-1)
(jumar:tree-delete-node! tree node t))
(dolist (node node-list-2)
(jumar:tree-delete-node! tree node nil))))
;; `jumar-default-reduce-tree-size-algorithm'
;; This algorithm is simple, but has a potential problem.
;; This algorithm first find out a node (in breadcrumbs), under which all nodes are preserved.
;; Then collect branches and nodes above to be deleted.
;; Consider the following case (with appropriate limit): (Breadcrumbs of) tree is
;;
;; | root
;; -+-
;; -+- there are many small branches
;; -+-
;; |
;; -| near leaf, there's a large branch
;; | leaf
;;
;; In this case, only the large branch can servive. After deletion, tree will be the following:
;;
;; | root
;; |
;; |
;; |
;; |
;; -| near leaf, there's a large branch
;; | leaf
;;
;; In many cases this will be not a problem. Although it can in the case that the current node
;; is near the place above "there are many small branches".
(defun jumar-default-reduce-tree-size-algorithm (root limit)
"This function is called from `jumar-default-reduce-tree-size'
if `jumar-reduce-tree-size-algorithm' is untouched.
Return (node-list-1 node-list-2). Here,
node-list-1 is list of nodes that should be `jumar:tree-delete-node!' with below-flag t;
node-list-2 is nodes that should be `jumar:tree-delete-node!' with below-flag nil.
After deletion, nodes below root is less than or equal to LIMIT elements.
LIMIT must be an integer greater than 2.
For example, if ROOT has less than or equal to LIMIT elements, return value is `(,root () ())."
(progn
(unless (and (wholenump limit) (< 2 limit))
(lwarn 'jumar :error "argument out of range."))
(if (>= limit (jumar:node-count-nodes root))
;; Trivial case.
`(,root () ())
;; First, find out a node, under which all nodes preserved.
(let1 child (jumar:node-child root)
(erfi:let lp1 ((node child) (n nil))
(let1 n (or n (jumar:node-count-nodes node))
(if (< limit n)
;; Go down to next node.
(if (and (null (jumar:node-lchildren node)) (null (jumar:node-rchildren node)))
(lp1 (jumar:node-child node) (- n 1))
(lp1 (jumar:node-child node) nil))
;; Second, go up to the root and collect branches to be deleted.
(erfi:let lp2 ((node node)
(m n)
(res1 '()))
(if (or (eq root node) (= m limit))
`(,node ,res1 ())
(lp2 (jumar:node-parent node)
(+ m 1)
(append (jumar:node-lchildren node) (jumar:node-rchildren node) res1)))))))))))
(defun jumar-reduce-list-tree-size (tree)
"reduce-size-function of TREE without no branches."
(let1 new-root (let1 limit (car (jumar:tree-quota tree))
(erfi:let lp ((node (jumar:tree-root tree))
(n (jumar:tree-size tree)))
(if (<= n limit)
node
(lp (jumar:node-child node) (- n 1)))))
(jumar:tree-set-new-root! tree new-root)))
;;;
;;; Data structure: jumarker
;;;
(defstruct (jumar:jumarker
(:constructor nil)
(:constructor jumar:make-jumarker:aux
(&optional
(marker (point-marker))
(winstart (window-start))
&aux
(time (current-time))
(state 'available)
(saved-datum nil))))
marker winstart time state saved-datum)
;; Types:
;; marker: Maybe Marker object of Emacs
;; time: Time object of Emacs (a list of three integers) ; the created time
;; state: Symbol
;; saved-datum: Maybe List `(,buf-file-name ,point ,line ,col ,text)
;;
;; Rules:
;; state := 'available | 'unavailable | 'reverting | 'broken
;; 'available : if and only if (buffur-live-p (marker-buffer marker)) is t.
;; (There's seconds it does not hold.)
;; 'unavailable : if and only if the buffer is killed (or missing).
;; 'reverting : while the moment `before-revert-hook' between `after-revert-hook'.
;; 'broken : if and only if `jumar-revive-marker-function' could not revive marker.
;; marker is nil if and only if state is 'broken.
;; saved-datum is nil if and only if state is 'available.
;; It is t:
;; (let1 children (jumar:node-children node)
;; (equal children (sort (copy-sequence children) 'jumar:jumarker-time<)))
;;; Real constructor
(defsubst jumar:make-jumarker (&optional marker winstart)
"Make a jumaker and register it to ovservation list."
;; We cannot pass marker and winstart as it is to constructor of CL struct.
;; Probably it is due to compiler macro. So we explicitely process optional arguments
;; as the constructor does.
(rlet1 jm (jumar:make-jumarker:aux (or marker (point-marker)) (or winstart (window-start)))
(push jm jumar:*observed-jumarker-list*)))
;;; Deletion
(defun jumar:jumarker-delete (jm)
"Delete marker in JM. It also unregister JM from observation list."
(progn
;; TODO: use `delq'
(setq jumar:*observed-jumarker-list* (erfi:delete jm jumar:*observed-jumarker-list* 'eq))
(when (eq 'available (jumar:jumarker-state jm))
(set-marker (jumar:jumarker-marker jm) nil))))
;;; State
(defun jumar:jumarker-change-state! (jm state &optional buffer)
"Return value is undefined."
(erfi:ecase state
((available)
(let1 m (let1 sd (jumar:jumarker-saved-datum jm)
(funcall jumar-revive-marker-function (or buffer (assq :file-path sd)) sd))
(unless (or (markerp m) (eq m 'broken))
(lwarn 'jumar :error
(concat "Type error: return value of `jumar-revive-marker-function'"
(format " must be an available marker or symbol 'broken: %s" m))))
(if (eq m 'broken)
(jumar:jumarker-change-state! jm 'broken)
(prog1 t
(setf (jumar:jumarker-marker jm) m)
(setf (jumar:jumarker-state jm) 'available)
(setf (jumar:jumarker-saved-datum jm) nil)))))
((unavailable reverting broken)
(when (eq 'available (jumar:jumarker-state jm))
(setf (jumar:jumarker-saved-datum jm) (jumar:jumarker->saved-datum jm))
(set-marker (jumar:jumarker-marker jm) nil))
(setf (jumar:jumarker-marker jm) nil)
(setf (jumar:jumarker-state jm) state))))
(defun jumar-default-revive-marker (buffer saved-datum)
"Default value of `jumar-revive-marker-function'.
Try to revive a marker in BUFFER from SAVED-DATUM.
Currently, it is not so smart: It only returns maker with point (in
SAVED-DATUM). (It is because I do not know what is the best way for
all buffers. So I leave a room to configure by users.)
BUFFER defaults to assoc value of :file-path in SAVED-DATUM."
(with-current-buffer buffer
(save-excursion
(goto-char (cdr (assq :point saved-datum)))
(point-marker))))
;;; Order
(defun jumar:jumarker-time< (jm1 jm2)
(let ((t1 (jumar:jumarker-time jm1))
(t2 (jumar:jumarker-time jm2)))
(or (< (car t1) (car t2))
(< (cadr t1) (cadr t2))
(< (caddr t1) (caddr t2)))))
;;; Jump
(defun jumar:marker-goto (marker)
(progn
; (set-window-buffer nil (marker-buffer marker))
(switch-to-buffer (marker-buffer marker))
(goto-char (marker-position marker))))
(defun jumar:jumarker-goto (jm)
(erfi:case (jumar:jumarker-state jm)
((available)
(let1 m (jumar:jumarker-marker jm)
(jumar:marker-goto m)
(when jumar-restore-window-start
(set-window-start nil (jumar:jumarker-winstart jm))
;; Sometimes marker is not between window-start and window-end.
;; Making marker be in display is more important than restore window start.
(jumar:marker-goto m))))
((unavailable)
(unless (and-let* ((file-path (cdr-safe (assq :file-path (jumar:jumarker-saved-datum jm))))
(buf (funcall jumar-reopen-file-function file-path)))
(prog1 t
;; Now, state of JM should be changed by call of `jumar:jumarker-hook-find-file'.
;; (if (eq 'available (jumar:jumarker-state jm))
;; (jumar:jumarker-goto jm)
;; (lwarn 'jumar :error "`jumar-reopen-file-function' violate the rule."))))
(erfi:case (jumar:jumarker-state jm)
((available) (jumar:jumarker-goto jm))
((broken) (jumar:message "Reopen failed and unable to jump."))
(else (lwarn 'jumar :error "`jumar-reopen-file-function' violate the rule.")))))
(jumar:message "Reopen failed and unable to jump.")))
(else
(lwarn 'jumar :error "Unable to jump. Unexpected case: %S" (jumar:jumarker-state jm)))))
;;; Conversion
(defun jumar:jumarker->position-datum (jm)
"[internal] Return a list `(,buf ,pos ,winstart ,buf-name ,line ,column ,text) that JM indicates.
Note that this is an internal function. The order and elements may be changed.
For extensions, use `jumar:jumarker->saved-datum'."
(if (not (eq 'available (jumar:jumarker-state jm)))
(let* ((sd (jumar:jumarker-saved-datum jm))
(file-path (cdr-safe (assq :file-path sd)))
(buf-name (cdr-safe (assq :buffer-name sd))))
`(nil
,@(mapcar (erfi:$ cdr $ assq <> sd $) '(:point :winstart))
,(if file-path (file-name-nondirectory file-path) buf-name)
,@(mapcar (erfi:$ cdr $ assq <> sd $) '(:line :column))
,(concat (propertize "Killed buffer" 'font-lock-face '(:background "gray20"))
" " file-path)))
(let* ((m (jumar:jumarker-marker jm))
(buf (marker-buffer m))
(buf-name (buffer-name buf))
(pos (marker-position m)))
(with-current-buffer (marker-buffer m)
(save-excursion
(save-restriction
(widen)
(goto-char pos)
(let* ((line (line-number-at-pos pos))
(column (- pos (point-at-bol)))
(text (if (null jumar-preview-text-length-limit)
""
(erfi:$
funcall jumar-preview-text-process-function <> column $
buffer-substring (point-at-bol) $
min (point-at-eol) (+ (point-at-bol) jumar-preview-text-length-limit)))))
(list buf pos (jumar:jumarker-winstart jm) buf-name line column text))))))))
(defun jumar:jumarker->saved-datum (jm)
"Return an alist of datum.
Calling this function is allowed only if JM is available.
Keys are :file-path, :buffer-name, :point, :winstart, :line, :column."
(destructuring-bind (buf pos winstart buf-name line column _) (jumar:jumarker->position-datum jm)
`((:file-path . ,(buffer-file-name buf))
(:buffer-name . ,(buffer-name buf))
(:point . ,pos)
(:winstart . ,winstart)
(:line . ,line)
(:column . ,column))))
;;; Hooks
(defvar jumar:*observed-jumarker-list* '()
"List of jumarkers observed to guarantee states of markers are correct.
Kill or revert buffers makes those unavailable. When file is reopend,
revive jumarkers in it.")
(defun jumar:jumarker-hook-kill-buffer ()
"This guarantees state of jumarkers are correct."
(let1 buf (current-buffer)
(dolist (jm jumar:*observed-jumarker-list*)
(if-let1 marker (jumar:jumarker-marker jm)
(when (eq buf (marker-buffer marker))
(jumar:jumarker-change-state! jm 'unavailable))))))
(add-hook 'kill-buffer-hook 'jumar:jumarker-hook-kill-buffer)
(defun jumar:jumarker-hook-before-revert ()
"This guarantees state of jumarkers are correct."
(let1 buf (current-buffer)
(dolist (jm jumar:*observed-jumarker-list*)
(if-let1 marker (jumar:jumarker-marker jm)
(when (eq buf (marker-buffer marker))
(jumar:jumarker-change-state! jm 'reverting))))))
(defun jumar:jumarker-hook-find-file ()
"This guarantees state of jumarkers are correct."
(let1 buf (current-buffer)
(dolist (jm jumar:*observed-jumarker-list*)
(when (and (not (eq 'available (jumar:jumarker-state jm)))
(string= (cdr (assq :file-path (jumar:jumarker-saved-datum jm)))
(or (buffer-file-name buf) (buffer-name buf))))
(jumar:jumarker-change-state! jm 'available (current-buffer))))))
(add-hook 'before-revert-hook 'jumar:jumarker-hook-before-revert)
(add-hook 'find-file-hook 'jumar:jumarker-hook-find-file)
;;;
;;; Core
;;;
(defvar jumar:*jm-tree* (jumar:make-tree 'jumar:jumarker-delete))
(defvar jumar:*jm-list* (jumar:make-tree 'jumar:jumarker-delete nil 'jumar-reduce-list-tree-size))
(defun jumar-init ()
"Initialize internal variables.
User has to call this function after modifying variables below:
`helm-jumar-candidate-number-limit'
`helm-jumar-map'"
(progn
(setq *helm-jumar-candidate-number-limit*
(let1 n-or-list helm-jumar-candidate-number-limit
(cond ((null n-or-list)
n-or-list)
((integerp n-or-list)
`(,n-or-list ,n-or-list))
((and (listp n-or-list)
(= 2 (length n-or-list)))
n-or-list)
(t
(error "Wrong type: `helm-jumar-candidate-number-limit'")))))
(when (featurep 'helm)
(helm-jumar:reset-sourecs))))
(defun jumar-default-reopen-file (file-path)
(if (funcall jumar-y-or-n-p (format "Reopen %s?" file-path))
(find-file file-path)
(prog1 nil
(jumar:message "File is not reopend."))))
;;;
;;; Very simple UI for jumar:*jm-tree*
;;;