]> git.eshelyaron.com Git - sweep.git/commitdiff
ADDED: new command 'sweeprolog-query-replace-term'
authorEshel Yaron <me@eshelyaron.com>
Wed, 20 Sep 2023 11:00:00 +0000 (13:00 +0200)
committerEshel Yaron <me@eshelyaron.com>
Fri, 22 Sep 2023 15:00:23 +0000 (17:00 +0200)
This patch extends the Term Search infrastructure to support
search-and-replace operations.  Namely, we replace the predicate
'sweep_term_search/2' with a new predicate 'sweep_term_replace/2' that
subsumes the functionality of the former.  The command
'sweeprolog-term-search' is greatly simplified and adjusted to work
with this new predicate, and we introduce a new command,
'sweeprolog-query-replace-term', that exposes the new
search-and-replace facility of 'sweep_term_replace/2' in an
interactive UI inspired by 'query-replace'.

* sweep.pl (sweep_term_search/2): Remove in favor of...
(sweep_term_replace/2): New predicate.
* sweeprolog.el: (sweeprolog-term-replace-edits): New function.
(sweeprolog-term-search): Rewrite.
(sweeprolog-term-search-last-search)
(sweeprolog-term-search-overlays)
(sweeprolog-term-search-repeat-count)
(sweeprolog-term-search-repeat-backward)
(sweeprolog-term-search-repeat-forward)
(sweeprolog-term-search-abort)
(sweeprolog-term-search-in-buffer)
(sweeprolog-term-search-next)
(sweeprolog-term-search-map): Remove, unused.
(sweeprolog-query-replace-term): New command.
(sweeprolog-mode-map): Bind it.
* sweeprolog-tests.el (term-search)
* sweep.texi (Term Search): Adjust.
(Term Replace): New section.

sweep.pl
sweep.texi
sweeprolog-tests.el
sweeprolog.el

index dd29e62c221b54d4dbb9129330ea15e18acdc3f8..c62aff622b48c035b6c6aa4812e6813390bb42c6 100644 (file)
--- a/sweep.pl
+++ b/sweep.pl
@@ -72,7 +72,7 @@
             sweep_format_head/2,
             sweep_format_term/2,
             sweep_current_functors/2,
-            sweep_term_search/2,
+            sweep_term_replace/2,
             sweep_terms_at_point/2,
             sweep_predicate_dependencies/2,
             sweep_async_goal/2,
@@ -1214,59 +1214,230 @@ sweep_current_functors(A0, Col) :-
             ),
             Col).
 
-sweep_term_search([Path0,TermString,GoalString], Res) :-
-    term_string(Term, TermString, [variable_names(TermVarNames)]),
+sweep_term_replace([FileName0,BodyIndent,TemplateString,GoalString,FinalString,RepString], Res) :-
+    term_string(Template, TemplateString, [variable_names(TemplateVarNames)]),
     term_string(Goal, GoalString, [variable_names(GoalVarNames)]),
-    maplist({GoalVarNames}/[TermVarName]>>ignore(memberchk(TermVarName, GoalVarNames)),
-            TermVarNames),
-    atom_string(Path, Path0),
-    setup_call_cleanup(prolog_open_source(Path, Stream),
-                       sweep_search_stream(Stream, Term, Goal, Res),
+    maplist(term_string, Final, FinalString),
+    term_string(Rep, RepString, [variable_names(VarNames0)]),
+    maplist({GoalVarNames}/[VarName]>>ignore(memberchk(VarName, GoalVarNames)),
+            TemplateVarNames),
+    maplist({VarNames0}/[VarName]>>ignore(memberchk(VarName, VarNames0)),
+            TemplateVarNames),
+    atom_string(FileName, FileName0),
+    xref_source(FileName),
+    sweep_module_path_(Module, FileName),
+    setup_call_cleanup(prolog_open_source(FileName, Stream),
+                       sweep_replace_stream(Stream, FileName, Module, BodyIndent, Final, Template-Goal, Rep-VarNames0, Res),
                        prolog_close_source(Stream)).
 
-sweep_search_stream(Stream, Term, Goal, Res) :-
-    prolog_read_source_term(Stream, Term0, _, [subterm_positions(TermPos)]),
-    sweep_search_stream_(Term0, TermPos, Stream, Term, Goal, Res).
+sweep_anon_var_names(Term, VarNames, AnonVars) :-
+    term_variables(Term, TermVars),
+    convlist({VarNames}/[Var,'_'=Var]>>(   \+ (   member(_=VV, VarNames),
+                                                  Var == VV
+                                              )
+                                       ),
+             TermVars, AnonVars).
 
-sweep_search_stream_(end_of_file, _, _, _, _, []) :-
-    !.
-sweep_search_stream_(Term0, TermPos, Stream, Term, Goal, Res) :-
-    findall([HS|HE],
-            sweep_match_term(TermPos, Term0, Term, Goal, HS, HE),
+sweep_replace_stream(Stream, FileName, Module, BodyIndent, Final, TemplateGoal, Rep-VarNames0, Res) :-
+    read_clause(Stream, Term, [subterm_positions(Pos), variable_names(VarNames1), syntax_errors(dec10)]),
+    !,
+    sweep_anon_var_names(Rep, VarNames0, AnonVars0),
+    sweep_anon_var_names(Term, VarNames1, AnonVars1),
+    maplist({VarNames1}/[Name0=Var,Name=Var]>>(   member(Name0=_, VarNames1)
+                                              ->  atom_concat(Name0, 'Fresh', Name)
+                                              ;   Name = Name0
+                                              ),
+            VarNames0, VarNames2),
+    append(AnonVars1, AnonVars0, AnonVars),
+    append(VarNames2, AnonVars, VarNames3),
+    append(VarNames1, VarNames3, VarNames),
+    sweep_replace_stream_(Term, Pos, Stream, FileName, Module, BodyIndent, Final, TemplateGoal, Rep-VarNames, Res).
+sweep_replace_stream(Stream, FileName, Module, BodyIndent, Final, TemplateGoal, RepVarNames, Res) :-
+    sweep_replace_stream(Stream, FileName, Module, BodyIndent, Final, TemplateGoal, RepVarNames, Res).
+
+sweep_replace_stream_(end_of_file, _, _, _, _, _, _, _, _, []) :- !.
+sweep_replace_stream_(Term, Pos, Stream, FileName, Module, BodyIndent, Final, TemplateGoal, RepVarNames, Res) :-
+    (   var(Term)
+    ->  State = clause
+    ;   memberchk(Term, [(_:-_),(_=>_),(_-->_)])
+    ->  State = clause
+    ;   Term = (:-_)
+    ->  State = directive
+    ;   State = head
+    ),
+    findall(Result,
+            sweep_replace_term(Pos, Term, FileName, Module, BodyIndent, 0, 1200, State, Final, TemplateGoal, RepVarNames,Result),
             Res,
             Tail),
-    sweep_search_stream(Stream, Term, Goal, Tail).
+    sweep_replace_stream(Stream, FileName, Module, BodyIndent, Final, TemplateGoal, RepVarNames, Tail).
+
+
+
+%!  sweep_replace_term(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, Precedence, State, Final, TemplateGoal, RepVarNames, Result) is nondet.
+
+sweep_replace_term(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, Precedence, State, Final, TemplateGoal, RepVarNames, Result) :-
+    (   sweep_replace_term_(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, Precedence, State, Final, TemplateGoal, RepVarNames, Result)
+    ;   sweep_replace_term_r(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, Precedence, State, Final, TemplateGoal, RepVarNames, Result)
+    ).
+
+sweep_replace_term_r(brace_term_position(_, _, Pos), {Term}, FileName, Module, BodyIndent, CurrentIndent0, _Precedence, State0, Final, TemplateGoal, RepVarNames, Result) :-
+    CurrentIndent is CurrentIndent0 + BodyIndent,
+    (   State0 == data
+    ->  State = data
+    ;   State0 == goal(2)
+    ->  State = goal(0)
+    ;   State = State0
+    ),
+    sweep_replace_term(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, 1200, State, Final, TemplateGoal, RepVarNames, Result).
+sweep_replace_term_r(list_position(_, _, Elms, Tail), List, FileName, Module, BodyIndent, CurrentIndent0, _Precedence, _State, Final, TemplateGoal, RepVarNames, Result) :-
+    CurrentIndent is CurrentIndent0 + BodyIndent,
+    (   nth0(I, Elms, Pos),
+        nth0(I, List, Term),
+        sweep_replace_term(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, 999, data, Final, TemplateGoal, RepVarNames, Result)
+    ;   Tail == none
+    ->  false
+    ;   list_tail(List, Term),
+        sweep_replace_term(Tail, Term, FileName, Module, BodyIndent, CurrentIndent, 999, data, Final, TemplateGoal, RepVarNames, Result)
+    ).
+sweep_replace_term_r(term_position(From, To, _, _, ArgsPos), Term, FileName, Module0, BodyIndent, CurrentIndent0, _, State0, Final, TemplateGoal, RepVarNames, Result) :-
+    CurrentIndent is CurrentIndent0 + BodyIndent,
+    compound_name_arguments(Term, Functor, Arguments),
+    length(Arguments, Arity),
+    (   Arguments == []         % nullary compound, e.g. foo()
+    ->  true
+    ;   Arguments = [Arg]
+    ->  ArgsPos = [ArgPos],
+        arg(1, ArgPos, ArgBeg),
+        arg(2, ArgPos, ArgEnd),
+        Module = Module0,
+        (   ArgBeg == From,
+            (   xref_op(FileName, op(Precedence0, Assoc, Functor))
+            ;   current_op(Precedence0, Assoc, Functor)
+            ),
+            memberchk(Assoc, [fx,fy])
+        ->  (   Assoc == fx
+            ->  Precedence is Precedence0 - 1
+            ;   Precedence = Precedence0
+            )
+        ;   ArgEnd == To,
+            (   xref_op(FileName, op(Precedence0, Assoc, Functor))
+            ;   current_op(Precedence0, Assoc, Functor)
+            ),
+            memberchk(Assoc, [xf,yf])
+        ->  (   Assoc == xf
+            ->  Precedence is Precedence0 - 1
+            ;   Precedence = Precedence0
+            )
+        ;   Precedence = 999
+        ),
+        sweep_replace_update_state(FileName, Module, Functor, Arity, 1, State0, State),
+        sweep_replace_term(ArgPos, Arg, FileName, Module, BodyIndent, CurrentIndent, Precedence, State, Final, TemplateGoal, RepVarNames, Result)
+    ;   Arguments = [Left, Right]
+    ->  ArgsPos = [LeftPos, RightPos],
+        arg(2, RightPos, ArgEnd),
+        (   ArgEnd == To,        % infix operator
+            (   xref_op(FileName, op(Precedence0, Assoc, Functor))
+            ;   current_op(Precedence0, Assoc, Functor)
+            ),
+            memberchk(Assoc, [xfx, xfy, yfy, yfx])
+        ->  (   Assoc == xfx
+            ->  LeftPrecedence is Precedence0 - 1,
+                RightPrecedence is Precedence0 - 1
+            ;   Assoc == xfy
+            ->  LeftPrecedence is Precedence0 - 1,
+                RightPrecedence = Precedence0
+            ;   Assoc == yfx
+            ->  LeftPrecedence = Precedence0,
+                RightPrecedence is Precedence0 - 1
+            ;   Assoc == yfy
+            ->  LeftPrecedence = Precedence0,
+                RightPrecedence = Precedence0
+            )
+        ;   LeftPrecedence = 999,
+            RightPrecedence = 999
+        ),
+        (   Functor = ':',
+            atom(Left),
+            State0 = goal(_)
+        ->  Module = Left,
+            LeftState = module,
+            RightState = State0
+        ;   Module = Module0,
+            sweep_replace_update_state(FileName, Module, Functor, Arity, 1, State0, LeftState),
+            sweep_replace_update_state(FileName, Module, Functor, Arity, 2, State0, RightState)
+        ),
+        (   sweep_replace_term(LeftPos, Left, FileName, Module, BodyIndent, CurrentIndent, LeftPrecedence, LeftState, Final, TemplateGoal, RepVarNames, Result)
+        ;   sweep_replace_term(RightPos, Right, FileName, Module, BodyIndent, CurrentIndent, RightPrecedence, RightState, Final, TemplateGoal, RepVarNames, Result)
+        )
+    ;   nth1(I, ArgsPos, ArgPos),
+        nth1(I, Arguments, Arg),
+        Module = Module0,
+        sweep_replace_update_state(FileName, Module, Functor, Arity, I, State0, ArgState),
+        sweep_replace_term(ArgPos, Arg, FileName, Module, BodyIndent, CurrentIndent, 999, ArgState, Final, TemplateGoal, RepVarNames, Result)
+    ).
+sweep_replace_term_r(dict_position(_, _, _, _, KeyValuePosList), Term, FileName, Module, BodyIndent, CurrentIndent0, _, _, Final, TemplateGoal, RepVarNames, Result) :-
+    CurrentIndent is CurrentIndent0 + BodyIndent,
+    member(key_value_position(_, _, _, _, Key, _, ValuePos), KeyValuePosList),
+    get_dict(Key, Term, Value),
+    sweep_replace_term(ValuePos, Value, FileName, Module, BodyIndent, CurrentIndent, 999, data, Final, TemplateGoal, RepVarNames, Result).
+sweep_replace_term_r(parentheses_term_position(_, _, Pos), Term, FileName, Module, BodyIndent, CurrentIndent, _, State, Final, TemplateGoal, RepVarNames, Result) :-
+    sweep_replace_term(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, 1200, State, Final, TemplateGoal, RepVarNames, Result).
+sweep_replace_term_r(quasi_quotation_position(_, _, Term, Pos, _), _, FileName, Module, BodyIndent, CurrentIndent0, _, _, Final, TemplateGoal, RepVarNames, Result) :-
+    CurrentIndent is CurrentIndent0 + BodyIndent,
+    sweep_replace_term(Pos, Term, FileName, Module, BodyIndent, CurrentIndent, 999, syntax, Final, TemplateGoal, RepVarNames, Result).
+
+sweep_replace_update_state(_, _, _, _, _, data, data) :- !.
+sweep_replace_update_state(_, _, _, _, _, syntax, data) :- !.
+sweep_replace_update_state(_, _, _, _, _, module, data) :- !.
+sweep_replace_update_state(_FileName, _Module, ':-', 1, 1, directive, goal(0)) :- !.
+sweep_replace_update_state(_FileName, _Module, ':-', 2, 1, clause, head) :- !.
+sweep_replace_update_state(_FileName, _Module, ':-', 2, 2, clause, goal(0)) :- !.
+sweep_replace_update_state(_FileName, _Module, '=>', 2, 1, clause, head) :- !.
+sweep_replace_update_state(_FileName, _Module, '=>', 2, 2, clause, goal(0)) :- !.
+sweep_replace_update_state(_FileName, _Module, '-->', 2, 1, clause, head) :- !.
+sweep_replace_update_state(_FileName, _Module, '-->', 2, 2, clause, goal(2)) :- !.
+sweep_replace_update_state(_FileName, _Module, _Functor, _Arity, _I, clause, data) :- !.
+sweep_replace_update_state(_FileName, _Module, _Functor, _Arity, _I, head, data) :- !.
+sweep_replace_update_state(_FileName, Module, Functor, Arity, I, goal(N0), State) :-
+    pi_head(Functor/Arity,Head),
+    (   (   (   @(predicate_property(Head, meta_predicate(Spec)), Module)
+            ;   catch(infer_meta_predicate(Head, Spec),
+                      error(permission_error(access, private_procedure, _),
+                            context(system:clause/2, _)),
+                  false)
+            )
+        ->  arg(I, Spec, A),
+            callable_arg(A, M)
+        )
+    ->  N is N0 + M,
+        State = goal(N)
+    ;   State = data
+    ).
+
+:- dynamic sweep_match_replacement/1.
 
-sweep_match_term(Pos, Term0, Term, Goal, From, To) :-
+sweep_replace_term_(Pos, Term, _FileName, Module, _BodyIndent, CurrentIndent, Precedence, State, Final, Template-Goal, Rep-VarNames, replace(Beg, End, New)) :-
     compound(Pos),
     Pos \= parentheses_term_position(_, _, _),
-    arg(1, Pos, From),
-    arg(2, Pos, To),
-    subsumes_term(Term, Term0),
-    \+ \+ (   Term = Term0,
-              catch(Goal, _, false)
-          ).
-sweep_match_term(brace_term_position(_, _, Arg), {Term0}, Term, Goal, From, To) :-
-    sweep_match_term(Arg, Term0, Term, Goal, From, To).
-sweep_match_term(list_position(_, _, Elms, _), Term0, Term, Goal, From, To) :-
-    nth0(I, Elms, Elm),
-    nth0(I, Term0, Term1),
-    sweep_match_term(Elm, Term1, Term, Goal, From, To).
-sweep_match_term(list_position(_, _, _, Tail), Term0, Term, Goal, From, To) :-
-    list_tail(Term0, Term1),
-    sweep_match_term(Tail, Term1, Term, Goal, From, To).
-sweep_match_term(term_position(_, _, _, _, SubPos), Term0, Term, Goal, From, To) :-
-    nth1(I, SubPos, Sub),
-    arg(I, Term0, Term1),
-    sweep_match_term(Sub, Term1, Term, Goal, From, To).
-sweep_match_term(dict_position(_, _, _, _, KeyValuePosList), Term0, Term, Goal, From, To) :-
-    member(key_value_position(_, _, _, _, Key, _, ValuePos), KeyValuePosList),
-    get_dict(Key, Term0, Term1),
-    sweep_match_term(ValuePos, Term1, Term, Goal, From, To).
-sweep_match_term(parentheses_term_position(_, _, ContentPos), Term0, Term, Goal, From, To) :-
-    sweep_match_term(ContentPos, Term0, Term, Goal, From, To).
-sweep_match_term(quasi_quotation_position(_, _, SyntaxTerm, SyntaxPos, _), _, Term, Goal, From, To) :-
-    sweep_match_term(SyntaxPos, SyntaxTerm, Term, Goal, From, To).
+    subsumes_term(Template, Term),
+    member(FS, Final),
+    subsumes_term(FS, State),
+    \+ \+ (   Template = Term,
+              catch(Goal, _, false),
+              with_output_to(
+                  string(New0),
+                  (   State = goal(_)
+                  ->  prolog_listing:portray_body(Rep, CurrentIndent, noindent, Precedence, current_output,
+                                                  [module(Module), variable_names(VarNames)])
+                  ;   prolog_listing:pprint(current_output, Rep, Precedence,
+                                            [module(Module), variable_names(VarNames)])
+                  )
+              ),
+              asserta(sweep_match_replacement(New0))
+          ),
+    retract(sweep_match_replacement(New)),
+    arg(1, Pos, Beg),
+    arg(2, Pos, End).
 
 list_tail([_|T0], T) :- nonvar(T0), T0 = [_|_], !, list_tail(T0, T).
 list_tail([_|T], T).
index e0162ea7cc8851b7aa6f3564488c3b617564a4a5..f880150dffa1fdf0914ef4ec9b8c45a834b3b7cc 100644 (file)
@@ -2264,24 +2264,19 @@ Search for Prolog terms matching a given search term in the current
 buffer (@code{sweeprolog-term-search}).
 @end table
 
-@findex sweeprolog-term-search-repeat-forward
-@deffn Command sweeprolog-term-search-repeat-forward
-Repeat the last Term Search, searching forward from point.
-@end deffn
-
-@findex sweeprolog-term-search-repeat-backward
-@deffn Command sweeprolog-term-search-repeat-backward
-Repeat the last Term Search, searching backward from point.
-@end deffn
-
 The command @code{sweeprolog-term-search}, bound by default to
 @kbd{C-c C-s} in Sweep Prolog mode buffers, prompts for a Prolog term
 to search for and finds terms in the current buffer that the search
 term subsumes.  It highlights all matching terms in the buffer and
-moves the cursor to the beginning of the next match after point.  For
+moves the cursor to the end of the next match after point.  For
 example, to find if-then-else constructs in the current buffer do
 @kbd{C-c C-s _ -> _ ; _ @key{RET}}.
 
+This command highlights the current match with the
+@code{sweeprolog-term-search-current} face, and all other matches with
+the @code{sweeprolog-term-search-match} face.  @xref{Face
+Customization,,,emacs,} for information about customizing faces.
+
 While prompting for a search term in the minibuffer, this command
 populates the ``future history'' with the Prolog terms at point, with
 the most nested term at point on top.  Typing @kbd{M-n} once in the
@@ -2291,25 +2286,159 @@ larger terms, up until the top-term at point.  @xref{Minibuffer
 History,,,emacs,}, for more information about minibuffer history
 commands.
 
-If you invoke @code{sweeprolog-term-search} with a prefix argument by
-typing @kbd{C-u C-c C-s}, you can further refine the search with an
-arbitrary Prolog goal for filtering out search results that fail it.
-The given goal runs for each matching term, and it may use variables
-from the search term to refer to the corresponding subterms of the
-matching term.  For example, you can find all places in your code
-where you have a call to @code{sub_string/5} with either the first or
-the last argument being a literal atom by typing @kbd{C-u C-c C-s
+If you invoke @code{sweeprolog-term-search} with a prefix argument
+(@kbd{C-u C-c C-s}), you can further refine the search with an
+arbitrary Prolog goal.  The given goal runs for each matching term,
+and if the goal fails @code{sweeprolog-term-search} disregards the
+corresponding match.  You can use variables from the search term in
+the goal to refer to the corresponding subterms of the matching
+term---for example, you can find all places in your code where you
+have a call to @code{sub_string/5} with either the first or the last
+argument being a literal atom by typing @kbd{C-u C-c C-s
 sub_string(Str, _, _, _, Sub) @key{RET} atom(Str) ; atom(Sub)
 @key{RET}}.
 
+If you call this command with a double prefix argument (@kbd{C-u C-u
+C-c C-s}), it also prompts you to specify the @dfn{class} of term to
+search for.  This can be one of the following symbols:
+
+@table @code
+@item clause
+Matches whole clauses.
+
+@item head
+Matches head terms.
+
+@item goal
+Matches goal terms.
+
+@item data
+Matches data terms.
+
+@item _
+Matches any term.
+@end table
+
+You can specify multiple classes in the minibuffer by delimiting them
+with @samp{,}, in which case @code{sweeprolog-term-search} matches
+terms that match any of the these classes.
+
+If you call @code{sweeprolog-term-search} with a negative prefix
+argument (@kbd{C-- C-c C-s}), it searches backward and moves to
+beginning of the first match that starts before point.
+
 @kindex C-s (Term Search)
 @kindex C-r (Term Search)
-Typing @kbd{C-s} immediately after a successful search invokes the
-command @code{sweeprolog-term-search-repeat-forward} which moves
-forward to the next match.  Likewise, typing @kbd{C-r} after a
-successful term search invokes the command
-@code{sweeprolog-term-search-repeat-backward} which moves backward to
-the previous match.
+@kindex C-m (Term Search)
+@kindex RET (Term Search)
+After invoking @code{sweeprolog-term-search}, use @kbd{C-s} to move to
+the next matching term and @kbd{C-r} to move backward to the previous
+match.  To exit term search, type @kbd{C-m} (or @kbd{@key{RET}}).
+Similarly to Isearch, @code{sweeprolog-term-search} sets the mark to
+the original point so you can easily return to where you were before
+beginning the search.  @xref{Basic Isearch,,,emacs,}.
+
+@node Term Replace
+@section Query Replace Term
+
+@cindex term replace
+@cindex replace term
+@cindex query replace term
+@cindex refactor replace
+@cindex replace, search and
+Sweep includes a powerful search and replace mechanism called
+@dfn{Term Replace}, that allows you to quickly and consistently
+transform some terms across a Prolog buffer.  Term Replace searches
+for terms with the same flexibility and precision of Term Search
+(@pxref{Term Search}), while letting you interactively transform and
+replace matching terms in place.  You can use Term Replace to refactor
+your code in many ways, such as extending a predicate with another
+argument, or replacing all calls to a predicate with another one while
+transposing some of the arguments.  If you're familiar with Emacs's
+Query Replace commands, you can think of Term Replace as a
+Prolog-specific superpowered version of @code{query-replace-regexp}
+(@pxref{Query Replace,,,emacs,}).  To initiate Term Replace, use the
+following command:
+
+@table @kbd
+@kindex C-c C-S
+@findex sweeprolog-query-replace-term
+@item C-c C-S
+Replace some terms after point matching a given template with a given
+replacement (@code{sweeprolog-query-replace-term}).
+@end table
+
+The command @code{sweeprolog-query-replace-term} (bound to @kbd{C-c
+C-S}) prompts for two Prolog terms, the @dfn{template term} and the
+@dfn{replacement term}, and then asks for each term in the buffer that
+matches the template if you want to replace it.  You can use variables
+in the template term to capture sub-terms of the matching term, and
+use them in the replacement term.  For example, if you want to
+transpose the two arguments in a bunch of calls to @code{=/2}, you can
+specify @code{R=L} as the template term and @code{L=R} as the
+replacement.
+
+This command uses the same underlying term search as @kbd{C-c C-s}
+(@code{sweeprolog-term-search}) does for finding matches, expect that
+@code{sweeprolog-query-replace-term} only searches from point to the
+end of buffer.  If you invoke @code{sweeprolog-query-replace-term}
+with an active region, it limits the operation to matching terms in
+the region.  @code{sweeprolog-query-replace-term} highlights the
+current match with the @code{sweeprolog-query-replace-term-current}
+face, and all other matches with the
+@code{sweeprolog-query-replace-term-match} face.  By default, these
+faces inherit from @code{sweeprolog-term-search-current} and
+@code{sweeprolog-term-search-match}, respectively.  Furthermore,
+similarly to @kbd{C-c C-s}, you can invoke
+@code{sweeprolog-query-replace-term} with a prefix argument to refine
+the search with an arbitrary Prolog goal that matching terms must
+satisfy, or with two prefix arguments to target only terms in certain
+contexts.  @xref{Term Search} for full details about prefix arguments
+and search refinement.
+
+@code{sweeprolog-query-replace-term} goes over the matching terms in
+turn and asks you what to do with each.  The available answers are:
+
+@table @kbd
+@item y
+Replace the current match and move to the next one.
+
+@item n
+Skip the current match without replacing it.
+
+@item t
+Replace the current match, show the result, and suggest reverting back
+before moving to the next match.
+
+@item q
+Quit without replacing the current match.
+
+@item .
+Replace the current match, and exit right away asking about further
+matches.
+
+@item !
+Replace the current match and all remaining matches without asking.
+
+@item e
+Edit the replacement term for the current match in the minibuffer, and
+then perform the replacement.
+
+@item C-r
+Enter recursive edit.  This allows you to pause the current Term
+Replace session, perform some edits, or otherwise use Emacs however
+you please, an then resume Term Replace from the same point by typing
+@kbd{C-M-c}.  @xref{Recursive Edit,,,emacs,}.
+@end table
+
+If you include a new variable in the replacement term
+that does not appear in the template term,
+@code{sweeprolog-query-replace-term} uses that variable as-is in each
+replacement, expect if the matching term happens to contain a variable
+with that name already, in which case this command adds the suffix
+@samp{Fresh} to the name of the new variable from the replacement.
+Including a new variable in the replacement term is useful, for
+example, for introducing a new argument to a predicate.
 
 @node Context Menu
 @section Context Menu
@@ -2372,10 +2501,11 @@ Rename a variable across the topmost Prolog term at point
 @end table
 
 @defopt sweeprolog-rename-variable-allow-existing
-If non-nil, allow selecting an existing variable name as the new name
-of a variable being renamed with @code{sweeprolog-rename-variable}.
-If it is the symbol @code{confirm}, allow but ask for confirmation
-first.  Defaults to @code{confirm}.
+If non-@code{nil}, allow selecting an existing variable name as the
+new name of a variable being renamed with
+@code{sweeprolog-rename-variable}.  If it is the symbol
+@code{confirm}, allow but ask for confirmation first.  Defaults to
+@code{confirm}.
 @end defopt
 
 The command @code{sweeprolog-rename-variable}, bound to @kbd{C-c C-r},
@@ -2643,9 +2773,10 @@ customizing the user option @code{sweeprolog-top-level-use-pty} to
 @code{nil}.
 
 @defopt sweeprolog-top-level-use-pty
-Whether to use pty for top-level communication.  If this is non-nil,
-Sweep top-level buffers communicate with their top-level threads via a
-pty, otherwise they use a local TCP connection.
+Whether to use pty for top-level communication.  If this is
+non-@code{nil}, Sweep top-level buffers communicate with their
+top-level threads via a pty, otherwise they use a local TCP
+connection.
 @end defopt
 
 @code{sweeprolog-top-level-use-pty} is on by default on systems where
index 5d0bca9100ecc8ee8e5760342b3cc3abed5aacb5..09f711fb864bdee123adff7b6208e00779a9afab 100644 (file)
@@ -113,7 +113,7 @@ foo(Foo, Bar) :- flatten(Bar, Baz), member(Foo, Baz).
                         (help-at-pt-kbd-string))))
 
 (sweeprolog-deftest terms-at-point ()
-  "Test `sweeprolog-term-search'."
+  "Test `sweeprolog-terms-at-point'."
   "
 recursive(Var) :-
     (   true
@@ -148,16 +148,21 @@ bar(bar(bar), bar{bar:bar}, [bar,bar|bar]).
 foo([Bar|Baz]).
 "
   (goto-char (point-min))
-  (sweeprolog-term-search "bar")
-  (should (= (point) 10))
-  (sweeprolog-term-search "bar")
-  (should (= (point) 24))
-  (sweeprolog-term-search "bar")
-  (should (= (point) 31))
-  (sweeprolog-term-search "bar")
-  (should (= (point) 35))
-  (sweeprolog-term-search "bar")
-  (should (= (point) 39)))
+  (let ((unread-command-events (listify-key-sequence (kbd "RET"))))
+    (sweeprolog-term-search "bar"))
+  (should (= (point) 13))
+  (let ((unread-command-events (listify-key-sequence (kbd "RET"))))
+    (sweeprolog-term-search "bar"))
+  (should (= (point) 27))
+  (let ((unread-command-events (listify-key-sequence (kbd "RET"))))
+    (sweeprolog-term-search "bar"))
+  (should (= (point) 34))
+  (let ((unread-command-events (listify-key-sequence (kbd "RET"))))
+    (sweeprolog-term-search "bar"))
+  (should (= (point) 38))
+  (let ((unread-command-events (listify-key-sequence (kbd "RET"))))
+    (sweeprolog-term-search "bar"))
+  (should (= (point) 42)))
 
 (sweeprolog-deftest beginning-of-next-top-term-header ()
   "Test finding the beginning of the first top term."
index 88173793cb5a799b37729b8901f0e2b4c71f1da9..19d89cc10b0310d86b9c772d83d4ac596ee81aaf 100644 (file)
@@ -460,6 +460,7 @@ pack completion candidates."
   "C-c C-o" #'sweeprolog-find-file-at-point
   "C-c C-q" #'sweeprolog-top-level-send-goal
   "C-c C-r" #'sweeprolog-rename-variable
+  "C-c C-S-s" #'sweeprolog-query-replace-term
   "C-c C-s" #'sweeprolog-term-search
   "C-c C-t" #'sweeprolog-top-level
   "C-c C-u" #'sweeprolog-update-dependencies
@@ -535,13 +536,6 @@ pack completion candidates."
   "C-c C-b" #'sweeprolog-top-level-example-display-source
   "C-c C-q" #'sweeprolog-top-level-example-done)
 
-(defvar-keymap sweeprolog-term-search-map
-  :doc "Transient keymap activated after `sweeprolog-term-search'."
-  "C-g" #'sweeprolog-term-search-abort
-  "C-m" #'sweeprolog-term-search-delete-overlays
-  "C-r" #'sweeprolog-term-search-repeat-backward
-  "C-s" #'sweeprolog-term-search-repeat-forward)
-
 (defvar-keymap sweeprolog-read-term-map
   :doc "Keymap used by `sweeprolog-read-term'."
   :parent minibuffer-local-map
@@ -2318,6 +2312,26 @@ inside a comment, string or quoted atom."
   "Face for highlighting Prolog breakpoints."
   :group 'sweeprolog-faces)
 
+(defface sweeprolog-term-search-match
+  '((t :inherit lazy-highlight))
+  "Face for highlighting term search matches."
+  :group 'sweeprolog-faces)
+
+(defface sweeprolog-term-search-current
+  '((t :inherit isearch))
+  "Face for highlighting the current term search match."
+  :group 'sweeprolog-faces)
+
+(defface sweeprolog-query-replace-term-match
+  '((t :inherit sweeprolog-term-search-match))
+  "Face for highlighting term replacement matches."
+  :group 'sweeprolog-faces)
+
+(defface sweeprolog-query-replace-term-current
+  '((t :inherit sweeprolog-term-search-current))
+  "Face for highlighting the current term replacement match."
+  :group 'sweeprolog-faces)
+
 ;;;; Font-lock
 
 (defun sweeprolog-analyze-start-font-lock (beg end)
@@ -4349,7 +4363,7 @@ work."
                      (buffer-substring-no-properties obeg oend))))
          (if (> opre pre)
              (signal 'scan-error
-                     (list (format "Cannot scan backwards infix operator of higher precedence %s." opre)
+                     (list (format "Cannot scan backwards beyond infix operator of higher precedence %s." opre)
                            obeg
                            oend))
            (goto-char oend)
@@ -4459,17 +4473,14 @@ work."
 
 (defun sweeprolog--backward-sexp ()
   (let ((point (point))
-        (prec (pcase (sweeprolog-last-token-boundaries)
-                (`(operator ,obeg ,oend)
-                 (unless (and nil
-                              (string= "." (buffer-substring-no-properties obeg oend))
-                              (member (char-syntax (char-after (1+ obeg))) '(?> ? )))
-                   (if-let ((pprec
-                             (sweeprolog-op-infix-precedence
-                              (buffer-substring-no-properties obeg oend))))
-                       (progn (goto-char obeg) (1- pprec))
-                     0)))
-                (_ 0))))
+        (prec (or (pcase (sweeprolog-last-token-boundaries)
+                    (`(,(or 'operator 'symbol) ,obeg ,oend)
+                     (when-let ((pprec
+                                 (sweeprolog-op-infix-precedence
+                                  (buffer-substring-no-properties obeg oend))))
+                       (goto-char obeg)
+                       (1- pprec))))
+                  0)))
     (condition-case error
         (sweeprolog--backward-term prec)
       (scan-error (when (= point (point))
@@ -4477,17 +4488,14 @@ work."
 
 (defun sweeprolog--forward-sexp ()
   (let ((point (point))
-        (prec (pcase (sweeprolog-next-token-boundaries)
-                (`(operator ,obeg ,oend)
-                 (unless (and nil
-                              (string= "." (buffer-substring-no-properties obeg oend))
-                              (member (char-syntax (char-after (1+ obeg))) '(?> ? )))
-                   (if-let ((pprec
-                             (sweeprolog-op-infix-precedence
-                              (buffer-substring-no-properties obeg oend))))
-                       (progn (goto-char oend) (1- pprec))
-                     0)))
-                (_ 0))))
+        (prec (or (pcase (sweeprolog-next-token-boundaries)
+                    (`(,(or 'operator 'symbol) ,obeg ,oend)
+                     (when-let ((pprec
+                                 (sweeprolog-op-infix-precedence
+                                  (buffer-substring-no-properties obeg oend))))
+                       (goto-char oend)
+                       (1- pprec))))
+                  0)))
     (condition-case error
         (sweeprolog--forward-term prec)
       (scan-error (when (= point (point))
@@ -5594,63 +5602,6 @@ properly."
 
 ;;;; Term Search
 
-(defvar sweeprolog-term-search-last-search nil
-  "Last term searched with `sweeprolog-term-search'.")
-
-(defvar-local sweeprolog-term-search-overlays nil
-  "List of `sweeprolog-term-search' overlays in the current buffer.")
-
-(defvar-local sweeprolog-term-search-repeat-count 0)
-
-(defun sweeprolog-term-search-delete-overlays ()
-  "Delete overlays created by `sweeprolog-term-search'."
-  (interactive "" sweeprolog-mode)
-  (mapc #'delete-overlay sweeprolog-term-search-overlays)
-  (setq sweeprolog-term-search-overlays nil))
-
-(defun sweeprolog-term-search-repeat-forward ()
-  "Repeat last `sweeprolog-term-search' searching forward from point."
-  (interactive "" sweeprolog-mode)
-  (setq sweeprolog-term-search-repeat-count
-        (mod (1+ sweeprolog-term-search-repeat-count)
-             (length sweeprolog-term-search-overlays)))
-  (sweeprolog-term-search (car sweeprolog-term-search-last-search)
-                          (cdr sweeprolog-term-search-last-search)))
-
-(defun sweeprolog-term-search-repeat-backward ()
-  "Repeat last `sweeprolog-term-search' searching backward from point."
-  (interactive "" sweeprolog-mode)
-  (setq sweeprolog-term-search-repeat-count
-        (mod (1- sweeprolog-term-search-repeat-count)
-             (length sweeprolog-term-search-overlays)))
-  (sweeprolog-term-search (car sweeprolog-term-search-last-search)
-                          (cdr sweeprolog-term-search-last-search) t))
-
-(defun sweeprolog-term-search-abort ()
-  "Abort term search and restore point to its original position."
-  (interactive "" sweeprolog-mode)
-  (goto-char (mark t))
-  (pop-mark)
-  (sweeprolog-term-search-delete-overlays)
-  (signal 'quit nil))
-
-(defun sweeprolog-term-search-in-buffer (term &optional goal buffer)
-  "Search for Prolog term TERM satisfying GOAL in buffer BUFFER.
-
-Return a list of (BEG . END) cons cells where BEG is the buffer
-position of the beginning of a matching term and END is its
-corresponding end position."
-  (setq goal   (or goal "true"))
-  (setq buffer (or buffer (current-buffer)))
-  (with-current-buffer buffer
-    (let ((offset (point-min)))
-      (mapcar (lambda (match)
-                (cons (+ offset (car match))
-                      (+ offset (cdr match))))
-              (sweeprolog--query-once "sweep" "sweep_term_search"
-                                      (list buffer-file-name
-                                            term goal))))))
-
 (defun sweeprolog-read-term-try ()
   "Try to read a Prolog term in the minibuffer.
 
@@ -5725,10 +5676,13 @@ moving point."
 (defvar sweeprolog-read-goal-history nil
   "History list for `sweeprolog-read-goal'.")
 
-(defun sweeprolog-read-term (&optional prompt)
-  "Read a Prolog term prompting with PROMPT (default \"?- \")."
+(defun sweeprolog-read-term (&optional prompt initial)
+  "Prompt for a Prolog term with PROMPT (defaults to \"?- \").
+
+If INITIAL is non-nil, use it as the initial contents of the
+minibuffer."
   (setq prompt (or prompt "?- "))
-  (read-from-minibuffer prompt nil
+  (read-from-minibuffer prompt initial
                         sweeprolog-read-term-map nil
                         'sweeprolog-read-term-history
                         (when (derived-mode-p 'sweeprolog-mode)
@@ -5747,90 +5701,324 @@ moving point."
                           (when (derived-mode-p 'sweeprolog-mode)
                             (sweeprolog-goals-at-point)))))
 
-(defun sweeprolog-term-search-next (point overlays backward)
-  "Return first overlay in OVERLAYS starting after POINT.
-If no overlay starts after POINT, return the first overlay.
-
-If BACKWARD is non-nil, return last overlay ending before POINT
-instead, or the last overlay if no overlay ends before POINT."
-  (if backward
-      (let* ((match nil)
-             (reversed (reverse overlays))
-             (first (car reversed)))
-        (while (and reversed (not match))
-          (let ((head (car reversed))
-                (tail (cdr reversed)))
-            (if (< (overlay-start head) point)
-                (setq match head)
-              (setq reversed tail))))
-        (or match first))
-    (let ((match nil)
-          (first (car overlays)))
-      (while (and overlays (not match))
-        (let ((head (car overlays))
-              (tail (cdr overlays)))
-          (if (< point (overlay-start head))
-              (setq match head)
-            (setq overlays tail))))
-      (or match first))))
-
-(defun sweeprolog-term-search (term &optional goal backward interactive)
-  "Search forward for Prolog term TERM in the current buffer.
-
-Optional argument GOAL is a goal that matching terms must
-satisfy, it may refer to variables occuring in TERM.
-
-If BACKWARD is non-nil, search backward instead.
+(defun sweeprolog-term-replace-edits (file template replacement condition classes)
+  (let ((state (mapcar (lambda (class)
+                         (pcase class
+                           ('goal "goal(_)")
+                           (_ (symbol-name class))))
+                       classes)))
+    (mapcar
+     (pcase-lambda (`(compound "replace" ,beg ,end ,rep))
+       (list (1+ beg) (1+ end) rep))
+     (without-restriction
+       (sweeprolog--query-once "sweep" "sweep_term_replace"
+                               (list file
+                                     sweeprolog-indent-offset
+                                     template
+                                     condition
+                                     state
+                                     replacement))))))
 
-If INTERACTIVE is non-nil, as it is when called interactively,
-push the current position to the mark ring before moving point.
-
-When called interactively with a prefix argument, prompt for
-GOAL."
-  (interactive (let* ((term (sweeprolog-read-term "[Term-search] ?- "))
-                      (goal (if current-prefix-arg
-                                (sweeprolog-read-goal
-                                 (concat "[Term-search goal for "
-                                         term
-                                         "] ?- "))
-                              "true")))
-                 (list term goal nil t))
+;;;###autoload
+(defun sweeprolog-term-search (template &optional backward condition class)
+  "Search for terms matching TEMPLATE.
+
+If BACKWARD is non-nil, search backward from point, otherwise
+search forward.
+
+CONDITION is a Prolog goal that this commands runs for each
+matching term.  If the goal fails this command disregards the
+corresponding match.  CONDITION can share variables with
+TEMPLATE, in which case this commands unifies these sharing
+variables with the corresponding subterms of the matching term.
+If CONDITION is omitted or nil, it defaults to \"true\".
+
+CLASS is the class of terms to target, it can be one of `clause',
+`head', `goal', `data' and `_'.  `clause' only matches whole
+clauses, `head' only matches head terms, `goal' only matches goal
+terms, `data' only matches data terms, and `_' matches any term.
+CLASS can also be a list of one or more of these symbols, in
+which a term matches if it matches any of the classes in CLASS.
+If CLASS is omitted or nil, it defaults to `_'.
+
+Interactively, prompt for TEMPLATE.  With a prefix argument
+\\[universal-argument], prompt for CONDITION.  With a double
+prefix argument \\[universal-argument] \\[universal-argument],
+prompt for CLASS as well.  A negative prefix argument
+\\[negative-argument] searches backward from point."
+  (interactive (let* ((template (sweeprolog-read-term "[Search] ?- "))
+                      (condition (when (member current-prefix-arg
+                                               '((4)  (-4)
+                                                 (16) (-16)))
+                                   (sweeprolog-read-goal
+                                    (concat "[Condition for matching "
+                                            template
+                                            "] ?- "))))
+                      (class (when (member current-prefix-arg
+                                           '((16) (-16)))
+                               (mapcar #'intern
+                                       (completing-read-multiple
+                                        (format-prompt "Replace terms of class" "_")
+                                        '("clause" "head" "goal" "data" "_")
+                                        nil t nil nil "_")))))
+                 (list template
+                       (and current-prefix-arg
+                            (< (prefix-numeric-value current-prefix-arg) 0))
+                       condition class))
                sweeprolog-mode)
-  (when interactive
-    (setq sweeprolog-term-search-repeat-count 0))
-  (sweeprolog-term-search-delete-overlays)
-  (setq sweeprolog-term-search-last-search (cons term goal))
-  (let ((matches (sweeprolog-term-search-in-buffer term goal)))
-    (if (not matches)
-        (message "No matching term found.")
-      (setq sweeprolog-term-search-overlays
-            (mapcar (lambda (match)
-                      (let* ((beg (car match))
-                             (end (cdr match))
-                             (overlay (make-overlay beg end)))
-                        (overlay-put overlay 'face 'lazy-highlight)
-                        (overlay-put overlay 'evaporate t)
-                        overlay))
-                    matches))
-      (let ((next
-             (sweeprolog-term-search-next
-              (point) sweeprolog-term-search-overlays backward)))
-        (overlay-put next 'face 'isearch)
-        (when interactive
-          (push-mark (point) t))
-        (goto-char (overlay-start next)))
-      (set-transient-map sweeprolog-term-search-map t
-                         #'sweeprolog-term-search-delete-overlays)
-      (message
-       (substitute-command-keys
-        (concat
-         "Match "
-         (number-to-string (1+ sweeprolog-term-search-repeat-count))
-         "/"
-         (number-to-string (length matches)) ".  "
-         "\\<sweeprolog-term-search-map>"
-         "\\[sweeprolog-term-search-repeat-forward] for next match, "
-         "\\[sweeprolog-term-search-repeat-backward] for previous match."))))))
+  (setq condition (or condition           "true")
+        class     (or (ensure-list class) '(_)))
+  (let* ((items
+          (seq-filter
+           (pcase-lambda (`(,beg ,end ,_))
+             (<= (point-min) beg end (point-max)))
+           (sweeprolog-term-replace-edits (buffer-file-name) template
+                                          "true" condition class)))
+         (groups
+          (seq-group-by (pcase-lambda (`(,beg ,end ,_)) (< beg (point)))
+                        items))
+         (after-point (alist-get nil groups))
+         (before-point (alist-get t groups))
+         (overlays
+          (mapcar
+           (pcase-lambda (`(,beg ,end ,_))
+             (let ((overlay (make-overlay beg end)))
+               (overlay-put overlay 'face 'sweeprolog-term-search-match)
+               overlay))
+           (append after-point before-point)))
+         (length (length items))
+         (index (if backward (1- length) 0)))
+    (push-mark (point) t)
+    (when backward
+      (setq overlays (reverse overlays)))
+    (let ((go t)
+          (inhibit-quit t)
+          (query-replace-map
+           (let ((map (make-sparse-keymap)))
+             (define-key map "\C-l" 'recenter)
+             (define-key map "\C-v" 'scroll-up)
+             (define-key map "\M-v" 'scroll-down)
+             (define-key map [next] 'scroll-up)
+             (define-key map [prior] 'scroll-down)
+             (define-key map [?\C-\M-v] 'scroll-other-window)
+             (define-key map [M-next] 'scroll-other-window)
+             (define-key map [?\C-\M-\S-v] 'scroll-other-window-down)
+             (define-key map [M-prior] 'scroll-other-window-down)
+             map)))
+      (if overlays
+          (while go
+            (unless
+                (with-local-quit
+                  (let* ((overlay (car overlays))
+                         (pos (if backward
+                                  (overlay-start overlay)
+                                (overlay-end overlay))))
+                    (overlay-put overlay 'priority 100)
+                    (overlay-put overlay 'face 'sweeprolog-term-search-current)
+                    (goto-char pos)
+                    (pcase
+                        (car (read-multiple-choice
+                              ;; TODO - maybe indicate a wrapped
+                              ;; search like Isearch does?
+                              (format "Match %d/%d"
+                                      (1+ index)
+                                      length)
+                              '((?\13  "next" "Next match")
+                                (?\12  "back" "Last match")
+                                (?\r  "exit" "Exit term search"))))
+                      (?\13
+                       (setq overlays (if backward
+                                          (cons overlay
+                                                (reverse (cdr overlays)))
+                                        (append (cdr overlays)
+                                                (list overlay)))
+                             index (if backward
+                                       index
+                                     (mod (1+ index) length))
+                             backward nil))
+                      (?\12
+                       (setq overlays (if backward
+                                          (append (cdr overlays)
+                                                  (list overlay))
+                                        (cons overlay
+                                              (reverse (cdr overlays))))
+                             index (if backward
+                                       (mod (1- index) length)
+                                     index)
+                             backward t))
+                      (?\r
+                       (setq go nil)
+                       t))
+                    (overlay-put overlay 'priority nil)
+                    (overlay-put overlay 'face 'sweeprolog-term-search-match)))
+              (goto-char (mark t))
+              (pop-mark)
+              (setq go nil)))
+        (message "No matching term found."))
+      (mapc #'delete-overlay overlays))))
+
+;;;###autoload
+(defun sweeprolog-query-replace-term (template replacement &optional condition class)
+  "Replace some terms after point matching TEMPLATE with REPLACEMENT.
+
+When the region is active, replace matching terms in region.
+
+Query before performing each replacement.
+
+Matching terms are those that the Prolog term TEMPLATE (given as
+a string) subsumes.  REPLACEMENT is a Prolog term to insert in
+place of matching terms.  REPLACEMENT can share variables with
+TEMPLATE, in which case this commands unifies these sharing
+variables with the corresponding subterms of the matching term.
+
+CONDITION is a Prolog goal that this commands runs for each
+matching term.  If the goal fails this command disregards the
+corresponding match and does not suggest replacing it.  CONDITION
+can share variables with TEMPLATE, similarly to REPLACEMENT.  If
+CONDITION is omitted or nil, it defaults to \"true\".
+
+CLASS is the class of terms to target, it can be one of `clause',
+`head', `goal', `data' and `_'.  `clause' only matches whole
+clauses, `head' only matches head terms, `goal' only matches goal
+terms, `data' only matches data terms, and `_' matches any term.
+CLASS can also be a list of one or more of these symbols, in
+which a term matches if it matches any of the classes in CLASS.
+If CLASS is omitted or nil, it defaults to `_'.
+
+Interactively, prompt for TEMPLATE and REPLACEMENT.  With a
+prefix argument \\[universal-argument], prompt for CONDITION.
+With a double prefix argument \\[universal-argument] \\[universal-argument],
+prompt for CLASS as well."
+  (interactive (let* ((template (sweeprolog-read-term "[Replace] ?- "))
+                      (replacement
+                       (sweeprolog-read-term
+                        (concat "[Replace " template " with] ?- ")))
+                      (condition (when current-prefix-arg
+                                   (sweeprolog-read-goal
+                                    (concat "[Condition for replacing "
+                                            template
+                                            "] ?- "))))
+                      (class (when (equal current-prefix-arg '(16))
+                               (mapcar #'intern
+                                       (completing-read-multiple
+                                        (format-prompt "Replace terms of class" "_")
+                                        '("clause" "head" "goal" "data" "_")
+                                        nil t nil nil "_")))))
+                 (list template replacement condition class))
+               sweeprolog-mode)
+  (setq condition (or condition           "true")
+        class     (or (ensure-list class) '(_)))
+  (let* ((bounds-beg (or (use-region-beginning) (point)))
+         (bounds-end (or (use-region-end) (point-max)))
+         (items
+          (seq-filter
+           (pcase-lambda (`(,beg ,end ,_))
+             (<= bounds-beg beg end bounds-end))
+           (sweeprolog-term-replace-edits (buffer-file-name) template
+                                          replacement condition class)))
+         (overlays
+          (mapcar
+           (pcase-lambda (`(,beg ,end ,rep))
+             (let ((overlay (make-overlay beg end)))
+               (overlay-put overlay 'face 'sweeprolog-query-replace-term-match)
+               (overlay-put overlay 'sweeprolog-term-replacement rep)
+               overlay))
+           items))
+         (count 0)
+         (last nil)
+         (try nil))
+    (cl-labels ((replace (b e r i)
+                  (undo-boundary)
+                  (combine-after-change-calls
+                    (delete-region b e)
+                    (insert r)
+                    ;; TODO - turn fresh variables in rep into holes
+                    (let ((inhibit-message t))
+                      (indent-region-line-by-line b (point))))
+                  (cl-incf count (if i -1 1))))
+      (if (use-region-p)
+          (deactivate-mark)
+        (push-mark (point) t))
+      (let ((inhibit-quit t))
+        (with-local-quit
+          (while overlays
+            (let* ((overlay (car overlays))
+                   (start (overlay-start overlay))
+                   (end (overlay-end overlay))
+                   (cur (buffer-substring start end))
+                   (rep (overlay-get overlay 'sweeprolog-term-replacement))
+                   (max-mini-window-height 0.5))
+              (overlay-put overlay 'priority 100)
+              (overlay-put overlay 'face 'sweeprolog-query-replace-term-current)
+              (goto-char start)
+              (setq last overlay)
+              (setq overlays (cdr overlays))
+              (pcase
+                  (car (read-multiple-choice
+                        (mapconcat #'identity
+                                   (list "Replace" (propertize cur 'face 'diff-removed)
+                                         (if try "back to" "with")
+                                         (propertize rep 'face 'diff-added)
+                                         "?")
+                                   (if (or (string-search "\n" cur)
+                                           (string-search "\n" rep))
+                                       "\n"
+                                     " "))
+                        '((?y  "yes"                       "Replace occurrence")
+                          (?n  "no"                        "Skip occurrence")
+                          (?t  "try"                       "Replace and suggest reverting back")
+                          (?q  "quit"                      "Quit")
+                          (?.  "last"                      "Replace and exit")
+                          (?!  "all"                       "Replace all remaining occurrences")
+                          (?e  "edit"                      "Edit replacement"))))
+                (?y
+                 (replace start end rep try)
+                 (setq try nil))
+                (?n
+                 (setq try nil))
+                (?t
+                 (replace start end rep try)
+                 (setq try (not try))
+                 (let ((rev (make-overlay start (point))))
+                   (overlay-put rev 'face 'sweeprolog-query-replace-term-match)
+                   (overlay-put rev 'sweeprolog-term-replacement cur)
+                   (setq overlays (cons rev overlays))))
+                (?q
+                 (mapc #'delete-overlay overlays)
+                 (setq overlays nil))
+                (?.
+                 (replace start end rep try)
+                 (mapc #'delete-overlay overlays)
+                 (setq overlays nil))
+                (?!
+                 (replace start end rep try)
+                 (while overlays
+                   (let* ((ov (car overlays))
+                          (ov-start (overlay-start ov))
+                          (ov-end (overlay-end ov))
+                          (ov-rep (overlay-get ov 'sweeprolog-term-replacement)))
+                     (goto-char ov-start)
+                     (delete-region ov-start ov-end)
+                     (insert ov-rep)
+                     (indent-region-line-by-line start (point))
+                     (cl-incf count)
+                     (delete-overlay ov)
+                     (setq overlays (cdr overlays)))))
+                (?e
+                 (replace start end
+                          (sweeprolog-read-term "[Edit replacement] ?- "
+                                                rep)
+                          try)))
+              (delete-overlay overlay)
+              (setq last nil))))
+        (mapc #'delete-overlay overlays)
+        (when last (delete-overlay last)))
+      (message (concat "Replaced %d "
+                       (ngettext "occurrence"
+                                 "occurrences"
+                                 count)
+                       " of %s.")
+               count template))))
 
 
 ;;;; Right-Click Context Menu