forked from szermatt/visual-replace
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
visual-replace.el
2294 lines (1973 loc) · 91.2 KB
/
visual-replace.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
;;; visual-replace.el --- A prompt for replace-string and query-replace -*- lexical-binding: t -*-
;; Copyright (C) 2020-2024 Stephane Zermatten
;; Author: Stephane Zermatten <[email protected]>
;; Maintainer: Stephane Zermatten <[email protected]>
;; Version: 1.1.1snapshot
;; Keywords: convenience, matching, replace
;; URL: http://github.com/szermatt/visual-replace
;; Package-Requires: ((emacs "26.1"))
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3 of the
;; License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see
;; `http://www.gnu.org/licenses/'.
;;; Commentary:
;;
;; This file provides the command `visual-replace', which provides a nicer
;; frontend for the commands `replace-string', `replace-regexp',
;; `query-replace' and `query-replace-regexp'.
;;
;; `visual-replace` allows editing both the text to replace and its
;; replacement at the same time and provide a preview of what the
;; replacement would look like in the current buffer.
;;
;; For details, see the documentation, at
;; https://visual-replace.readthedocs.io/en/latest/ or in the Info
;; node visual-replace, if it is installed.
(require 'gv)
(require 'isearch)
(require 'rect)
(require 'seq)
(require 'thingatpt)
(require 'cl-lib)
(eval-when-compile (require 'subr-x)) ;; if-let
;;; Code:
(defcustom visual-replace-keep-incomplete t
"Make value from interrupted session available.
When this is on, the first element of the history might contain
incomplete value from the last minibuffer session that was
interrupted."
:type 'boolean
:group 'visual-replace)
(defcustom visual-replace-preview t
"If true, highlight the matches while typing."
:type 'boolean
:group 'visual-replace)
(defcustom visual-replace-preview-delay 0.1
"Highlight matchs after that many seconds of inactivity.
When `visual-replace-preview' is enabled, only refresh the preview
after the user stopped typing for that long. Increasing this
value on slow machines or connection is a good idea. Decreasing
this value lower than 0.1s might cause issues."
:type 'number
:group 'visual-replace)
(defcustom visual-replace-preview-max-duration 0.1
"How much time to spend computing the preview.
Allow that much time to compute the preview. If computing the
preview takes longer than that, give up. This avoids allowing
Emacs freezing because of an overly complex query."
:type 'number
:group 'visual-replace)
(defcustom visual-replace-first-match t
"Jump to the first match if there isn't one visible.
With this set, the buffer might jump around just so it can show a
match.
This option ensures that there's always a match visible, so you
can see what the replacement will look like, once it's applied."
:type 'boolean
:group 'visual-replace)
(defcustom visual-replace-first-match-max-duration 0.05
"How much time to spend looking for the first match."
:type 'number
:group 'visual-replace)
(defcustom visual-replace-initial-scope nil
"Set initial scope for visual replace sessions.
By default, the initial scope is:
- the active region, if there is one
- from point if `visual-replace-default-to-full-scope' nil
- the full buffer otherwise
With this option set, the initial scope ignores the active region
entirely and is always set to either \\='from-point or \\='full."
:type '(choice
(const :tag "Default" nil)
(const :tag "From Point" from-point)
(const :tag "Full Buffer" full))
:group 'visual-replace)
(defcustom visual-replace-default-to-full-scope nil
"Have scope default to full if there's no active region.
With this option set and there is no active region, the region is
set to \\='full instead of \\='from-point.
Ignored if `visual-replace-initial-scope' is set.
See also `visual-replace-initial-scope'."
:type 'boolean
:group 'visual-replace)
(defcustom visual-replace-display-total nil
"If non-nil, display the total match count in the prompt.
When enabled, Visual Replace counts all matches within the buffer
with a lower priority than the preview highlights and displays
the result in the prompt, just before the arrow."
:type 'boolean
:group 'visual-replace)
(defcustom visual-replace-max-matches-for-total 1000
"Maximum number of matches to process in the preview.
If there are more than that many matches, stop attempting to
compute the total even if `visual-replace-display-total' is
non-nil."
:type 'number
:group 'visual-replace)
(defcustom visual-replace-max-size-for-search (* 4 1024 1024)
"Maximum buffer region to search, in bytes.
Visual replace will not attempt to count matches if the buffer is
that large and will stop looking for a first match after going
through that much data."
:type 'number
:group 'visual-replace)
(defcustom visual-replace-min-length 3
"Only do search or preview for string lengths >= this value.
Setting this too low a number might result in strange highlights
happening when starting to type, and possibly slowdowns."
:type 'number
:group 'visual-replace)
(defcustom visual-replace-keep-initial-position nil
"If non-nil, always go back to the point `visual-replace' was called from.
If nil the point stays where it was moved in preview mode, by
commands such as with `visual-replace-next-match'. A mark is
pushed at the original position to go back to with
`exchange-point-and-mark', if necessary."
:type 'boolean
:group 'visual-replace)
(defface visual-replace-match
'((t :inherit query-replace))
"How to display the string that was matched.
This is the face that's used to highlight matches, before a
replacement has been defined."
:group 'visual-replace)
(defface visual-replace-match-count
'((t :inherit minibuffer-prompt))
"How to display match count in the prompt.
To further configure what the match count looks like, see
`visual-replace-match-count-format'."
:group 'visual-replace)
(defface visual-replace-separator
'((t :inherit minibuffer-prompt))
"Face used to display the arrow between search and replace fields."
:group 'visual-replace)
(defface visual-replace-delete-match
'((((class color)) :strike-through t :background "red" :foreground "black")
(t :inverse-video t))
"How to display the string to be replaced.
This is the face that's used to show the replacement string, once a replacement
has been defined."
:group 'visual-replace)
(defface visual-replace-replacement
'((t (:inherit (match))))
"How to display the replacement string.
This is the face that's used to show the replacement string, once
a replacement has been defined."
:group 'visual-replace)
(defface visual-replace-match-highlight
'((t :weight bold :inherit (visual-replace-match)))
"How to display the matched string, in a highlighted match.
This is the face that's used to highlight matches at point,
before a replacement has been defined."
:group 'visual-replace)
(defface visual-replace-delete-match-highlight
'((t (:weight bold :inherit (visual-replace-delete-match))))
"How to display the string to be replaced, in a highlighted match.
This is the face that's used to show the replacement string when
the pointer is currently inside the match."
:group 'visual-replace)
(defface visual-replace-replacement-highlight
'((t (:weight bold :inherit (visual-replace-replacement))))
"How to display the replacement string, in a highlighted match.
This is the face that's used to show the replacement string, when
the pointer is currently inside the match."
:group 'visual-replace)
(defface visual-replace-region
'((t :inherit region))
"Highlight for the region in which replacements occur."
:group 'visual-replace)
(defcustom visual-replace-match-count-format "[%s]"
"Format used to decorate the match count in the prompt.
It must be a string acceptable to `format' that contains a single
%s."
:type '(choice
(const :tag "Brackets" "[%s]")
(const :tag "Parentheses" "(%s)")
(const :tag "No decorations" "%s")
string)
:group 'visual-replace)
(defcustom visual-replace-highlight-match-at-point t
"If non-nil, highlight match at point in the preview.
Visual replace normally the highlight match at point, to make it
easier to see the current match when navigating with
`visual-replace-next' and `visual-replace-prev'.
Set this to nil to turn it off."
:type 'boolean
:group 'visual-replace)
(defcustom visual-replace-defaults-hook nil
"Hook run when visual replace is called with no initial arguments.
Functions registered to this hook are run when visual replace is
called normally, with no initial text or setup. That is, when you
call `visual-replace' and not `visual-replace-from-isearch' or
`visual-replace-thing-at-point'.
This allows changing the search arguments for visual replace, by
registering the relevant command to this hook. For example, if
you always want to start in regexp mode, run
`visual-replace-toggle-regexp' from this hook.
To run code in every case, register it with
`visual-replace-minibuffer-mode-hook' instead."
:type 'hook
:group 'visual-replace
:options '(visual-replace-toggle-regexp
visual-replace-toggle-word
visual-replace-toggle-case-fold
visual-replace-toggle-lax-ws))
(defcustom visual-replace-minibuffer-mode-hook nil
"Hook run when visual replace starts.
Functions registered to this hook are run when visual replace is
started in the minibuffer. This allows changing the initial
state. It's also useful to enable the query mode by default, by
calling `visual-replace-toggle-query' from this hook..
To manipulate search and replace arguments, you most likely want
to customize `visual-replace-defaults-hook' instead."
:type 'hook
:group 'visual-replace
:options '(visual-replace-toggle-query))
(defvar visual-replace-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [remap isearch-toggle-regexp] #'visual-replace-toggle-regexp)
(define-key map [remap isearch-toggle-word] #'visual-replace-toggle-word)
(define-key map [remap isearch-toggle-case-fold] #'visual-replace-toggle-case-fold)
(define-key map [remap isearch-toggle-lax-whitespace] #'visual-replace-toggle-lax-ws)
(define-key map (kbd "RET") #'visual-replace-enter)
(define-key map (kbd "<return>") #'visual-replace-enter)
(define-key map (kbd "TAB") #'visual-replace-tab)
(define-key map (kbd "<tab>") #'visual-replace-tab)
(define-key map (kbd "<up>") #'visual-replace-prev-match)
(define-key map (kbd "<down>") #'visual-replace-next-match)
(define-key map [remap yank] #'visual-replace-yank)
(define-key map [remap yank-pop] #'visual-replace-yank-pop)
map)
"Map of minibuffer keyboard shortcuts available when editing a query.
Note also the shortcuts bound to a prefix key that correspond to
the shortcut used to start `visual-replace'. See
`visual-replace-secondary-mode-map'.
Inherits from `minibuffer-mode-map'.")
(defvar visual-replace-secondary-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "r") #'visual-replace-toggle-regexp)
(define-key map (kbd "SPC") #'visual-replace-toggle-scope)
(define-key map (kbd "q") #'visual-replace-toggle-query)
(define-key map (kbd "w") #'visual-replace-toggle-word)
(define-key map (kbd "c") #'visual-replace-toggle-case-fold)
(define-key map (kbd "s") #'visual-replace-toggle-lax-ws)
(define-key map (kbd "a")
(if (eval-when-compile (>= emacs-major-version 29))
;; not using #' to avoid by-compilation error,
;; because of the version-specific availability.
'visual-replace-apply-one-repeat
#'visual-replace-apply-one))
(define-key map (kbd "u") #'visual-replace-undo)
map)
"Keyboard shortcuts specific to `visual-replace'.
This map is, by default, bound to the prefix that corresponds to
the shortcut that was used to trigger `visual-replace'. It is
Active while `visual-replace-read' is running on the minibuffer.")
(defvar visual-replace-transient-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "<down>") #'visual-replace-next-match)
(define-key map (kbd "<up>") #'visual-replace-prev-match)
(define-key map (kbd "u") #'visual-replace-undo)
map)
"Keyboard shortcuts installed by `visual-replace-apply-on-repeat'.
The keys defined here are installed in a transient map installed after
applying one replacement. This allows applying or skipping other replacements.
Visual replace adds to this the last key of the key sequence used
to call `visual-replace-apply-one-repeat', to easily repeat the command.
To leave the map, type anything that's not on the map.")
(define-minor-mode visual-replace-minibuffer-mode
"Local minibuffer mode for `visual-replace'.
Not normally turned on manually."
:keymap visual-replace-mode-map)
(defvar visual-replace-global-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [remap query-replace] #'visual-replace)
(define-key map [remap replace-string] #'visual-replace)
(define-key map [remap isearch-query-replace] #'visual-replace-from-isearch)
(define-key map [remap isearch-query-replace-regexp] #'visual-replace-from-isearch)
map))
;;;###autoload
(define-minor-mode visual-replace-global-mode
"Global mode for remapping `query-replace' to `visual-replace'."
:keymap visual-replace-global-mode-map
:global t
:group 'visual-replace)
(defvar visual-replace--on-click-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "<mouse-1>") 'visual-replace-on-click)
map)
"Call `visual-replace-on-click' when a match is clicked.")
(defvar visual-replace-functions nil
"Hooks that modify a `visual-replace-args' instance, just before execution.
The hooks are called in order, with one argument, the
`visual-replace-args' instance to modify.")
(cl-defstruct (visual-replace-args (:constructor visual-replace-make-args)
(:copier visual-replace-copy-args)
(:type vector))
"Query/replace arguments.
This structure collects arguments to pass to `visual-replace'.
`visual-replace-read` builds such a structure, but also accepts
one, as initial value.
`visual-replace-make-args' creates new instances.
`visual-replace-copy-args' to make copies of existing instances.
Slots:
from Text to modify. Might be a regexp if regexp is t.
to Replacement string.
regexp if t, from is a regexp and to might include back-references,
such as `\\&' and `\\N'.
query if t, replacement behaves as `query-replace'.
word if t, from is a word
case-fold overrides `case-fold-search` for the current query
lax-ws-non-regexp overrides `replace-lax-whitespace` for the current query
lax-ws-regexp overrides `replace-regexp-lax-whitespace` for the current query
To read or edit the lax-ws value that's appropriate to the
current value of regexp, call `visual-replace-args-lax-ws'.
"
from to
regexp query word
(case-fold case-fold-search)
(lax-ws-non-regexp replace-lax-whitespace)
(lax-ws-regexp replace-regexp-lax-whitespace))
(cl-defstruct
(visual-replace--scope
(:copier nil)
(:constructor visual-replace--make-scope
(initial-scope
&aux
(type (cond
(visual-replace-initial-scope visual-replace-initial-scope)
((and (numberp initial-scope) visual-replace-default-to-full-scope) 'full)
((numberp initial-scope) 'from-point)
((eq initial-scope 'from-point) 'from-point)
((eq initial-scope 'region) 'region)
((eq initial-scope 'full) 'full)
(initial-scope (error "Invalid INITIAL-SCOPE value: %s" initial-scope))
((region-active-p) 'region)
(visual-replace-default-to-full-scope 'full)
(t 'from-point)))
(point (if (numberp initial-scope) initial-scope (point)))
(bounds (when (region-active-p)
(visual-replace--ranges-fix
(region-bounds))))
(left-col (when (and bounds rectangle-mark-mode)
(apply #'min
(mapcar (lambda (range)
(visual-replace--col (car range)))
bounds))))
(right-col (when (and bounds rectangle-mark-mode)
(apply #'max
(mapcar (lambda (range)
(visual-replace--col (cdr range)))
bounds))))
(topleft-edge (when bounds
(apply #'min (mapcar #'car bounds))))
(line-count
(if (region-active-p)
(count-lines (region-beginning) (region-end))
0)))))
"Stores the current scope and all possible scopes and their ranges.
The scope is tied to the buffer that was active when
`visual-replace--make-scope' was called."
;; 'from-point, 'full or 'region. See also visual-replace--scope-types.
type
;; value of (point) at creation time, for 'from-point
(point nil :read-only t)
;; (region-bounds) at creation time, for 'region
(bounds nil :read-only t)
;; column of the left edge, if the region is a rectangle.
(left-col nil :read-only t)
;; column of the right edge, if the region is a rectangle.
(right-col nil :read-only t)
;; point containing the top/left edge of the region
(topleft-edge nil :read-only t)
;; number of line the region contains or 0
(line-count 0 :read-only t))
(defconst visual-replace--scope-types '(region from-point full)
"Valid values for `visual-replace--scope-type'.")
(defun visual-replace-args-lax-ws (args)
"Return the appropriate lax whitespace setting for ARGS.
Returns either lax-ws-non-regexp or lax-ws-regexp, depending on
the value of the regexp slot."
(if (visual-replace-args-regexp args)
(visual-replace-args-lax-ws-regexp args)
(visual-replace-args-lax-ws-non-regexp args)))
(defun visual-replace-args-lax-ws-default (args)
"Return the appropriate default for lax whitespace for ARGS.
Returns either `replace-lax-whitespace' or
`replace-lax-whitespace', depending on the value of the regexp
slot."
(if (visual-replace-args-regexp args)
replace-regexp-lax-whitespace
replace-lax-whitespace))
(defvar visual-replace-read-history nil
"History of `visual-replace-read`.
Each entry is a struct `visual-replace-args'.")
(defvar visual-replace--scope nil
"What replace applies to.
This is an instance of the struct `visual-replace--scope'.")
(defvar visual-replace--calling-buffer nil
"Buffer from which `visual-replace' was called.")
(defvar visual-replace--calling-window nil
"Window from which `visual-replace' was called.
As window layout might change, always access it through
`visual-replace--find-window'.")
(defvar visual-replace--minibuffer nil
"Minibuffer in which `visual-replace' is running.")
(defvar visual-replace--match-ovs nil
"Overlays added for the preview in the calling buffer.")
(defvar visual-replace--total-ov nil
"Overlay added to the minibuffer to display match count.
Only relevant if `visual-replace-display-total' is non-nil.")
(defvar visual-replace--preview-state nil
"Represents the state of the preview.
The preview is a set of overlays stored in
`visual-replace--match-ovs'. This variable keeps track of the
state that was current when that set of overlays was created.
If non-nil, this is a vector:
[ARGS RANGES POINT IS-COMPLETE SCOPE TOO-MANY-MATCHES]
ARGS is a `visual-replace-range' element that was used to produce
these overlays.
RANGES is the range within the buffer Visual Replace looked for
matches for the preview.
POINT is the value of (point) that was last used to update the
highlights of the overlays.
IS-COMPLETE is non-nil once the whole buffer was searched, so
overlays are complete.
SCOPE is the scope that was current at the time the state was
setup.
TOO-MANY-MATCHES is non-nil if search for all matches was
attempted but hit the max matches limit.")
(defsubst visual-replace--init-preview-state ()
"Initialize `visual-replace--preview-state'."
(setq visual-replace--preview-state (make-vector 6 nil)))
(defsubst visual-replace--preview-args ()
"`visual-replace-args' current when the preview was last updated."
(when-let ((s visual-replace--preview-state))
(aref s 0)))
(gv-define-setter visual-replace--preview-args (args)
`(setf (aref visual-replace--preview-state 0) ,args))
(defsubst visual-replace--preview-ranges ()
"Visible ranges current when the preview was last updated."
(when-let ((s visual-replace--preview-state))
(aref s 1)))
(gv-define-setter visual-replace--preview-ranges (ranges)
`(setf (aref visual-replace--preview-state 1) ,ranges))
(defsubst visual-replace--preview-point ()
"Point current when the preview was last updated."
(when-let ((s visual-replace--preview-state))
(aref s 2)))
(gv-define-setter visual-replace--preview-point (point)
`(setf (aref visual-replace--preview-state 2) ,point))
(defsubst visual-replace--preview-is-complete ()
"Non-nil once the set of match overlays is complete."
(when-let ((s visual-replace--preview-state))
(aref s 3)))
(gv-define-setter visual-replace--preview-is-complete (is-complete)
`(setf (aref visual-replace--preview-state 3) ,is-complete))
(defsubst visual-replace--preview-scope ()
"Scope that was current when the preview was last updated."
(when-let ((s visual-replace--preview-state))
(aref s 4)))
(gv-define-setter visual-replace--preview-scope (scope)
`(setf (aref visual-replace--preview-state 4) ,scope))
(defsubst visual-replace--preview-too-many-matches ()
"Scope that was current when the preview was last updated."
(when-let ((s visual-replace--preview-state))
(aref s 5)))
(gv-define-setter visual-replace--preview-too-many-matches (val)
`(setf (aref visual-replace--preview-state 5) ,val))
(defvar visual-replace--scope-ovs nil
"Overlay that highlight the replacement region.")
(defvar visual-replace--incomplete nil
"Replacement text entered, but not confirmed.")
(defvar visual-replace--idle-search-timer nil
"Timer used to run search in the background.
Such timer is created by `visual-replace--schedule-idle-search'
and can be cancelled by
`visual-replace--reset-idle-search-timer'.")
(defvar visual-replace--undo-marker nil
"A marker put into the undo list.
This marker is added to `buffer-undo-list' by the first call to
`visual-replace-apply-one' to mark the beginning of history for
`visual-replace-undo'.")
(defvar-local visual-replace-last-tab-marker nil
"Marker on where the cursor was at when TAB was last called.
This is a local variable in the minibuffer in visual replace
mode.")
(defun visual-replace-enter ()
"Confirm the current text to replace.
If both the text to replace and its replacement have been
defined, execute the replacement. If only the text to replace
has been defined, create a new field to fill in the replacement.
See also `visual-replace-tab'."
(interactive)
(visual-replace--assert-in-minibuffer)
(visual-replace--update-separator (visual-replace-args--from-minibuffer))
(let ((separator-start (visual-replace--separator-start))
(separator-end (visual-replace--separator-end)))
(cond
((and (= (point) (minibuffer-prompt-end))
(= (point) separator-start))
(exit-minibuffer))
((and (<= (point) separator-start)
(= (point-max) separator-end))
(goto-char (point-max)))
(t (exit-minibuffer)))))
(defun visual-replace-tab ()
"Replacement for TAB while building args for `visual-replace'.
Introduce a separator or navigate between fields.
See also `visual-replace-enter'."
(interactive)
(visual-replace--assert-in-minibuffer)
(visual-replace--update-separator (visual-replace-args--from-minibuffer))
(let ((separator-start (visual-replace--separator-start))
(separator-end (visual-replace--separator-end))
(marker visual-replace-last-tab-marker)
(start-pos (point))
(goal-area))
(if (<= (point) separator-start)
;; search string -> replacement
(setq goal-area (cons separator-end (point-max)))
;; replacement -> search string
(setq goal-area (cons (minibuffer-prompt-end)
separator-start)))
;; go to the beginning of the goal area or to the position
;; the cursor was previously.
(if (and (markerp marker)
(>= marker (car goal-area))
(<= marker (cdr goal-area)))
(goto-char marker)
(goto-char (cdr goal-area)))
;; remember the position TAB was called for next time.
(unless (markerp marker)
(setq marker (make-marker))
(setq visual-replace-last-tab-marker marker))
(move-marker marker start-pos)))
(defun visual-replace-yank ()
"Replacement for `yank' while building args for `visual-replace'.
When editing the text to be replaced, insert the text at point.
Multiple calls to `visual-replace-yank` put more and more of the text
at point into the field.
When editing the replacement text, insert the original text.
See also `visual-replace-yank-pop'."
(interactive)
(visual-replace--assert-in-minibuffer)
(let* ((separator-start (visual-replace--separator-start))
(separator-end (visual-replace--separator-end))
(from-text (buffer-substring-no-properties
(minibuffer-prompt-end)
(or separator-start (point-max)))))
(cond
;; in the modification section
((and separator-start (>= (point) separator-end))
(insert from-text))
;; in the original section
(t (insert (with-current-buffer visual-replace--calling-buffer
;; If the text we're looking at is exactly
;; from-text, we likely moved to that point with
;; visual-replace-next or -prev. Skip the matching
;; text.
(when (and (> (length from-text) 0)
(equal from-text (buffer-substring-no-properties
(point)
(min (+ (length from-text) (point))
(point-max)))))
(goto-char (+ (length from-text) (point))))
(let ((start (point)))
(skip-syntax-forward " ")
(or
(> (skip-syntax-forward "w_") 0)
(progn (goto-char (1+ (point))) t))
(buffer-substring-no-properties start (point)))))))))
(defun visual-replace-yank-pop ()
"Replacement for `yank-pop' while building args for `visual-replace'.
The first time it's called, executes a `yank', then a `yank-pop'."
(interactive)
(visual-replace--assert-in-minibuffer)
(if (memq last-command '(yank yank-pop))
(progn (setq this-command 'yank-pop)
(call-interactively #'yank-pop))
;; If previous command was not a yank, call yank. This gives
;; access to yank for the modified test.
(setq this-command 'yank)
(yank)))
(defun visual-replace-toggle-regexp ()
"Toggle the regexp flag while building arguments for `visual-replace'."
(interactive)
(visual-replace--assert-in-minibuffer)
(let ((args (visual-replace-args--from-minibuffer)))
(if (visual-replace-args-regexp args)
(setf (visual-replace-args-regexp args) nil)
(setf (visual-replace-args-regexp args) t)
(setf (visual-replace-args-word args) nil))
(visual-replace--update-separator args)))
(defun visual-replace-toggle-query ()
"Toggle the query flag while building arguments for `visual-replace'."
(interactive)
(visual-replace--assert-in-minibuffer)
(let ((args (visual-replace-args--from-minibuffer)))
(setf (visual-replace-args-query args)
(not (visual-replace-args-query args)))
(visual-replace--update-separator args)))
(defun visual-replace-toggle-word ()
"Toggle the word-delimited flag while building arguments for `visual-replace'."
(interactive)
(visual-replace--assert-in-minibuffer)
(let ((args (visual-replace-args--from-minibuffer)))
(if (visual-replace-args-word args)
(setf (visual-replace-args-word args) nil)
(setf (visual-replace-args-word args) t)
(setf (visual-replace-args-regexp args) nil))
(visual-replace--update-separator args)))
(defun visual-replace-toggle-case-fold ()
"Toggle the case-fold flag while building arguments for `visual-replace'."
(interactive)
(visual-replace--assert-in-minibuffer)
(let ((args (visual-replace-args--from-minibuffer)))
(setf (visual-replace-args-case-fold args)
(not (visual-replace-args-case-fold args)))
(visual-replace--update-separator args)))
(defun visual-replace-toggle-lax-ws ()
"Toggle the lax-ws flag while building arguments for `visual-replace'."
(interactive)
(visual-replace--assert-in-minibuffer)
(let* ((args (visual-replace-args--from-minibuffer))
(newval (not (visual-replace-args-lax-ws args))))
(setf (visual-replace-args-lax-ws-regexp args) newval)
(setf (visual-replace-args-lax-ws-non-regexp args) newval)
(visual-replace--update-separator args)))
(defun visual-replace-toggle-scope (&optional scope)
"Toggle the SCOPE type.
If unspecified, SCOPE defaults to the variable
`visual-replace--scope'."
(interactive)
(visual-replace--assert-in-minibuffer)
(let* ((scope (or scope visual-replace--scope))
(type (visual-replace--scope-type scope)))
(setf (visual-replace--scope-type scope)
(if (visual-replace--scope-bounds scope)
(pcase type
('region 'full)
(_ 'region))
(pcase type
('from-point 'full)
(_ 'from-point)))))
(visual-replace--show-scope)
(visual-replace--reset-preview)
(visual-replace--update-preview))
(defun visual-replace-read (&optional initial-args initial-scope)
"Read arguments for `query-replace'.
INITIAL-ARGS is used to set the prompt's initial state, if
specified. It must be a `visual-replace-args' struct.
INITIAL-SCOPE is used to initialize the replacement scope,
\\='region \\='from-point or \\='full. If it is a number, it is
used as point for \\='from-point. By default, the scope is
\\='region if the region is active, or \\='from-point otherwise."
(barf-if-buffer-read-only)
(if visual-replace-keep-initial-position
(save-excursion
(visual-replace-read--internal initial-args initial-scope nil))
(visual-replace-read--internal initial-args initial-scope 'push-mark)))
(defun visual-replace-read--internal (&optional initial-args initial-scope push-mark)
"Private implementation of `visual-replace-read'.
See `visual-replace-read' for a description of the behavior of
this function and of INITIAL-ARGS and INITIAL-SCOPE.
If PUSH-MARK is non-nil, push a mark to the current point."
(let ((history-add-new-input nil)
(visual-replace--calling-buffer (current-buffer))
(visual-replace--calling-window (selected-window))
(visual-replace--minibuffer nil)
(visual-replace--scope (visual-replace--make-scope initial-scope))
(visual-replace--undo-marker nil)
(minibuffer-allow-text-properties t) ; separator uses text-properties
(minibuffer-history (mapcar #'visual-replace-args--text visual-replace-read-history))
(initial-input (let* ((args (or initial-args (visual-replace-make-args)))
(text (visual-replace-args--text args))
(from (visual-replace-args-from args)))
(cons text (if from (1+ (length text)) 0))))
(visual-replace--preview-state nil)
(after-change (lambda (_beg _end _len)
(visual-replace--reset-preview)
(visual-replace--update-preview)))
(trigger (this-command-keys-vector))
(visual-replace--total-ov nil)
(default-value)
(text)
(timer))
(setq default-value (car minibuffer-history))
(when visual-replace--incomplete
(push visual-replace--incomplete minibuffer-history))
(when push-mark
(push-mark nil 'nomsg))
(unwind-protect
(progn
(deactivate-mark)
(add-hook 'after-change-functions after-change 0 'local)
(when visual-replace-preview
(setq timer (run-with-idle-timer
visual-replace-preview-delay
'repeat #'visual-replace--update-preview)))
(minibuffer-with-setup-hook
(lambda ()
(setq visual-replace--total-ov
(when visual-replace-display-total
(let ((ov (make-overlay (point-min) (point-min))))
(overlay-put ov 'face 'visual-replace-match-count)
ov)))
(when visual-replace-keep-incomplete
(add-hook 'after-change-functions #'visual-replace--after-change 0 'local))
(setq visual-replace--minibuffer (current-buffer))
(visual-replace-minibuffer-mode t)
(unless initial-args
(run-hooks 'visual-replace-defaults-hook))
(when trigger
(let ((mapping
;; Emacs 26 lookup-key cannot take a list
;; of keymaps, using this code for backward
;; compatibility.
(catch 'has-binding
(dolist (map (current-active-maps))
(let ((func (lookup-key map trigger)))
(when (functionp func)
(throw 'has-binding func)))))))
(when (or (eq mapping #'visual-replace)
(eq (command-remapping mapping) #'visual-replace))
(local-set-key trigger visual-replace-secondary-mode-map))))
(visual-replace--show-scope)
(setq-local yank-excluded-properties (append '(separator display face) yank-excluded-properties))
(setq-local text-property-default-nonsticky
(append '((separator . t) (face . t))
text-property-default-nonsticky)))
(setq text (read-from-minibuffer
(concat "Replace "
(visual-replace--scope-text)
(if default-value (format " [%s]" default-value) "")
": ")
initial-input nil nil nil (car search-ring) t))))
;; unwind
(remove-hook 'after-change-functions after-change 'local)
(visual-replace--reset-idle-search-timer)
(when timer
(cancel-timer timer))
(isearch-clean-overlays)
(visual-replace--clear-scope)
(visual-replace--clear-preview))
(unless quit-flag (setq visual-replace--incomplete nil))
(let* ((final-args (visual-replace-args--from-text text))
(from (visual-replace-args-from final-args))
(to (visual-replace-args-to final-args)))
(cond
((or quit-flag (null to) nil)
(setq final-args (visual-replace-make-args)))
((and (zerop (length from)) (zerop (length to)))
(setq final-args (car visual-replace-read-history))
(unless final-args
(error "Nothing to replace")))
(t
(when (visual-replace-args-regexp final-args)
(visual-replace--warn from))
(add-to-history query-replace-from-history-variable from nil t)
(add-to-history query-replace-to-history-variable to nil t)
(add-to-history 'visual-replace-read-history final-args nil t)))
;; visual-replace argument list
(list final-args (visual-replace--scope-ranges)))))
(defun visual-replace (args ranges)
"Replace text.
ARGS specifies the text to replace, the replacement and any
flags. It is a `visual-replace-args' struct, usually one created by
`visual-replace-read'.
Replacement applies in the current buffer on RANGES, a list
of (start . end) as returned by `region-bounds'."
(interactive (visual-replace-read (visual-replace-make-args
:word (and current-prefix-arg (not (eq current-prefix-arg '-))))))
(barf-if-buffer-read-only)
(let* ((origin (make-marker))
(args (visual-replace-preprocess args))
(from (visual-replace-args-from args))
(ranges (visual-replace--ranges-fix ranges)))
(unless ranges
(error "Empty range; nothing to replace"))
(unwind-protect
(progn
(set-marker origin (point))
(unless (and (stringp from) (not (zerop (length from))))
(error "Nothing to replace"))
(let ((case-fold-search (visual-replace-args-case-fold args))
(replace-lax-whitespace
(visual-replace-args-lax-ws-non-regexp args))
(replace-regexp-lax-whitespace
(visual-replace-args-lax-ws-regexp args))
(query-replace-skip-read-only t)
(start (apply #'min (mapcar #'car ranges)))
(end (apply #'max (mapcar #'cdr ranges)))
(noncontiguous-p (if (cdr ranges) t nil))
;; when noncontiguous-p is non-nil, perform-replace
;; calls region-extract-function to get the ranges to
;; apply the searches on.
(region-extract-function
(lambda (arg)
(unless (eq arg 'bounds)
(error "unsupported: (funcall region-extract-function %s)" arg))
(visual-replace--ranges-fix ranges))))
(cl-letf (((symbol-function 'push-mark) (lambda (&rest _args))))
;; perform-replace sets the mark at an uninteresting
;; position. Redefining push-mark avoids that.
(perform-replace
from
(query-replace-compile-replacement
(visual-replace-args-to args)
(visual-replace-args-regexp args))
(visual-replace-args-query args)
(visual-replace-args-regexp args)
(visual-replace-args-word args)
1 nil start end nil noncontiguous-p)))
(goto-char origin))
(set-marker origin nil))))
;;;###autoload
(defun visual-replace-from-isearch ()
"Switch from isearch to `visual-replace'.
This function attempts to copy as much of the current state of