From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Gregory Heytings via "Emacs development discussions." Newsgroups: gmane.emacs.devel Subject: A solution to display completion candidates after point in a minibuffer Date: Fri, 02 Oct 2020 15:36:37 +0000 Message-ID: Reply-To: Gregory Heytings Mime-Version: 1.0 Content-Type: text/plain; format=flowed; charset=US-ASCII Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="27267"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Alpine 2.22 (NEB 394 2020-01-19) To: emacs-devel@gnu.org Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Fri Oct 02 17:38:17 2020 Return-path: Envelope-to: ged-emacs-devel@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1kON8T-0006zQ-2z for ged-emacs-devel@m.gmane-mx.org; Fri, 02 Oct 2020 17:38:17 +0200 Original-Received: from localhost ([::1]:33474 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1kON8R-0007Cn-Vp for ged-emacs-devel@m.gmane-mx.org; Fri, 02 Oct 2020 11:38:16 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]:46746) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1kON76-00056p-Kp for emacs-devel@gnu.org; Fri, 02 Oct 2020 11:36:52 -0400 Original-Received: from mx.sdf.org ([205.166.94.24]:62575) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1kON73-0003Pq-7i for emacs-devel@gnu.org; Fri, 02 Oct 2020 11:36:52 -0400 Original-Received: from sdf.org (IDENT:ghe@otaku.sdf.org [205.166.94.8]) by mx.sdf.org (8.15.2/8.14.5) with ESMTPS id 092FaekH023370 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256 bits) verified NO); Fri, 2 Oct 2020 15:36:40 GMT Original-Received: (from ghe@localhost) by sdf.org (8.15.2/8.12.8/Submit) id 092Favql007290; Fri, 2 Oct 2020 15:36:57 GMT Received-SPF: pass client-ip=205.166.94.24; envelope-from=ghe@sdf.org; helo=mx.sdf.org X-detected-operating-system: by eggs.gnu.org: First seen = 2020/10/02 11:36:43 X-ACL-Warn: Detected OS = ??? X-Spam_score_int: -18 X-Spam_score: -1.9 X-Spam_bar: - X-Spam_report: (-1.9 / 5.0 requ) BAYES_00=-1.9, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.io gmane.emacs.devel:256947 Archived-At: 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