From 1effbd3c95edbb3dbfe4131f86d0f8813835e3ab Mon Sep 17 00:00:00 2001 From: Eshel Yaron Date: Wed, 20 Sep 2023 13:00:00 +0200 Subject: [PATCH] ADDED: new command 'sweeprolog-query-replace-term' 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 | 261 ++++++++++++++++++---- sweep.texi | 195 +++++++++++++--- sweeprolog-tests.el | 27 ++- sweeprolog.el | 532 ++++++++++++++++++++++++++++++-------------- 4 files changed, 755 insertions(+), 260 deletions(-) diff --git a/sweep.pl b/sweep.pl index dd29e62..c62aff6 100644 --- 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). diff --git a/sweep.texi b/sweep.texi index e0162ea..f880150 100644 --- a/sweep.texi +++ b/sweep.texi @@ -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 diff --git a/sweeprolog-tests.el b/sweeprolog-tests.el index 5d0bca9..09f711f 100644 --- a/sweeprolog-tests.el +++ b/sweeprolog-tests.el @@ -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." diff --git a/sweeprolog.el b/sweeprolog.el index 8817379..19d89cc 100644 --- a/sweeprolog.el +++ b/sweeprolog.el @@ -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-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) + '((? "next" "Next match") + (? "back" "Last match") + (? "exit" "Exit term search")))) + (? + (setq overlays (if backward + (cons overlay + (reverse (cdr overlays))) + (append (cdr overlays) + (list overlay))) + index (if backward + index + (mod (1+ index) length)) + backward nil)) + (? + (setq overlays (if backward + (append (cdr overlays) + (list overlay)) + (cons overlay + (reverse (cdr overlays)))) + index (if backward + (mod (1- index) length) + index) + backward t)) + (? + (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 -- 2.39.5