Subject: bug#74561: [PATCH] Allow limiting the size of *Completions*

From: Spencer Baugh <[email-masked]>
Date: Tue, 11 Feb 2025 14:44:46 -0500

Eli Zaretskii <[email-masked]> writes:

>> Cc: João Távora <[email-masked]>,
>>  [email-masked], [email-masked],
>>  Stefan Monnier <[email-masked]>, [email-masked]
>> From: [email-masked]
>> Date: Tue, 11 Feb 2025 15:03:31 +0000 (UTC)
>> 
>> +(defcustom completions-insert-lazily 200
>> +  "If non-nil, completions are inserted lazily.
>
> Does the default value mean a change in user-facing behavior of
> completion commands?

Nope, no user-facing behavior changes.  In fact, this probably shouldn't
be user-customizable at all, since it's an internal optimization with
essentially no cost.  So I've changed this into a defvar
completions--insert-lazily.

>> +When a number, only that many completions are inserted, and the
>
> Please use "If", not "When".  The latter could be misinterpreted as
> meaning some time-related condition, which is not what you mean here.

Done.

> Also, what about the :version tag?

No longer a defcustom.

> And this user option needs a NEWS entry.

Added a NEWS entry for the change in general.

Updated patch:

>From fd4ca55084fa24998c09cabbb6e7dd2a88164abd Mon Sep 17 00:00:00 2001
From: Spencer Baugh <[email-masked]>
Date: Mon, 13 Jan 2025 14:32:18 -0500
Subject: [PATCH] Lazily highlight and insert candidates in *Completions*

>From profiling, the main bottleneck in completion over large
completion sets is display-completion-list, when there are many
available candidates.  For example, in my large monorepo, when
completing over the 589196 files or the 73897 branches, even
with the candidates narrowed down by typing some prefix to
complete, TAB (when it shows *Completions*) or ? is slow, mostly
in display-completion-list.

However, rendering all the completion candidates is unnecessary if
the *Completions* window is never scrolled to see those candiates.  By
eagerly inserting only some candidates and lazily highlighting and
inserting the remaining candidates only when necessary, performance is
much improved.

* lisp/minibuffer.el (completion--insert-strings): Insert completions
lazily. (bug#74561)
(completion--lazy-insert-strings)
(completions--insert-lazily)
(completions--lazy-insert-button) Add.
(minibuffer-completion-help): Set completion-lazy-hilit.
(with-minibuffer-completions-window): Call
completion--lazy-insert-strings.
* lisp/simple.el (completion-setup-function): Preserve
buffer-locals required for lazy completion insertion.
(switch-to-completions): Call
completion--lazy-insert-strings.
---
 etc/NEWS           | 10 ++++++++++
 lisp/minibuffer.el | 47 ++++++++++++++++++++++++++++++++++++++++++++--
 lisp/simple.el     |  5 ++++-
 3 files changed, 59 insertions(+), 3 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 9fe46d818bd..61a6d9a3319 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -176,6 +176,16 @@ will still be on that candidate after "*Completions*" is updated with a
 new list of completions.  The candidate is automatically deselected when
 the "*Completions*" buffer is hidden.
 
+---
+*** "*Completions*" is displayed faster with many completion candidates.
+The "*Completions*" buffer is now created and displayed faster when it
+contains many completion candidates.  When intially created and
+displayed in a window, only enough completion candidates are inserted
+into the "*Completions*" buffer to fill the window.  The remaining
+completion candidates are inserted automatically if you run a command
+which interacts with "*Completions*" buffer, such as
+'switch-to-completions'.
+
 ** Windows
 
 +++
diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index b401e4a920c..d08874f1cbb 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -2230,6 +2230,14 @@ completions-header-format
                  (string :tag "Format string for heading line"))
   :version "29.1")
 
+(defvar completions--insert-lazily 200
+  "If non-nil, completions are inserted lazily.
+
+Only this many completions are inserted, and the rest are inserted
+lazily by `completion--lazy-insert-strings'.")
+
+(defvar-local completions--lazy-insert-button nil)
+
 (defun completion--insert-strings (strings &optional group-fun)
   "Insert a list of STRINGS into the current buffer.
 The candidate strings are inserted into the buffer depending on the
@@ -2257,8 +2265,41 @@ completion--insert-strings
         ;; Align to tab positions for the case
         ;; when the caller uses tabs inside prefix.
         (setq colwidth (- colwidth (mod colwidth completion-tab-width))))
-      (funcall (intern (format "completion--insert-%s" completions-format))
-               strings group-fun length wwidth colwidth columns))))
+      (let ((truncated-strings
+             (and completions--insert-lazily
+                  (< completions--insert-lazily (length strings))
+                  (take completions--insert-lazily strings)))
+            (start (point)))
+        (funcall (intern (format "completion--insert-%s" completions-format))
+                 (mapcar (lambda (candidate)
+                           (if (consp candidate)
+                               (setcar candidate (completion-lazy-hilit (car candidate)))
+                             (completion-lazy-hilit candidate)))
+                         (or truncated-strings strings))
+                 group-fun length wwidth colwidth columns)
+        (when truncated-strings
+          (newline)
+          (setq-local completions--lazy-insert-button
+                      (insert-button "[Completions truncated, click here to insert the rest.]"
+                                     'action #'completion--lazy-insert-strings))
+          (button-put completions--lazy-insert-button 'group-fun group-fun)
+          (button-put completions--lazy-insert-button 'completions-start (copy-marker start))
+          (button-put completions--lazy-insert-button 'completion-strings strings))))))
+
+(defun completion--lazy-insert-strings (&optional button)
+  (setq button (or button completions--lazy-insert-button))
+  (when button
+    (let ((completions--insert-lazily nil)
+          (completion-lazy-hilit t)
+          (standard-output (current-buffer))
+          (inhibit-read-only t)
+          (group-fun (button-get button 'group-fun))
+          (strings (button-get button 'completion-strings)))
+      (save-excursion
+        (goto-char (button-get button 'completions-start))
+        (delete-region (point) (button-end button))
+        (setq-local completions--lazy-insert-button nil)
+        (completion--insert-strings strings group-fun)))))
 
 (defun completion--insert-horizontal (strings group-fun
                                               length wwidth
@@ -2644,6 +2685,7 @@ minibuffer-completion-help
          (end (or end (point-max)))
          (string (buffer-substring start end))
          (md (completion--field-metadata start))
+         (completion-lazy-hilit completions--insert-lazily)
          (completions (completion-all-completions
                        string
                        minibuffer-completion-table
@@ -4986,6 +5028,7 @@ with-minibuffer-completions-window
                             (get-buffer-window "*Completions*" 0)))))
      (when window
        (with-selected-window window
+         (completion--lazy-insert-strings)
          ,@body))))
 
 (defcustom minibuffer-completion-auto-choose t
diff --git a/lisp/simple.el b/lisp/simple.el
index e1c0dd4a092..57553343c04 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -10455,10 +10455,12 @@ completion-setup-function
                 (buffer-substring (minibuffer-prompt-end) (point)))))))
     (with-current-buffer standard-output
       (let ((base-position completion-base-position)
-            (insert-fun completion-list-insert-choice-function))
+            (insert-fun completion-list-insert-choice-function)
+            (lazy-button completion--lazy-insert-button))
         (completion-list-mode)
         (when completions-highlight-face
           (setq-local cursor-face-highlight-nonselected-window t))
+        (setq-local completion--lazy-insert-button lazy-button)
         (setq-local completion-base-position base-position)
         (setq-local completion-list-insert-choice-function insert-fun))
       (setq-local completion-reference-buffer mainbuf)
@@ -10504,6 +10506,7 @@ switch-to-completions
                           (progn (minibuffer-completion-help)
                                  (get-buffer-window "*Completions*" 0)))))
     (select-window window)
+    (completion--lazy-insert-strings)
     (when (bobp)
       (cond
        ((and (memq this-command '(completion-at-point minibuffer-complete))
-- 
2.39.3