Speed up xref rendering for matches on very long lines

* lisp/progmodes/xref.el (xref--insert-xrefs): Cut up the current
line into pieces here for multiple matches's summaries, so that
xref--insert-xrefs can do less work (bug#46859).
(xref--insert-xrefs): Do less work.
(xref--outdated-p):
Update accordingly to how the summary creation logic changed.
(xref--buf-pairs-iterator): Update to the new calling convention.
(xref-location-column): Effectively rename back to
xref-file-location-column since the generic version is now unused.

* test/lisp/progmodes/xref-tests.el
(xref-matches-in-directory-finds-two-matches-on-the-same-line)
(xref-matches-in-directory-finds-an-empty-line-regexp-match):
Adjust to the xref-location-column change.
(xref-matches-in-files-trims-summary-for-matches-on-same-line):
New test.

* test/lisp/progmodes/xref-resources/file1.txt:
Change contents slightly to test the new xref--outdated-p code.
This commit is contained in:
Dmitry Gutov 2021-03-08 04:25:15 +02:00
parent 05adcefa1f
commit 8e103ebef1
4 changed files with 78 additions and 50 deletions

View file

@ -97,10 +97,6 @@ This is typically the filename.")
"Return the line number corresponding to the location."
nil)
(cl-defgeneric xref-location-column (_location)
"Return the exact column corresponding to the location."
nil)
(cl-defgeneric xref-match-length (_item)
"Return the length of the match."
nil)
@ -130,7 +126,7 @@ in its full absolute form."
(defclass xref-file-location (xref-location)
((file :type string :initarg :file)
(line :type fixnum :initarg :line :reader xref-location-line)
(column :type fixnum :initarg :column :reader xref-location-column))
(column :type fixnum :initarg :column :reader xref-file-location-column))
:documentation "A file location is a file/line/column triple.
Line numbers start from 1 and columns from 0.")
@ -713,10 +709,7 @@ references displayed in the current *xref* buffer."
(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)))
(if (xref--outdated-p item)
(message "Search result out of date, skipping")
(cond
((null file-buf)
@ -733,18 +726,38 @@ references displayed in the current *xref* buffer."
(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))))
(defun xref--outdated-p (item)
"Check that the match location at current position is up-to-date.
ITEMS is an xref item which "
;; FIXME: The check should most likely be a generic function instead
;; of the assumption that all matches' summaries relate to the
;; buffer text in a particular way.
(let* ((summary (xref-item-summary item))
;; Sometimes buffer contents include ^M, and sometimes Grep
;; output includes it, and they don't always match.
(strip (lambda (s) (if (string-match "\r\\'" s)
(substring-no-properties s 0 -1)
s)))
(stripped-summary (funcall strip summary))
(lendpos (line-end-position))
(check (lambda ()
(let ((comparison-end
(+ (point) (length stripped-summary))))
(and (>= lendpos comparison-end)
(equal stripped-summary
(buffer-substring-no-properties
(point) comparison-end)))))))
(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)))))
(or
;; Either summary contains match text and after
;; (2nd+ match on the line)...
(funcall check)
;; ...or it starts at bol, includes the match and after.
(and (< (point) (+ (line-beginning-position)
(length stripped-summary)))
(save-excursion
(forward-line 0)
(funcall check)))))))
;; FIXME: Write a nicer UI.
(defun xref--query-replace-1 (from to iter)
@ -886,30 +899,24 @@ GROUP is a string for decoration purposes and XREF is an
(length (and line (format "%d" line)))))
for line-format = (and max-line-width
(format "%%%dd: " max-line-width))
with prev-line-key = nil
with prev-group = nil
with prev-line = nil
do
(xref--insert-propertized '(face xref-file-header xref-group t)
group "\n")
(cl-loop for (xref . more2) on xrefs do
(with-slots (summary location) xref
(let* ((line (xref-location-line location))
(new-summary summary)
(line-key (list (xref-location-group location) line))
(prefix
(if line
(propertize (format line-format line)
'face 'xref-line-number)
" ")))
(cond
((not line) " ")
((equal line prev-line) "")
(t (propertize (format line-format line)
'face 'xref-line-number)))))
;; Render multiple matches on the same line, together.
(when (and line (equal prev-line-key line-key))
(when-let ((column (xref-location-column location)))
(delete-region
(save-excursion
(forward-line -1)
(move-to-column (+ (length prefix) column))
(point))
(point))
(setq new-summary (substring summary column) prefix "")))
(when (and (equal prev-group group)
(not (equal prev-line line)))
(insert "\n"))
(xref--insert-propertized
(list 'xref-item xref
'mouse-face 'highlight
@ -917,9 +924,10 @@ GROUP is a string for decoration purposes and XREF is an
'help-echo
(concat "mouse-2: display in another window, "
"RET or mouse-1: follow reference"))
prefix new-summary)
(setq prev-line-key line-key)))
(insert "\n"))))
prefix summary)
(setq prev-line line
prev-group group))))
(insert "\n")))
(defun xref--analyze (xrefs)
"Find common filenames in XREFS.
@ -1678,20 +1686,30 @@ Such as the current syntax table and the applied syntax properties."
syntax-needed)))))
(defun xref--collect-matches-1 (regexp file line line-beg line-end syntax-needed)
(let (matches)
(let (match-pairs matches)
(when syntax-needed
(syntax-propertize line-end))
;; FIXME: This results in several lines with the same
;; summary. Solve with composite pattern?
(while (and
;; REGEXP might match an empty string. Or line.
(or (null matches)
(or (null match-pairs)
(> (point) line-beg))
(re-search-forward regexp line-end t))
(let* ((beg-column (- (match-beginning 0) line-beg))
(end-column (- (match-end 0) line-beg))
(push (cons (match-beginning 0)
(match-end 0))
match-pairs))
(setq match-pairs (nreverse match-pairs))
(while match-pairs
(let* ((beg-end (pop match-pairs))
(beg-column (- (car beg-end) line-beg))
(end-column (- (cdr beg-end) line-beg))
(loc (xref-make-file-location file line beg-column))
(summary (buffer-substring line-beg line-end)))
(summary (buffer-substring (if matches (car beg-end) line-beg)
(if match-pairs
(caar match-pairs)
line-end))))
(when matches
(cl-decf beg-column (- (car beg-end) line-beg))
(cl-decf end-column (- (car beg-end) line-beg)))
(add-face-text-property beg-column end-column 'xref-match
t summary)
(push (xref-make-match summary loc (- end-column beg-column))

View file

@ -1,2 +1,2 @@
foo foo
foo foo
bar

View file

@ -0,0 +1 @@
match some words match more match ends here

View file

@ -59,15 +59,15 @@
(should (string-match-p "file1\\.txt\\'" (xref-location-group (nth 1 locs))))
(should (equal 1 (xref-location-line (nth 0 locs))))
(should (equal 1 (xref-location-line (nth 1 locs))))
(should (equal 0 (xref-location-column (nth 0 locs))))
(should (equal 4 (xref-location-column (nth 1 locs))))))
(should (equal 1 (xref-file-location-column (nth 0 locs))))
(should (equal 5 (xref-file-location-column (nth 1 locs))))))
(ert-deftest xref-matches-in-directory-finds-an-empty-line-regexp-match ()
(let ((locs (xref-tests--locations-in-data-dir "^$")))
(should (= 1 (length locs)))
(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-location-column (nth 0 locs))))))
(should (equal 0 (xref-file-location-column (nth 0 locs))))))
(ert-deftest xref-matches-in-files-includes-matches-from-all-the-files ()
(let ((matches (xref-matches-in-files "bar"
@ -78,6 +78,15 @@
(lambda (match) (equal (xref-item-summary match) "bar"))
matches))))
(ert-deftest xref-matches-in-files-trims-summary-for-matches-on-same-line ()
(let ((matches (xref-matches-in-files "match"
(directory-files xref-tests--data-dir t
"\\`[^.]"))))
(should (= 3 (length matches)))
(should
(equal (mapcar #'xref-item-summary matches)
'(" match some words " "match more " "match ends here")))))
(ert-deftest xref--buf-pairs-iterator-groups-markers-by-buffers-1 ()
(let* ((xrefs (xref-tests--matches-in-data-dir "foo"))
(iter (xref--buf-pairs-iterator xrefs))