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

From: [email-masked]
Date: Tue, 11 Feb 2025 15:03:31 +0000 (UTC)

Improved the patch further on feedback from Stefan.  It now also uses
completion-lazy-hilit, which further improves performance.

I'm not sure if completion-lazy-hilit-fn needs to be saved and restored
(like group-fun is being saved and restored).  The current version of
this patch doesn't save completion-lazy-hilit-fn, and that seems to work
fine, because completion-lazy-hilit-fn hasn't changed by the time we
call completion--lazy-insert-strings.  But maybe this is not robust in
some case?

>From 454ce335856c6ab550447f7ed1d79dbb67d6f3ef 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 (completions-insert-lazily): Add defcustom
for inserting completions lazily. (bug#74561)
(completion--lazy-insert-completions)
(completion--lazy-insert-group-fun)
(completion--lazy-insert-start, completion--lazy-insert-end):
Add.
(completion--insert-strings): Check completions-insert-lazily.
(minibuffer-completion-help): Set completion-lazy-hilit.
(completion--lazy-insert-strings): Add.
(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.
---
 lisp/minibuffer.el | 49 ++++++++++++++++++++++++++++++++++++++++++++--
 lisp/simple.el     |  5 ++++-
 2 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index 1f33bda5f65..9dca6bb2d15 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -2230,6 +2230,16 @@ completions-header-format
                  (string :tag "Format string for heading line"))
   :version "29.1")
 
+(defcustom completions-insert-lazily 200
+  "If non-nil, completions are inserted lazily.
+
+When a number, only that many completions are inserted, and the
+rest are inserted lazily."
+  :type '(choice (const :tag "Render all completions immediately" nil)
+                 (integer :tag "Only render this many completions")))
+
+(defvar-local completion--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 +2267,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 completion--lazy-insert-button
+                      (insert-button "[Completions truncated, click here to insert the rest.]"
+                                     'action #'completion--lazy-insert-strings))
+          (button-put completion--lazy-insert-button 'group-fun group-fun)
+          (button-put completion--lazy-insert-button 'completions-start (copy-marker start))
+          (button-put completion--lazy-insert-button 'completion-strings strings))))))
+
+(defun completion--lazy-insert-strings (&optional button)
+  (setq button (or button completion--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 completion--lazy-insert-button nil)
+        (completion--insert-strings strings group-fun)))))
 
 (defun completion--insert-horizontal (strings group-fun
                                               length wwidth
@@ -2620,6 +2663,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
@@ -4962,6 +5006,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.44.0