emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

A solution to display completion candidates after point in a minibuffer


From: Gregory Heytings
Subject: A solution to display completion candidates after point in a minibuffer
Date: Fri, 02 Oct 2020 15:36:37 +0000
User-agent: Alpine 2.22 (NEB 394 2020-01-19)


Hi,

Displaying completion candidates after point in a minibuffer has raised a similar problem over the last years (see for instance bug#24293, bug#39379, bug#43519 and bug#43572): when there are too many completion candidates, the minibuffer prompt is hidden. The root of this problem is that resize_mini_window() (in xdisp.c) computes the start of the visible part of the minibuffer so as to make the end of the minibuffer contents appear.

So far the solution to this problem has been to manually compute the size of the completion candidates so that they do not use (together with the prompt and the user input) more than max-mini-window-height lines. Another solution, which as far as I know has not been used, would be to set resize-mini-windows to nil, and to resize the miniwindow manually with enlarge-window, but this solution also involves nontrivial computations, and fiddles with a user setting.

The good news is that it is in fact possible to convince Emacs to do the opposite of what resize_mini_window() does:

(defvar-local start-display-at-beginning-of-minibuffer nil)
(defun start-display-at-beginning-of-minibuffer (&rest args)
  (when (and start-display-at-beginning-of-minibuffer (minibufferp))
    (set-window-start-at-begin 1 (point))))
(defun set-window-start-at-begin (beg end)
  (set-window-start nil beg)
  (unless (pos-visible-in-window-p end nil t) (set-window-start-at-begin (+ beg 
(/ (- end beg) 2)) end)))
(setq window-scroll-functions (cons 'start-display-at-beginning-of-minibuffer 
window-scroll-functions))
(add-hook 'post-command-hook 'start-display-at-beginning-of-minibuffer)

This works with at least Emacs 24, 25, 26, 27 and 28.

This means that displaying completion candidates after point in a minibuffer becomes a trivial task: it suffices to insert the completion candidates in the minibuffer, without worrying at all about their size (or about the size of the prompt and user input), and Emacs will display as many of these candidates as possible, given the user preferences (max-mini-window-height, resize-mini-windows, ...).

As a proof of concept, displaying completion candidates vertically with icomplete is as easy as:

(setq icomplete-separator "\n")
(add-hook 'icomplete-minibuffer-setup-hook (lambda () (setq 
start-display-at-beginning-of-minibuffer t)))
(defun icomplete-vertical-reformat-completions (completions)
  (save-match-data
    (if (string-match "^\\((.*)\\|\\[.+\\]\\)?{\\(\\(?:.\\|\n\\)+\\)}" 
completions)
        (format "%s \n%s" (or (match-string 1 completions) "") (match-string 2 
completions))
      completions)))
(advice-add 'icomplete-completions :filter-return 
#'icomplete-vertical-reformat-completions)

A few comments, for the curious:

1. Obviously, when the miniwindow is forced to be small (for example with (setq max-mini-window-height 1)), the prompt will be hidden when the prompt and user input are too large.

2. The only drawback of the above solution is that is is not possible to display an ellipsis ("...") at the end of the completion candidates list, to indicate that some completion candidates are not displayed. It seems to me that this is a minor limitation.

3. If the face used in the minibuffer has a height that is not a multiple of the height of the default face, the last completion candidate will be only partially visible.

4. set-window-start-at-begin is very fast, on my (not very recent) computer it takes about a half millisecond. In the normal case, that is, when the miniwindow is not too small, set-window-start-at-begin merely does (set-window-start nil 1).

5. set-better-window-start could in theory enter an infinite loop (that is, raise a "Lisp nesting exceeds 'max-lisp-eval-depth'" error) if setting window-start near point failed. This cannot happen in practice.

6. Instead of using (+ beg (/ (- end beg) 2)) in set-better-window-start, one could be tempted to use (window-body-width), but this does not work when the face used in the minibuffer is not the default face. One could also be tempted to use (/ (window-text-width nil t) (car (window-text-pixel-size nil 1 2))), but this does not work with a variable width face.

7. (add-hook 'post-command-hook 'start-display-at-beginning-of-minibuffer) is necessary only with variable width faces, but it is does not harm to use it with fixed width faces.

8. I believe there is only one thing missing in the "vertical icomplete" implementation above, namely removing the first line when it is empty. This can be done as follows:

(defun icomplete-vertical-reformat-completions (completions)
  (save-match-data
    (if (string-match "^\\((.*)\\|\\[.+\\]\\)?{\\(\\(?:.\\|\n\\)+\\)}" 
completions)
        (let* ((c (match-string 2 completions))
               (nl (= (aref c 0) 10))
               (cc (if nl (substring c 1) c)))
          (format "%s \n%s" (or (match-string 1 completions) "") cc))
      completions)))

Gregory



reply via email to

[Prev in Thread] Current Thread [Next in Thread]