3

Is there a way to make some regions of the buffer invisible (I mean not evaluated by) to query-replace and similar commands?

Let me better explain with an example. Let's take a piece of LaTeX code

... in the following equation ,
\begin{equation}
x + y = z ,
\end{equation}

I need to replace the first " ," with "," (deleting the extra space) but the second " ," string, in the equation ambient it's not a problem and I'd like my query-replace to ignore it.

I figure out I could use a loop to assign some text properties to the regions I need to ignore but I've got no idea if that kind of text properties exist.

I'm also open to any kind of suggestion.

Workaround. Following Drew's answer I wrote this code that seems to work:

(defun skip-if-my-ignore (beg end)
  "Return nil iff some text BEG to END has non-`nil' property `my-ignore'."
  (catch 'skip-if-my-ignore
    (let ((pos  beg))
      (while (< pos end)
        (when (get-text-property pos 'my-ignore) (throw 'skip-if-my-ignore nil))
        (setq pos  (1+ pos))))
    t))

(defun query-replace-skipping-math-env ()
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (while (search-forward-regexp "\\\\begin{equation}" nil t)
      (save-excursion
        (let ((b (copy-marker (match-beginning 0)))
              (e (copy-marker (search-forward-regexp "\\\\end{equation}" nil t ))))    
          (put-text-property b e 'my-ignore t))))
        (let ((isearch-filter-predicate  'skip-if-my-ignore))
          (perform-replace " ," "," t nil nil 1 nil (point-min) (point-max)))
        (remove-list-of-text-properties (point-min) (point-max) '(my-ignore))))

I tested it on:

... in the following equation ,
\begin{equation}
x + y = z ,
\end{equation}

... also in the following equation ,
\begin{equation}
2x + 2y = 2z ,
\end{equation}
Gabriele Nicolardi
  • 1,199
  • 8
  • 17
  • Not sure if I understand your question., `query-replace` already lets you decide whether to replace each one, or do you think it's inconvenient? – xuchunyang Jan 21 '19 at 06:12
  • @xuchunyang I want `query-replace` to ignore some portions of the text. I need it to save time, avoiding to skip manually some queries. – Gabriele Nicolardi Jan 21 '19 at 11:22

2 Answers2

3

Yes. Set variable isearch-filter-predicate to a function that returns nil for the text that you want to ignore for search and query-replace.

(Set it back to its default value of isearch-filter-visible when you're done. Alternatively, define your own search or query-replace function that binds the variable and then invokes regular Isearch or query-replace. That way it will be restored when your function is done.)

For example, you can put a text property my-ignore on some text, giving it a non-nil value. Then this function returns nil for what would normally be search hits on such text, so they are ignored for search and query-replace:

(defun skip-if-my-ignore (beg end)
  "Return nil iff some text BEG to END has non-`nil' property `my-ignore'."
  (catch 'skip-if-my-ignore
    (let ((pos  beg))
      (while (< pos end)
        (when (get-text-property pos 'my-ignore) (throw 'skip-if-my-ignore nil))
        (setq pos  (1+ pos))))
    t))

;; Make Isearch and query-replace ignore text with non-`nil' property `my-ignore'.
(setq isearch-filter-predicate  'skip-if-my-ignore)

But if you know how to identify the text you want to ignore, in order to put a text property on those parts, then you could just put that identification code into the filter predicate and not bother adding the text property. IOW, you can let the predicate check directly whether the text from BEG to END should be ignored.


There unfortunately is no Emacs doc for using isearch-filter-predicate in the Elisp manual (or the Emacs manual). But the doc string of that variable at least tells you this:

isearch-filter-predicate is a variable defined in isearch.el.

Its value is isearch-filter-visible

This variable can be risky when used as a file-local variable.

Documentation:

Predicate that filters the search hits that would normally be available.

Search hits that dissatisfy the predicate are skipped. The function has two arguments: the positions of start and end of text matched by the search. If this function returns nil, continue searching without stopping at this match.

If you use add-function to modify this variable, you can use the isearch-message-prefix advice property to specify the prefix string displayed in the search message.

That tells you (a bit obscurely) that a potential search hit is excluded (ignored) if the predicate returns nil.

BEG and END are the limits of the current (potential) search hit. The potential hit is excluded (ignored) if the predicate returns nil. In our case we presumably want it to return nil if any of the text in the potential hit has the text property.

Drew
  • 75,699
  • 9
  • 109
  • 225
  • Honestly I did not completely understand your anwer (I mean the `(beg end)` stuff) but I tried to apply it in my workaround. I updated my question with the code I wrote. It seems to work. Could you please confirm I've done it the right way? – Gabriele Nicolardi Jan 20 '19 at 22:23
  • Yes, looks good. I added the doc string of `isearch-filter-predicate`, which explains that args `BEG` and `END` of the function are the limits of the current (potential) search hit. The potential hit is *excluded* (ignored) if the predicate returns `nil`. In our case we presumably want it to return `nil` if *any* of the text in the potential hit has the text property. – Drew Jan 20 '19 at 23:16
  • I found that the `read-only` property combined with the `query-replace-skip-read-only` variable also works for this purpose. – Gabriele Nicolardi Feb 26 '19 at 23:35
  • You can provide that as an additional answer. Comments can be deleted at any time. Having that alternative answer can help readers who have the same question. (You can also accept your own answer if you prefer it. You can change which is the accepted answer at any time.) – Drew Feb 26 '19 at 23:51
  • Comment from Matteo Gamboz: "This should be a comment to @Drew answer but I don't have enough reputation to comment... You might want to use `text-property-any` in the predicate function. `(text-property-any START END PROPERTY VALUE &optional OBJECT)` Check text from `START` to `END` for property `PROPERTY` equaling `VALUE`. If so, return the position of the first character whose property `PROPERTY` is `eq` to `VALUE`. Otherwise return `nil`." – Drew Oct 04 '19 at 13:58
  • @MatteoGamboz: `text-property-any` and `text-property-not-all` check for `PROPERTY` with a *particular* `VALUE`. In this case, presumably the OP wants to check for any non-`nil` value for property `my-ignore`. But if testing a particular value is OK then you're absolutely right that `text-property-any` would be a good way to do this. – Drew Oct 04 '19 at 14:22
0

I found that the read-only text property combined with the query-replace-skip-read-only variable also works for this purpose. Here's a simplified example of my code:

(defun query-replace-skipping-math-env ()
  (interactive)
  (save-excursion
    (let ((query-replace-skip-read-only t))

      (goto-char (point-min))
      (unwind-protect
      (with-silent-modifications
        (progn
        (while (search-forward-regexp "\\\\begin{equation}" nil t)
          (save-excursion
        (let ((b (match-beginning 0))
              (e (search-forward-regexp "\\\\end{equation}" nil t)))

          (put-text-property b e 'read-only t)))))

    ;; *REPLACEMENTS*       
    (perform-replace " ," 
             "," t nil nil 1 nil (point-min) (point-max)))

    ;; *UNWINDFORMS*:
    (let ((inhibit-read-only t))
      (with-silent-modifications
        (remove-list-of-text-properties (point-min) (point-max) '(read-only))))))))

This code has the advantage of using only built-in functions and macros.

Gabriele Nicolardi
  • 1,199
  • 8
  • 17