-
Notifications
You must be signed in to change notification settings - Fork 4
/
core.cljs
2311 lines (1959 loc) · 80.3 KB
/
core.cljs
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
(ns paredit-cm.core
"paredit operations (exported)"
(:require [clojure.string :as str]
[cljsjs.codemirror]
[cljsjs.codemirror.mode.clojure]
[cljsjs.codemirror.keymap.emacs]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; MIT License
;;
;; Copyright (c) 2017 Andrew Cheng
;;
;; 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.
;;
;; Jon Anthony (2019,2020):
;; Many changes and fixes for working with newer codemirror releases
;;
;; ** PAREDI PROJECT CONVENTIONS **
;;
;; consider this notation: aXbc
;;
;; in the unit tests as well as here, aXbc contains a single capital X which
;; represents the position of the cursor. aXbc means the code mirror instance's
;; value is 'abc' and a block-style cursor is on 'b' (a bar-style cursor would
;; be between 'a' and 'b'). aXbc is what you would see if you typed a capital X
;; in this example code mirror.
;;
;; 'cur' is for the current position's cursor (on 'b' in the example).
;; 'left-cur' is for position 'a'. 'right-cur' is for position 'c'.
;;
;; if there is a current cursor cur and a new cursor, then the new cursor will
;; be named cur' (the single quote is part of the name, so read it aloud as
;; cursor-prime)
;;
;; when there are two cursors (as in the beginning and ending of a selection) we
;; use c1 and c2. it feels strange to call them 'start' and 'end' when those are
;; the names codemirror uses to refer to the ends of a token.
;;
;; the following all refer to the values for the token at 'cur': 'start' 'line'
;; 'ch' 'i' 'string' 'type'
;;
;; use the same prefixes 'left-' and 'right-' when referring to the same kinds
;; of values belonging to 'left-cur' and 'right-cur'
;;
;; ints *other than i, the code mirror index* are named with a single character
;; like 'x'. neighboring values are represented alphabetically, so (inc x) would
;; be named 'y' and (dec x) would be named 'w'.
;;
;; s1 is a string. similarly s1, s2, and s
;;
;; for numerical values like 'offset', lower is for left and higher is for
;; right, just as for code mirror's index i.
;;
;; sp is a 'skipping predicate'. these are used with a trampoline wrapper like
;; 'skip' to move along the text in code mirror until our predicate is
;; satisfied. in many cases, the predicate will push and pop openers/closers off
;; a stack and when the stack is empty and we satisfy some additional condition,
;; then we stop and return the cursor.
;;
;; functions with names ending in -sp are skipping predicates.
;;
;; currently we're assuming perfect matching of openers/closers so we don't
;; actually keep track of the stack -- we just inc and dec an int until it gets
;; to 0 and our other conditions are satisfied
;;
;; any trampoline use should be limited by the cm character count, to guard
;; against infinite loops. we'll start at the limit and count down, stopping
;; when it goes negative.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(enable-console-print!)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; general helper methods
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def openers #{ "(" "[" "{" })
(def closers #{ ")" "]" "}" })
(def pair {"(" ")", "[" "]", "{" "}", "\"" "\"",
")" "(", "]" "[", "}" "{"})
(defn pair?
"true if the two strings are a matching open/close pair "
[s1 s2]
(= (pair s1) s2))
(defn opener? [s] (contains? openers s))
(defn closer? [s] (contains? closers s))
(defn is-bracket-type?
[t]
(and t (str/starts-with? t "bracket")))
(defn char-count
"returns the number of characters in the code mirror instance"
[cm]
(-> cm .getValue count))
(defn cursor
"get cur, the position of the cursor"
([cm] (.getCursor cm)) ;; get current cursor
([cm i] (.posFromIndex cm i))) ;; get cursor for index i
(defn index
"get the index i for the cursor's position"
([cm] (index cm (cursor cm)))
([cm cur] (when cur (.indexFromPos cm cur))))
(defn bof?
"true if at beginning of file"
[cm cur]
(zero? (index cm cur)))
(defn eof?
"true if at end of file"
[cm cur]
(= (index cm cur) (char-count cm)))
(defn token
"get token at cursor"
[cm cur]
(.getTokenAt cm cur true))
(defn get-type
"get the type at the current cursor."
([cm]
(get-type cm (cursor cm)))
([cm cur]
(.-type (token cm cur))))
(defn get-string
"gets the string of the current token"
([cm] (get-string cm (cursor cm)))
([cm cur] (when cur (.-string (token cm cur)))))
(defn line-length
"gets the length of the current line"
([cm] (line-length cm (cursor cm)))
([cm cur] (when cur (count (.getLine cm (.-line cur))))))
(defn last-token
"returns the last token of a line"
[cm cur]
(->> cur .-line (.getLineTokens cm) last))
(defn last-cur
"returns the last cursor of a line"
([cm] (last-cur cm (cursor cm)))
([cm cur] (let [end (.-end (last-token cm cur))
diff (- end (.-ch cur))]
(cursor cm (+ diff (index cm cur))))))
(defn get-info
"make info from CodeMirror more conveniently accessed by our code.
we'll use destructuring and just name what we rant. hypothesizing
that performance hit won't be that bad."
([cm] (get-info cm (cursor cm)))
([cm cur]
(when cur (let [tok (token cm cur)
eof (eof? cm cur)
bof (bof? cm cur)
i (index cm cur)
left-cur (when-not bof (cursor cm (dec i)))
right-cur (when-not eof (cursor cm (inc i)))]
{:cur cur
:line (.-line cur)
:ch (.-ch cur)
:i i
:tok tok
:string (.-string tok)
:start (.-start tok)
:end (.-end tok)
:type (.-type tok)
:top (-> tok .-state .-indentStack nil?) ;; true for toplevel
:eof eof
:bof bof
:left-char (when-not bof (.getRange cm left-cur cur))
:right-char (when-not eof (.getRange cm cur right-cur))
:left-cur left-cur
:right-cur right-cur
:mode (.-mode (.-state tok))}))))
(defn comment-or-string?
"true if the type is comment or string. a lot of editing behavior (like
movement and deletion) is similar when you are in a string or in a comment, so
often this is the predicate for that behavior."
[type]
(or (= type "comment")
(= type "string")))
(defn indent-line
"indent the current line"
[cm]
(->> cm cursor .-line (.indentLine cm)))
(defn escaped-char-name? [stg]
(let [escnames #{"\\newline", "\\space", "\\tab",
"\\formfeed", "\\backspace", "\\return"}]
(when (escnames stg) (dec (count stg)))))
(defn in-escaped-char?
"returns true if backslash is to the left and cursor is on an escaped char"
([cm cur]
(in-escaped-char? cm cur 0))
([cm cur offset]
(let [{:keys [ch start end type]} (get-info cm cur)]
#_(js/console.log start ch end type)
(and (= type "string-2") (and (< start ch) (< ch end))))))
(defn escaped-char-to-left?
"returns true if an escaped char and its backslash are to the left"
[cm cur]
(let [{:keys [ch end type string]} (get-info cm cur)]
(and (= type "string-2") (= ch end))))
(defn escaped-char-to-right?
"returns true if an escaped char and its backslash is to the right"
[cm cur]
(let [cur+ (cursor cm 0)
{:keys [type]} (get-info cm cur)]
(and (not= type "string-2"))
(set! cur+.line cur.line)
(set! cur+.ch (inc cur.ch))
(in-escaped-char? cm cur)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; paredit-open-round (
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn insert
"insert text at current cursor. move cursor to the end of inserted text minus
optional offset. the offset is for moving the cursor immediately after the
insert and before returning. example: inserting a pair of brackets and placing
the cursor inside the pair. this returns the new cursor."
([cm text] (insert cm text 0))
([cm text offset] (insert cm text offset (cursor cm)))
([cm text offset cur]
(let [{:keys [line ch]} (get-info cm cur)]
(.replaceRange cm text cur)
(.setCursor cm line (+ (+ ch (count text)) offset))
(cursor cm))))
(defn ^:export open-round
"paredit-open-round exposed for keymap. unlike traditional emacs paredit, this
supports brackets [] {} () but not double-quote"
([cm] (open-round cm "("))
([cm c]
(let [{:keys [type left-char right-char]} (get-info cm)]
(cond
;; escaping the next character:
(= "\\" left-char) (insert cm c)
;; typing in a comment or string as-is:
(comment-or-string? type) (insert cm c)
;; insert a pair, pad with a space to the left and/or right if necessary,
;; and move the cursor into the pair before returning:
:else
(let [need-left-padding (and (not= " " left-char)
(not (opener? left-char)))
need-right-padding (and (not= " " right-char)
(not (closer? right-char)))]
(insert cm
(str (when need-left-padding " ")
c (pair c)
(when need-right-padding " "))
(if need-right-padding -2 -1)))))))
(defn ^:export open-brace
"open curly brace with matching close brace"
([cm] (open-round cm "{")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; paredit-close-round )
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn parent-closer-sp ;; -sp see 'skipping predicate' below
"finds the *parent* closing bracket. behavior when used with skip: pushes
opening brackets that appear along the way on a stack. closing brackets pop
them off. stops when encountering a closing bracket while the stack is empty.
assuming the cm has matched brackets for now. moves to the right."
[cm cur state]
(let [{:keys [string type top eof]} (get-info cm cur)]
(cond
;; 'push' opener on our 'stack':
(and (is-bracket-type? type) (opener? string)), (inc state)
;; stop if we see a closer while our 'stack' is empty:
(and (is-bracket-type? type) (closer? string) (zero? state)), :yes
;; closer means we 'pop' off the 'stack', unless eof
(and (is-bracket-type? type) (closer? string) (not= 0 state) eof), :eof
;; closer means we 'pop' off the 'stack':
(and (is-bracket-type? type) (closer? string) (not= 0 state)), (dec state)
;; we can* rely on code mirror to tell us if we're at the top
;; level: (* NOT in [cljsjs/codemirror "5.21.0-2"] ... but maybe
;; in a later version ... until we can figure out how to refer
;; to the latest codemirror in our tests, the tests will have to
;; live here in order to get the codemirror that is included in
;; the script tag on the demo index.html page)
;; TODO: investigate whether we can use this, given CodeMirror version:
;; top, :stop
;; stack stays unchanged. move to the next thing:
:default, state)))
(defn token-start
"returns the cursor for the start of the current token"
[cm cur]
(let [{:keys [i line start ch type]} (get-info cm cur)]
(cursor cm (- i (- ch start)))))
(defn token-end
"returns the cursor for the end of the current token"
([cm cur] (token-end cm cur 0))
([cm cur offset]
(let [{:keys [i line end ch type]} (get-info cm cur)]
(cursor cm (+ i offset (- end ch))))))
(defn token-end-index
"take an index. get its token. return index of that token's end."
[cm i]
(->> i
(cursor cm)
(token-end cm)
(index cm)))
(defn guard [] (println "past"))
(defn skip-trampoline-helper
"returns the cursor that satsifies skipping predicate 'sp' or nil if eof
reached. does this by making sp something we can trampoline. sp takes these
args: cm, cursor, state. counts down 'n' to 0 in order to guard against
infinite loops."
[cm cur sp state n]
(if (>= n 0)
(let [{:keys [left-cur right-cur i]} (get-info cm cur)
result (sp cm cur state)]
#_(js/console.log result)
(case result
:eof nil
:stop nil
:yes cur
:left left-cur
:right right-cur
:end-of-this-token (token-end cm cur)
:start-of-this-tok (token-start cm cur)
(let [next-cur (token-end cm cur 1)] #_(js/console.log next-cur)
(fn [] ;; for trampoline
(skip-trampoline-helper cm next-cur sp result (dec n))))))
(guard)))
(defn skip-trampoline-helper-left
"like skip-trampoline-helper but in the opposite direction."
[cm cur sp state n]
(if (>= n 0)
(let [{:keys [left-cur right-cur i start ch]} (get-info cm cur)
result (sp cm cur state)]
#_(js/console.log result)
(case result
:bof nil
:stop nil
:yes left-cur
:right right-cur
:end-of-this-token (token-end cm cur)
:start-of-this-tok (token-start cm cur)
(let [next-cur (if (= ch start)
(cursor cm (dec i))
(cursor cm (- i (- ch start))))]
(fn [] ;; for trampoline
(skip-trampoline-helper-left cm next-cur sp result (dec n))))))
(guard)))
(defn skip
"returns the cursor that satisfies sp or nil if either eof reached
or we found out sp could not be satisfied. see skip-to for more
info."
([cm sp] (skip cm sp (cursor cm)))
([cm sp cur]
(when-let [right-cur (:right-cur (get-info cm cur))]
(trampoline skip-trampoline-helper cm right-cur sp 0 (char-count cm)))))
(defn skip-left
"returns the cursor that satisfies sp or nil if either bof reached
or we found out sp could not be satisfied. see skip-to for more
info."
[cm sp]
(when-let [cur (cursor cm)]
(trampoline skip-trampoline-helper-left cm cur sp 0 (char-count cm))))
(defn delete-whitespace
"if cur is in whitespace, deletes it optionally without ruining indentation."
([cm] (delete-whitespace cm (cursor cm) true))
([cm cur] (delete-whitespace cm cur true))
([cm cur indent-after]
(let [{:keys [start end line ch i type]} (get-info cm cur)
c1 (cursor cm (+ i (- start ch)))
c2 (cursor cm (+ i (- end ch)))]
(when (nil? type)
(.replaceRange cm "" c1 c2)
(if indent-after (.indentLine cm line))))))
;; todo
(defn just-one-space
([cm] (just-one-space cm (cursor cm) true))
([cm cur] (just-one-space cm cur true))
([cm cur indent-after]
(let [{:keys [start end line ch i type]} (get-info cm cur)
c1 (cursor cm (+ i (- start ch)))
c2 (cursor cm (+ i (- end ch)))]
(when (nil? type)
(.replaceRange cm " " c1 c2)
(if indent-after (.indentLine cm line))))))
(defn skip-to
"moves to the cursor that satisfies sp or doesn't move if eof reached.
starts at current cursor for cm. sp stands for 'skipping predicate'
which returns:
- :yes if sp is satisfied.
- :stop if we know we will not be satisfied with any future result.
- :left if the cursor to the left is what we want.
- new non-nil state if not satisfied. this state is used with the
next iteration after we skip to the end of the current token. an sp
takes cm, cursor, state."
[cm sp]
(when-let [cur' (skip cm sp)]
(.setCursor cm cur')
cur'))
(defn move-past-parent-closer
"moves cursor to just outside the closing bracket, or if there is
none then doesn't move at all."
;; emacs has this extending the current selection if there is one.
[cm]
(when-let [cur (skip-to cm parent-closer-sp)]
(delete-whitespace cm (:left-cur (get-info cm)))
cur))
(defn ^:export close-round
"paredit-close-round exposed for keymap. skips to end of current
list even if it ends with ] or }. but if you're in a string or
comment then this just inserts the bracket. requires CodeMirror
mode's parser uses state with indentStack because that's how we
can tell we've reached the end of a top level form and avoid
entering the next top level form. 's' is the character as a string."
([cm] (close-round cm ")"))
([cm s]
(let [{:keys [type left-char]} (get-info cm)]
(cond
(= "\\" left-char) (insert cm s)
(comment-or-string? type) (insert cm s)
:else (move-past-parent-closer cm)))))
(defn ^:export close-brace
"close curly brace like close-rond"
([cm] (close-round cm "}")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; paredit-close-round-and-newline paredit-open-square paredit-close-square
;; paredit-doublequote
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ^:export close-round-and-newline
([cm] (close-round-and-newline cm ")"))
([cm s]
(if (comment-or-string? (get-type cm))
(insert cm s)
(when (close-round cm s)
(.execCommand cm "newlineAndIndent")))))
;; question: is there a better way than .execCommand?
(defn ^:export open-square [cm] (open-round cm "["))
(defn ^:export close-square [cm] (close-round cm "]"))
(defn ^:export doublequote [cm]
(let [{:keys [type left-char right-char ch cur]} (get-info cm)]
(cond
;; about to escape this char so insert as-is:
(= "\\" left-char) (insert cm "\"")
;; we're in a string so escape this doublequote:
(= type "string") (insert cm "\\\"")
;; we're in code. pad with a space to the left and/or right if necessary
;; to separate it from neighboring code. after inserting, move the cursor
;; to between the quotes:
:else (insert cm
(str (when (not= " " left-char) " ") ;; left padding
"\"\""
(when (and (not= " " right-char)
(not= "\n" right-char))
" ")) ;; right padding
(if (or (= " " right-char)
(= "\n" right-char)) -1 -2)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; paredit-meta-doublequote M-"
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn word? [type]
(or (= type "atom")
(= type "builtin")
(= type "number")
(= type "variable")
(= type "keyword")
(= type "meta")))
(defn at-a-word?
"returns true if at a word of code"
[cm cur]
(word? (get-type cm cur)))
(defn in-a-word?
"true if in a word AND not at the end of that word. false if in whitespace or
a string or a comment or at a bracket."
[cm]
(let [cur (cursor cm), i (index cm cur)]
(and (at-a-word? cm cur)
(not= i (token-end-index cm i)))))
(defn start-of-a-string?
"returns true if at the start of a string."
[cm cur]
(let [{:keys [string type start ch left-char]} (get-info cm cur)]
#_(js/console.log right-char type string ch start)
(and (= left-char "\"")
(= type "string")
(= 1 (- ch start)))))
(defn start-of-a-string2? [cm cur]
(let [i (index cm cur)
p2 (cursor cm (inc i))]
#_(js/console.log cur p2)
(start-of-a-string? cm p2)))
(defn end-of-a-string?
"returns true if just to the right of a closing doublequote of a string."
[cm cur]
(let [{:keys [type ch end string left-char]} (get-info cm cur)]
#_(js/console.log left-char type string ch end)
(and (= type "string")
(= ch end)
(= left-char "\""))))
(defn end-of-next-sibling-sp ;; -sp see 'skipping predicate'
"returns the cursor at the end of the sibling to the right or nil if
no sibling or eof. does not exit the containing form. does this by
skipping past any comments or whitespace, and branches depending on
whether an opening bracket or doublequote is encountered (sp
satisfied when encountering a closing bracket that empties the
stack) vs the beginning of a word (return token at the end of the
word). assuming the cm has matched brackets for now."
[cm cur stack]
(let [dq "\""
info (get-info cm cur)
{:keys [string type eof ch end tok]} info
stack-empty (zero? stack)
one-left (= 1 stack)
;; for multi-line strings
start-of-stg? (start-of-a-string? cm cur)
end-of-stg? (end-of-a-string? cm cur)
empty-stg? (when end-of-stg?
(and (= tok.type "string")
(= tok.string "\"\"")))
string-extends (or (not= dq (last string))
(= "\\" (last (drop-last string))))]
#_(js/console.log stack stack-empty string type ch end cur string-extends
#_(escaped-char-to-right? cm cur)
start-of-stg?
end-of-stg?)
(cond ;; we return a keyword when we know where to stop, stack otherwise.
;; skip whitespace
(or (nil? type) (and (= type "error") (= string ","))), stack
(and (escaped-char-to-left? cm cur) stack-empty), :yes
(and (word? type) stack-empty (= ch end)), :yes
(and (is-bracket-type? type) (closer? string) one-left), :yes
(and end-of-stg? one-left), :yes
eof, :eof
;; skip comments
(= type "comment"), stack
;; strings ...............................................................
empty-stg? :end-of-this-token
;; our starting point is at beginning of a string and it doesn't extend
(and start-of-stg?
(and (not string-extends) stack-empty)), :end-of-this-token
;; We are in a nested form, at start of string, but it doesn't extend
(and start-of-stg?
(not stack-empty)
(not string-extends)), stack
;; entering a multi-line string, push " onto stack
(and start-of-stg?
string-extends), (inc stack)
;; at end of string and stack already empty, we must have started in the
;; middle of the string
(and end-of-stg? stack-empty), :stop
;; at end of string and stack about to be empty, we've found the end of
;; the string -- handled before checking for eof above
;; in string, the end of this string is our goal ...
;; ... but the end of this string is on a different line:
(and (= type "string")
#_(not stack-empty) #_one-left
string-extends), stack
(and (= type "string")
stack-empty
(not string-extends)), :end-of-this-token
;; in string, the end of this string is our goal ...
;; ... the end is on this line:
(and (= type "string") one-left), :end-of-this-token
;; in string, need to get out of this form, pop stack
(and (= type "string")
(not stack-empty)), (dec stack)
;; escaped chars .........................................................
;; inside an escaped char and the end of it is what we want
(and (in-escaped-char? cm cur) stack-empty), :end-of-this-token
;; To the right of escaped char, keep going
(and (escaped-char-to-right? cm cur) stack-empty), :start-of-this-tok
;; in an escaped char inside the next sibling
(in-escaped-char? cm cur), stack
;; at end of an escaped char which was the next sibling -- handled before
;;checking for eof above
;; at end of an escaped char inside the next sibling
(escaped-char-to-left? cm cur), stack
;; words .................................................................
;; reached the end of a word which was the next sibling -- handled before
;;checking for eof above
;; in a word that is the next sibling, the end of it is what we want
(and (word? type) stack-empty), :end-of-this-token
;; in a word that is inside the next sibling
(word? type), stack
;; brackets ..............................................................
;; push opener on stack
(and (is-bracket-type? type) (opener? string)), (inc stack)
;; we've reached the end of a form -- handled before checking for eof
;;above
;; there was no sibling
(and (is-bracket-type? type) (closer? string) stack-empty), :stop
;; passing through the guts of a sibling form (.. (guts)|..)
(and (is-bracket-type? type) (closer? string)), (dec stack)
:default, :stop)))
(defn end-of-next-sibling
"get the cursor for the end of the sibling to the right."
([cm]
(skip cm end-of-next-sibling-sp))
([cm cur]
(when cur
(.setCursor cm cur)
(skip cm end-of-next-sibling-sp))))
#_(let [cm (get-ddb [:tabs :extns :ed3 :cms :$ed])
cur (.getCursor cm)
info (pe/get-info cm cur)
tok (info :tok)]
[(pe/start-of-a-string? cm cur) (pe/end-of-a-string? cm cur)
(info :left-char) (info :right-char) tok.string]
#_(console.log (pe/token-end cm cur 1))
#_(console.log (pe/cursor cm (+ 9 1 (- 8 8))))
#_(pe/get-info cm (pe/cursor cm (+ 9 1)))
#_(pe/end-of-a-string? cm cur))
(defn start-of-prev-sibling-sp ;; -sp see 'skipping predicate'
"returns the cursor at the start of the sibling to the left or nil
if no sibling or eof. does not exit the containing form. does this
by skipping past any comments or whitespace, and branches depending
on whether a bracket or doublequote is encountered (sp satisfied
when encountering an opening bracket that empties the stack) vs the
beginning of a word (return token at the start of the
word). assuming the cm has matched brackets for now."
[cm cur stack]
(let [info (get-info cm cur)
{:keys [string type bof ch start tok]} info
stack-empty (zero? stack)
one-left (= 1 stack)
string-extends (not= "\"" (first string)) ; for multiline strings
start-of-stg? (start-of-a-string? cm cur)
end-of-stg? (end-of-a-string? cm cur)
empty-stg? (when start-of-stg?
(and (= tok.type "string")
(= tok.string "\"\"")))]
#_(js/console.log stack stack-empty string type ch start cur string-extends
;;(escaped-char-to-left? cm cur)
;;(escaped-char-to-right? cm cur)
start-of-stg?
end-of-stg?)
(cond ;; we return a keyword when we know where to stop, stack otherwise.
;; check these before checking for bof:
;; in a multi-line string, keep searching for the first line of it:
(and start-of-stg? one-left string-extends), stack
;; at the first line of a string and we want its opening doublequote:
(and start-of-stg? one-left), :yes
;; at the start of a word:
(and (word? type) stack-empty (= ch start)), :yes
;; at the opener we were looking for:
(and (is-bracket-type? type) (opener? string) one-left), :yes
bof, :bof; reached beginning of file
(and (start-of-a-string2? cm cur)
(not stack-empty)), stack #_(dec stack)
;; at the start of an escaped char:
(and (escaped-char-to-right? cm cur) stack-empty), stack
;; skip whitespace
(or (nil? type) (and (= type "error") (= string ","))), stack
;; skip comments
(= type "comment"), stack
;; strings ...............................................................
empty-stg? :start-of-this-tok
;; our starting point is at end of a string and it doesn't extend
(and end-of-stg?
(and (not string-extends) stack-empty)), :start-of-this-tok
;; We are in a nested form, at end of string, but it doesn't extend
(and end-of-stg?
(not stack-empty)
(not string-extends)) stack
;; entering a multi-line string from the right; push " onto stack
(and end-of-stg?
string-extends), (inc stack)
;; at start of string and stack already empty, we must have started in
;; the middle of the string.
(and start-of-stg?
stack-empty), :stop
;; at start of string and stack about to be empty, we've found the end of
;; the string -- handled before check for bof above
;; in string, the start of it is our goal ...
;; ... but the start of this string is on a higher line:
(and (= type "string")
#_(not stack-empty)
string-extends), stack
;; it's on this line:
(and (= type "string")
stack-empty
(not string-extends)), :start-of-this-tok
;; in string, the start of this string is our goal ...
;;; ... and the start is on this line:
(and (= type "string") one-left) :start-of-this-tok
;; in string, need to get out of this form, pop stack
(and (= type "string")
(not stack-empty)), (dec stack)
;; escaped chars .........................................................
;; inside an escaped char and the start of it is what we want
(and (in-escaped-char? cm cur) stack-empty), :start-of-this-tok
;; To the left of escaped char, keep going
(and (escaped-char-to-left? cm cur) stack-empty), :start-of-this-tok
;; in an escaped char inside the prev sibling
(or (in-escaped-char? cm cur)
(escaped-char-to-left? cm cur)), stack
;; at start of an escaped char which was the prev sibling -- handled
;; before check for bof above
;; at start of an escaped char inside the prev sibling
(escaped-char-to-right? cm cur), stack
;; words .................................................................
;; reached the start of a word which was the prev sibling -- handled
;; before check for bof above
;; in a word that is the prev sibling, the start of it is what we want
(and (word? type) stack-empty), :start-of-this-tok
;; in a word that is inside the prev sibling
(word? type), stack
;; brackets ..............................................................
;; push closer on stack
(and (is-bracket-type? type) (closer? string)), (inc stack)
;; we've reached the start of a form -- handled before check for
;; bof above
;; there was no prev sibling, avoid exiting the form
(and (is-bracket-type? type) (opener? string) stack-empty), :stop
;; passing through the guts of a sibling form (.. X(guts)..)
(and (is-bracket-type? type) (opener? string)), (dec stack)
:default :stop)))
(defn start-of-prev-sibling
"return the cursor at the start of the sibling to the left."
([cm]
(skip-left cm start-of-prev-sibling-sp))
([cm cur]
(when cur
(.setCursor cm cur)
(skip-left cm start-of-prev-sibling-sp))))
(defn escape-string
"escapes a string, replacing backslashes and doublequotes. wraps
result in a new pair of doublequotes."
[s]
(str "\""
(-> s
(str/replace #"[\\]" "\\\\")
(str/replace #"[\"]" "\\\""))
"\""))
(defn stringify-selection
"turns selection into a string, escaping backslashes and doublequotes"
[cm]
(->> cm .getSelection escape-string (.replaceSelection cm)))
(defn stringify
"turns the region from cur-1 to cur-2 into a string, escaping
backslashes and doublequotes"
[cm cur-1 cur-2]
(.setSelection cm cur-1 cur-2)
(stringify-selection cm)
(.setCursor cm (cursor cm (inc (index cm cur-1)))))
(defn exit-string
"moves cursor right, out of the current string"
[cm]
(let [{:keys [type i ch end]} (get-info cm)]
(when (= type "string")
(.setCursor cm (cursor cm (+ i (- end ch)))))))
(defn in-string?
"returns true if token is in the middle of a string."
([cm] (in-string? cm (cursor cm)))
([cm cur]
(let [type (get-type cm cur)]
(or (= type "string")
(= type "string-2")))))
(defn ^:export meta-doublequote
"paredit meta-doublequote exposed for keymap.
if in a string, moves cursor out of the string to the right.
if in a comment, insert a doublequote.
if in an escaped char, do nothing.
otherwise starts a string that that continues to the end of the next
form, escaping backslashes and doublequotes."
[cm]
(let [{:keys [type eof cur]} (get-info cm)]
(cond
eof :do-nothing
(in-escaped-char? cm cur) :do-nothing
(in-string? cm cur) (exit-string cm)
(= type "comment") (insert cm "\"")
(in-a-word? cm) (stringify cm cur (token-end cm cur))
:else (stringify cm cur (end-of-next-sibling cm)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; paredit-comment-dwim
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn left
"given a pair of cursors c1 and c2, returns the left-most one"
[cm c1 c2]
(let [i1 (index cm c1)
i2 (index cm c2)]
(if (< i1 i2) c1 c2)))
(defn right
"given a pair of cursors c1 and c2, returns the right-most one"
[cm c1 c2]
(let [i1 (index cm c1)
i2 (index cm c2)]
(if (< i1 i2) c2 c1)))
(defn selection-info
"like get-info but for the first selection. gets the cursor to the left of the
selection, the start, the end, the text selected, the starting and ending line
numbers. nil if nothing selected."
[cm]
(when (.somethingSelected cm)
(let [first-sel (-> cm .listSelections first)
text (-> cm .getSelections first)
anchor (.-anchor first-sel)
head (.-head first-sel)
left-of-start (left cm anchor head)
start-cur (cursor cm (inc (index cm left-of-start)))
end-cur (right cm anchor head)]
[left-of-start start-cur end-cur text
(.-line start-cur) (.-line end-cur)])))
(defn get-types
"get the types from cursors c1 to c2. assumes 1 is to the left of 2 and not
vice versa."
[cm c1 c2]
(loop [types [], cur c1]
(let [{:keys [type right-cur]} (get-info cm cur)
types' (conj types type)]
(if (= cur c2)
types'
(recur types' right-cur)))))
(defn selection-completely-satisfies-pred?
"true if every position's type satisfies pred, for the entire (first)
selection"
[cm pred]
(when-let [[_ c1 c2] (selection-info cm)]
(every? pred (get-types cm c1 c2))))
(defn selection-completely-whitespace? [cm]
(selection-completely-satisfies-pred? cm nil?))
(defn not-code? [type] (or (nil? type) (= type "comment")))
(defn selection-completely-non-code? [cm]
(selection-completely-satisfies-pred? cm not-code?))
(defn to-comment
"starts each line in 's' with ;; and appends 'post-script'"
[s postscript]
(let [cmnt (->> s
str/split-lines
(map #(str/replace % #"^" ";; "))
(str/join "\n"))]
(str cmnt "\n" postscript)))
(defn uncomment
"removes leading whitespace and semicolons from lines in 's'"
[s]
(->> s
str/split-lines
(map #(str/replace % #"^\s*;+" ""))
(str/join "\n")))
(defn indent-lines
"indents lines from a to z (line numbers). assumes a is before z."
[cm a z]
(doseq [line (range a (inc z))]