-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathess-smart-equals.el
1626 lines (1469 loc) · 71 KB
/
ess-smart-equals.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
;;; ess-smart-equals.el --- flexible, context-sensitive assignment key for R/S -*- lexical-binding: t; -*-
;; Copyright (C) 2015-2019 Christopher R. Genovese, all rights reserved.
;; Author: Christopher R. Genovese <[email protected]>
;; Maintainer: Christopher R. Genovese <[email protected]>
;; Keywords: R, S, ESS, convenience
;; URL: https://github.com/genovese/ess-smart-equals
;; Version: 0.3.2
;; Package-Version: 0.3.2
;; Package-Requires: ((emacs "25.1") (ess "18.10"))
;;; License:
;;
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;; Floor, Boston, MA 02110-1301, USA.
;;
;;; Commentary:
;;
;; Assignment in R is syntactically complicated by a few features:
;; 1. the historical role of '_' (underscore) as an assignment
;; character in the S language; 2. the somewhat
;; inconvenient-to-type, if conceptually pure, '<-' operator as the
;; preferred assignment operator; 3. the ability to use either an
;; '=', '<-', and a variety of other operators for assignment; and
;; 4. the multiple roles that '=' can play, including for setting
;; named arguments in a function call.
;;
;; This package offers a flexible, context-sensitive assignment key
;; for R and S that is, by default, tied to the '=' key. This key
;; inserts or completes relevant, properly spaced operators
;; (assignment, comparison, etc.) based on the syntactic context in
;; the code. It allows very easy cycling through the possible
;; operators in that context. The contexts, the operators, and
;; their cycling order in each context are customizable.
;;
;; The package defines a minor mode `ess-smart-equals-mode',
;; intended for S-language modes (e.g., ess-r-mode,
;; inferior-ess-r-mode, and ess-r-transcript-mode), that when
;; enabled in a buffer activates the '=' key to to handle
;; context-sensitive completion and cycling of relevant operators.
;; When the mode is active and an '=' is pressed:
;;
;; 1. With a prefix argument or in specified contexts (which for
;; most major modes means in strings or comments), just
;; insert '='.
;;
;; 2. If an operator relevant to the context lies before point
;; (with optional whitespace), it is replaced, cyclically, by the
;; next operator in the configured list for that context.
;;
;; 3. Otherwise, if a prefix of an operator relevant to the
;; context lies before point, that operator is completed.
;;
;; 4. Otherwise, the highest priority relevant operator is inserted
;; with surrounding whitespace (see `ess-smart-equals-no-spaces').
;;
;; Consecutive presses of '=' cycle through the relevant operators.
;; After an '=', a backspace (or other configurable keys) removes
;; the last operator and tab offers a choice of operators by completion.
;; (Shift-backspace will delete one character only and restore the
;; usual maning of backspace.) See `ess-smart-equals-cancel-keys'.
;;
;; By default, the minor mode activates the '=' key, but this can
;; be customized by setting the option `ess-smart-equals-key' before
;; this package is loaded.
;;
;; The function `ess-smart-equals-activate' arranges for the minor mode
;; to be activated by mode hooks for any given list of major modes,
;; defaulting to ESS major modes associated with R (ess-r-mode,
;; inferior-ess-r-mode, ess-r-transcript-mode, ess-roxy-mode).
;;
;; Examples
;; --------
;; In the left column below, ^ marks the location at which an '='
;; key is pressed, the remaining columns show the result of
;; consecutive presses of '=' using the package's default settings.
;; position of point.
;;
;; Before '=' Press '=' Another '=' Another '='
;; ---------- --------- ----------- -----------
;; foo^ foo <- ^ foo <<- ^ foo = ^
;; foo ^ foo <- ^ foo <<- ^ foo = ^
;; foo<^ foo <- ^ foo <<- ^ foo = ^
;; foo=^ foo = ^ foo -> ^ foo ->> ^
;; foo(a^ foo(a = ^ foo( a == ^ foo( a != ^
;; if( foo=^ if( foo == ^ if( foo != ^ if( foo <= ^
;; if( foo<^ if( foo < ^ if( foo > ^ if( foo >= ^
;; "foo ^ "foo =^ "foo ==^ "foo ===^
;; #...foo ^ #...foo =^ #...foo ==^ #...foo ===^
;;
;;
;; As a bonus, the value of the variable
;; `ess-smart-equals-extra-ops' when this package is loaded,
;; determines some other smart operators that may prove useful.
;; Currently, only `brace', `paren', and `percent' are supported,
;; causing `ess-smart-equals-open-brace',
;; `ess-smart-equals-open-paren', and `ess-smart-equals-percent'
;; to be bound to '{', '(', and '%', respectively. The first two
;; of these configurably places a properly indented and spaced
;; matching pair at point or around the region if active. The
;; paren pair also includes a magic space with a convenient keymap
;; for managing parens. See the readme. See the customizable
;; variable `ess-smart-equals-brace-newlines' for configuring the
;; newlines in braces. The third operator
;; (`ess-smart-equals-percent') performs matching of %-operators.
;;
;; Finally, the primary user facing functions are named with a
;; prefix `ess-smart-equals-' to avoid conflicts with other
;; packages. Because this is long, the internal functions and
;; objects use a shorter (but still distinctive) prefix `essmeq-'.
;;
;;
;; Installation and Initialization
;; -------------------------------
;; The package can be loaded from MELPA using `package-install' or another
;; Emacs package manager. Alternatively, you can clone or download the source
;; directly from the github repository and put the file `ess-smart-equals.el'
;; in your Emacs load path.
;;
;; A variety of activation options is described below, but tl;dr:
;; the recommended way to activate the mode (e.g., in your init
;; file) is either directly with
;;
;; (setq ess-smart-equals-extra-ops '(brace paren percent))
;; (with-eval-after-load 'ess-r-mode
;; (require 'ess-smart-equals)
;; (ess-smart-equals-activate))
;;
;; or with use-package:
;;
;; (use-package ess-smart-equals
;; :init (setq ess-smart-equals-extra-ops '(brace paren percent))
;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode)
;; :config (ess-smart-equals-activate))
;;
;; A more detailed description follows, if you want to see variations.
;;
;; To activate, you need only do
;;
;; (with-eval-after-load 'ess-r-mode
;; (require 'ess-smart-equals)
;; (ess-smart-equals-activate))
;;
;; somewhere in your init file, which will add `ess-smart-equals-mode' to
;; a prespecified (but customizable) list of mode hooks.
;;
;; For those who use the outstanding `use-package', you can do
;;
;; (use-package ess-smart-equals
;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode)
;; :config (ess-smart-equals-activate))
;;
;; somewhere in your init file. An equivalent but less concise version
;; of this is
;;
;; (use-package ess-smart-equals
;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode)
;; :hook ((ess-r-mode . ess-smart-equals-mode)
;; (inferior-ess-r-mode . ess-smart-equals-mode)
;; (ess-r-transcript-mode . ess-smart-equals-mode)
;; (ess-roxy-mode . ess-smart-equals-mode))
;;
;; To also activate the extra smart operators and bind them automatically,
;; you can replace this with
;;
;; (use-package ess-smart-equals
;; :init (setq ess-smart-equals-extra-ops '(brace paren percent))
;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode)
;; :config (ess-smart-equals-activate))
;;
;; Details on customization are provided in the README file.
;;
;; Testing
;; -------
;; To run the tests, install cask and do `cask install' in the
;; ess-smart-equals project directory. Then, at the command line,
;; from the project root directory do
;;
;; cask exec ert-runner
;; cask exec ecukes --reporter magnars
;;
;; and if manual testing is desired do
;;
;; cask emacs -Q -l test/manual-init.el --eval '(cd "~/")' &
;;
;; Additional test cases are welcome in pull requests.
;;
;;; Change Log:
;;
;; 0.3.x -- Breaking changes in functionality, design, and configuration.
;; No longer relies on `ess-S-assign' which was deprecated in
;; ESS. Now provides more powerful context-sensitive, prioritized
;; operator lists with cycling and completion. The mode is now,
;; properly, a local minor mode, which can be added automatically
;; to relevant mode hooks for ESS R modes. Updated required
;; versions of emacs and ESS.
;;
;; 0.2.2 -- Fix for deprecated ESS variables `ess-S-assign' and
;; `ess-smart-S-assign-key'. Thanks to Daniel Gomez (@dangom).
;;
;; 0.2.1 -- Initial release with simple insertion and completion, with
;; space padding for the operators except for a single '='
;; used to specify named arguments in function calls. Relies on
;; ESS variables `ess-S-assign' and `ess-smart-S-assign-key'
;; to specify preferred operator for standard assignments.
;;; Code:
(eval-when-compile (require 'cl-lib))
(eval-when-compile (require 'subr-x))
(eval-when-compile (require 'pcase))
(require 'map)
(require 'skeleton)
(require 'ess-r-mode) ;; included in ess package
;;; Utility Macros
(defmacro essmeq--with-struct-slots (type spec-list inst &rest body)
"Execute BODY with vars in SPEC-LIST bound to slots in struct INST of TYPE.
TYPE is an unquoted symbol corresponding to a type defined by
`cl-defstruct'. SPEC-LIST is a list, each of whose entries can
either be a symbol naming both a slot and a variable or a list of
two symbols (VAR SLOT) associating VAR with the specified SLOT.
INST is an expression giving a structure of type TYPE as defined
by `cl-defstruct', and BODY is a list of forms.
This code was based closely on code given at
www.reddit.com/r/emacs/comments/8pbbpe/why_no_withslots_for_cldefstruct/
which was in turn borrowed from the EIEIO package."
(declare (indent 3) (debug (sexp sexp sexp def-body)))
(let ((obj (make-symbol "struct")))
`(let ((,obj ,inst))
(cl-symbol-macrolet ;; each spec => a symbol macro to an (aref ....)
,(mapcar (lambda (entry)
(let* ((slot-var (if (listp entry) (car entry) entry))
(slot (if (listp entry) (cadr entry) entry))
(idx (cl-struct-slot-offset type slot)))
(list slot-var `(aref ,obj ,idx))))
spec-list)
(unless (cl-typep ,obj ',type)
(error "%s is not of type %s" ',inst ',type))
,(if (cdr body) `(progn ,@body) (car body))))))
(defmacro essmeq--with-matcher (spec-list inst &rest body)
"Execute BODY with vars in SPEC-LIST bound to slots essmeq-matcher INST.
SPEC-LIST is a list, each of whose entries can either be a symbol
naming both a slot and a variable or a list of two symbols (VAR
SLOT) associating VAR with the specified SLOT. INST is an
expression giving a structure of type essmeq-matcher, and BODY is
a list of forms."
(declare (indent 2) (debug (sexp sexp def-body)))
`(essmeq--with-struct-slots essmeq-matcher ,spec-list ,inst
,@body))
(defmacro essmeq--with-temporary-insert (text where &rest body)
"Inserting TEXT after point, execute BODY, delete TEXT.
Returns the value of BODY and does not change point."
(declare (indent 2) (debug (sexp def-body)))
(let ((txt (make-symbol "text"))
(len (make-symbol "text-len"))
(after (eq where :after)))
`(let ((,txt ,text)
(,len ,(if (stringp text) (length text) `(length ,txt))))
(save-excursion
,(if after `(save-excursion (insert ,txt)) `(insert ,txt))
(prog1 (save-excursion ,@body)
(delete-char ,(if after len `(- ,len))))))))
(defmacro essmeq--with-markers (specs &rest body)
"Execute BODY with markers defined by SPEC. Markers cleared after BODY forms.
Return the value of the last form in BODY. Markers are guaranteed
to be cleared even if BODY exits non-locally. Note that as a
consequence, the markers themselves should not be returned. If
any of the markers is a desired value of this form, either
`ess-smart-equals-copy-marker' or `copy-marker' should be used,
but note that unlike the former, the latter does not copy the
insertion type of the marker by default.
SPEC is a list whose elements must have one of the following
forms: 1. SYMBOL; 2. (SYMBOL POSITION), where POSITION is an
expression used to initialize the marker as in the corresponding
argument to `set-marker'; or 3. (SYMBOL POSITION INSERTION-TYPE),
were INSERTION-TYPE is either t or nil (the default) as in
`set-marker-insertion-type'.
If POSITION is one of the forms (point), (point-min),
or (point-max), those expressions are not evaluated but instead
the marker is created with the corresponding `point-marker',
`point-min-marker', or `point-max-marker', respectively."
(declare (indent 1) (debug (sexp def-body)))
(let ((marker-symbols (mapcar (lambda (s) (if (listp s) (car s) s)) specs))
(marker-info (mapcar (lambda (s) (if (listp s) (cadr s) nil)) specs))
(marker-itypes
(delq nil (mapcar (lambda (s)
(if (and (listp s) (caddr s))
(cons (car s) (caddr s))
nil))
specs))))
(unless (cl-every #'symbolp marker-symbols)
(error "Only symbols allowed in first entry of marker specification."))
`(let (,@(cl-map 'list (lambda (s i)
(cond
((equal i '(point))
`(,s (point-marker)))
((equal i '(point-min))
`(,s (point-min-marker)))
((equal i '(point-max))
`(,s (point-max-marker)))
((null i)
`(,s (make-marker)))
(t
`(,s (set-marker (make-marker) ,i)))))
marker-symbols marker-info))
,@(mapcar (lambda (s) `(set-marker-insertion-type ,(car s) ,(cdr s)))
marker-itypes)
(unwind-protect ,(if (cdr body) `(progn ,@body) (car body))
,@(mapcar (lambda (m) `(set-marker ,m nil)) marker-symbols)))))
;;; Marker Interface
(defun ess-smart-equals-make-marker (&optional position type init)
"Like `make-marker' but also optionally initializes POSITION and TYPE.
POSITION can be any value of the same argument in `set-marker'.
TYPE is nil or t, as with the corresponding argument to
`set-marker-insertion-type'. INIT, if non-nil, should be nullary
function (e.g,. point-marker) to be called instead of `make-marker'
to initialize the marker
Returns the initialized marker."
(let ((m (if init (funcall init) (make-marker))))
(when position (set-marker m position))
(when type (set-marker-insertion-type m t))
m))
(defun ess-smart-equals-copy-marker (&optional marker type)
"Like `copy-marker' but copies insertion type if MARKER but not TYPE is given."
(copy-marker marker (or type (and marker (marker-insertion-type marker)))))
(defsubst ess-smart-equals-clear-marker (marker)
"Reset MARKER so that it points nowhere and does not affect current buffer."
(set-marker marker nil))
;;; Behavior Configuration
(defcustom ess-smart-equals-padding-left 'one-space
"Specifies padding used on left side of inserted and completed operators.
This can have one of the following values:
* The symbol `one-space' means to insert exactly one space, eliminating
any other contiguous whitespace on the left.
* The symbol `no-space' means to eliminate all adjacent whitespace on
the left.
* The symbol `some-space' means to ensure there is at least one space
on the left, either in existing whitespace (which is kept as is)
or by inserting a space if none.
* The symbol `none' means to insert no padding and make no change
to the surrounding whitespace. (A nil value has the same effect
but is marginally slower.)
* A string means to insert that string on the left.
* A function with signature (begin-ws begin &optional extent)
When inserting or completing an operator this function
should insert desired padding on the right. The function is
called within a `save-excursion', so point can be moved and
insertions made. In this case, the function is called with
two positions: BEGIN-WS is the position of the leftmost
contiguous whitespace character to the left of the operator
and BEGIN is the position of the left side of the operator.
When removing an operator, this function should return the
beginning of the padded region assuming that an operator has
just been inserted and padded (i.e., by calling this
function). It should not change the current buffer. This
case is distinguished by having the third argument EXTENT eq
to t and *both* BEGIN-WS and BEGIN pointing to the leftmost
point of the inserted operator.
"
:group 'ess-edit
:type '(choice (const :tag "Only One Space" one-space)
(const :tag "No Spaces" no-space)
(const :tag "At Least One Space" some-space)
(const :tag "No Padding" none)
string
function))
(defcustom ess-smart-equals-padding-right 'one-space
"Specifies padding used on right side of inserted and completed operators.
This can have one of the following values:
* The symbol `one-space' means to insert exactly one space, eliminating
any other contiguous whitespace on the right.
* The symbol `no-space' means to eliminate all adjacent whitespace on
the right.
* The symbol `some-space' means to ensure there is at least one space
on the right, either in existing whitespace (which is kept as is)
or by inserting a space if none.
* The symbol `none' means to insert no padding and make no change
to the surrounding whitespace. (A nil value has the same effect
but is marginally slower.)
* A string means to insert that string on the right.
* A function with signature (end end-ws &optional extent)
When inserting or completing an operator this function
should insert desired padding on the right. The function is
called within a `save-excursion', so point can be moved and
insertions made. In this case, the function is called with
two positions: END is the position of the right side of the
inserted operator and END-WS is the position of the
rightmost contiguous whitespace character to the right of
the operator.
When removing an operator, this function should return the
beginning of the padded region assuming that an operator has
just been inserted and padded (i.e., by calling this
function). It should not change the current buffer. This
case is distinguished by having the third argument EXTENT eq
to t and *both* END and END-WS pointing to the rightmost
point of the inserted operator.
"
:group 'ess-edit
:type '(choice (const :tag "Only One Space" one-space)
(const :tag "No Spaces" no-space)
(const :tag "At Least One Space" some-space)
(const :tag "No Padding" none)
string
function))
(defvar-local ess-smart-equals-narrow-function nil
"If non-nil, a nullary function to restrict syntax checking to a region.
This is useful in cases such as `inferior-ess-r-mode' where
attention should be focused on a prompt line or the region
between prompts, both for efficiency and because output or
erroneous input on earlier prompts can confuse the syntax
checker. See `ess-smart-equals-repl-narrow' and
`ess-smart-equals-mode-options'.")
(defcustom ess-smart-equals-insertion-hook nil
"A function called when an operator is inserted into the current buffer.
This (non-standard hook) function should accept six arguments
CONTEXT MATCH-TYPE STRING START OLD-END PAD
where CONTEXT is a context symbol, representing a key in the
inner alists of `ess-smart-equals-contexts'; MATCH-TYPE is one of
the keywords :exact, :partial, :no-match, or :literal; STRING is
the operator string that was inserted; START is the buffer
positions at the beginning of the inserted string (plus padding);
OLD-END was the ending position of the previous content in the
buffer; and PAD is a string giving the padding used on either
side of the inserted operator, typically either empty or a single
space.
This feature is experimental and may be removed in a future version."
:group 'ess-edit
:type '(choice (const :tag "None" nil) function))
(defcustom ess-smart-equals-default-modes
'(ess-r-mode inferior-ess-r-mode ess-r-transcript-mode ess-roxy-mode)
"List of major modes where `ess-smart-equals-activate' binds '=' by default."
:group 'ess-edit
:type '(repeat symbol))
(defcustom ess-smart-equals-brace-newlines '((open after)
(close before))
"Controls auto-newlines for braces in `electric-smart-equals-open-brace'.
Only applicable when `ess-smart-equals-extra-ops' contains the
symbol `brace'. This is an alist with keys `open' and `close' and
with values that are lists containing the symbols `after' and/or
`before', indicating when a newline should be placed. A missing
key is equivalent to a nil value, meaning to place no newlines.
This can be controlled via Emacs's customization mechanism or can
be added to your ESS style specification, as preferred."
:group 'ess-edit
:type '(alist :key-type (choice (const open) (const close))
:value-type (repeat (choice (const before) (const after)))))
;;; Specialized overriding context and transient exit functions
(defvar-local ess-smart-equals-overriding-context nil
"If non-nil, a context symbol that overrides the usual context calculation.
Intended to be used in a transient manner, see
`ess-smart-equals-transient-exit-function'.")
(defvar-local ess-smart-equals-transient-exit-function nil
"If non-nil, a nullary function to be called on exit from the transient keymap.
This can be used, for instance, to clear an overriding context.
See `essmeq--transient-map'")
(defvar-local essmeq--stop-transient nil
"A nullary function called to deactivate the most recent transient map.
This is set automatically and should not be set explicitly. If
non-nil, a nullary function to be called on exit from the
transient keymap. This can be used, for instance, to clear an
overriding context if something goes awry. See
`essmeq--transient-map'.")
(defun ess-smart-equals-clear-overriding-context ()
"Transient exit function that resets both itself and any overriding context.
This is a convenience function for fixing a context during one
cycle of smart equals insertion. See
`ess-smart-equals-overriding-context' and
`ess-smart-equals-transient-exit-function'.."
(setq ess-smart-equals-overriding-context nil
ess-smart-equals-transient-exit-function nil))
(defun ess-smart-equals-set-overriding-context (context)
"Force context to be symbol CONTEXT for next insertion only.
This sets `ess-smart-equals-transient-exit-function' to clear the context
the next time the transient map in `ess-smart-equals' exits."
(setq ess-smart-equals-overriding-context context
ess-smart-equals-transient-exit-function
#'ess-smart-equals-clear-overriding-context))
;;; Key Configuration and Utilities
(defun ess-smart-equals-refresh-mode ()
"Re-enable `ess-smart-equals-mode' in all buffers where it is enabled.
This has the effect of refreshing all the mode's keymaps,
contexts, and options. It is intended for use in customization
setters for options that affect pre-computed tables or keymaps,
but it can be used interactively as well, for instance, after
manually updating the values of such options."
(interactive)
(dolist (buf (buffer-list))
(with-current-buffer buf
(when (and (boundp 'ess-smart-equals-mode) ess-smart-equals-mode)
(let ((inhibit-message t))
(ess-smart-equals-mode 1))))))
(defcustom ess-smart-equals-key "="
"The key for smart assignment operators when `ess-smart-equals-mode' active.
For changes in this variable to take effect, some precomputed
information must be refreshed in `ess-smart-equals-mode' buffers.
Changing the variable through the customization mechanism does
such a refresh automatically. If instead you manually change the
value of this option (e.g., with `setq'), you can either disabled
and re-enabled the minor mode in one such buffer or do
M-x ess-smart-equals-refresh-mode
interactively, or (ess-smart-equals-refresh-mode) in lisp, to
make this change take effect."
:group 'ess-edit
:type 'string
:initialize 'custom-initialize-default
:set (lambda (sym val)
(set-default sym val)
(ess-smart-equals-refresh-mode)))
(defcustom ess-smart-equals-extra-ops nil
"If non-nil, a symbol list of extra smart operators to bind in the mode map.
Currently, only `brace' and `paren' are supported.
For changes in this variable to take effect, some precomputed
information must be refreshed in `ess-smart-equals-mode' buffers.
Changing the variable through the customization mechanism does
such a refresh automatically. If instead you manually change the
value of this option (e.g., with `setq'), you can either disabled
and re-enabled the minor mode in one such buffer or do
M-x ess-smart-equals-refresh-mode
interactively, or (ess-smart-equals-refresh-mode) in lisp, to
make this change take effect."
:group 'ess-edit
:type '(choice (const nil) (repeat (const brace) (const paren)))
:initialize 'custom-initialize-default
:set (lambda (sym val)
(set-default sym val)
(ess-smart-equals-refresh-mode)))
(defcustom ess-smart-equals-cancel-keys (list [backspace]
(kbd "<DEL>"))
"List of keys transiently bound to cancel operator insertion or cycling.
A shifted version of each will instead delete backwards a
character, clearing the transient keymap and making it easy to
delete only part of an operator if desired.
For changes in this variable to take effect, some precomputed
information must be refreshed in `ess-smart-equals-mode' buffers.
Changing the variable through the customization mechanism does
such a refresh automatically. If instead you manually change the
value of this option (e.g., with `setq'), you can either disabled
and re-enabled the minor mode in one such buffer or do
M-x ess-smart-equals-refresh-mode
interactively, or (ess-smart-equals-refresh-mode) in lisp, to
make this change take effect."
:group 'ess-edit
:type '(repeat
(choice string (restricted-sexp :match-alternatives (vectorp))))
:initialize 'custom-initialize-default
:set (lambda (sym val)
(set-default sym val)
(ess-smart-equals-refresh-mode)))
(defun essmeq--transient-equals (&optional literal)
"A version of `ess-smart-equals' for use in the transient key map.
This detects previous use of `ess-smart-equals-percent' and clears that
operator if the user switches to equals."
(interactive "P")
(ignore literal)
(when (eq last-command 'ess-smart-equals-percent)
(let ((ess-smart-equals-overriding-context '%))
(essmeq--remove 'only-match)))
(call-interactively #'ess-smart-equals))
(defun essmeq--make-transient-map (&optional cancel-keys)
"Resets transient keymap used after `ess-smart-equals'.
CANCEL-KEYS, if non-nil, is a list of keys in that map that will
clear the last insertion. It defaults to
`ess-smart-equals-cancel-keys', which see. See also
`essmeq--transient-map'."
(let ((cancel-keys (or cancel-keys ess-smart-equals-cancel-keys))
(percentp (memq 'percent ess-smart-equals-extra-ops))
(map (make-sparse-keymap)))
(if (not percentp)
(define-key map (kbd ess-smart-equals-key) #'ess-smart-equals)
(define-key map "%" #'ess-smart-equals-percent)
(define-key map (kbd ess-smart-equals-key) #'essmeq--transient-equals))
(define-key map "\t" #'essmeq--selected)
(dolist (key cancel-keys)
(define-key map key #'essmeq--remove)
(when (and (or (stringp key) (vectorp key))
(= (length key) 1))
(define-key map ;; make shift-cancel just do regular backspace
(vector (if (listp (aref key 0))
(cons 'shift (aref key 0))
(list 'shift (aref key 0))))
'delete-backward-char)))
map))
(defun essmeq--make-mode-map ()
"Returns the `ess-smart-equals-mode' keymap using current parameter values."
(let ((map (make-sparse-keymap)))
(define-key map ess-smart-equals-key 'ess-smart-equals)
(when (memq 'brace ess-smart-equals-extra-ops)
(define-key map "{" 'ess-smart-equals-open-brace))
(when (memq 'paren ess-smart-equals-extra-ops)
(define-key map "(" 'ess-smart-equals-open-paren))
(when (memq 'percent ess-smart-equals-extra-ops)
(define-key map "%" 'ess-smart-equals-percent))
map))
(defvar ess-smart-equals-mode-map (essmeq--make-mode-map)
"Keymap used in `ess-smart-equals-mode' binding smart operators.")
(defvar essmeq--transient-map (essmeq--make-transient-map)
"Map bound transiently after `ess-smart-equals' key is pressed.
The map continues to be active as long as that key is pressed.")
(defun ess-smart-equals-update-keymaps ()
"Force update of `ess-smart-equals-mode' keymaps to adjust for config changes.
This should not usually need to be done explicitly by the user."
(interactive)
;; The `ess-smart-equals-mode' entry in `minor-mode-map-alist' is identical
;; to `ess-smart-equals-mode-map', if the map is set. In this case,
;; simply doing `setq' will break the synchrony and the new map will
;; not be reflected in the minor mode bindings. So we use `setcdr' instead.
(if (keymapp ess-smart-equals-mode-map)
(setcdr ess-smart-equals-mode-map (cdr (essmeq--make-mode-map)))
(setq ess-smart-equals-mode-map (essmeq--make-mode-map)))
(setq essmeq--transient-map (essmeq--make-transient-map)))
(defun essmeq--keep-transient ()
"Predicate that returns t when the transient keymap should be maintained."
(let ((command-keys (this-command-keys-vector)))
(or (equal command-keys (vconcat ess-smart-equals-key))
(and (memq 'percent ess-smart-equals-extra-ops)
(equal command-keys (vector ?%))))))
;;; Buffer Contents
(defun essmeq--whitespace-span-forward (&optional position)
"Scan forward from POSITION to the end of contiguous whitespace.
Return the position after contiguous whitespace but stopping at
any character with a non-nil `essmeq--magic-space' text property.
POS defaults to point."
(let ((pos (or position (point))))
(save-excursion
(when position (goto-char pos))
(skip-syntax-forward " ")
(let* ((after-ws (point))
(magic-pos (text-property-any pos after-ws 'essmeq--magic-space t)))
(or magic-pos after-ws)))))
(defun essmeq--whitespace-span-backward (&optional position)
"Scan backward from POSITION to the beginning of contiguous whitespace.
Return the position at the start of contiguous whitespace. POS
defaults to point."
(let ((pos (or position (point))))
(save-excursion
(when position (goto-char pos))
(skip-syntax-backward " ")
(point))))
(defun essmeq--find-padded-region (beg end)
"Find the padding extent for unpadded text spanning BEG..END in current buffer.
The assumption is that the text has been inserted with padding
according to the padding rules specified by
`ess-smart-equals-padding-left' and
`ess-smart-equals-padding-right', which see. This assumption is
not checked; specifically, this does not check that BEG..END is
free of spaces nor that the padding characters around that region
are correct.
Return (BEG' . END') where BEG' and END' are the beginning and ending
positions of the padded region BEG..END under the padding rules."
(cons
(cond ;; left padding (must come second to avoid affecting end)
((memq ess-smart-equals-padding-left '(one-space some-space))
(1- beg))
((memq ess-smart-equals-padding-left '(no-space none))
beg)
((stringp ess-smart-equals-padding-left)
(- beg (length ess-smart-equals-padding-left)))
((functionp ess-smart-equals-padding-left)
(funcall ess-smart-equals-padding-left beg beg t)))
(cond ;; right padding
((memq ess-smart-equals-padding-right '(one-space some-space))
(1+ end))
((memq ess-smart-equals-padding-right '(no-space none))
end)
((stringp ess-smart-equals-padding-right)
(+ end (length ess-smart-equals-padding-right)))
((functionp ess-smart-equals-padding-right)
(funcall ess-smart-equals-padding-right end end t)))))
(defun essmeq--normalize-padding (beg end)
"Adjust space padding on either side of BEG and END in the current buffer.
Return (BEG' . END') where BEG' and END' are the beginning and ending
positions of the padded region BEG..END that account for insertions
and deletions.
The type of padding used, if any, on each side is determined by
the values of the options `ess-smart-equals-padding-left' and
`ess-smart-equals-padding-right', which see."
(let ((beg-ws (essmeq--whitespace-span-backward beg))
(end-ws (essmeq--whitespace-span-forward end)))
(essmeq--with-markers ((mbeg beg) (mend end t))
(cond ;; right padding
((eq ess-smart-equals-padding-right 'one-space)
(delete-region end end-ws)
(save-excursion (goto-char mend) (insert " ")))
((eq ess-smart-equals-padding-right 'some-space)
(unless (> end-ws end)
(save-excursion (goto-char mend) (insert " "))))
((eq ess-smart-equals-padding-right 'no-space)
(delete-region end end-ws))
((eq ess-smart-equals-padding-right 'none))
((stringp ess-smart-equals-padding-right)
(save-excursion
(goto-char mend)
(insert ess-smart-equals-padding-right)))
((functionp ess-smart-equals-padding-right)
(save-excursion
(goto-char mend)
(funcall ess-smart-equals-padding-right end end-ws))))
(cond ;; left padding (must come second to avoid affecting end)
((eq ess-smart-equals-padding-left 'one-space)
(delete-region beg-ws beg)
(save-excursion (goto-char mbeg) (insert " ")))
((eq ess-smart-equals-padding-left 'some-space)
(unless (< beg-ws beg)
(save-excursion (goto-char mbeg) (insert " "))))
((eq ess-smart-equals-padding-left 'no-space)
(delete-region beg-ws beg))
((eq ess-smart-equals-padding-left 'none))
((stringp ess-smart-equals-padding-left)
(save-excursion
(goto-char mbeg)
(insert ess-smart-equals-padding-left)))
((functionp ess-smart-equals-padding-left)
(save-excursion
(goto-char mbeg)
(funcall ess-smart-equals-padding-left beg-ws beg))))
(cons (marker-position mbeg) (marker-position mend)))))
(defun essmeq--replace-region (text start end &optional ignore-padding)
"Replace region START..END with TEXT plus optional padding in current buffer.
Padding is determined by customization options as in
`essmeq--normalize-padding', but if IGNORE-PADDING is non-nil,
these settings are ignored and no padding is added.
Return (TSTART . TEND) giving, respectively, the starting and ending
positions of the (padded) text."
(essmeq--with-markers ((mstart start) (mend end t))
(delete-region mstart mend)
(save-excursion
(goto-char mstart)
(insert text))
(if ignore-padding
(cons (marker-position mstart) (marker-position mend))
(essmeq--normalize-padding mstart mend))))
(defun essmeq--after-whitespace-p (&optional pos)
"Does POS (point by default) follow a whitespace character?"
(eq (char-syntax (char-before pos)) ?\ ))
;;; Finite-State Machine for Operator Matching
;;
;; We do backwards anchored matching of operator lists using a
;; pre-built finite-state machine. This offers several advantages
;; over a direct sequence of regular expression matches. First, in
;; benchmarks with compiled code, the FSM matcher gives comparable,
;; though typically better, performance than the regexp approach.
;; Second, backwards regex matching in emacs (excluding
;; looking-back, which is slow) does not give the longest match,
;; requiring disambiguation between say '<-' and '<<-'. Third, we
;; can handle partial matches automatically with the information
;; computed at fsm build time, making it easy to offer completion.
;; Fourth, we can control priority order in the match easily and
;; can associate additional information with the matched operator.
;; Note that the search is backward, so the FSM matching starts
;; in state 0 at the *end* of the strings.
;;
;; Each FSM is reepresented by an `essmeq-matcher' object (a struct).
;; This has several slots:
;;
;; :fsm The finite state machine. Each state is either nil or an
;; alist mapping characters to transitions of the form
;; (CHAR NEXT-STATE ACCEPTED?) where ACCEPTED? is either
;; nil when the transition is not to an accepting state
;; or an index into the target string vector when it is.
;; Note that acceptance is signaled on the *transition*
;; not in the state itself, so many accepting states are
;; often nil. Because the search is anchored, a state
;; can only accept zero or one strings and there can
;; be multiple accepting states along the path.
;;
;; :targets The vector of strings being matched, without padding.
;;
;; :span The maximum length of the target strings
;;
;; :data A vector, the same length as targets, of optional
;; associated data.
;;
;; :partial A table of links that can be used for computing
;; partial matches. Each partial element is a list of
;; the form (CHAR (STATE . SLEN)), where STATE is a
;; candidate STATE to start in for finding the partial
;; match and SLEN is the number of skipped characters
;; at the end of the string for that partial match.
;;
;; These matchers are built with `essmeq--build-fsm' and matched
;; with `essmeq--match' (for exact matches) and `essmeq--complete'
;; (for partial matches).
(cl-defstruct (essmeq-matcher
(:constructor nil)
(:constructor essmeq-make-matcher
(strings
&key
(fsm (make-vector
(1+ (apply #'+ (mapcar #'length strings)))
nil))
(span (apply #'max 0 (mapcar #'length strings)))
(info (make-vector (length strings) nil))
(partial nil)
&aux
(targets (vconcat strings))
(data (vconcat info))))
(:copier essmeq-copy-matcher)
(:predicate essmeq-matcher-p))
fsm targets span data partial)
(defun essmeq--build-fsm (ops &optional data)
"Build backward matching finite-state machine for string vector OPS."
(declare (pure t) (side-effect-free t))
(let ((fsm (make-vector (1+ (apply #'+ (mapcar #'length ops))) nil))
(partial nil)
(next-state 1) ;; start state 0 always exists
(max-len 0)
(num-ops (length ops))
(op-index 0))
(while (< op-index num-ops)
(let* ((state 0)
(op (elt ops op-index))
(len (length op))
(ind (1- len)))
(when (> len max-len)
(setq max-len len))
(while (> ind 0)
(if-let* ((ch (aref op ind))
(in-state (aref fsm state))
(goto (assoc ch in-state)))
(setq state (cadr goto)) ; transition exists, follow it
(push (cl-list* ch next-state nil) (aref fsm state)) ; new state
(when (> state 0) ; goto for partial match
(push (cons state (- len ind 1)) (map-elt partial ch)))
(setq state next-state
next-state (1+ next-state)))
(setq ind (1- ind)))
(if-let* ((ch (aref op 0))
(in-state (aref fsm state))
(goto (assoc ch in-state)))
(setf (cddr goto) op-index) ; transition exists, accept it
(push (cl-list* ch next-state op-index) (aref fsm state)) ; new accept
(when (> state 0) ; goto for partial match
(push (cons state (- len 1)) (map-elt partial ch)))
(setq next-state (1+ next-state))))
(setq op-index (1+ op-index)))
(essmeq-make-matcher ops
:fsm (cl-map 'vector #'nreverse
(substring fsm 0 next-state))
:span max-len
:info (if data (vconcat data) nil)
:partial (mapcar (lambda (x)
(cl-callf reverse (cdr x))
x)
(nreverse partial)))))
(defun essmeq--match (fsm &optional pos bound)
"Search backward to exactly match a string specified by machine FSM.
Anchor the search at POS, or at point if nil. BOUND, if non-nil,
limits the search to positions not before position BOUND. Assumes
that surrounding whitespace is handled elsewhere.
Return a dotted list of the form (ACCEPT 0 START . POS) if a match
exists, or nil otherwise. ACCEPT is the number of the accepting
state in FSM, START is the position of the matching string's
beginning, and POS is the position where scanning started, as
passed to this function."
(let* ((pos (or pos (point)))
(limit (or bound (point-min)))
(state 0)
(accepted nil)
(start pos))
(while (and (not (eq state :fail)) (>= start limit))
(if-let (next (assoc (char-before start) (aref fsm state)))
(setq state (cadr next)
accepted (cddr next)
start (1- start))
(setq state :fail)))
(if accepted
(cl-list* accepted 0 start pos)
nil)))
(defun essmeq--complete (fsm partial &optional pos bound)
"Search backward for farthest partial match to a string specified by FSM.
A partial match is a prefix of one of the target operators; the
`farthest' match is the one that moves the position as far back
as possible. Note that this respects the priority order only for
equally far matches.
FSM is the finite-state machine from an `essmeq-matcher'; PARTIAL
is an alist mapping characters to a list of (STATE . SLEN) pairs,
where STATE represents a state to jump to for partial match from POS
and SLEN is the length of the omitted suffix for that partial match.
The search is anchored at POS, or at point if nil. BOUND, if non-nil,
limits the search to positions not before position BOUND. Assumes
that surrounding whitespace is handled elsewhere.
Return a dotted list of the form (ACCEPT SLEN START . POS) if a
match exists, or nil otherwise. ACCEPT is the number of the
accepting state in FSM, SLEN is the length of the missing suffix
in the partially matched string (0 for full match), START is the
position of the matching string's beginning, and POS is the
position where scanning started, as passed to this function."
(let* ((pos (or pos (point)))