diff options
| author | Dmitry Gutov | 2016-05-05 02:52:34 +0300 |
|---|---|---|
| committer | Dmitry Gutov | 2016-05-05 03:26:04 +0300 |
| commit | 922c7a3e48e649ad67bd12b1f83343b730dd1bc4 (patch) | |
| tree | 551beacca9963c49e4d3a8e14be27b80e4728b83 | |
| parent | 3fe351072841becbb1902c19f784890949f41c1d (diff) | |
| download | emacs-922c7a3e48e649ad67bd12b1f83343b730dd1bc4.tar.gz emacs-922c7a3e48e649ad67bd12b1f83343b730dd1bc4.zip | |
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.
| -rw-r--r-- | lisp/progmodes/xref.el | 131 | ||||
| -rw-r--r-- | test/automated/xref-tests.el | 29 |
2 files changed, 110 insertions, 50 deletions
diff --git a/lisp/progmodes/xref.el b/lisp/progmodes/xref.el index 62cef235988..17bfdb69f8f 100644 --- a/lisp/progmodes/xref.el +++ b/lisp/progmodes/xref.el | |||
| @@ -521,58 +521,86 @@ references displayed in the current *xref* buffer." | |||
| 521 | (let ((fr (read-regexp "Xref query-replace (regexp)" ".*"))) | 521 | (let ((fr (read-regexp "Xref query-replace (regexp)" ".*"))) |
| 522 | (list fr | 522 | (list fr |
| 523 | (read-regexp (format "Xref query-replace (regexp) %s with: " fr))))) | 523 | (read-regexp (format "Xref query-replace (regexp) %s with: " fr))))) |
| 524 | (let ((reporter (make-progress-reporter (format "Saving search results...") | 524 | (let* (item xrefs iter) |
| 525 | 0 (line-number-at-pos (point-max)))) | 525 | (save-excursion |
| 526 | (counter 0) | 526 | (while (setq item (xref--search-property 'xref-item)) |
| 527 | pairs item) | 527 | (when (xref-match-length item) |
| 528 | (push item xrefs)))) | ||
| 528 | (unwind-protect | 529 | (unwind-protect |
| 529 | (progn | 530 | (progn |
| 530 | (save-excursion | 531 | (goto-char (point-min)) |
| 531 | (goto-char (point-min)) | 532 | (setq iter (xref--buf-pairs-iterator (nreverse xrefs))) |
| 532 | ;; TODO: This list should be computed on-demand instead. | 533 | (xref--query-replace-1 from to iter)) |
| 533 | ;; As long as the UI just iterates through matches one by | 534 | (funcall iter :cleanup)))) |
| 534 | ;; one, there's no need to compute them all in advance. | 535 | |
| 535 | ;; Then we can throw away the reporter. | 536 | (defun xref--buf-pairs-iterator (xrefs) |
| 536 | (while (setq item (xref--search-property 'xref-item)) | 537 | (let (chunk-done item next-pair file-buf pairs all-pairs) |
| 537 | (when (xref-match-length item) | 538 | (lambda (action) |
| 538 | (save-excursion | 539 | (pcase action |
| 539 | (let* ((loc (xref-item-location item)) | 540 | (:next |
| 540 | (beg (xref-location-marker loc)) | 541 | (when (or xrefs next-pair) |
| 541 | (end (move-marker (make-marker) | 542 | (setq chunk-done nil) |
| 542 | (+ beg (xref-match-length item)) | 543 | (when next-pair |
| 543 | (marker-buffer beg)))) | 544 | (setq file-buf (marker-buffer (car next-pair)) |
| 544 | ;; Perform sanity check first. | 545 | pairs (list next-pair) |
| 545 | (xref--goto-location loc) | 546 | next-pair nil)) |
| 546 | ;; FIXME: The check should probably be a generic | 547 | (while (and (not chunk-done) |
| 547 | ;; function, instead of the assumption that all | 548 | (setq item (pop xrefs))) |
| 548 | ;; matches contain the full line as summary. | 549 | (save-excursion |
| 549 | ;; TODO: Offer to re-scan otherwise. | 550 | (let* ((loc (xref-item-location item)) |
| 550 | (unless (equal (buffer-substring-no-properties | 551 | (beg (xref-location-marker loc)) |
| 551 | (line-beginning-position) | 552 | (end (move-marker (make-marker) |
| 552 | (line-end-position)) | 553 | (+ beg (xref-match-length item)) |
| 553 | (xref-item-summary item)) | 554 | (marker-buffer beg)))) |
| 554 | (user-error "Search results out of date")) | 555 | (let ((pair (cons beg end))) |
| 555 | (progress-reporter-update reporter (cl-incf counter)) | 556 | (push pair all-pairs) |
| 556 | (push (cons beg end) pairs))))) | 557 | ;; Perform sanity check first. |
| 557 | (setq pairs (nreverse pairs))) | 558 | (xref--goto-location loc) |
| 558 | (unless pairs (user-error "No suitable matches here")) | 559 | (if (xref--outdated-p item |
| 559 | (progress-reporter-done reporter) | 560 | (buffer-substring-no-properties |
| 560 | (xref--query-replace-1 from to pairs)) | 561 | (line-beginning-position) |
| 561 | (dolist (pair pairs) | 562 | (line-end-position))) |
| 562 | (move-marker (car pair) nil) | 563 | (message "Search result out of date, skipping") |
| 563 | (move-marker (cdr pair) nil))))) | 564 | (cond |
| 565 | ((null file-buf) | ||
| 566 | (setq file-buf (marker-buffer beg)) | ||
| 567 | (push pair pairs)) | ||
| 568 | ((equal file-buf (marker-buffer beg)) | ||
| 569 | (push pair pairs)) | ||
| 570 | (t | ||
| 571 | (setq chunk-done t | ||
| 572 | next-pair pair)))))))) | ||
| 573 | (cons file-buf pairs))) | ||
| 574 | (:cleanup | ||
| 575 | (dolist (pair all-pairs) | ||
| 576 | (move-marker (car pair) nil) | ||
| 577 | (move-marker (cdr pair) nil))))))) | ||
| 578 | |||
| 579 | (defun xref--outdated-p (item line-text) | ||
| 580 | ;; FIXME: The check should probably be a generic function instead of | ||
| 581 | ;; the assumption that all matches contain the full line as summary. | ||
| 582 | (let ((summary (xref-item-summary item)) | ||
| 583 | (strip (lambda (s) (if (string-match "\r\\'" s) | ||
| 584 | (substring-no-properties s 0 -1) | ||
| 585 | s)))) | ||
| 586 | (not | ||
| 587 | ;; Sometimes buffer contents include ^M, and sometimes Grep | ||
| 588 | ;; output includes it, and they don't always match. | ||
| 589 | (equal (funcall strip line-text) | ||
| 590 | (funcall strip summary))))) | ||
| 564 | 591 | ||
| 565 | ;; FIXME: Write a nicer UI. | 592 | ;; FIXME: Write a nicer UI. |
| 566 | (defun xref--query-replace-1 (from to pairs) | 593 | (defun xref--query-replace-1 (from to iter) |
| 567 | (let* ((query-replace-lazy-highlight nil) | 594 | (let* ((query-replace-lazy-highlight nil) |
| 568 | current-beg current-end current-buf | 595 | (continue t) |
| 596 | did-it-once buf-pairs pairs | ||
| 597 | current-beg current-end | ||
| 569 | ;; Counteract the "do the next match now" hack in | 598 | ;; Counteract the "do the next match now" hack in |
| 570 | ;; `perform-replace'. And still, it'll report that those | 599 | ;; `perform-replace'. And still, it'll report that those |
| 571 | ;; matches were "filtered out" at the end. | 600 | ;; matches were "filtered out" at the end. |
| 572 | (isearch-filter-predicate | 601 | (isearch-filter-predicate |
| 573 | (lambda (beg end) | 602 | (lambda (beg end) |
| 574 | (and current-beg | 603 | (and current-beg |
| 575 | (eq (current-buffer) current-buf) | ||
| 576 | (>= beg current-beg) | 604 | (>= beg current-beg) |
| 577 | (<= end current-end)))) | 605 | (<= end current-end)))) |
| 578 | (replace-re-search-function | 606 | (replace-re-search-function |
| @@ -581,19 +609,22 @@ references displayed in the current *xref* buffer." | |||
| 581 | (while (and (not found) pairs) | 609 | (while (and (not found) pairs) |
| 582 | (setq pair (pop pairs) | 610 | (setq pair (pop pairs) |
| 583 | current-beg (car pair) | 611 | current-beg (car pair) |
| 584 | current-end (cdr pair) | 612 | current-end (cdr pair)) |
| 585 | current-buf (marker-buffer current-beg)) | ||
| 586 | (xref--with-dedicated-window | ||
| 587 | (pop-to-buffer current-buf)) | ||
| 588 | (goto-char current-beg) | 613 | (goto-char current-beg) |
| 589 | (when (re-search-forward from current-end noerror) | 614 | (when (re-search-forward from current-end noerror) |
| 590 | (setq found t))) | 615 | (setq found t))) |
| 591 | found)))) | 616 | found)))) |
| 592 | ;; FIXME: Despite this being a multi-buffer replacement, `N' | 617 | (while (and continue (setq buf-pairs (funcall iter :next))) |
| 593 | ;; doesn't work, because we're not using | 618 | (if did-it-once |
| 594 | ;; `multi-query-replace-map', and it would expect the below | 619 | ;; Reuse the same window for subsequent buffers. |
| 595 | ;; function to be called once per buffer. | 620 | (switch-to-buffer (car buf-pairs)) |
| 596 | (perform-replace from to t t nil))) | 621 | (xref--with-dedicated-window |
| 622 | (pop-to-buffer (car buf-pairs))) | ||
| 623 | (setq did-it-once t)) | ||
| 624 | (setq pairs (cdr buf-pairs)) | ||
| 625 | (setq continue | ||
| 626 | (perform-replace from to t t nil nil multi-query-replace-map))) | ||
| 627 | (unless did-it-once (user-error "No suitable matches here")))) | ||
| 597 | 628 | ||
| 598 | (defvar xref--xref-buffer-mode-map | 629 | (defvar xref--xref-buffer-mode-map |
| 599 | (let ((map (make-sparse-keymap))) | 630 | (let ((map (make-sparse-keymap))) |
diff --git a/test/automated/xref-tests.el b/test/automated/xref-tests.el index b288e2d7584..079b196aa8b 100644 --- a/test/automated/xref-tests.el +++ b/test/automated/xref-tests.el | |||
| @@ -60,3 +60,32 @@ | |||
| 60 | (should (string-match-p "file2\\.txt\\'" (xref-location-group (nth 0 locs)))) | 60 | (should (string-match-p "file2\\.txt\\'" (xref-location-group (nth 0 locs)))) |
| 61 | (should (equal 1 (xref-location-line (nth 0 locs)))) | 61 | (should (equal 1 (xref-location-line (nth 0 locs)))) |
| 62 | (should (equal 0 (xref-file-location-column (nth 0 locs)))))) | 62 | (should (equal 0 (xref-file-location-column (nth 0 locs)))))) |
| 63 | |||
| 64 | (ert-deftest xref--buf-pairs-iterator-groups-markers-by-buffers-1 () | ||
| 65 | (let* ((xrefs (xref-collect-matches "foo" "*" xref-tests-data-dir nil)) | ||
| 66 | (iter (xref--buf-pairs-iterator xrefs)) | ||
| 67 | (cons (funcall iter :next))) | ||
| 68 | (should (null (funcall iter :next))) | ||
| 69 | (should (string-match "file1\\.txt\\'" (buffer-file-name (car cons)))) | ||
| 70 | (should (= 2 (length (cdr cons)))))) | ||
| 71 | |||
| 72 | (ert-deftest xref--buf-pairs-iterator-groups-markers-by-buffers-2 () | ||
| 73 | (let* ((xrefs (xref-collect-matches "bar" "*" xref-tests-data-dir nil)) | ||
| 74 | (iter (xref--buf-pairs-iterator xrefs)) | ||
| 75 | (cons1 (funcall iter :next)) | ||
| 76 | (cons2 (funcall iter :next))) | ||
| 77 | (should (null (funcall iter :next))) | ||
| 78 | (should-not (equal (car cons1) (car cons2))) | ||
| 79 | (should (= 1 (length (cdr cons1)))) | ||
| 80 | (should (= 1 (length (cdr cons2)))))) | ||
| 81 | |||
| 82 | (ert-deftest xref--buf-pairs-iterator-cleans-up-markers () | ||
| 83 | (let* ((xrefs (xref-collect-matches "bar" "*" xref-tests-data-dir nil)) | ||
| 84 | (iter (xref--buf-pairs-iterator xrefs)) | ||
| 85 | (cons1 (funcall iter :next)) | ||
| 86 | (cons2 (funcall iter :next))) | ||
| 87 | (funcall iter :cleanup) | ||
| 88 | (should (null (marker-position (car (nth 0 (cdr cons1)))))) | ||
| 89 | (should (null (marker-position (cdr (nth 0 (cdr cons1)))))) | ||
| 90 | (should (null (marker-position (car (nth 0 (cdr cons2)))))) | ||
| 91 | (should (null (marker-position (cdr (nth 0 (cdr cons2)))))))) | ||