forked from szermatt/visual-replace
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
visual-replace.el
1570 lines (1360 loc) · 61.8 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.0.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 'seq)
(require 'thingatpt)
(require 'rect)
(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)
(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-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-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-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)
(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)
(define-key map [remap kill] #'visual-replace-kill)
(define-key map [remap kill-whole-line] #'visual-replace-kill-whole-line)
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.")
(defvar visual-replace--match-ovs nil
"Overlays added for the preview in the calling buffer.")
(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--first-match-timer nil
"Timer scheduled to search for a first match to display.")
(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--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--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)
(let ((separator-start (visual-replace--separator-start))
(separator-end (visual-replace--separator-end)))
(cond
;; in the modification section
((and separator-start (>= (point) separator-end))
(insert (buffer-substring-no-properties (minibuffer-prompt-end)
separator-start)))
;; in the original section
(t (insert (with-current-buffer visual-replace--calling-buffer
(let ((start (point)))
(forward-symbol 1)
(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)
(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-kill ()
"Replacement for command`kill' for `visual-replace'.
This kills to separator or end of line."
(interactive)
(let ((separator-start (visual-replace--separator-start)))
(if (and separator-start (< (point) separator-start))
(kill-region (point) separator-start)
(kill-line))))
(defun visual-replace-kill-whole-line ()
"Replacement for command `kill-whole-line' for `visual-replace'.
This kills the whole section."
(interactive)
(let ((separator-start (visual-replace--separator-start)))
(cond
((and separator-start (< (point) separator-start))
(kill-region (minibuffer-prompt-end) separator-start))
(separator-start
(kill-region (visual-replace--separator-end) (line-end-position)))
(t (kill-region (minibuffer-prompt-end) (line-end-position))))))
(defun visual-replace-toggle-regexp ()
"Toggle the regexp flag while building arguments for `visual-replace'."
(interactive)
(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 'forced)))
(defun visual-replace-toggle-query ()
"Toggle the query flag while building arguments for `visual-replace'."
(interactive)
(let ((args (visual-replace-args--from-minibuffer)))
(setf (visual-replace-args-query args)
(not (visual-replace-args-query args)))
(visual-replace--update-separator args 'forced)))
(defun visual-replace-toggle-word ()
"Toggle the word-delimited flag while building arguments for `visual-replace'."
(interactive)
(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 'forced)))
(defun visual-replace-toggle-case-fold ()
"Toggle the case-fold flag while building arguments for `visual-replace'."
(interactive)
(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 'forced)))
(defun visual-replace-toggle-lax-ws ()
"Toggle the lax-ws flag while building arguments for `visual-replace'."
(interactive)
(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 'forced)))
(defun visual-replace-toggle-scope (&optional scope)
"Toggle the SCOPE type.
If unspecified, SCOPE defaults to the variable
`visual-replace--scope'."
(interactive)
(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))
(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)
(let ((history-add-new-input nil)
(visual-replace--calling-buffer (current-buffer))
(visual-replace--calling-window (selected-window))
(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))))
(trigger (this-command-keys-vector))
(default-value)
(text)
(timer))
(setq default-value (car minibuffer-history))
(when visual-replace--incomplete
(push visual-replace--incomplete minibuffer-history))
(save-excursion
(unwind-protect
(progn
(deactivate-mark)
(when visual-replace-preview
(setq timer (run-with-idle-timer
visual-replace-preview-delay
#'repeat #'visual-replace--update-preview)))
(minibuffer-with-setup-hook
(lambda ()
(when visual-replace-keep-incomplete
(add-hook 'after-change-functions #'visual-replace--after-change 0 'local))
(visual-replace-minibuffer-mode t)
(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
(when timer (cancel-timer timer))
(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))))
(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
isearch as possible, with the text being searched set as query
for `visual-replace'. Replacement starts at the current point."
(interactive)
(let ((args
(visual-replace-make-args
:from isearch-string
:to "" ; Go directly to the replacement prompt.
:regexp isearch-regexp
:word isearch-regexp-function
:case-fold isearch-case-fold-search)))
(when (seq-position isearch-string ?\ )
(if isearch-regexp
(setf (visual-replace-args-lax-ws-regexp args)
isearch-regexp-lax-whitespace)
(setf (visual-replace-args-lax-ws-non-regexp args)
isearch-lax-whitespace)))
(isearch-done nil t)
(isearch-clean-overlays)
(apply #'visual-replace (visual-replace-read args))))
;;;###autoload
(defun visual-replace-thing-at-point (&optional thing)
"Start visual replace for the thing at point.
THING defaults to symbol. It can be set to anything that
`thing-at-point` understands."
(interactive)
(let* ((thing (or thing 'symbol))
(bounds (bounds-of-thing-at-point thing)))
(unless bounds
(error "No %s at point" (symbol-name thing)))
(apply
#'visual-replace
(visual-replace-read
(visual-replace-make-args
:from (buffer-substring-no-properties
(car bounds)
(cdr bounds))
;; Go directly to the replacement prompt.
:to "")
(car bounds)))))
;;;###autoload
(defun visual-replace-selected ()
"Start visual replace for replacing text in region or the current word.
Falls back to `visual-replace-thing-at-point' if the region is
not active."
(interactive)
(if (region-active-p)
(apply
#'visual-replace
(visual-replace-read
(visual-replace-make-args
:from (buffer-substring-no-properties
(min (mark) (point))
(max (mark) (point)))
:to "")
(min (mark) (point))))
(visual-replace-thing-at-point)))
(defun visual-replace-args--text (args &optional force-separator)
"Build the text representation of ARGS, a `visual-replace-args' struct.
The text representation is the content of minibuffer that would result
in such a struct being returned by `visual-replace-read'.
Unless FORCE-SEPARATOR is non-nil, only add a separator if
necessary, to capture flags defined in ARGS."
(let ((flags-text
(concat
(if (visual-replace-args-query args) "?" "")
(cond ((eq (visual-replace-args-lax-ws args)
(visual-replace-args-lax-ws-default args))
"")
((visual-replace-args-lax-ws args) "(lax ws)")
(t "(strict ws)"))
(cond ((eq (visual-replace-args-case-fold args) case-fold-search) "")
((visual-replace-args-case-fold args) "i")
(t "c"))
(if (visual-replace-args-regexp args) ".*" "")
(if (visual-replace-args-word args) "w" "")))
(from-text (or (visual-replace-args-from args) "")))
(if (and (not force-separator)
(null (visual-replace-args-to args))
(equal "" flags-text))
from-text
(let ((stored-args (visual-replace-copy-args args)))
(setf (visual-replace-args-from stored-args) nil)
(setf (visual-replace-args-to stored-args) nil)
(concat
from-text
(propertize " "
'display (concat " →" flags-text " ")
'visual-replace-args stored-args
'face 'minibuffer-prompt
'separator t)
(or (visual-replace-args-to args) ""))))))
(defun visual-replace-args--separator (args)
"Return the separator for ARGS, a `visual-replace-args'."
(let ((flag-args (visual-replace-copy-args args)))
(setf (visual-replace-args-from flag-args) nil)
(setf (visual-replace-args-to flag-args) nil)
(visual-replace-args--text flag-args 'forced)))
(defun visual-replace-args--from-text (text)
"Build a `visual-replace-args' that corresponds to TEXT.
TEXT is the textual content of the minibuffer, with properties."
(if-let ((start (text-property-any 0 (length text) 'separator t text)))
(let ((end (or (text-property-not-all start (length text) 'separator t text)
(length text)))
(args (visual-replace-copy-args
(get-text-property start 'visual-replace-args text))))
(setf (visual-replace-args-from args)
(substring-no-properties text 0 start))
(setf (visual-replace-args-to args)
(substring-no-properties text end))
args)
(visual-replace-make-args :from text)))
(defun visual-replace-args--from-minibuffer ()
"Build a `visual-replace-args' from the minibuffer content."
(visual-replace-args--from-text
(with-selected-window
(or (active-minibuffer-window)
(minibuffer-window))
(minibuffer-contents))))
(defun visual-replace--scope-text ()
"Build prompt text that reflects the current scope.
This returns text for all prompt, with different visibility
spec. `visual-replace--show-scope' sets the appropriate
spec for the current state."
(mapconcat
(lambda (type)
(let ((text (pcase type
('region
(format "in region (%sL)"
(visual-replace--scope-line-count visual-replace--scope)))
('from-point "from point")
('full "in buffer"))))
(add-text-properties 0 (length text)
(list 'invisible type)
text)
text))
visual-replace--scope-types
""))
(defun visual-replace--scope-ranges (&optional scope)
"Return the regions replacement with SCOPE should work on.
If unspecified, SCOPE defaults to the variable
`visual-replace--scope'.
Returns a list of (start . end)"
(with-current-buffer visual-replace--calling-buffer
(let ((scope (or scope visual-replace--scope)))
(pcase (visual-replace--scope-type scope)
('from-point (list (cons (visual-replace--scope-point scope) (point-max))))
('full (list (cons (point-min) (point-max))))
('region (visual-replace--scope-bounds scope))))))
(defun visual-replace--show-scope (&optional scope)
"Update the display to reflect the state of SCOPE.
If unspecified, SCOPE defaults to the variable
`visual-replace--scope'.
This must be called every time `visual-replace--scope' is
changed."
(let* ((scope (or scope visual-replace--scope))
(type (visual-replace--scope-type scope))
(buf visual-replace--calling-buffer))
(dolist (s visual-replace--scope-types)
(if (eq s type)
(remove-from-invisibility-spec s)
(add-to-invisibility-spec s)))
(let ((ovs (delq nil
(mapcar
(lambda (ov)
(if (and ov (eq buf (overlay-buffer ov)))
ov
(delete-overlay ov)))
visual-replace--scope-ovs)))
(new-ovs nil)
(left-col (visual-replace--scope-left-col scope))
(right-col (visual-replace--scope-right-col scope)))
(with-current-buffer buf
(cond
;; full doesn't need highlighting
((eq 'full type))
;; highlight a rectangular region
((and (eq 'region type) left-col)
(save-excursion
(goto-char (visual-replace--scope-topleft-edge scope))
(dotimes (_ (visual-replace--scope-line-count scope))
(let ((ov (or (car ovs) (make-overlay 1 1)))
(before "")
(after "")
left-point right-point)
(setq ovs (cdr ovs))
(push ov new-ovs)
(move-to-column left-col)
(setq left-point (point))
(if (< (current-column) left-col)
(setq before (spaces-string (- left-col (current-column)))
after (spaces-string (- right-col left-col))
right-point left-point)
(move-to-column right-col)
(setq right-point (point))
(when (< (current-column) right-col)
(setq after (spaces-string (- right-col (current-column))))))
(put-text-property 0 (length before) 'face 'default before)
(put-text-property 0 (length after) 'face 'visual-replace-region after)
(forward-line)
(overlay-put ov 'priority 1000)
(overlay-put ov 'face 'visual-replace-region)
(overlay-put ov 'before-string before)
(overlay-put ov 'after-string after)
(move-overlay ov left-point right-point)))))
;; highlight the scope ranges
(t
(dolist (range (visual-replace--scope-ranges scope))
(let ((ov (or (car ovs) (make-overlay 1 1))))
(setq ovs (cdr ovs))
(push ov new-ovs)
(overlay-put ov 'priority 1000)
(overlay-put ov 'face 'visual-replace-region)
(move-overlay ov (car range) (cdr range)))))))
(dolist (ov ovs)
(delete-overlay ov))
(setq visual-replace--scope-ovs (nreverse new-ovs)))))
(defun visual-replace--clear-scope ()
"Get rid of any scope highlight overlay."
(when visual-replace--scope-ovs
(dolist (ov visual-replace--scope-ovs)