-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathhighlight-indent-guides.el
1034 lines (966 loc) · 48.5 KB
/
highlight-indent-guides.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
;;; highlight-indent-guides.el --- Minor mode to highlight indentation -*- lexical-binding: t; -*-
;;
;; Copyright (c) 2015 DarthFennec
;;
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;;
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
;;
;; Author: DarthFennec <[email protected]>
;; Version: 0.9.2
;; Package-Requires: ((emacs "26.1"))
;; URL: https://github.com/DarthFennec/highlight-indent-guides
;; Keywords: convenience
;;; Commentary:
;; This minor mode highlights indentation levels via font-lock. Indent widths
;; are dynamically discovered, which means this correctly highlights in any
;; mode, regardless of indent width, even in languages with non-uniform
;; indentation such as Haskell. This mode works properly around hard tabs and
;; mixed indentation, and it behaves well in large buffers.
;;
;; To install, put this file in your load-path, and do
;; M-x highlight-indent-guides-mode to enable it. To enable it automatically in
;; most programming modes, use the following:
;;
;; (add-hook 'prog-mode-hook 'highlight-indent-guides-mode)
;;
;; To set the display method, use:
;;
;; (setq highlight-indent-guides-method METHOD)
;;
;; Where METHOD is either 'fill, 'column, 'character, or 'bitmap.
;;
;; To change the character used for drawing guide lines with the 'character
;; method, use:
;;
;; (setq highlight-indent-guides-character ?ch)
;;
;; By default, this mode automatically inspects your theme and chooses
;; appropriate colors for highlighting. To tweak the subtlety of these colors,
;; use the following (all values are percentages):
;;
;; (setq highlight-indent-guides-auto-odd-face-perc 15)
;; (setq highlight-indent-guides-auto-even-face-perc 15)
;; (setq highlight-indent-guides-auto-character-face-perc 20)
;;
;; Or, to manually set the colors used for highlighting, use:
;;
;; (setq highlight-indent-guides-auto-enabled nil)
;;
;; (set-face-background 'highlight-indent-guides-odd-face "color")
;; (set-face-background 'highlight-indent-guides-even-face "color")
;; (set-face-foreground 'highlight-indent-guides-character-face "color")
;;; Code:
(require 'color)
(defgroup highlight-indent-guides nil
"Indentation highlighting."
:group 'faces)
(defface highlight-indent-guides-odd-face '((t nil))
"Face to highlight odd indent levels."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-even-face '((t nil))
"Face to highlight even indent levels."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-character-face '((t nil))
"Face to highlight guide line characters and bitmaps."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-top-odd-face '((t nil))
"Face to highlight odd indent levels."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-top-even-face '((t nil))
"Face to highlight even indent levels."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-top-character-face '((t nil))
"Face to highlight guide line characters and bitmaps."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-stack-odd-face '((t nil))
"Face to highlight odd indent levels."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-stack-even-face '((t nil))
"Face to highlight even indent levels."
:group 'highlight-indent-guides)
(defface highlight-indent-guides-stack-character-face '((t nil))
"Face to highlight guide line characters and bitmaps."
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-character ?\x2502
"Character to use to display guide lines."
:type 'character
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-method 'fill
"Method to use when displaying indent guides.
This can be `fill', `column', `character', or `bitmap'."
:type '(choice (const fill)
(const column)
(const character)
(const bitmap))
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-responsive nil
"Whether responsive highlights should be used.
This allows different highlight colors to be used in response to the location of
the point. If this is nil, no responsive highlighting is used. If this is
`top', the indent level of the current line is colored distinctly. If this is
`stack', three colorations are used: one for the current indent level (as with
`top'), one for all parent levels of the current indent level, and one for all
other levels."
:type '(choice (const nil) (const top) (const stack))
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-enabled t
"Whether to automatically calculate faces.
If this is enabled, highlight-indent-guides will use the current theme's
background color to automatically calculate reasonable indent guide colors."
:type 'boolean
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-suppress-auto-error nil
"Whether to suppress the error that sometimes prints when calculating faces.
When automatically calculating faces, sometimes there will be an error that
\"`default' face is not set properly\". If this flag is enabled,
highlight-indent-guides will not print this error. This can be helpful in
situations where faces are calculated correctly, but the error is printed
anyway, which can be annoying."
:type 'boolean
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-highlighter-function
'highlight-indent-guides--highlighter-default
"Determine the correct face to use for a given indentation level.
Customizable function which applies faces to indentation. The function is
called once per indentation character, and takes three parameters: LEVEL is the
indentation level of the character, with 0 being the outermost level.
RESPONSIVE is either nil, `top', or `stack', depending on which responsive class
the character falls into. DISPLAY is the current display method setting, which
can be `fill', `column', or `character'. The return value is either the face to
apply to the guide character, or nil if the guide should not be displayed at
all. The results of this function are cached per indentation character, so the
function should consistently return the same output given the same input."
:type 'function
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-bitmap-function
'highlight-indent-guides--bitmap-dots
"Determine the shape of the indent guide bitmap.
Customizable function which `draws' the indent guide bitmap. The function is
called once per indentation character, and takes three parameters: WIDTH and
HEIGHT are the pixel width and height of the character, and CREP is the
character that should be used to represent a colored pixel. The return value is
a list of strings, with each string representing a row of pixels. The list
should be HEIGHT in size, and each string in the list should be WIDTH in size.
Each character represents a pixel, and should be CREP if the pixel is colored,
and ZREP if it isn't colored."
:type 'function
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-odd-face-perc 5
"Color adjustment percentage for highlight-indent-guides-odd-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-even-face-perc 10
"Color adjustment percentage for highlight-indent-guides-even-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-character-face-perc 10
"Color adjustment percentage for highlight-indent-guides-character-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-top-odd-face-perc 25
"Color adjustment percentage for highlight-indent-guides-odd-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-top-even-face-perc 30
"Color adjustment percentage for highlight-indent-guides-even-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-top-character-face-perc 30
"Color adjustment percentage for highlight-indent-guides-character-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-stack-odd-face-perc 15
"Color adjustment percentage for highlight-indent-guides-odd-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-stack-even-face-perc 20
"Color adjustment percentage for highlight-indent-guides-even-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-auto-stack-character-face-perc 20
"Color adjustment percentage for highlight-indent-guides-character-face.
This is used to automatically calculate the indent guide faces from the
background color."
:type 'number
:group 'highlight-indent-guides)
(defcustom highlight-indent-guides-delay 0.1
"The number of seconds to wait for an idle state before redrawing.
This is only useful if `highlight-indent-guides-responsive' is not nil."
:type 'number
:group 'highlight-indent-guides)
(defvar highlight-indent-guides--idle-timer nil
"The idle timer object for responsive mode.")
(defvar highlight-indent-guides--line-cache '(nil nil nil)
"The line cache for responsive mode.")
(make-variable-buffer-local 'highlight-indent-guides--line-cache)
(defvar highlight-indent-guides--bitmap-memo (make-hash-table :test 'equal)
"The memoization cache for bitmap guide data.")
;;;###autoload
(define-minor-mode highlight-indent-guides-mode
"Display indent guides in a buffer."
:lighter" h-i-g"
:group highlight-indent-guides
(let ((fill-method-keywords
'((highlight-indent-guides--fill-keyword-matcher
0 (highlight-indent-guides--fill-highlighter) t)))
(column-method-keywords
'((highlight-indent-guides--column-keyword-matcher
0 (highlight-indent-guides--column-highlighter) t)))
(character-method-keywords
'((highlight-indent-guides--column-keyword-matcher
0 (highlight-indent-guides--character-highlighter) t)))
(bitmap-method-keywords
'((highlight-indent-guides--column-keyword-matcher
0 (highlight-indent-guides--bitmap-highlighter) t))))
(when highlight-indent-guides--idle-timer
(cancel-timer highlight-indent-guides--idle-timer)
(setq highlight-indent-guides--idle-timer nil))
(if highlight-indent-guides-mode
(progn
;; set highlight-indent-guides--line-cache so it becomes buffer-local
;; After this, we can destructively modify it just fine, as every
;; buffer has a unique object.
(setq highlight-indent-guides--line-cache (list nil nil nil))
(unless (daemonp) (highlight-indent-guides-auto-set-faces))
(add-to-list 'after-make-frame-functions
'highlight-indent-guides--auto-set-faces-with-frame)
(add-to-list 'font-lock-extra-managed-props 'display)
(add-to-list 'text-property-default-nonsticky
(cons 'highlight-indent-guides-prop t))
(setq highlight-indent-guides--idle-timer
(run-with-idle-timer
highlight-indent-guides-delay t
'highlight-indent-guides--try-update-line-cache))
(font-lock-add-keywords
nil
(pcase highlight-indent-guides-method
(`fill fill-method-keywords)
(`column column-method-keywords)
(`character character-method-keywords)
(`bitmap bitmap-method-keywords))
t)
(jit-lock-register 'highlight-indent-guides--guide-region))
(setq after-make-frame-functions
(delete 'highlight-indent-guides--auto-set-faces-with-frame
after-make-frame-functions))
(font-lock-remove-keywords nil fill-method-keywords)
(font-lock-remove-keywords nil column-method-keywords)
(font-lock-remove-keywords nil character-method-keywords)
(jit-lock-unregister 'highlight-indent-guides--guide-region)
(highlight-indent-guides--unguide-region (point-min) (point-max))
(font-lock-flush))))
(defun highlight-indent-guides--try-merge-ranges (&rest args)
"Given multiple character position ranges (ARGS), merge where possible.
When ranges are calculated separately, there is a possibility of overlap, which
can cause unnecessary redraws. This function merges overlapping ranges to
minimize redraws."
(let ((ranges (sort (delq nil args) (lambda (x y) (> (car x) (car y)))))
curr next results)
(unless (null ranges)
(setq curr (pop ranges))
(while ranges
(setq next (pop ranges))
(if (<= (car curr) (+ 2 (cdr next)))
(setq curr (cons (car next) (max (cdr curr) (cdr next))))
(setq results (cons curr results))
(setq curr next)))
(setq results (cons curr results))
results)))
(defun highlight-indent-guides--discover-ranges (sect1 sect2)
"Given two sections SECT1 and SECT2, discover the ranges where they differ.
Gives a list of two ranges that should be redrawn when the point moves between
SECT1 and SECT2. This is the shallowest indent level that is not shared."
(if (not (eq highlight-indent-guides-responsive 'stack))
(list (car sect1) (car sect2))
(let ((rsect1 (reverse sect1))
(rsect2 (reverse sect2)))
(catch 'return
(while t
(if (and (cdr rsect1) (cdr rsect2) (eq (car rsect1) (car rsect2)))
(setq rsect1 (cdr rsect1) rsect2 (cdr rsect2))
(throw 'return (list (car rsect1) (car rsect2)))))))))
(defun highlight-indent-guides--update-line-cache ()
"Update the line cache.
The line cache tracks the current line data to make it easy for the drawing
functions to quickly access the needed context data for responsive mode. This
function is called whenever the current line data changes."
(let ((higp 'highlight-indent-guides-prop))
(save-excursion
(beginning-of-line)
(while (and (not (eobp))
(or (let ((s (syntax-ppss))) (or (nth 3 s) (nth 4 s)))
(looking-at "[[:space:]]*$")))
(forward-line))
(back-to-indentation)
(unless (bolp) (nth 5 (get-text-property (1- (point)) higp))))))
(defun highlight-indent-guides--try-update-line-cache ()
"Update the line cache, if necessary.
This function is called whenever the point moves in a way that might change the
line cache. It only updates the cache when absolutely necessary."
(when (and highlight-indent-guides-responsive
highlight-indent-guides-mode)
(let ((cached-pt (car highlight-indent-guides--line-cache))
(cached-ln (nth 1 highlight-indent-guides--line-cache))
(cached-dt (nth 2 highlight-indent-guides--line-cache))
(pt (point))
ln dt rng)
(catch 'nochange
(when (eq pt cached-pt) (throw 'nochange nil))
(setcar highlight-indent-guides--line-cache pt)
(setq ln (line-number-at-pos))
(when (eq ln cached-ln) (throw 'nochange nil))
(setcar (cdr highlight-indent-guides--line-cache) ln)
(setq dt (highlight-indent-guides--update-line-cache))
(when (equal dt cached-dt) (throw 'nochange nil))
(setcar (cddr highlight-indent-guides--line-cache) dt)
(setq rng (highlight-indent-guides--discover-ranges dt cached-dt))
(dolist (range (apply 'highlight-indent-guides--try-merge-ranges rng))
(highlight-indent-guides--overdraw (car range) (cdr range)))))))
(defun highlight-indent-guides--iscdr (sub sup)
"Calculate whether SUB is a cdr of SUP."
(if (null sub) t
(while (and sup (not (eq sub sup))) (setq sup (cdr sup)))
(eq sub sup)))
(defun highlight-indent-guides--calc-guides (prev-guides)
"Calculate the indent guides for a line.
PREV-GUIDES are the previous line's indent guides, and INDENT is this line's
indent width."
(let ((indent (current-indentation))
(guides (car prev-guides))
(sections (cdr prev-guides))
oldsections)
(while (and guides (< indent (car guides)))
(set-marker (cdar sections) (line-end-position 0))
(setq oldsections sections)
(setq sections (cdr sections))
(setq guides (cdr guides)))
(when (and (< 0 indent) (or (null guides) (> indent (car guides))))
(if oldsections (setq sections oldsections)
(let* ((lbp (line-beginning-position))
(begmark (copy-marker lbp)) (endmark (copy-marker lbp)))
(setq sections (cons (cons begmark endmark) sections))))
(setq guides (cons indent guides)))
(cons guides sections)))
(defun highlight-indent-guides--get-guides ()
"Extract the indent guides from a line, by reading the text properties."
(save-excursion
(catch 'invalid
(let (prop face seg sect nface nseg nsect guides fst)
(while (looking-at "[[:space:]]")
(setq prop (get-text-property (point) 'highlight-indent-guides-prop))
(setq nface (car prop) nseg (nth 1 prop) nsect (nth 5 prop))
(setq fst (nth 2 prop))
(unless (natnump nface) (throw 'invalid t))
(unless (or seg nseg)
(when (and fst (eq face nface)) (throw 'invalid t))
(when (not (or fst (eq face nface))) (throw 'invalid t)))
(unless (highlight-indent-guides--iscdr sect nsect)
(throw 'invalid t))
(let ((l (- (length nsect) (length sect) (length nseg))))
(when fst (setq l (1- l)))
(when nseg (setq l (1+ l)))
(when (not (zerop l)) (throw 'invalid t)))
(unless (and (eq face nface) (equal seg nseg))
(let ((col (current-column)))
(when (and face (not (eq face nface)))
(setq guides (cons col guides)))
(dolist (segment nseg)
(setq guides (cons (+ segment col) guides))
(setq nface (1+ nface))))
(setq face nface seg nseg))
(setq sect nsect)
(forward-char))
(dolist (section sect)
(unless (and (eq (marker-buffer (car section)) (current-buffer))
(eq (marker-buffer (cdr section)) (current-buffer))
(<= (car section) (point) (cdr section)))
(throw 'invalid t)))
(let ((col (current-column)))
(when (< 0 col) (setq guides (cons col guides))))
(cons guides sect)))))
(defun highlight-indent-guides--get-prev-guides ()
"Scan up the buffer to find a starting point to calculate guides from."
(let ((guides t))
(while (and (nlistp guides) (let ((p (point)))
(or (/= -1 (forward-line -1))
(not (goto-char p)))))
(unless (or (let ((s (syntax-ppss))) (or (nth 3 s) (nth 4 s)))
(looking-at "[[:space:]]*$"))
(setq guides (highlight-indent-guides--get-guides))))
(if (listp guides) guides nil)))
(defun highlight-indent-guides--guide-line (guides)
"Draw the indent guides specified by GUIDES on the current line."
(let ((guides (reverse (car guides)))
(sections (cdr guides))
(column (current-column))
(currpt (point))
(starter t)
(face 0) currcol currface props oldprop newprop subsect)
(while guides
(setq props nil)
(setq currcol column)
(setq currface face)
(setq currpt (point))
(forward-char)
(setq column (current-column))
(while (and guides (< (car guides) column))
(setq props (cons (- (car guides) currcol) props))
(setq guides (cdr guides))
(setq face (1+ face)))
(setq props (reverse props))
(when (and props (zerop (car props)))
(setq props (cdr props))
(setq currface (1+ currface))
(setq starter t))
(setq subsect (nthcdr (1- (length guides)) sections))
(setq oldprop (get-text-property currpt 'highlight-indent-guides-prop))
(setq newprop
(list currface props starter (- column currcol) nil subsect))
(when (and oldprop
(eq (car newprop) (car oldprop))
(equal (nth 1 newprop) (nth 1 oldprop))
(eq (nth 2 newprop) (nth 2 oldprop))
(eq (nth 3 newprop) (nth 3 oldprop)))
(setcar (nthcdr 4 newprop) (nth 4 oldprop)))
(when guides
(add-text-properties
currpt (1+ currpt) `(highlight-indent-guides-prop ,newprop)))
(setq starter nil))))
(defun highlight-indent-guides--replace-section (old search replace)
"Replace in a list OLD section prefixes SEARCH with REPLACE.
All lines in the same section should have the same (eq) section prefixes. If
the prefix changes on some lines, all other lines in the section need to be
updated to match."
(let* ((oldlen (length old))
(replen (length replace))
(minlen (min oldlen replen))
(cparent (nthcdr (- oldlen minlen) (cons nil old)))
(cold (nthcdr (- oldlen minlen) old))
(csearch (nthcdr (- replen minlen) search))
(crepl (nthcdr (- replen minlen) replace)))
(while (and cold (not (eq cold csearch)))
(setq cparent (cdr cparent))
(setq cold (cdr cold))
(setq csearch (cdr csearch))
(setq crepl (cdr crepl)))
(if (null cold) old
(setcdr cparent crepl)
(if (car cparent) old (cdr cparent)))))
(defun highlight-indent-guides--guide-region (start end)
"Add or update indent guides in the buffer region from START to END."
(with-silent-modifications
(save-excursion
(goto-char start)
(beginning-of-line)
(let ((prop 'highlight-indent-guides-prop)
(guides (highlight-indent-guides--get-prev-guides))
(eof (< 0 (forward-line)))
(startl (point)) (endl end)
chunk oldguides oldsect newsect lf le rng)
;; for the given region, extract old guides and calculate new guides
(while (not (or eof (and (>= (point) endl)
(not (eq oldguides t))
(equal (car guides) (car oldguides))
(eq (cdr guides) (cdr oldguides)))))
(if (or (let ((s (syntax-ppss))) (or (nth 3 s) (nth 4 s)))
(looking-at "[[:space:]]*$"))
(setq chunk (cons (list (point)) chunk))
(let ((tmpguides (cdr guides)) ends)
(while tmpguides
(when (car tmpguides)
(setq ends (cons (marker-position (cdar tmpguides)) ends)))
(setq tmpguides (cdr tmpguides)))
(setq guides (highlight-indent-guides--calc-guides guides))
(setq endl (max endl (or (nth (length (cdr guides)) ends) 0))))
(setq oldguides (highlight-indent-guides--get-guides))
(setq chunk (cons (list (point) guides oldguides) chunk)))
(setq eof (< 0 (forward-line)))
;; expand sections if necessary
(when (or eof (and (>= (point) endl)
(not (eq oldguides t))
(equal (car guides) (car oldguides))))
(let ((lep (line-end-position 0)))
(dolist (guide (cdr guides))
(when (and (cdr guide) (> lep (cdr guide)))
(set-marker (cdr guide) lep)))))
;; ensure chunk is flush with surrounding sections
(when (and (>= (point) endl)
(not (eq oldguides t))
(equal (car guides) (car oldguides))
(not (eq (cdr guides) (cdr oldguides))))
(setq guides (cons (car guides) (cdr guides)))
(let ((ng (cdr guides)) (og (cdr oldguides)) (badguide t)
abovestart aboveend belowstart belowend above below)
(while (and og ng (nlistp badguide))
(when (eq (cdr og) (cdr ng)) (setq badguide (cons og ng)))
(setq ng (cdr ng) og (cdr og)))
(setq abovestart (caar (cdr badguide)) aboveend startl)
(setq belowstart (point) belowend (cdar (car badguide)))
(setq above (- aboveend abovestart) below (- belowend belowstart))
(if (>= (- belowstart abovestart) below) (setq endl belowend)
(if (>= 0 above)
(let ((ng (cdr guides)) (og (cdr oldguides)))
;; transform existing lines in chunk to use new sections
(while (and og ng)
(set-marker (caar og) (caar ng))
(setq ng (cdr ng) og (cdr og)))
(dolist (line chunk)
(when (cdr line)
(setcdr (nth 1 line)
(highlight-indent-guides--replace-section
(cdr (nth 1 line))
(cdr guides) (cdr oldguides))))))
(goto-char abovestart)
(setq guides (highlight-indent-guides--get-prev-guides))
(setq eof (< 0 (forward-line)))
(setq startl (point) oldguides nil chunk nil))))))
;; rewrite text properties for all lines in chunk
(dolist (line chunk)
(goto-char (car line))
(if (cdr line)
(setq lf (save-excursion (back-to-indentation) (point)))
(setq lf (car line)))
(setq le (line-end-position))
(unless (and (null (get-text-property lf prop))
(eq le (next-single-property-change lf prop nil le)))
(remove-text-properties lf le (list prop nil)))
(when (or (eq t (nth 2 line))
(not (equal (car (nth 1 line)) (car (nth 2 line))))
(not (eq (cdr (nth 1 line)) (cdr (nth 2 line)))))
(highlight-indent-guides--guide-line (nth 1 line))))
;; update the line cache if necessary
(when (car highlight-indent-guides--line-cache)
(goto-char (car highlight-indent-guides--line-cache))
(setq oldsect (nth 2 highlight-indent-guides--line-cache))
(setq newsect (highlight-indent-guides--update-line-cache))
(setcar (cddr highlight-indent-guides--line-cache) newsect))
;; refontify updated regions
(if (equal oldsect newsect)
(font-lock-fontify-region startl endl)
(setq rng (highlight-indent-guides--discover-ranges oldsect newsect))
(dolist (range (highlight-indent-guides--try-merge-ranges
(cons startl endl) (car rng) (cadr rng)))
(font-lock-fontify-region (car range) (cdr range))))))))
(defun highlight-indent-guides--unguide-region (start end)
"Remove all indent guides in the buffer region from START to END."
(with-silent-modifications
(remove-text-properties start end '(highlight-indent-guides-prop nil))))
(defun highlight-indent-guides--fill-keyword-matcher (limit)
"Search for indent guides between the point and LIMIT.
Find the next character that is part of any indentation. This is meant to be
used as a `font-lock-keywords' matcher."
(let* ((pos (point))
(prop 'highlight-indent-guides-prop)
(face (car (get-text-property pos prop))))
(while (and (not (natnump face)) (< pos limit))
(setq pos (next-single-property-change pos prop nil limit))
(setq face (car (get-text-property pos prop))))
(when (< pos limit)
(set-match-data (list (copy-marker pos) (copy-marker (1+ pos))))
(goto-char (1+ pos)))))
(defun highlight-indent-guides--column-keyword-matcher (limit)
"Search for indent guides between the point and LIMIT.
Find the next character that contains the first column of an indentation level.
This is meant to be used as a `font-lock-keywords' matcher."
(let* ((pos (point))
(prop 'highlight-indent-guides-prop)
(propval (get-text-property pos prop)))
(while (and (not (and (natnump (car propval))
(or (nth 2 propval) (nth 1 propval))))
(< pos limit))
(setq pos (1+ pos))
(setq propval (get-text-property pos prop))
(while (and (< pos limit) (not (natnump (car propval))))
(setq pos (next-single-property-change pos prop nil limit))
(setq propval (get-text-property pos prop))))
(when (< pos limit)
(set-match-data (list (copy-marker pos) (copy-marker (1+ pos))))
(goto-char (1+ pos)))))
(defun highlight-indent-guides--highlighter-default (level responsive display)
"Determine the correct face to use for a given indentation level.
Uses the LEVEL, RESPONSIVE context, and DISPLAY method to decide on a correct
face for any given indentation. This is the default implementation of
`highlight-indent-guides-highlighter-function'."
(cond ((null responsive)
(cond ((or (eq display 'character) (eq display 'bitmap))
'highlight-indent-guides-character-face)
((zerop (mod level 2))
'highlight-indent-guides-even-face)
(t 'highlight-indent-guides-odd-face)))
((eq responsive 'top)
(cond ((or (eq display 'character) (eq display 'bitmap))
'highlight-indent-guides-top-character-face)
((zerop (mod level 2))
'highlight-indent-guides-top-even-face)
(t 'highlight-indent-guides-top-odd-face)))
((eq responsive 'stack)
(cond ((or (eq display 'character) (eq display 'bitmap))
'highlight-indent-guides-stack-character-face)
((zerop (mod level 2))
'highlight-indent-guides-stack-even-face)
(t 'highlight-indent-guides-stack-odd-face)))
(t nil)))
(defmacro highlight-indent-guides--cache-highlight (type prop hlkey &rest body)
"Memoize the highlighter results in the character's properties.
If a cached result with the right TYPE (`fill', `column', or `character') is
contained in PROP with a responsive context matching HLKEY, return that result
instead of calculating a new one. Otherwise, calculate a new result by running
BODY, cache it in PROP, and return it."
`(let ((cache (nth 4 ,prop)) plist result)
(if (and (eq ,type (car cache))
(setq result (plist-get (cdr cache) ,hlkey)))
result
(setq result (progn ,@body))
(setq plist (plist-put (cdr cache) ,hlkey result))
(setcar (nthcdr 4 ,prop) (cons ,type plist))
result)))
(defun highlight-indent-guides--should-highlight (prop)
"Determine how a guide should be highlighted in responsive mode.
The guide's data is given as PROP."
(if (null highlight-indent-guides-responsive) nil
(let ((currseg (nth 5 prop))
(segct (max 1 (+ (length (nth 1 prop)) (if (nth 2 prop) 1 0))))
(cacheseg (nth 2 highlight-indent-guides--line-cache))
(isstack (eq highlight-indent-guides-responsive 'stack))
result)
(dotimes (_ segct result)
(cond ((equal cacheseg currseg)
(setq result (cons 'top result)))
((and isstack (highlight-indent-guides--iscdr currseg cacheseg))
(setq result (cons 'stack result)))
(t (setq result (cons nil result))))
(setq currseg (cdr currseg))))))
(defun highlight-indent-guides--fill-highlighter ()
"Apply highlighting to the indentation.
Return highlighting information for the matched character. Highlights all
indentation characters in alternating colors. This is meant to be used as a
`font-lock-keywords' face definition."
(let* ((prop (get-text-property (match-beginning 0) 'highlight-indent-guides-prop))
(shouldhl (highlight-indent-guides--should-highlight prop)))
(highlight-indent-guides--cache-highlight
'fill prop shouldhl
(let ((highlighter highlight-indent-guides-highlighter-function)
(facep (car prop)) (segs (nth 1 prop)) (cwidth (nth 3 prop))
(pseg 0) face showstr)
(if (null segs) (funcall highlighter facep (car shouldhl) 'fill)
(setq showstr (make-string cwidth ?\s))
(dolist (seg segs)
(setq face (funcall highlighter facep (pop shouldhl) 'fill))
(when face (add-text-properties pseg seg `(face ,face) showstr))
(setq pseg seg)
(setq facep (1+ facep)))
(setq face (funcall highlighter facep (pop shouldhl) 'fill))
(when face (add-text-properties pseg cwidth `(face ,face) showstr))
`(face nil display ,showstr))))))
(defun highlight-indent-guides--column-highlighter ()
"Apply highlighting to the indentation.
Return highlighting information for the matched character. Highlights the
first column of each indentation level in alternating colors. This is meant to
be used as a `font-lock-keywords' face definition."
(let* ((prop (get-text-property (match-beginning 0) 'highlight-indent-guides-prop))
(shouldhl (highlight-indent-guides--should-highlight prop)))
(highlight-indent-guides--cache-highlight
'column prop shouldhl
(let ((highlighter highlight-indent-guides-highlighter-function)
(facep (car prop)) (segs (nth 1 prop))
(starter (nth 2 prop)) (cwidth (nth 3 prop))
face showstr)
(if (and (null segs) (eq cwidth 1))
(funcall highlighter facep (car shouldhl) 'column)
(setq showstr (make-string cwidth ?\s))
(when starter
(setq face (funcall highlighter facep (pop shouldhl) 'column))
(when face (add-text-properties 0 1 `(face ,face) showstr)))
(dolist (seg segs)
(setq face (funcall highlighter facep (pop shouldhl) 'column))
(when face (add-text-properties seg (1+ seg) `(face ,face) showstr))
(setq facep (1+ facep)))
`(face nil display ,showstr))))))
(defun highlight-indent-guides--character-highlighter ()
"Apply highlighting to the indentation.
Return highlighting information for the matched character. Displays a character
in place of the first column of each indentation level. This is meant to be
used as a `font-lock-keywords' face definition."
(let* ((prop (get-text-property (match-beginning 0) 'highlight-indent-guides-prop))
(shouldhl (highlight-indent-guides--should-highlight prop)))
(highlight-indent-guides--cache-highlight
'character prop shouldhl
(let ((highlighter highlight-indent-guides-highlighter-function)
(facep (car prop)) (segs (nth 1 prop))
(starter (nth 2 prop)) (cwidth (nth 3 prop))
face showstr)
(if (and (null segs) (eq cwidth 1))
(progn
(setq face (funcall highlighter facep (car shouldhl) 'character))
(when face
(setq showstr
(char-to-string highlight-indent-guides-character)))
`(face ,face display ,showstr))
(setq showstr (make-string cwidth ?\s))
(when starter
(setq face (funcall highlighter facep (pop shouldhl) 'character))
(when face
(aset showstr 0 highlight-indent-guides-character)
(add-text-properties 0 1 `(face ,face) showstr)))
(dolist (seg segs)
(setq face (funcall highlighter facep (pop shouldhl) 'character))
(when face
(aset showstr seg highlight-indent-guides-character)
(add-text-properties seg (1+ seg) `(face ,face) showstr))
(setq facep (1+ facep)))
`(face nil display ,showstr))))))
(defmacro highlight-indent-guides--memoize-bitmap (idx &rest body)
"Memoize data for the bitmap highlighter.
Run and return BODY. Memoize the result using the key IDX. If the macro is
called again with the same IDX, return the memoized data instead of running BODY
again."
`(let ((val (gethash ,idx highlight-indent-guides--bitmap-memo)))
(if val val
(setq val (progn ,@body))
(puthash ,idx val highlight-indent-guides--bitmap-memo)
val)))
(defun highlight-indent-guides--bitmap-highlighter ()
"Apply highlighting to the indentation.
Return highlighting information for the matched character. Displays a bitmap in
place of the first column of each indentation level. This is meant to be used
as a `font-lock-keywords' face definition."
(let* ((prop (get-text-property (match-beginning 0) 'highlight-indent-guides-prop))
(shouldhl (highlight-indent-guides--should-highlight prop)))
(highlight-indent-guides--cache-highlight
'bitmap prop shouldhl
(let ((highlighter highlight-indent-guides-highlighter-function)
(facep (car prop)) (segs (nth 1 prop))
(starter (nth 2 prop)) (cwidth (nth 3 prop))
(width (frame-char-width)) (height (frame-char-height))
face facelist showbmp)
(if (and (null segs) (eq cwidth 1))
(progn
(setq face (funcall highlighter facep (car shouldhl) 'bitmap))
(when face
(setq showbmp
(highlight-indent-guides--memoize-bitmap
(concat ";" (number-to-string width)
":" (number-to-string height)
":" (face-foreground face))
(highlight-indent-guides--draw-bitmap
(highlight-indent-guides--build-bitmap
width height (face-foreground face))))))
`(face nil display ,(copy-sequence showbmp)))
(setq facelist (make-list cwidth nil))
(when starter
(setq face (funcall highlighter facep (pop shouldhl) 'bitmap))
(when face (setcar facelist (face-foreground face))))
(dolist (seg segs)
(setq face (funcall highlighter facep (pop shouldhl) 'bitmap))
(when face (setcar (nthcdr seg facelist) (face-foreground face)))
(setq facep (1+ facep)))
(setq showbmp
(highlight-indent-guides--memoize-bitmap
(concat ";" (number-to-string width)
":" (number-to-string height)
":" (mapconcat 'identity facelist ":"))
(highlight-indent-guides--concat-bitmap width height facelist)))
`(face nil display ,(copy-sequence showbmp)))))))
(defun highlight-indent-guides--concat-bitmap (width height facelist)
"Build a concatenated PBM image based on FACELIST.
FACELIST represents characters in the guide block (nil for no guide, and a color
string for a guide with that color). WIDTH and HEIGHT are the width and height
of each character in the block."
(let ((res (make-list height nil))
nextbmp)
(while facelist
(if (null (car facelist))
(let ((zlen 0))
(while (and facelist (null (car facelist)))
(setq zlen (+ zlen width))
(setq facelist (cdr facelist)))
(dotimes (i height)
(setcar (nthcdr i res) (append (nth i res) (make-list zlen " 65535 0 65535")))))
(setq nextbmp (highlight-indent-guides--build-bitmap width height (car facelist)))
(setq facelist (cdr facelist))
(dotimes (i height)
(setcar (nthcdr i res) (append (nth i res) (nth i nextbmp))))))
(highlight-indent-guides--draw-bitmap res)))
(defun highlight-indent-guides--draw-bitmap (lines)
"Using pixel data LINES, build a PBM image."
(let* ((width (length (car lines)))
(height (length lines))
(data (concat "P3 " (number-to-string width) " " (number-to-string height) " 65535")))
(dolist (line lines) (setq data (concat data (apply 'concat line))))
`(image :type pbm :data ,data :mask heuristic :ascent center)))
(defun highlight-indent-guides--build-bitmap (width height face)
"Build a PBM image string.
The image is of dimensions WIDTH and HEIGHT, and color FACE, and generated by
`highlight-indent-guides-bitmap-function'."
(highlight-indent-guides--memoize-bitmap
(concat (number-to-string width) ":" (number-to-string height) ":" face)
(funcall highlight-indent-guides-bitmap-function
width height
(highlight-indent-guides--pbm-color face) " 65535 0 65535")))
(defun highlight-indent-guides--pbm-color (color)
"Create a PBM color string from the Emacs color string COLOR."
(highlight-indent-guides--memoize-bitmap
color
(let* ((rgb (color-name-to-rgb color))
(r (number-to-string (floor (* 65536 (car rgb)))))
(g (number-to-string (floor (* 65536 (nth 1 rgb)))))
(b (number-to-string (floor (* 65536 (nth 2 rgb))))))
(concat " " r " " g " " b))))
(defun highlight-indent-guides--bitmap-line (width height crep zrep)
"Define a solid guide line, two pixels wide.
Use WIDTH, HEIGHT, CREP, and ZREP as described in
`highlight-indent-guides-bitmap-function'."
(let* ((left (/ (- width 2) 2))
(right (- width left 2))
(row (append (make-list left zrep) (make-list 2 crep) (make-list right zrep)))
rows)
(dotimes (_ height rows)
(setq rows (cons row rows)))))
(defun highlight-indent-guides--bitmap-dots (width height crep zrep)
"Define a dotted guide line, with 2x2 pixel dots, and 3 or 4 dots per row.
Use WIDTH, HEIGHT, CREP, and ZREP as described in
`highlight-indent-guides-bitmap-function'."
(let* ((left (/ (- width 2) 2))
(right (- width left 2))
(space3 (/ height 3))
(space31 (/ (- space3 2) 2))
(space4 (/ height 4))
(space41 (/ (- space4 2) 2))
(row1 (append (make-list left zrep) (make-list 2 crep) (make-list right zrep)))
(row2 (make-list width zrep))
space space1 rows)
(if (< (abs (- space4 space41 space41)) (abs (- space3 space31 space31)))
(setq space space4 space1 space41)
(setq space space3 space1 space31))
(dotimes (i height rows)
(let ((x (mod (- i space1) space)))
(if (or (eq x 0) (eq x 1))
(setq rows (cons row1 rows))
(setq rows (cons row2 rows)))))))
(defun highlight-indent-guides--overdraw (start end)
"Overdraw the guides in the region from START to END.
This function is like `font-lock-fontify-region' or `font-lock-ensure', except
it only draws indent guides. This function is called to update the display
whenever the active indent level changes, as long as responsive guides are
enabled. This function is used because it avoids doing extra work like clearing
existing fontification, redrawing syntax and other keywords, or calling jit-lock
recursively."
(with-silent-modifications
(save-excursion
(save-restriction
(let ((matcher
(pcase highlight-indent-guides-method
(`fill 'highlight-indent-guides--fill-keyword-matcher)
(`column 'highlight-indent-guides--column-keyword-matcher)
(`character 'highlight-indent-guides--column-keyword-matcher)
(`bitmap 'highlight-indent-guides--column-keyword-matcher)))
(highlight
(pcase highlight-indent-guides-method
(`fill 'highlight-indent-guides--fill-highlighter)
(`column 'highlight-indent-guides--column-highlighter)
(`character 'highlight-indent-guides--character-highlighter)
(`bitmap 'highlight-indent-guides--bitmap-highlighter))))
(unless font-lock-dont-widen (widen))
(goto-char start)
(while (and (< (point) end) (funcall matcher end))
(unless (> (point) (match-beginning 0)) (forward-char 1))
(font-lock-apply-highlight (list 0 (list highlight) t))))))))
;;;###autoload
(defun highlight-indent-guides-auto-set-faces (&rest _)
"Automatically calculate indent guide faces.
If this feature is enabled, calculate reasonable values for the indent guide
colors based on the current theme's colorscheme, and set them appropriately.
This runs whenever a theme is loaded, but it can also be run interactively."
(interactive)
(when highlight-indent-guides-auto-enabled
(let* ((bk (face-background 'default nil 'default))
(fg (color-name-to-rgb (face-foreground 'default nil 'default)))
(bg (color-name-to-rgb bk))
(oddf 'highlight-indent-guides-odd-face)
(evenf 'highlight-indent-guides-even-face)
(charf 'highlight-indent-guides-character-face)
(toddf 'highlight-indent-guides-top-odd-face)
(tevenf 'highlight-indent-guides-top-even-face)
(tcharf 'highlight-indent-guides-top-character-face)
(soddf 'highlight-indent-guides-stack-odd-face)
(sevenf 'highlight-indent-guides-stack-even-face)
(scharf 'highlight-indent-guides-stack-character-face)
(oddp highlight-indent-guides-auto-odd-face-perc)
(evenp highlight-indent-guides-auto-even-face-perc)
(charp highlight-indent-guides-auto-character-face-perc)
(toddp highlight-indent-guides-auto-top-odd-face-perc)
(tevenp highlight-indent-guides-auto-top-even-face-perc)
(tcharp highlight-indent-guides-auto-top-character-face-perc)
(soddp highlight-indent-guides-auto-stack-odd-face-perc)
(sevenp highlight-indent-guides-auto-stack-even-face-perc)
(scharp highlight-indent-guides-auto-stack-character-face-perc)
mod fl bl)
(if (not (and fg bg))
(unless highlight-indent-guides-suppress-auto-error
(message "Error: %s: %s"
"highlight-indent-guides cannot auto set faces"
"`default' face is not set properly"))
(setq fl (nth 2 (apply 'color-rgb-to-hsl fg)))
(setq bl (nth 2 (apply 'color-rgb-to-hsl bg)))
(setq mod (cond ((< fl bl) -1) ((> fl bl) 1) ((< 0.5 bl) -1) (t 1)))