Skip to content

Commit

Permalink
Merge remote-tracking branch 'gh/review-prompter-internals'
Browse files Browse the repository at this point in the history
  • Loading branch information
aadcg committed Nov 18, 2022
2 parents 3ae2229 + 51e5933 commit 7912fa0
Show file tree
Hide file tree
Showing 27 changed files with 233 additions and 197 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ Nyxt.app
_build/submodules/system-index.txt

# Ignore PNG and XCF
*.png
*.xcf
*.png
# Except for this foler
!libraries/prompter/*.png

# Ignore compiled lisp files
*.FASL
Expand Down
38 changes: 36 additions & 2 deletions libraries/prompter/README.org
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
This prompter library is heavily inspired by Emacs' minibuffer and [[https://emacs-helm.github.io/helm/][Helm]].
* Class overview

It only deals with the backend side of things, it does not handle any display.
The key objects are =prompters=, =sources= and =suggestions=.

A =prompt= is an interface for user interactions that holds one or more
=sources=, and each of those are populated by =suggestions=.

Other central concepts include:

- =prompt=
+ =selection= :: A single =suggestion=, in any. Think of it as the currently
selected =suggestion=, belonging to a =source=.
- =source=
+ =marks= :: A list of =suggestions=, when =multi-selection-p= is non-nil.
+ =actions= :: A list of functions that run on =suggestions=.
- =return-actions= :: General purpose.
- =marks-actions= :: On =marks= change (event-driven).
- =selection-actions= :: On =selection= change (event-driven).

Example: Find below a graphical visualization of a single prompt with sources 1
and 2, and suggestions A, B, C and D. The =marks= is the list composed by
Suggestions A and C. The =selection= is Suggestion B.

[[file:example.png]]

Remarks:

A =prompt= always has a single =selection=, whereas a =source= has either a
single =selection= or none (when =prompt= has multiple =sources=).

=marks= is a concept related to =source= not =prompt=, unlike that of
=selection=.

* Features

Non-exhaustive list of features:

Expand All @@ -17,3 +48,6 @@ Non-exhaustive list of features:
- Selection actions (automatically run persistent actions on selection change).
- Marks actions (automatically run persistent actions on marks change).
- Automatically return the prompt when narrowed down to a single suggestion.

This library is heavily inspired by Emacs' minibuffer and [[https://emacs-helm.github.io/helm/][Helm]]. It only deals
with the backend side of things, it does not handle any display.
Binary file added libraries/prompter/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 12 additions & 6 deletions libraries/prompter/prompter-source.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,17 @@ recompute the match-data for instance.")
The predicate works the same as the `sort' predicate.")

(return-actions
'(identity)
:type list
#'identity
:type (or null
(or function function-symbol)
(cons (or function function-symbol) *))
:accessor nil
:export nil
:documentation "List of funcallables that can be run on `suggestion's of
this source. This is the low-level implementation, see the `return-actions'
function for the public interface.")
function for the public interface.
For convenience, it may be initialized with a single function or symbol, in
which case it will be automatically turned into a list.")

(update-notifier
(make-channel)
Expand Down Expand Up @@ -275,7 +279,7 @@ to compute asynchronously.")
(multi-selection-p
nil
:type boolean
:documentation "Whether multiple candidates can be marked.")
:documentation "Whether multiple `suggestion's can be marked.")

(resumer
nil
Expand Down Expand Up @@ -310,7 +314,7 @@ Also see `selection-actions-enabled-p'."))
(:accessor-name-transformer (class*:make-name-transformer name))
(:documentation "A prompter source instance is meant to be used by a
`prompter' object. See its `sources' slot. A source is a consistent collection
of suggestions, filters, return-actions.
of suggestions, filters and actions.
When a `prompter' `input' is set, the `update' function is called over all
sources. This function pipelines `initial-suggestions' through
Expand All @@ -331,7 +335,7 @@ call."))
(run-thread "Prompter mark action thread" (funcall action (marks source)))))

(defmethod default-selection-action ((source prompter:source))
(first (slot-value source 'selection-actions)))
(first (selection-actions source)))

(export-always 'object-attributes)
(defgeneric object-attributes (object source)
Expand Down Expand Up @@ -626,6 +630,8 @@ If you are looking for a source that just returns its plain suggestions, use `so
(calispel:? wait-channel))
(setf (selection-actions source) (uiop:ensure-list (selection-actions source)))
(setf (marks-actions source) (uiop:ensure-list (marks-actions source)))
(setf (slot-value source 'return-actions)
(uiop:ensure-list (slot-value source 'return-actions)))
source)

(export-always 'attributes-keys-non-default)
Expand Down
153 changes: 71 additions & 82 deletions libraries/prompter/prompter.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ A new object is created on every new input."))
(the (values cl-containers:ring-buffer-reverse &optional)
(containers:make-ring-buffer size :last-in-first-out)))

;; Same as `source' as to why we wrap in `eval-always'.
;; Eval at read-time because `make' is generated using the class' initargs.
(sera:eval-always
(define-class prompter ()
((input
Expand Down Expand Up @@ -97,8 +97,7 @@ when the suggestions are narrowed down to just one item.")
(history
(make-history)
:type (or containers:ring-buffer-reverse null)
:documentation
"History of inputs for the prompter.
:documentation "History of inputs for the prompter.
If nil, no history is used.")

(result-channel
Expand Down Expand Up @@ -127,16 +126,16 @@ See also `result-channel'.")
(:export-accessor-names-p t)
(:accessor-name-transformer (class*:make-name-transformer name))
(:documentation "The prompter is an interface for user interactions.
A prompter object holds multiple sources (of type `source') which
contain a list of `suggestion's.
A prompter object holds multiple `source's which contain a list of
`suggestion's.
You can call `destroy' to call the registered termination functions of the
prompter and its sources.
Call `destroy' to the register termination functions of the prompter and its
sources.
Suggestions are computed asynchronously when `input' is updated.
Use `all-ready-p' and `next-ready-p' to know when the prompter is ready.
Sources suggestions can be retrieved, possibly partially, even when the
compution is not finished.")))
`suggestion's are computed asynchronously when `input' is updated.
Use `all-ready-p' and `next-ready-p' to access whether the prompter is ready.
Sources' suggestions can be retrieved, possibly partially, even when the
computation is not finished.")))

(defun update-sources (prompter &optional (text ""))
(setf (sync-queue prompter) (make-instance 'sync-queue))
Expand Down Expand Up @@ -367,34 +366,33 @@ Empty sources are skipped."
(union marks suggestion-values)
(intersection marks suggestion-values))))))))))

(defun resolve-selection (prompter) ; TODO: Write tests for this!
"Return the list of selected values.
If there is no marks, the current selection value is returned as a list of one element.
For instance, if the selected element value is NIL, this returns '(NIL).
If there is no element, NIL is returned."
(defun resolve-marks (prompter) ; TODO: Write tests for this!
"Return the list of marked `suggestion's.
When `marks' is nil, the current selection value is returned as a list of one
element.
For instance, if the selected element value is NIL, this returns '(NIL). If
there is no element, NIL is returned."
(or (all-marks prompter)
(mapcar #'value (uiop:ensure-list (selected-suggestion prompter)))))

(export-always 'return-actions)
(defun return-actions (prompter)
"Return the list of contextual return-actions.
Without marks, it's the list of return-actions for the current source.
With marks, it's the intersection of the return-actions of the sources that contain the
marked elements."
(alex:if-let ((marked-sources
(remove-if (complement #'marks) (sources prompter))))
(reduce #'intersection (mapcar (lambda (source)
(slot-value source 'return-actions))
marked-sources))
"Return the list of contextual `return-actions'.
When `marks' is non-nil, return the list of `return-actions' shared by every
marked element; otherwise return the list of `return-actions' for the current
`source'."
(alex:if-let ((marked-sources (remove-if (complement #'marks) (sources prompter))))
(reduce #'intersection
(mapcar (lambda (source) (slot-value source 'return-actions))
marked-sources))
(slot-value (selected-source prompter) 'return-actions)))

(defun history-pushnew (history element &key (test #'equal) )
(alex:when-let ((previous-element-index (containers:element-position
history
element
:test test)))
(containers:delete-item-at history
previous-element-index))
(alex:when-let ((previous-element-index (containers:element-position history
element
:test test)))
(containers:delete-item-at history previous-element-index))
(containers:insert-item history element))


Expand All @@ -409,17 +407,14 @@ If input is already in history, move to first position."
(export-always 'return-selection)
(defun return-selection (prompter
&optional (return-action (default-return-action prompter)))
"Call RETURN-ACTION over selection and send the results to PROMPTER's `result-channel'.
The selection is the collection of marked suggestions across all sources.
If there is no marked suggestion, send the currently selected suggestion
instead."
(unless return-action
(setf return-action #'identity))
"Call RETURN-ACTION over `marks' and send the results to PROMPTER's `result-channel'.
See `resolve-marks' for a reference on how `marks' are handled."
(unless return-action (setf return-action #'identity))
(setf (returned-p prompter) t)
(add-input-to-history prompter)
(alex:when-let ((selection-values (resolve-selection prompter)))
(let ((return-action-result (funcall return-action selection-values)))
(calispel:! (result-channel prompter) return-action-result)))
(alex:when-let ((marks (resolve-marks prompter)))
(calispel:! (result-channel prompter)
(funcall return-action marks)))
(destroy prompter))

(export-always 'toggle-selection-actions-enabled)
Expand All @@ -429,14 +424,11 @@ instead."
(setf (selection-actions-enabled-p source) (not (selection-actions-enabled-p source))))

(export-always 'next-ready-p)
(defun next-ready-p (prompter &optional timeout)
(defun next-ready-p (prompter)
"Block and return next PROMPTER ready source.
It's the next source that's done updating.
If all sources are done, return T.
This is unblocked when the PROMPTER is `destroy'ed.
TIMEOUT is deprecated."
(declare (ignore timeout)) ; Deprecated.
This is unblocked when the PROMPTER is `destroy'ed."
(when prompter
;; We let-bind `sync-queue' here so that it remains the same object throughout
;; this function, since the slot is subject to be changed concurrently when
Expand All @@ -445,46 +437,41 @@ TIMEOUT is deprecated."
(if (= (length (ready-sources sync-queue))
(length (sources prompter)))
t
(progn
(calispel:fair-alt
((calispel:? (ready-channel sync-queue) next-source)
(cond
((null next-source)
nil)
(t
(push next-source (ready-sources sync-queue))
;; Update selection when update is done:
(select-first prompter)
next-source)))
((calispel:? (sync-interrupt-channel sync-queue))
nil))))
(calispel:fair-alt
((calispel:? (ready-channel sync-queue) next-source)
(cond
((null next-source)
nil)
(t
(push next-source (ready-sources sync-queue))
;; Update selection when update is done:
(select-first prompter)
next-source)))
((calispel:? (sync-interrupt-channel sync-queue))
nil)))
;; No sync-queue if no input was ever set.
t)))

(export-always 'all-ready-p)
(defun all-ready-p (prompter &optional timeout)
"Return non-nil when all prompter sources are ready.
After timeout has elapsed for one source, return nil."
(sera:nlet check ((next-source (next-ready-p prompter timeout)))
(cond
((eq t next-source)
t)
((null next-source)
nil)
(t
(check (next-ready-p prompter timeout))))))
(defun all-ready-p (prompter)
"Return non-nil when all PROMPTER sources are ready."
(sera:nlet check ((next-source (next-ready-p prompter)))
(if (typep next-source 'boolean)
next-source
(check (next-ready-p prompter)))))

(export-always 'make)
(define-function make
(append '(&rest args)
`(&key sources ,@(public-initargs 'prompter)))
"Return prompter object.
"Return `prompter' object.
The arguments are the initargs of the `prompter' class.
As a special case, the `:sources' keyword argument not only accepts `source'
objects but also symbols. Example:
objects but also symbols.
(prompter:make :sources 'prompter:raw-source)"
Example:
(prompter:make :sources 'prompter:raw-source)"
(apply #'make-instance 'prompter args))

(export-always 'selected-source)
Expand All @@ -493,25 +480,28 @@ objects but also symbols. Example:

(export-always 'selected-suggestion)
(defun selected-suggestion (prompter)
"Return selected prompt-buffer suggestion.
"Return selected PROMPTER `suggestion'.
Return source as second value."
(let* ((source (first (selection prompter))))
(values (nth (second (selection prompter)) (suggestions source)) source)))
(let ((source (first (selection prompter))))
(values (nth (second (selection prompter)) (suggestions source))
source)))

(export-always 'selected-suggestion-position)
(defun selected-suggestion-position (prompter)
"Return selected prompt-buffer suggestion position among current source
"Return selected PROMPTER `suggestion' position among current `source'
suggestions."
(second (selection prompter)))

(export-always 'all-marks)
(defun all-marks (prompter)
"Return the list of the marked suggestions in the prompter."
"Return the list of `prompter''s `marks'.
Note that `marks' is a slot of `source', and `prompter' may have multiple
sources."
(alex:mappend #'marks (sources prompter)))

(export-always 'all-suggestions)
(defun all-suggestions (prompter)
"Return the list of the suggestions in the prompter."
"Return the list of PROMPTER's `suggestion's."
(alex:mappend #'suggestions (sources prompter)))

(export-always 'default-return-action)
Expand All @@ -520,8 +510,7 @@ suggestions."

(export-always 'resume)
(defun resume (prompter)
"Calls each source `resumer' function over the source.
This is meant to be called when a prompter is resumed."
(mapc (lambda (source)
(maybe-funcall (resumer source) source))
"Call each source `resumer' function over the source.
Meant to be called when PROMPTER is resumed."
(mapc (lambda (source) (maybe-funcall (resumer source) source))
(sources prompter)))
4 changes: 2 additions & 2 deletions source/auto-rules.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ For the storage format see the comment in the header of your `auto-rules-file'."
(make-instance 'prompter:raw-source
:name "New URL")
(make-instance 'global-history-source
:return-actions '())))))
:return-actions #'identity))))
(when (typep url 'nyxt::history-entry)
(setf url (url url)))
(add-modes-to-auto-rules
Expand Down Expand Up @@ -322,7 +322,7 @@ For the storage format see the comment in the header of your `auto-rules-file'."
(make-instance 'prompter:raw-source
:name "New URL")
(make-instance 'global-history-source
:return-actions '())))))
:return-actions #'identity)))))
(setf url (url url))
(add-modes-to-auto-rules (url-infer-match url)
:include (rememberable-of (modes (current-buffer)))
Expand Down
2 changes: 1 addition & 1 deletion source/browser.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ set of useful URLs or preparing a list to send to a someone else."
:sources (make-instance 'buffer-source
:constructor (remove-if #'internal-url-p (buffer-list)
:key #'url)
:return-actions '(identity)
:return-actions #'identity
:multi-selection-p t))))
(unwind-protect
(spinneret:with-html-string
Expand Down
Loading

0 comments on commit 7912fa0

Please sign in to comment.