1
0
mirror of https://git.savannah.gnu.org/git/emacs.git synced 2024-11-30 08:09:04 +00:00

Rework xref-query-replace-in-results

* lisp/progmodes/xref.el (xref-query-replace-in-results): Collect
all xrefs from the buffer first, then delegate most of the
processing to the value returned by xref--buf-pairs-iterator.
(xref--buf-pairs-iterator): New function.  Return an "iterator"
which partitions returned markers into buffers, and only processes
markers from one buffer at a time.  When an xref is out of date,
skip it with a message instead of signaling error (bug#23284).
(xref--outdated-p): Extract from xref--buf-pairs-iterator.  Trim
CR from both strings before comparing.
(xref--query-replace-1): Remove the variable current-buf, no need
to track it anymore.  Simplify the filter-predicate and search
functions accordingly.  Iterate over buffer-markers pairs returned
by the iterator, and call `perform-replace' for each of them.  Use
multi-query-replace-map (bug#23284).  Use `switch-to-buffer' every
time after the first, in order not to jump between windows.

* test/automated/xref-tests.el
(xref--buf-pairs-iterator-groups-markers-by-buffers-1)
(xref--buf-pairs-iterator-groups-markers-by-buffers-2)
(xref--buf-pairs-iterator-cleans-up-markers): New tests.
This commit is contained in:
Dmitry Gutov 2016-05-05 02:52:34 +03:00
parent 3fe3510728
commit 922c7a3e48
2 changed files with 110 additions and 50 deletions

View File

@ -521,58 +521,86 @@ references displayed in the current *xref* buffer."
(let ((fr (read-regexp "Xref query-replace (regexp)" ".*")))
(list fr
(read-regexp (format "Xref query-replace (regexp) %s with: " fr)))))
(let ((reporter (make-progress-reporter (format "Saving search results...")
0 (line-number-at-pos (point-max))))
(counter 0)
pairs item)
(let* (item xrefs iter)
(save-excursion
(while (setq item (xref--search-property 'xref-item))
(when (xref-match-length item)
(push item xrefs))))
(unwind-protect
(progn
(save-excursion
(goto-char (point-min))
;; TODO: This list should be computed on-demand instead.
;; As long as the UI just iterates through matches one by
;; one, there's no need to compute them all in advance.
;; Then we can throw away the reporter.
(while (setq item (xref--search-property 'xref-item))
(when (xref-match-length item)
(save-excursion
(let* ((loc (xref-item-location item))
(beg (xref-location-marker loc))
(end (move-marker (make-marker)
(+ beg (xref-match-length item))
(marker-buffer beg))))
;; Perform sanity check first.
(xref--goto-location loc)
;; FIXME: The check should probably be a generic
;; function, instead of the assumption that all
;; matches contain the full line as summary.
;; TODO: Offer to re-scan otherwise.
(unless (equal (buffer-substring-no-properties
(line-beginning-position)
(line-end-position))
(xref-item-summary item))
(user-error "Search results out of date"))
(progress-reporter-update reporter (cl-incf counter))
(push (cons beg end) pairs)))))
(setq pairs (nreverse pairs)))
(unless pairs (user-error "No suitable matches here"))
(progress-reporter-done reporter)
(xref--query-replace-1 from to pairs))
(dolist (pair pairs)
(move-marker (car pair) nil)
(move-marker (cdr pair) nil)))))
(goto-char (point-min))
(setq iter (xref--buf-pairs-iterator (nreverse xrefs)))
(xref--query-replace-1 from to iter))
(funcall iter :cleanup))))
(defun xref--buf-pairs-iterator (xrefs)
(let (chunk-done item next-pair file-buf pairs all-pairs)
(lambda (action)
(pcase action
(:next
(when (or xrefs next-pair)
(setq chunk-done nil)
(when next-pair
(setq file-buf (marker-buffer (car next-pair))
pairs (list next-pair)
next-pair nil))
(while (and (not chunk-done)
(setq item (pop xrefs)))
(save-excursion
(let* ((loc (xref-item-location item))
(beg (xref-location-marker loc))
(end (move-marker (make-marker)
(+ beg (xref-match-length item))
(marker-buffer beg))))
(let ((pair (cons beg end)))
(push pair all-pairs)
;; Perform sanity check first.
(xref--goto-location loc)
(if (xref--outdated-p item
(buffer-substring-no-properties
(line-beginning-position)
(line-end-position)))
(message "Search result out of date, skipping")
(cond
((null file-buf)
(setq file-buf (marker-buffer beg))
(push pair pairs))
((equal file-buf (marker-buffer beg))
(push pair pairs))
(t
(setq chunk-done t
next-pair pair))))))))
(cons file-buf pairs)))
(:cleanup
(dolist (pair all-pairs)
(move-marker (car pair) nil)
(move-marker (cdr pair) nil)))))))
(defun xref--outdated-p (item line-text)
;; FIXME: The check should probably be a generic function instead of
;; the assumption that all matches contain the full line as summary.
(let ((summary (xref-item-summary item))
(strip (lambda (s) (if (string-match "\r\\'" s)
(substring-no-properties s 0 -1)
s))))
(not
;; Sometimes buffer contents include ^M, and sometimes Grep
;; output includes it, and they don't always match.
(equal (funcall strip line-text)
(funcall strip summary)))))
;; FIXME: Write a nicer UI.
(defun xref--query-replace-1 (from to pairs)
(defun xref--query-replace-1 (from to iter)
(let* ((query-replace-lazy-highlight nil)
current-beg current-end current-buf
(continue t)
did-it-once buf-pairs pairs
current-beg current-end
;; Counteract the "do the next match now" hack in
;; `perform-replace'. And still, it'll report that those
;; matches were "filtered out" at the end.
(isearch-filter-predicate
(lambda (beg end)
(and current-beg
(eq (current-buffer) current-buf)
(>= beg current-beg)
(<= end current-end))))
(replace-re-search-function
@ -581,19 +609,22 @@ references displayed in the current *xref* buffer."
(while (and (not found) pairs)
(setq pair (pop pairs)
current-beg (car pair)
current-end (cdr pair)
current-buf (marker-buffer current-beg))
(xref--with-dedicated-window
(pop-to-buffer current-buf))
current-end (cdr pair))
(goto-char current-beg)
(when (re-search-forward from current-end noerror)
(setq found t)))
found))))
;; FIXME: Despite this being a multi-buffer replacement, `N'
;; doesn't work, because we're not using
;; `multi-query-replace-map', and it would expect the below
;; function to be called once per buffer.
(perform-replace from to t t nil)))
(while (and continue (setq buf-pairs (funcall iter :next)))
(if did-it-once
;; Reuse the same window for subsequent buffers.
(switch-to-buffer (car buf-pairs))
(xref--with-dedicated-window
(pop-to-buffer (car buf-pairs)))
(setq did-it-once t))
(setq pairs (cdr buf-pairs))
(setq continue
(perform-replace from to t t nil nil multi-query-replace-map)))
(unless did-it-once (user-error "No suitable matches here"))))
(defvar xref--xref-buffer-mode-map
(let ((map (make-sparse-keymap)))

View File

@ -60,3 +60,32 @@
(should (string-match-p "file2\\.txt\\'" (xref-location-group (nth 0 locs))))
(should (equal 1 (xref-location-line (nth 0 locs))))
(should (equal 0 (xref-file-location-column (nth 0 locs))))))
(ert-deftest xref--buf-pairs-iterator-groups-markers-by-buffers-1 ()
(let* ((xrefs (xref-collect-matches "foo" "*" xref-tests-data-dir nil))
(iter (xref--buf-pairs-iterator xrefs))
(cons (funcall iter :next)))
(should (null (funcall iter :next)))
(should (string-match "file1\\.txt\\'" (buffer-file-name (car cons))))
(should (= 2 (length (cdr cons))))))
(ert-deftest xref--buf-pairs-iterator-groups-markers-by-buffers-2 ()
(let* ((xrefs (xref-collect-matches "bar" "*" xref-tests-data-dir nil))
(iter (xref--buf-pairs-iterator xrefs))
(cons1 (funcall iter :next))
(cons2 (funcall iter :next)))
(should (null (funcall iter :next)))
(should-not (equal (car cons1) (car cons2)))
(should (= 1 (length (cdr cons1))))
(should (= 1 (length (cdr cons2))))))
(ert-deftest xref--buf-pairs-iterator-cleans-up-markers ()
(let* ((xrefs (xref-collect-matches "bar" "*" xref-tests-data-dir nil))
(iter (xref--buf-pairs-iterator xrefs))
(cons1 (funcall iter :next))
(cons2 (funcall iter :next)))
(funcall iter :cleanup)
(should (null (marker-position (car (nth 0 (cdr cons1))))))
(should (null (marker-position (cdr (nth 0 (cdr cons1))))))
(should (null (marker-position (car (nth 0 (cdr cons2))))))
(should (null (marker-position (cdr (nth 0 (cdr cons2))))))))