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,
),
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).
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
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
@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},
@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
(help-at-pt-kbd-string))))
(sweeprolog-deftest terms-at-point ()
- "Test `sweeprolog-term-search'."
+ "Test `sweeprolog-terms-at-point'."
"
recursive(Var) :-
( true
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."
"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
"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
"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)
(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)
(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))
(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))
;;;; 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.
(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)
(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