From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Eshel Yaron via "Bug reports for GNU Emacs, the Swiss army knife of text editors" Newsgroups: gmane.emacs.bugs Subject: bug#66948: [PATCH] Add Completion Preview mode Date: Sun, 05 Nov 2023 11:26:07 +0100 Message-ID: Reply-To: Eshel Yaron Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="33047"; mail-complaints-to="usenet@ciao.gmane.io" Cc: =?UTF-8?Q?Jo=C3=A3o_?= =?UTF-8?Q?T=C3=A1vora?= , Dmitry Gutov To: 66948@debbugs.gnu.org Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Sun Nov 05 11:26:36 2023 Return-path: Envelope-to: geb-bug-gnu-emacs@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 1qzaKy-0008Oo-14 for geb-bug-gnu-emacs@m.gmane-mx.org; Sun, 05 Nov 2023 11:26:36 +0100 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1qzaKp-000622-TE; Sun, 05 Nov 2023 05:26:27 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1qzaKo-00061s-6r for bug-gnu-emacs@gnu.org; Sun, 05 Nov 2023 05:26:26 -0500 Original-Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1qzaKn-0002aZ-Tf for bug-gnu-emacs@gnu.org; Sun, 05 Nov 2023 05:26:25 -0500 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1qzaLO-00074f-HT; Sun, 05 Nov 2023 05:27:02 -0500 X-Loop: help-debbugs@gnu.org Resent-From: Eshel Yaron Original-Sender: "Debbugs-submit" Resent-CC: joaotavora@gmail.com, dmitry@gutov.dev, bug-gnu-emacs@gnu.org Resent-Date: Sun, 05 Nov 2023 10:27:02 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: report 66948 X-GNU-PR-Package: emacs X-GNU-PR-Keywords: patch X-Debbugs-Original-To: bug-gnu-emacs@gnu.org X-Debbugs-Original-Xcc: =?UTF-8?Q?Jo=C3=A3o_?= =?UTF-8?Q?T=C3=A1vora?= , Dmitry Gutov Original-Received: via spool by submit@debbugs.gnu.org id=B.169918002027183 (code B ref -1); Sun, 05 Nov 2023 10:27:02 +0000 Original-Received: (at submit) by debbugs.gnu.org; 5 Nov 2023 10:27:00 +0000 Original-Received: from localhost ([127.0.0.1]:36163 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qzaLL-00074L-Rt for submit@debbugs.gnu.org; Sun, 05 Nov 2023 05:27:00 -0500 Original-Received: from lists.gnu.org ([2001:470:142::17]:34124) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qzaLJ-000740-08 for submit@debbugs.gnu.org; Sun, 05 Nov 2023 05:26:57 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1qzaKc-00061O-VT for bug-gnu-emacs@gnu.org; Sun, 05 Nov 2023 05:26:15 -0500 Original-Received: from mail.eshelyaron.com ([107.175.124.16] helo=eshelyaron.com) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1qzaKa-0002aI-Kc for bug-gnu-emacs@gnu.org; Sun, 05 Nov 2023 05:26:14 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=eshelyaron.com; s=mail; t=1699179971; bh=wOH43aytx5PmGVfPjPBRl7DHdi3M04rvlLdMEPHdxHA=; h=From:To:Subject:Date:From; b=KretEk6DJNMWR2cU779WrwZ+4lvyKnvufaUp+Esx0AEZOYbfCj2HM/oDwOJ0dAy9B JawU+X192qF/aTr4FwzXsCOgKmSQzxKk/HEGAaCzbedeHlacYbcycwEgfqVQJPfjtu q7vtx5AplCdYO2E26fx/hzqMrRjno88+8B3IDery0ZVm9Hn35+jCusxEc2LA7BDivQ W5MDTw5jy1za8D2xMhbj5kdAfW7OaCHT07shcgqe5cTUzh9FF47wje9u1L7uSi+6TX xiV6bX0+OaHebPKXvk72a4qCdBQAJ00vxVjvxCCZBxKSQT3IWRBAuitQksjep9u73S RdOb/l1poIJJQ== X-Hashcash: 1:20:231105:bug-gnu-emacs@gnu.org::QyEBVRUEnFOA2SJw:AAZU Received-SPF: pass client-ip=107.175.124.16; envelope-from=me@eshelyaron.com; helo=eshelyaron.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001, T_SCC_BODY_TEXT_LINE=-0.01 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:273817 Archived-At: --=-=-= Content-Type: text/plain Tags: patch Following the recent discussion on emacs-devel[0], I'm attaching a patch that adds a new Completion Preview minor mode. I've been testing this patch with different completion backends, and so far I'm pretty happy with the results. I've made some tweaks compared to the version I posted on emacs-devel to make the implementation more robust, and in particular to accommodate for some peculiarities of LSP-based completion, which I've tested mostly with `eglot` but also with `lsp-mode`. In terms of documentation, this patch extends "(emacs)Symbol Completion" with a description of Completion Preview mode. That seemed like a good place to discuss this mode, but let me know if somewhere else would be preferable. Another question is whether to have RET/TAB/both accept and insert the completion suggestion. In VS Code, both RET and TAB do that AFAICT. If we want to, we can easily do that too by binding RET/TAB in `completion-preview-active-mode-map`. Alternatively, we can just suggest doing so in the documentation. The current patch binds neither RET nor TAB, so to accept the suggestion you complete with C-M-i. Personally, I find that convenient enough, but I can imagine that many users might expect TAB or RET to work too. [0] https://lists.gnu.org/archive/html/emacs-devel/2023-10/msg00693.html --=-=-= Content-Type: text/patch; charset=utf-8 Content-Disposition: attachment; filename=0001-Add-Completion-Preview-mode.patch Content-Transfer-Encoding: quoted-printable >From 33966f20c97cbe24fb6e6be86fced26e51d392e4 Mon Sep 17 00:00:00 2001 From: Eshel Yaron Date: Thu, 2 Nov 2023 16:58:31 +0100 Subject: [PATCH] Add Completion Preview mode This adds a new minor mode, 'completion-preview-mode', that displays in-buffer completion suggestions with an inline "preview" overlay. * lisp/completion-preview.el: New file. * doc/emacs/programs.texi (Symbol Completion): Document it. * etc/NEWS: Announce it. --- doc/emacs/programs.texi | 61 ++++++++++++ etc/NEWS | 6 ++ lisp/completion-preview.el | 199 +++++++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 lisp/completion-preview.el diff --git a/doc/emacs/programs.texi b/doc/emacs/programs.texi index 7746bc8bc23..91104a3446c 100644 --- a/doc/emacs/programs.texi +++ b/doc/emacs/programs.texi @@ -1701,6 +1701,67 @@ Symbol Completion In Text mode and related modes, @kbd{M-@key{TAB}} completes words based on the spell-checker's dictionary. @xref{Spelling}. =20 +@cindex completion preview +@cindex preview completion +@cindex suggestion preview +@cindex Completion Preview mode +@findex completion-preview-mode +@vindex completion-preview-mode + Completion Preview mode is a minor mode that shows you symbol +completion suggestions as type. When you enable Completion Preview +mode in a buffer (with @kbd{M-x completion-preview-mode}), Emacs +examines the text around point after certain commands you invoke and +automatically suggests a possible completion. Emacs displays this +suggestion with an inline preview right after point, so you see in +advance exactly how the text will look if you accept the completion +suggestion---that's why it's called a preview. + +For example, suppose that you have an Emacs Lisp buffer with the +following code: + +@lisp +(defun doit (foobarbaz) + fo@point{} +@end lisp + +If you type another @samp{o}, the preview appears after point, +suggesting that you complete the text to @samp{foobarbaz}: + +@lisp +(defun doit (foobarbaz) + foo@point{}barbaz +@end lisp + +Here, the text @samp{barbaz} after point is the completion preview. +You can accept the completion suggestion with @kbd{M-@key{TAB}} to +actually inserts @samp{barbaz} and move point after it: + +@lisp +(defun doit (foobarbaz) + foobarbaz@point{} +@end lisp + +If you want to ignore a completion suggestion, just go on editing or +moving around the buffer. Completion Preview mode continues to update +the suggestion as you type according to the text around point. + +@vindex completion-preview-exact-match-only +@vindex completion-preview-commands +@vindex completion-preview-minimum-symbol-length +If you set the user option @code{completion-preview-exact-match-only} +to non-@code{nil}, Completion Preview mode only suggests a completion +candidate when its the only possible completion for the (partial) +symbol at point. The user option @code{completion-preview-commands} +says which commands should trigger the completion preview, by default +that is only @code{self-insert-command}, which corresponds to when you +type regular characters in the buffer. The user option +@code{completion-preview-minimum-symbol-length} specifies a minimum +number of consecutive characters with word or symbol syntax that +should appear around point for Emacs to suggest a completion. By +default, this option is set to 3, so Emacs suggests a completion if +you type @samp{foo}, but typing just @samp{fo} doesn't trigger +completion preview. + @node MixedCase Words @section MixedCase Words @cindex camel case diff --git a/etc/NEWS b/etc/NEWS index c06a013466f..22513ccf98f 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1020,6 +1020,12 @@ It highlights parens via =E2=80=98show-paren-mode=E2= =80=99 and =E2=80=98blink-matching-paren=E2=80=99 in a user-friendly way, avoids reporting alleged paren mismatches and makes sexp navigation more intuitive. =20 ++++ +*** New minor mode 'completion-preview-mode'. +This minor mode shows you symbol completion suggestions as you type, +using an inline preview. New user options in the 'completion-preview' +customization group control exactly when Emacs displays this preview. + --- ** The highly accessible Modus themes collection has eight items. The 'modus-operandi' and 'modus-vivendi' are the main themes that have diff --git a/lisp/completion-preview.el b/lisp/completion-preview.el new file mode 100644 index 00000000000..7a8ce05bbac --- /dev/null +++ b/lisp/completion-preview.el @@ -0,0 +1,199 @@ +;;; completion-preview.el --- Preview completion with inline overlay -*- = lexical-binding: t; -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; Author: Eshel Yaron +;; Maintainer: Eshel Yaron +;; Keywords: abbrev convenience + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This library provides the Completion Preview mode. This minor mode +;; displays the top completion candidate for the symbol at point in an +;; overlay after point. If you want to enable Completion Preview mode +;; in all programming modes, add the following to your Emacs init: +;; +;; (add-hook 'prog-mode-hook #'completion-preview-mode) +;; +;; Also check out the customization group `completion-preview` for +;; some user options that you may want to tweak. + +;;; Code: + +(defgroup completion-preview nil + "In-buffer completion preview." + :group 'completion) + +(defcustom completion-preview-exact-match-only nil + "Whether to show completion preview only when there is an exact match. + +If this option is non-nil, Completion Preview mode only shows the +preview overlay when there is exactly one completion candidate +that matches the symbol at point, otherwise it shows the top +candidate also when there are multiple matching candidates." + :type 'boolean) + +(defcustom completion-preview-commands '(self-insert-command) + "List of commands that should trigger completion preview." + :type '(repeat (function :tag "Command" :value self-insert-command))) + +(defcustom completion-preview-minimum-symbol-length 3 + "Minimum length of the symbol at point for showing completion preview." + :type 'natnum) + +(defcustom completion-preview-hook + '(completion-preview-require-certain-commands + completion-preview-require-minimum-symbol-length) + "Hook for functions that determine whether to show preview completion. + +Completion Preview mode calls each of these functions in order +after each command, and only displays the completion preview when +all of the functions return non-nil." + :type 'hook) + +(defvar completion-preview-sort-function #'minibuffer--sort-by-length-alpha + "Sort function to use for choosing a completion candidate to preview.") + +(defface completion-preview + '((t :inherit shadow)) + "Face for completion preview overlay.") + +(defface completion-preview-exact + '((t :underline t :inherit completion-preview)) + "Face for exact completion preview overlay.") + +(defvar-local completion-preview--overlay nil) + +(defvar-local completion-preview--skip nil) + +(defun completion-preview-require-certain-commands () + "Check if `this-command' is one of `completion-preview-commands'." + (memq this-command completion-preview-commands)) + +(defun completion-preview-require-minimum-symbol-length () + "Check if the length of symbol at point is at least above a certain thre= shold. +`completion-preview-minimum-symbol-length' determines that threshold." + (pcase (bounds-of-thing-at-point 'symbol) + (`(,beg . ,end) + (<=3D completion-preview-minimum-symbol-length (- end beg))))) + +(defun completion-preview-hide () + "Hide the completion preview." + (when completion-preview--overlay + (delete-overlay completion-preview--overlay) + (setq completion-preview--overlay nil))) + +(defun completion-preview--make-overlay (pos string) + "Make a new completion preview overlay at POS showing STRING." + (completion-preview-hide) + (add-text-properties 0 1 '(cursor 1) string) + (setq completion-preview--overlay (make-overlay pos pos)) + (overlay-put completion-preview--overlay 'after-string string) + completion-preview--overlay) + +(define-minor-mode completion-preview-active-mode + "Mode for when the completion preview is active." + :interactive nil + (if completion-preview-active-mode + (add-hook 'completion-at-point-functions #'completion-preview-insert= -1 t) + (remove-hook 'completion-at-point-functions #'completion-preview-inser= t t) + (completion-preview-hide))) + +(defun completion-preview--exit-function (func) + "Return an exit function that hides the completion preview and calls FUN= C." + (lambda (&rest args) + (completion-preview-active-mode -1) + (when func (apply func args)))) + +(defun completion-preview--update () + "Update completion preview." + (pcase (let ((completion-preview--skip t)) + (run-hook-with-args-until-success 'completion-at-point-function= s)) + (`(,beg ,end ,table . ,plist) + (let* ((pred (plist-get plist :predicate)) + (exit-fn (completion-preview--exit-function + (plist-get plist :exit-function))) + (string (buffer-substring beg end)) + (md (completion-metadata string table pred)) + (sort-fn (or (completion-metadata-get md 'cycle-sort-function) + (completion-metadata-get md 'display-sort-functio= n) + completion-preview-sort-function)) + (all (completion-all-completions string table pred + (- (point) beg) md)) + (last (last all)) + (base (or (cdr last) 0)) + (bbeg (+ beg base)) + (prefix (substring string base))) + (when last + (setcdr last nil) + (let* ((filtered + (seq-filter (apply-partially #'string-prefix-p prefix) al= l)) + (sorted (funcall sort-fn filtered)) + (multi (cadr sorted)) ; multiple candidates + (cand (car sorted))) + (when (and cand (not (and multi completion-preview-exact-match-= only))) + (let* ((face (if multi 'completion-preview 'completion-previe= w-exact)) + (after (propertize (substring cand (length prefix)) 'f= ace face))) + (unless (string-empty-p after) + (overlay-put (completion-preview--make-overlay end after) + 'completion-preview-data + (list bbeg end (list cand) + :exit-function exit-fn)) + (completion-preview-active-mode)))))))))) + +(defun completion-preview--show () + "Show completion preview." + (when completion-preview-active-mode + (let* ((data (overlay-get completion-preview--overlay 'completion-prev= iew-data)) + (beg (car data)) + (cands (caddr data)) + (cand (car cands)) + (plist (cdddr data)) + (len (length cand)) + (end (+ beg len)) + (after (overlay-get completion-preview--overlay 'after-string)) + (face (get-text-property 0 'face after))) + (if (and (< beg (point) end) + (string-prefix-p (buffer-substring beg (point)) cand)) + (overlay-put + (completion-preview--make-overlay + (point) (propertize (substring cand (- (point) beg)) 'face fac= e)) + 'completion-preview-data (append (list beg (point) cands) plist= )) + (completion-preview-active-mode -1)))) + (while-no-input (completion-preview--update))) + +(defun completion-preview--post-command () + "Create, update or delete completion preview post last command." + (if (run-hook-with-args-until-failure 'completion-preview-hook) + (completion-preview--show) + (completion-preview-active-mode -1))) + +(defun completion-preview-insert () + "Completion at point function for inserting the current preview." + (when (and completion-preview-active-mode (not completion-preview--skip)) + (overlay-get completion-preview--overlay 'completion-preview-data))) + +;;;###autoload +(define-minor-mode completion-preview-mode + "Show in-buffer completion preview as you type." + :lighter " CP" + (if completion-preview-mode + (add-hook 'post-command-hook #'completion-preview--post-command nil = t) + (remove-hook 'post-command-hook #'completion-preview--post-command t) + (completion-preview-active-mode -1))) + +(provide 'completion-preview) +;;; completion-preview.el ends here --=20 2.42.0 --=-=-=--