Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add `selectrum-update-candidates-function' #27

Merged
merged 2 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog].

## Unreleased
### Breaking changes
* The way to dynamically generate the candidate list has changed.
Instead of rebinding `selectrum-preprocess-candidates-function` and
`selectrum-refine-candidates-function`, you simply pass a function
as the COLLECTION argument to `selectrum-read`. This function takes
one argument, the current user input, and returns the list of
candidates as strings. Alternatively, it can return an alist whose
`candidates` key is the candidate list and whose `input` key is a
transformed user input to use for highlighting.

As part of this change, `selectrum-refine-candidates-function` no
longer can return an alist; that functionality should instead be
moved to the CANDIDATES function. (This feature was never properly
supported in the first place if you tried to use it in a way that
can't be done equivalently in the CANDIDATES function.)

See [#27].

### Enhancements
* In `read-file-name`, when a default is provided (for example in the
`dired-do-rename` command), we actually use it as the initial
Expand All @@ -26,11 +44,12 @@ The format is based on [Keep a Changelog].
in accordance with the `completing-read` API ([#34]).

[#25]: https://github.com/raxod502/selectrum/pull/25
[#27]: https://github.com/raxod502/selectrum/pull/27
[#30]: https://github.com/raxod502/selectrum/issues/30
[#31]: https://github.com/raxod502/selectrum/issues/31
[#32]: https://github.com/raxod502/selectrum/issues/32
[#33]: https://github.com/raxod502/selectrum/issues/33
[#34]: https://github.com/raxod502/selectrum/issues/34
[#33]: https://github.com/raxod502/selectrum/pull/33
[#34]: https://github.com/raxod502/selectrum/pull/34

## 1.0 (released 2020-03-23)
### Added
Expand Down
26 changes: 10 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,9 @@ its public API some of the functions that wrap `selectrum-read`:
* `selectrum-read-library-name` (to override `read-library-name`)

You can use these functions in defining variants of Selectrum-based
commands.
commands. If you need to do something more complicated than just
calling one of these functions with special options, then define your
own function and call `selectrum-read` directly.

### Sorting, filtering, and highlighting

Expand All @@ -296,12 +298,13 @@ user option:

* `selectrum-preprocess-candidates-function` takes the original list
of candidates and sorts it (actually, it can do any sort of
preprocessing it wants). This preprocessing only happens once.
preprocessing it wants). Usually preprocessing only happens once.
However, if a function is passed to `selectrum-read` to generate the
candidate list dynamically based on the user input, then
preprocessing happens instead after each input change.
* `selectrum-refine-candidates-function` takes the preprocessed list
and filters it using the user's input (actually, it can produce the
final list of candidates however it wants, including generating it
on the fly). This refinement happens every time the user input is
updated.
and filters it using the user's input. This refinement happens every
time the user input is updated.
* `selectrum-highlight-candidates-function` takes a list of the
refined candidates that are going to be displayed in the minibuffer,
and propertizes them with highlighting.
Expand Down Expand Up @@ -342,19 +345,10 @@ which may be applied to candidates using `propertize`:
Note that sorting, filtering, and highlighting is done on the standard
values of candidates, before any of these text properties are handled.

There is one final detail: the `selectrum-refine-candidates-function`
may return, in addition to the refined list of candidates, a
transformed user input which will be used for highlighting (for
`find-file`, this is the basename of the file in the user input).

To really understand how these pieces work together, it is best to
inspect the source code of `selectrum-read-buffer` and
`selectrum-read-file-name` (an effort has been made to make the code
readable). Note that both of these functions operate by temporarily
rebinding `selectrum-candidate-preprocess-function` and
`selectrum-candidate-refine-function` in order to generate candidates
on the fly and then sort and filter them using the original values of
these functions.
readable).

### Hooks

Expand Down
169 changes: 90 additions & 79 deletions selectrum.el
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,12 @@ INPUT is a string, CANDIDATES is a list of strings."
#'selectrum-default-candidate-refine-function
"Function used to decide which candidates should be displayed.
Receives two arguments, the user input (a string) and the list of
candidates (strings) as returned by
`selectrum-preprocess-candidates-function'. Returns a new list of
candidates. Should not modify the input list. The returned list
may be modified by Selectrum, so a copy of the input should be
made. (Beware that `cl-remove-if' doesn't make a copy if there's
nothing to remove.)

Instead of a list of strings, may alternatively return an alist
with the following keys:
- `candidates': list of strings, as above
- `input' (optional): transformed user input, used for
highlighting"
candidates (strings).

Returns a new list of candidates. Should not modify the input
list. The returned list may be modified by Selectrum, so a copy
of the input should be made. (Beware that `cl-remove-if' doesn't
make a copy if there's nothing to remove.)"
:type 'function)

(defun selectrum-default-candidate-preprocess-function (candidates)
Expand Down Expand Up @@ -315,17 +309,29 @@ This is used to prevent point from moving into the candidates.")

(defvar selectrum--preprocessed-candidates nil
"Preprocessed list of candidates.
This is derived from the collection passed to `selectrum-read'
just once, and is subsequently passed to
`selectrum-preprocess-candidates-function' every time the user
input changes in order to generate
This is derived from the argument passed to `selectrum-read'. If
the collection is a list it is processed once by
`selectrum-preprocess-candidates-function' and saved in this
variable. If the collection is a function then that function is
stored in this variable instead, so that it can be called to get
a new list of candidates every time the input changes. (See
`selectrum-read' for more details on function collections.)

With a standard candidate list, the value of this variable is
subsequently passed to `selectrum-refine-candidates-function'.
With a dynamic candidate list (generated by a function), then the
returned list is subsequently passed also through
`selectrum-preprocess-candidates-function' each time the user
input changes. Either way, the results end up in
`selectrum--refined-candidates'.")

(defvar selectrum--refined-candidates nil
"Refined list of candidates to be displayed.
This is derived from `selectrum--preprocessed-candidates' by
`selectrum-refine-candidates-function' every time the user input
changes, and is subsequently passed to
`selectrum-refine-candidates-function' (and, in the case of a
dynamic candidate list, also
`selectrum-preprocess-candidates-function') every time the user
input changes, and is subsequently passed to
`selectrum-highlight-candidates-function'.")

(defvar selectrum--current-candidate-index nil
Expand Down Expand Up @@ -387,14 +393,21 @@ Passed to various hook functions.")
;; Reset the persistent input, so that it will be nil if
;; there's no special attention needed.
(setq selectrum--visual-input nil)
(let ((result (funcall selectrum-refine-candidates-function
input selectrum--preprocessed-candidates)))
(if (stringp (car result))
(setq selectrum--refined-candidates result)
(setq selectrum--refined-candidates
(alist-get 'candidates result))
(setq input (or (alist-get 'input result) input))
(setq selectrum--visual-input input)))
(let ((cands (if (functionp selectrum--preprocessed-candidates)
(funcall selectrum-preprocess-candidates-function
(let ((result
(funcall
selectrum--preprocessed-candidates
input)))
(if (stringp (car result))
result
(setq input (or (alist-get 'input result)
input))
(setq selectrum--visual-input input)
(alist-get 'candidates result))))
selectrum--preprocessed-candidates)))
(setq selectrum--refined-candidates
(funcall selectrum-refine-candidates-function input cands)))
(setq selectrum--refined-candidates
(selectrum--move-to-front-destructive
selectrum--default-candidate
Expand Down Expand Up @@ -500,7 +513,9 @@ provided, rather than providing one of their own."
(setq selectrum--end-of-input-marker (point-marker))
(set-marker-insertion-type selectrum--end-of-input-marker t)
(setq selectrum--preprocessed-candidates
(funcall selectrum-preprocess-candidates-function candidates))
(if (functionp candidates)
candidates
(funcall selectrum-preprocess-candidates-function candidates)))
(setq selectrum--default-candidate default-candidate)
;; Make sure to trigger an "user input changed" event, so that
;; candidate refinement happens in `post-command-hook' and an index
Expand Down Expand Up @@ -635,15 +650,27 @@ ignores the currently selected candidate, if one exists."
(cl-defun selectrum-read
(prompt candidates &rest args &key
default-candidate initial-input require-match)
"Prompt user with PROMPT to select one of CANDIDATES, list of strings.
Return the selected string. PROMPT should generally end in a
colon and space. Additional keyword ARGS are accepted.
DEFAULT-CANDIDATE, if provided, is sorted first in the list if
it's present. INITIAL-INPUT, if provided, is inserted into the
user input area initially (with point at the end). REQUIRE-MATCH,
if non-nil, means the user must select one of the listed
candidates (so, for example, \\[selectrum-submit-exact-input] has
no effect)."
"Prompt user with PROMPT to select one of CANDIDATES.
Return the selected string.

CANDIDATES is a list of strings or a function to dynamically
generate them. If CANDIDATES is a function, then it receives one
argument, the current user input, and returns the list of
strings.

Instead of a list of strings, the function may alternatively
return an alist with the following keys:
- `candidates': list of strings, as above.
- `input' (optional): transformed user input, used for
highlighting (see `selectrum-highlight-candidates-function').

PROMPT should generally end in a colon and space. Additional
keyword ARGS are accepted. DEFAULT-CANDIDATE, if provided, is
sorted first in the list if it's present. INITIAL-INPUT, if
provided, is inserted into the user input area initially (with
point at the end). REQUIRE-MATCH, if non-nil, means the user must
select one of the listed candidates (so, for example,
\\[selectrum-submit-exact-input] has no effect)."
(setq selectrum--read-args (cl-list* prompt candidates args))
(let ((keymap (make-sparse-keymap)))
;; Use `map-apply' instead of `map-do' as the latter is not
Expand Down Expand Up @@ -709,38 +736,30 @@ less appropriate. It also allows you to view hidden buffers,
which is otherwise impossible due to tricky behavior of Emacs'
completion machinery. For PROMPT, DEF, REQUIRE-MATCH, and
PREDICATE, see `read-buffer'."
(let* ((selectrum-should-sort-p nil)
(orig-preprocess-function selectrum-preprocess-candidates-function)
(orig-refine-function selectrum-refine-candidates-function)
(selectrum-preprocess-candidates-function #'ignore)
(selectrum-refine-candidates-function
(lambda (input _)
(let* ((buffers (mapcar #'buffer-name (buffer-list)))
(candidates (if predicate
(cl-delete-if-not predicate buffers)
buffers)))
(if (string-prefix-p " " input)
(progn
(setq input (substring input 1))
(setq candidates
(cl-delete-if-not
(lambda (name)
(string-prefix-p " " name))
candidates)))
(setq candidates
(cl-delete-if
(lambda (name)
(string-prefix-p " " name))
candidates)))
`((candidates . ,(funcall
orig-refine-function
input
(funcall
orig-preprocess-function
candidates)))
(input . ,input))))))
(let ((selectrum-should-sort-p nil)
(candidates
(lambda (input)
(let* ((buffers (mapcar #'buffer-name (buffer-list)))
(candidates (if predicate
(cl-delete-if-not predicate buffers)
buffers)))
(if (string-prefix-p " " input)
(progn
(setq input (substring input 1))
(setq candidates
(cl-delete-if-not
(lambda (name)
(string-prefix-p " " name))
candidates)))
(setq candidates
(cl-delete-if
(lambda (name)
(string-prefix-p " " name))
candidates)))
`((candidates . ,candidates)
(input . ,input))))))
(selectrum-read
prompt nil
prompt candidates
:default-candidate def
:require-match require-match)))

Expand All @@ -756,11 +775,8 @@ PREDICATE, see `read-file-name'."
(let* ((minibuffer-completing-file-name t)
(dir (file-name-as-directory
(expand-file-name (or dir default-directory))))
(orig-preprocess-function selectrum-preprocess-candidates-function)
(orig-refine-function selectrum-refine-candidates-function)
(selectrum-preprocess-candidates-function #'ignore)
(selectrum-refine-candidates-function
(lambda (input _)
(candidates
(lambda (input)
(let* ((new-input (file-name-nondirectory input))
(dir (or (file-name-directory input) dir))
(entries (selectrum--map-destructive
Expand Down Expand Up @@ -793,15 +809,10 @@ PREDICATE, see `read-file-name'."
;; May happen in case user quits out
;; of a TRAMP prompt.
(quit))))))
`((candidates . ,(funcall
orig-refine-function
new-input
(funcall
orig-preprocess-function
entries)))
`((candidates . ,entries)
(input . ,new-input))))))
(selectrum-read
prompt nil
prompt candidates
:default-candidate (when-let ((default (or initial default-filename)))
(file-name-nondirectory
(directory-file-name default)))
Expand Down