Abbrev suggestions helps users remember to use defined abbrevs

* lisp/abbrev.el (abbrev-suggest): New defcustom.
    (abbrev-suggest-hint-threshold): New defcustom.
    (abbrev--suggest-get-active-tables-including-parents): New defun.
    (abbrev--suggest-get-active-abbrev-expansions): New defun.
    (abbrev--suggest-count-words): New defun.
    (abbrev--suggest-get-previous-words): New defun.
    (abbrev--suggest-above-threshold): New defun.
    (abbrev--suggest-saved-recommendations): New defvar.
    (abbrev--suggest-inform-user): New defun.
    (abbrev--suggest-shortest-abbrev): New defun.
    (abbrev--suggest-maybe-suggest): New defun.
    (abbrev--suggest-get-totals): New defun.
    (abbrev-suggest-show-report): New defun.
    (expand-abbrev): If the previous word was not an abbrev, maybe
    suggest an abbrev to the user.
    * doc/emacs/abbrevs.texi (Abbrev suggestions): New section.
    * etc/NEWS: Announce abbrev suggestions.
This commit is contained in:
Mathias Dahl 2020-09-26 15:03:58 -04:00
parent e7012148c0
commit f43d9d94aa
3 changed files with 181 additions and 1 deletions

View file

@ -28,6 +28,7 @@ Automatic Typing}.
* Abbrev Concepts:: Fundamentals of defined abbrevs.
* Defining Abbrevs:: Defining an abbrev, so it will expand when typed.
* Expanding Abbrevs:: Controlling expansion: prefixes, canceling expansion.
* Abbrevs Suggestions:: Get suggestions about defined abbrevs.
* Editing Abbrevs:: Viewing or editing the entire list of defined abbrevs.
* Saving Abbrevs:: Saving the entire list of abbrevs for another session.
* Dynamic Abbrevs:: Abbreviations for words already in the buffer.
@ -223,6 +224,37 @@ changing this function you can make arbitrary changes to
the abbrev expansion. @xref{Abbrev Expansion,,, elisp, The Emacs Lisp
Reference Manual}.
@node Abbrev Suggestions
@section Abbrev Suggestions
You can get abbrev suggestions when you manually type text for which
there is currently an active defined abbrev. For example, if there is
an abbrev @samp{foo} with the expansion @samp{find outer otter}, and
you manually type @samp{find outer otter}, the abbrev suggestion
feature will notice this and show a hint in the echo area when you
have stopped typing.
@vindex abbrev-suggest
Enable the abbrev suggestion feature by setting
@code{abbrev-suggest} to @code{t}.
@vindex abbrev-suggest-hint-threshold
Controls when to suggest an abbrev to the user. The variable
defines the number of characters that the user must save in order to
get a suggestion. For example, if the user types @samp{foo bar}
(seven characters) and there is an abbrev @samp{fubar} defined (five
characters), the user will not get any suggestion unless the threshold
is set to the number 2 or lower. With the default value 3, the user
would not get any suggestion, because the savings in using the abbrev
are not above the threshold. If you always want to get abbrev
suggestions, set this variable to 0.
@findex abbrev-suggest-show-report
The command @code{abbrev-suggest-show-report} can be used to show a
buffer with all abbrev suggestions from the current editing session.
This can be useful if you get several abbrev suggestions and don't
remember them all.
@node Editing Abbrevs
@section Examining and Editing Abbrevs

View file

@ -1271,6 +1271,16 @@ This value customizes Emacs to use the style recommended in Damian
Conway's book "Perl Best Practices" for indentation and formatting
of conditionals.
** Abbrev mode
+++
*** Abbrev can now suggest pre-existing abbrevs based on typed text.
A new user option, 'abbrev-suggest', enables the new abbrev suggestion
feature. When enabled, if a user manually type a piece of text that
could have been written by using an abbrev, a hint will be displayed
in the echo area, mentioning the abbrev that could have been used
instead.
* New Modes and Packages in Emacs 28.1

View file

@ -824,6 +824,142 @@ see `define-abbrev' for details."
"Function that `expand-abbrev' uses to perform abbrev expansion.
Takes no argument and should return the abbrev symbol if expansion took place.")
(defcustom abbrev-suggest nil
"Non-nil means suggest abbrevs to the user.
By enabling this option, if abbrev mode is enabled and if the
user has typed some text that exists as an abbrev, suggest to the
user to use the abbrev by displaying a message in the echo area."
:type 'boolean
:version "28.1")
(defcustom abbrev-suggest-hint-threshold 3
"Threshold for when to inform the user that there is an abbrev.
The threshold is the number of characters that differ between the
length of the abbrev and the length of the expansion. The
thinking is that if the expansion is only one or a few characters
longer than the abbrev, the benefit of informing the user is not
that big. If you always want to be informed, set this value to
`0' or less. This setting only applies if `abbrev-suggest' is
non-nil."
:type 'number
:version "28.1")
(defun abbrev--suggest-get-active-tables-including-parents ()
"Return a list of all active abbrev tables, including parent tables."
(let* ((tables (abbrev--active-tables))
(all tables))
(dolist (table tables)
(setq all (append (abbrev-table-get table :parents) all)))
all))
(defun abbrev--suggest-get-active-abbrev-expansions ()
"Return a list of all the active abbrev expansions.
Includes expansions from parent abbrev tables."
(let (expansions)
(dolist (table (abbrev--suggest-get-active-tables-including-parents))
(mapatoms (lambda (e)
(let ((value (symbol-value (abbrev--symbol e table))))
(when value
(push (cons value (symbol-name e)) expansions))))
table))
expansions))
(defun abbrev--suggest-count-words (expansion)
"Return the number of words in EXPANSION.
Expansion is a string of one or more words."
(length (split-string expansion " " t)))
(defun abbrev--suggest-get-previous-words (n)
"Return the N words before point, spaces included."
(let ((end (point)))
(save-excursion
(backward-word n)
(replace-regexp-in-string
"\\s " " "
(buffer-substring-no-properties (point) end)))))
(defun abbrev--suggest-above-threshold (expansion)
"Return non-nil if the abbrev in EXPANSION provides significant savings.
A significant saving, here, is the difference in length between
the abbrev and the abbrev expansion. EXPANSION is a cons cell
where the car is the expansion and the cdr is the abbrev."
(>= (- (length (car expansion))
(length (cdr expansion)))
abbrev-suggest-hint-threshold))
(defvar abbrev--suggest-saved-recommendations nil
"Keeps a list of expansions that have abbrevs defined.
The user can show this list by calling
`abbrev-suggest-show-report'.")
(defun abbrev--suggest-inform-user (expansion)
"Display a message to the user about the existing abbrev.
EXPANSION is a cons cell where the `car' is the expansion and the
`cdr' is the abbrev."
(run-with-idle-timer
1 nil
(lambda ()
(message "You can write `%s' using the abbrev `%s'."
(car expansion) (cdr expansion))))
(push expansion abbrev--suggest-saved-recommendations))
(defun abbrev--suggest-shortest-abbrev (new current)
"Return the shortest abbrev of NEW and CURRENT.
NEW and CURRENT are cons cells where the `car' is the expansion
and the `cdr' is the abbrev."
(if (not current)
new
(if (< (length (cdr new))
(length (cdr current)))
new
current)))
(defun abbrev--suggest-maybe-suggest ()
"Suggest an abbrev to the user based on the word(s) before point.
Uses `abbrev-suggest-hint-threshold' to find out if the user should be
informed about the existing abbrev."
(let (words abbrev-found word-count)
(dolist (expansion (abbrev--suggest-get-active-abbrev-expansions))
(setq word-count (abbrev--suggest-count-words (car expansion))
words (abbrev--suggest-get-previous-words word-count))
(let ((case-fold-search t))
(when (and (> word-count 0)
(string-match (car expansion) words)
(abbrev--suggest-above-threshold expansion))
(setq abbrev-found (abbrev--suggest-shortest-abbrev
expansion abbrev-found)))))
(when abbrev-found
(abbrev--suggest-inform-user abbrev-found))))
(defun abbrev--suggest-get-totals ()
"Return a list of all expansions and how many times they were used.
Each expansion is a cons cell where the `car' is the expansion
and the `cdr' is the number of times the expansion has been
typed."
(let (total cell)
(dolist (expansion abbrev--suggest-saved-recommendations)
(if (not (assoc (car expansion) total))
(push (cons (car expansion) 1) total)
(setq cell (assoc (car expansion) total))
(setcdr cell (1+ (cdr cell)))))
total))
(defun abbrev-suggest-show-report ()
"Show the user a report of abbrevs he could have used."
(interactive)
(let ((totals (abbrev--suggest-get-totals))
(buf (get-buffer-create "*abbrev-suggest*")))
(set-buffer buf)
(erase-buffer)
(insert "** Abbrev expansion usage **
Below is a list of expansions for which abbrevs are defined, and
the number of times the expansion was typed manually. To display
and edit all abbrevs, type `M-x edit-abbrevs RET'\n\n")
(dolist (expansion totals)
(insert (format " %s: %d\n" (car expansion) (cdr expansion))))
(display-buffer buf)))
(defun expand-abbrev ()
"Expand the abbrev before point, if there is an abbrev there.
Effective when explicitly called even when `abbrev-mode' is nil.
@ -831,7 +967,9 @@ Calls the value of `abbrev-expand-function' with no argument to do
the work, and returns whatever it does. (That return value should
be the abbrev symbol if expansion occurred, else nil.)"
(interactive)
(funcall abbrev-expand-function))
(or (funcall abbrev-expand-function)
(if abbrev-suggest
(abbrev--suggest-maybe-suggest))))
(defun abbrev--default-expand ()
"Default function to use for `abbrev-expand-function'.