summaryrefslogtreecommitdiff
path: root/.config/emacs/libraries/selector.el
diff options
context:
space:
mode:
Diffstat (limited to '.config/emacs/libraries/selector.el')
-rw-r--r--.config/emacs/libraries/selector.el654
1 files changed, 654 insertions, 0 deletions
diff --git a/.config/emacs/libraries/selector.el b/.config/emacs/libraries/selector.el
new file mode 100644
index 0000000..8b16637
--- /dev/null
+++ b/.config/emacs/libraries/selector.el
@@ -0,0 +1,654 @@
+;;; -*- lexical-binding: t; -*-
+;;; Commentary:
+;; modified version of lcolonq: https://github.com/lcolonq/emacs
+;;; Code:
+
+
+(require 'dash)
+(require 'recentf)
+
+(defgroup selector nil
+ "Efficient selection and navigation."
+ :group 'convenience)
+
+(defgroup selector-faces nil
+ "Faces for `selector'."
+ :group 'selector
+ :group 'faces)
+
+(defface selector-source-name
+ '((default :underline t :inherit bold)
+ (((class color) (background dark)) :foreground "white"))
+ "Face used to highlight source names.")
+
+(defface selector-highlight
+ '((t :inherit highlight))
+ "Face used to highlight the current selection.")
+
+(defvar selector-minibuffer-lines 20
+ "Number of lines to display in the minibuffer.")
+
+(defvar selector-exit-hook nil
+ "Hook run when exiting minibuffer selection.")
+
+(defvar selector--sources '())
+(defvar selector--last nil)
+(defvar selector--matching '())
+(defvar selector--source 0)
+(defvar selector--index 0)
+(defvar selector--action nil)
+(defvar selector--result nil)
+(defvar selector--drawn-this-frame 0)
+
+(defvar selector-minibuffer-map (make-sparse-keymap))
+(define-key selector-minibuffer-map (kbd "\\") (lambda () (interactive) nil))
+(define-key selector-minibuffer-map (kbd "\\") (lambda () (interactive) nil))
+(define-key selector-minibuffer-map (kbd "C-g") 'selector-quit)
+(define-key selector-minibuffer-map (kbd "C-c") 'selector-quit)
+(define-key selector-minibuffer-map (kbd "<return>") 'selector-do)
+(define-key selector-minibuffer-map (kbd "<backtab>") 'selector-previous)
+(define-key selector-minibuffer-map (kbd "<tab>") 'selector-next)
+(define-key selector-minibuffer-map (kbd "<up>") 'selector-previous)
+(define-key selector-minibuffer-map (kbd "<down>") 'selector-next)
+(define-key selector-minibuffer-map (kbd "<left>") 'selector-previous-source)
+(define-key selector-minibuffer-map (kbd "<right>") 'selector-next-source)
+(define-key selector-minibuffer-map (kbd "C-p") 'selector-previous)
+(define-key selector-minibuffer-map (kbd "C-n") 'selector-next)
+(define-key selector-minibuffer-map (kbd "C-b") 'selector-previous-source)
+(define-key selector-minibuffer-map (kbd "C-f") 'selector-next-source)
+(defun selector-minibuffer-line (str)
+ "Write STR to the minibuffer."
+ (goto-char (point-max))
+ (insert (concat "\n" str)))
+
+(defun selector-minibuffer-line-face (str face)
+ "Write STR to the minibuffer in FACE."
+ (cl-incf selector--drawn-this-frame) ;; sort of an ugly hack,
+ ;; but this fixes the annoying "cursor jumping" glitch when multiple monitors
+ ;; are active. the real fix probably should live in selector-nearby?
+ (when (< selector--drawn-this-frame selector-minibuffer-lines)
+ (let ((before (point)))
+ (selector-minibuffer-line str)
+ (goto-char before)
+ (forward-line)
+ (put-text-property (line-beginning-position) (point-max) 'face face))))
+
+(defun selector-minibuffer-clear ()
+ "Clear minibuffer."
+ (save-excursion
+ (goto-char (minibuffer-prompt-end))
+ (delete-region (line-end-position) (point-max))))
+
+(defun selector-minibuffer-input ()
+ "Get current minibuffer input."
+ (buffer-substring-no-properties
+ (minibuffer-prompt-end)
+ (line-end-position)))
+
+(cl-defstruct (selector-candidate (:constructor selector-candidate--create)
+ (:copier nil)
+ (:conc-name selector-candidate--))
+ type (display nil :type string) face value action)
+
+(cl-defun selector-candidate-create
+ (display &key
+ value
+ (type 'normal)
+ (face 'default)
+ (action '()))
+ "Create a candidate.
+DISPLAY is the string to display (using FACE) / match against.
+VALUE is the value to pass to actions when the candidate is selected.
+TYPE is either normal or dummy - dummy candidates always appear in the
+results list regardless of the input pattern.
+ACTION is an alist mapping keybindings to candidate-specific actions."
+ (selector-candidate--create
+ :type type
+ :display display
+ :face face
+ :value (if value value display)
+ :action action))
+
+(defun selector-candidate-display (candidate)
+ "Return the display string for CANDIDATE."
+ (cond ((selector-candidate-p candidate) (selector-candidate--display candidate))
+ (t candidate)))
+
+(defun selector-candidate-value (candidate)
+ "Return the value of CANDIDATE."
+ (cond ((selector-candidate-p candidate) (selector-candidate--value candidate))
+ (t candidate)))
+
+(defun selector-candidate-type (candidate)
+ "Return the candidate type of CANDIDATE."
+ (cond ((selector-candidate-p candidate) (selector-candidate--type candidate))
+ (t 'normal)))
+
+(defun selector-candidate-face (candidate)
+ "Return the display face for CANDIDATE."
+ (cond ((selector-candidate-p candidate) (selector-candidate--face candidate))
+ (t 'default)))
+
+(defun selector-candidate-action (candidate)
+ "Return the actions for CANDIDATE."
+ (cond ((selector-candidate-p candidate) (selector-candidate--action candidate))
+ (t '())))
+
+(defun selector-candidate-display-string (candidate)
+ "Return the display of CANDIDATE as a string."
+ (let ((display (selector-candidate-display candidate)))
+ (cond ((stringp display) display)
+ (t (error "Invalid candidate display %s for candidate %s (of type %s)"
+ display candidate (type-of display))))))
+
+(defun selector-highlight-candidate (candidate)
+ "Return a copy of CANDIDATE with the face set to selector-highlight."
+ (selector-candidate--create
+ :type (selector-candidate-type candidate)
+ :display (selector-candidate-display candidate)
+ :face 'selector-highlight
+ :value (selector-candidate-value candidate)
+ :action (selector-candidate-action candidate)))
+
+(defun selector-match (candidate regex)
+ "Determine whether CANDIDATE is a match for REGEX."
+ (let ((type (selector-candidate-type candidate)))
+ (cond ((eq 'dummy type) t)
+ (t (string-match-p regex (selector-candidate-display-string candidate))))))
+
+(cl-defstruct (selector-source (:constructor selector-source--create)
+ (:copier nil))
+ name candidates actions keymap)
+
+(defun selector-action-function (action)
+ "Return the function associated with ACTION."
+ (if (functionp action)
+ action
+ (cdr action)))
+
+(defun selector-actions-keymap (actions)
+ "Return a keymap for ACTIONS."
+ (let ((keymap (make-sparse-keymap)))
+ (set-keymap-parent keymap selector-minibuffer-map)
+ (mapc
+ (lambda (a)
+ (unless (functionp a)
+ (define-key keymap (car a)
+ (lambda () (interactive)
+ (selector-do (cdr a))))))
+ actions)
+ keymap))
+
+(cl-defun selector-source-create (name &key candidates (actions '()))
+ "Create a new source named NAME with the given CANDIDATES and ACTIONS.
+
+CANDIDATES is either:
+- A list of candidates
+- A function returning a list of candidates given a regex
+ACTIONS is a list of actions, which can be:
+- functions taking candidate values as arguments
+- pairs of key strings and such functions"
+ (selector-source--create
+ :name name
+ :candidates
+ (if (functionp candidates) candidates
+ (--map (if (selector-candidate-p it) it (selector-candidate-create it))
+ candidates))
+ :actions actions
+ :keymap (selector-actions-keymap actions)))
+
+(defun selector-matching-candidates (candidates pattern)
+ "Return the candidates in CANDIDATES matching PATTERN."
+ (cond ((functionp candidates) (funcall candidates pattern))
+ (t (let ((regex (selector-pattern-regex pattern)))
+ (--filter (selector-match it regex) candidates)))))
+
+(defun selector-filter-source (source pattern)
+ "Return a copy of SOURCE including only the candidates matching PATTERN."
+ (selector-source--create
+ :name (selector-source-name source)
+ :candidates (selector-matching-candidates (selector-source-candidates source) pattern)
+ :actions (selector-source-actions source)
+ :keymap (selector-source-keymap source)))
+
+(defun selector-pattern-regex (pattern)
+ "Convert PATTERN into a regular expression."
+ (apply #'string-join
+ (--map (concat "\\(" it "\\)") (split-string pattern))
+ '(".*")))
+
+(defun selector-matching-sources (sources pattern)
+ "Return the sources in SOURCES matching PATTERN."
+ (let* ((matches (--map (selector-filter-source it pattern) sources)))
+ (-filter #'selector-source-candidates matches)))
+
+(defun selector-display-source (source)
+ "Display SOURCE."
+ (when source
+ (selector-minibuffer-line-face (selector-source-name source) 'selector-source-name)
+ (--map (selector-minibuffer-line-face
+ (selector-candidate-display-string it)
+ (selector-candidate-face it))
+ (selector-source-candidates source))))
+
+(defun selector-nearby (sources)
+ "Filter SOURCES to only include candidates close to the selected candidate."
+ (let* ((adjacent
+ (--map
+ (cond ((and (< (cdr it) (+ selector--source selector-minibuffer-lines))
+ (> (cdr it) selector--source))
+ (cons (car it) 'g))
+ ((= (cdr it) selector--source)
+ (cons (car it) 'e))
+ (t nil))
+ (-zip-pair sources (number-sequence 0 (length sources))))))
+ (--map
+ (when it
+ (let* ((candidates (selector-source-candidates (car it))))
+ (selector-source--create
+ :name (selector-source-name (car it))
+ :candidates
+ (cond ((eq (cdr it) 'g)
+ (-take selector-minibuffer-lines candidates))
+ (t
+ (cl-loop for i from (max (- selector--index
+ (- (/ selector-minibuffer-lines 2) 1))
+ 0)
+ for j in (-take
+ selector-minibuffer-lines
+ (-drop
+ (- selector--index
+ (- (/ selector-minibuffer-lines 2) 1))
+ candidates))
+ collect (if (= i selector--index)
+ (selector-highlight-candidate j)
+ j))))
+ :actions (selector-source-actions (car it))
+ :keymap (selector-source-keymap (car it)))))
+ adjacent)))
+
+(defun selector-update-transient-map ()
+ "Update the transient keymap to match the current source."
+ (let ((source (car (nthcdr selector--source selector--matching))))
+ (when source
+ (set-transient-map (selector-source-keymap source)))))
+
+(defun selector-minibuffer-render ()
+ "Draw matching candidates to minibuffer."
+ (setq selector--drawn-this-frame 0)
+ (save-excursion
+ (let ((pattern (selector-minibuffer-input)))
+ (unless (string= pattern selector--last)
+ (setq selector--last pattern
+ selector--index 0
+ selector--source 0
+ selector--matching (selector-matching-sources selector--sources pattern))))
+ (-map #'selector-display-source (selector-nearby selector--matching))
+ (goto-char (minibuffer-prompt-end))
+ (put-text-property (line-end-position) (point-max) 'readonly t))
+ (selector-update-transient-map))
+
+(defun selector-minibuffer-setup (initial)
+ "Ready minibuffer for completion with INITIAL as initial input."
+ (add-hook 'pre-command-hook 'selector-minibuffer-clear nil t)
+ (add-hook 'post-command-hook 'selector-minibuffer-render nil t)
+ (setq-local max-mini-window-height selector-minibuffer-lines)
+ (when initial
+ (save-excursion
+ (minibuffer-prompt-end)
+ (insert initial)))
+ (end-of-line)
+ (selector-update-transient-map))
+
+(defun selector-previous-source ()
+ "Move to the previous source."
+ (interactive)
+ (setq selector--index 0)
+ (setq selector--source (if (= selector--source 0)
+ (- (length selector--matching) 1)
+ (- selector--source 1))))
+
+(defun selector-next-source ()
+ "Move to the next source."
+ (interactive)
+ (setq selector--index 0)
+ (setq selector--source (% (+ selector--source 1) (length selector--matching))))
+
+(defun selector-previous ()
+ "Move to the previous candidate."
+ (interactive)
+ (let* ((new-source-index (if (= selector--source 0)
+ (- (length selector--matching) 1)
+ (- selector--source 1)))
+ (source (car (nthcdr new-source-index selector--matching))))
+ (setq selector--index (- selector--index 1))
+ (when (< selector--index 0)
+ (setq selector--index (- (length (selector-source-candidates source)) 1)
+ selector--source new-source-index))))
+
+(defun selector-next ()
+ "Move to the next candidate."
+ (interactive)
+ (let* ((source (car (nthcdr selector--source selector--matching))))
+ (setq selector--index (+ selector--index 1))
+ (when (= selector--index (length (selector-source-candidates source)))
+ (setq selector--index 0
+ selector--source (% (+ selector--source 1) (length selector--matching))))))
+
+(defun selector-quit ()
+ "Quit the selection interface without running an action."
+ (interactive)
+ (run-hooks 'selector-exit-hook)
+ (keyboard-escape-quit))
+
+(defun selector-do (&optional action-function)
+ "Act upon selected candidate.
+If ACTION-FUNCTION is given use it, otherwise use the first action for the candidate."
+ (interactive)
+ (if (null selector--matching)
+ (progn
+ (setq selector--action (lambda (x) x)
+ selector--result nil))
+ (progn
+ (let* ((source (car (nthcdr selector--source selector--matching)))
+ (candidate (car (nthcdr selector--index (selector-source-candidates source)))))
+ (setq selector--action (cond (action-function
+ action-function)
+ ((selector-candidate-action candidate)
+ (selector-candidate-action candidate))
+ (t
+ (let ((actions (selector-source-actions source)))
+ (if actions
+ (selector-action-function (car actions))
+ (lambda (x) x)))))
+ selector--result (selector-candidate-value candidate)))))
+ (run-hooks 'selector-exit-hook)
+ (exit-minibuffer))
+
+;;;###autoload
+(cl-defun selector (sources &key prompt initial)
+ "Select a candidate and run an action using SOURCES.
+Display PROMPT as the prompt, or \"pattern: \" if not given.
+Use INITIAL as the initial input."
+ (setq selector--sources sources
+ selector--last nil
+ selector--matching (selector-matching-sources sources "")
+ selector--source 0
+ selector--index 0
+ selector--action nil
+ selector--result nil)
+ (let ((inhibit-message t))
+ (minibuffer-with-setup-hook
+ (apply-partially 'selector-minibuffer-setup initial)
+ (read-from-minibuffer (or prompt "pattern: ") nil selector-minibuffer-map)))
+ (funcall selector--action selector--result))
+
+(defvar selector-completing-read-candidate-transformer (lambda (x) x))
+
+;;;###autoload
+(defun selector-completing-read (prompt collection &optional predicate require-match
+ initial-input hist def inherit-input-method)
+ "Replacement for `completing-read'.
+PROMPT, COLLECTION, PREDICATE, REQUIRE-MATCH, INITIAL-INPUT, HIST, DEF, and
+INHERIT-INPUT-METHOD have the same meaning as in `completing-read'."
+ (ignore predicate hist def inherit-input-method)
+ (let ((unspecified-source
+ (if require-match
+ '()
+ (list
+ (selector-source-create
+ "Other"
+ :candidates
+ (list (selector-candidate-create
+ "Specify"
+ :type 'dummy
+ :action (lambda (_) (selector-input)))))))))
+ (or
+ (cond ((functionp collection)
+ (selector
+ (cons
+ (selector-source-create
+ "Completions"
+ :candidates
+ (-non-nil
+ (--map
+ (when-let ((disp (funcall selector-completing-read-candidate-transformer it)))
+ (selector-candidate-create disp :value it))
+ (funcall collection "" nil t))))
+ unspecified-source)
+ :prompt prompt
+ :initial initial-input))
+ ((hash-table-p collection)
+ (selector
+ (cons
+ (selector-source-create
+ "Completions"
+ :candidates (hash-table-keys collection))
+ unspecified-source)
+ :prompt prompt
+ :initial initial-input))
+ ((obarrayp collection)
+ (let ((candidates (list)))
+ (mapatoms (lambda (x) (push (selector-candidate-create (symbol-name x)) candidates)) collection)
+ (selector
+ (cons (selector-source-create "Completions" :candidates candidates) unspecified-source)
+ :prompt prompt
+ :initial initial-input)))
+ (t (selector
+ (cons (selector-source-create
+ "Completions"
+ :candidates
+ (--map (if (consp it)
+ (selector-candidate-create (car it))
+ it)
+ collection))
+ unspecified-source)
+ :prompt prompt
+ :initial initial-input)))
+ (selector-input))))
+
+(defun selector-input () "Return last minibuffer input." selector--last)
+
+(defvar selector-extended-command-actions
+ (list (lambda (c)
+ (add-to-list 'extended-command-history c)
+ (command-execute (intern-soft c)))
+ (cons (kbd "C-h") (lambda (c) (describe-function (intern-soft c))))))
+
+;;;###autoload
+(defun selector-extended-commands-source ()
+ "Source for extended commands (`M-x')."
+ (selector-source-create
+ "Commands"
+ :candidates
+ (-map
+ #'selector-candidate-create
+ (all-completions "" obarray #'commandp))
+ :actions
+ selector-extended-command-actions))
+
+;;;###autoload
+(defun selector-extended-command-history-source ()
+ "Source for extended command history."
+ (selector-source-create
+ "Command History"
+ :candidates
+ (-map
+ #'selector-candidate-create
+ extended-command-history)
+ :actions
+ selector-extended-command-actions))
+
+;;;###autoload
+(defun selector-apropos-command-source ()
+ "Source for command lookup."
+ (selector-source-create
+ "Commands"
+ :candidates
+ (lambda (r) (-map #'selector-candidate-create (all-completions r obarray #'commandp)))
+ :actions
+ (list (lambda (c) (describe-function (intern-soft c))))))
+
+;;;###autoload
+(defun selector-apropos-function-source ()
+ "Source for function lookup."
+ (selector-source-create
+ "Functions"
+ :candidates
+ (lambda (r) (-map #'selector-candidate-create (all-completions r obarray #'fboundp)))
+ :actions
+ (list (lambda (c) (describe-function (intern-soft c))))))
+
+;;;###autoload
+(defun selector-apropos-variable-source ()
+ "Source for variable lookup."
+ (selector-source-create
+ "Variables"
+ :candidates
+ (lambda (r) (-map #'selector-candidate-create
+ (all-completions
+ r obarray
+ (lambda (x) (let ((sym (intern-soft x)))
+ (and (boundp sym) (not (keywordp sym))))))))
+ :actions
+ (list (lambda (c) (describe-variable (intern-soft c))))))
+
+(defvar selector-buffer-actions
+ (list 'switch-to-buffer
+ (cons (kbd "M-D") 'kill-buffer)))
+
+;;;###autoload
+(defun selector-buffers-source (&optional sort-pred)
+ "Source for open buffers.
+An optional SORT-PRED may be provided to sort the buffers (see `sort')."
+ (selector-source-create
+ "Buffers"
+ :candidates
+ (--map (selector-candidate-create (buffer-name it))
+ (if sort-pred (sort (buffer-list) sort-pred) (buffer-list)))
+ :actions
+ selector-buffer-actions))
+
+;;;###autoload
+(defun selector-create-buffer-source ()
+ "Dummy source to create a buffer."
+ (selector-source-create
+ "Other"
+ :candidates
+ (list (selector-candidate-create
+ "Create buffer"
+ :type 'dummy
+ :action (lambda (_) (switch-to-buffer (selector-input)))))))
+
+(defvar selector-file-actions
+ (list 'find-file
+ (cons (kbd "M-D") (lambda (f)
+ (when (y-or-n-p (concat "Delete file " f "? "))
+ (delete-file f))))))
+
+;;;###autoload
+(defun selector-files-source ()
+ "Source for files in current directory."
+ (selector-source-create
+ "Files"
+ :candidates
+ (-map #'selector-candidate-create (directory-files default-directory))
+ :actions
+ selector-file-actions))
+
+;;;###autoload
+(defun selector-create-file-source ()
+ "Dummy source to create a file."
+ (selector-source-create
+ "Other"
+ :candidates
+ (list (selector-candidate-create
+ "Create file"
+ :type 'dummy
+ :action (lambda (_) (find-file (selector-input)))))))
+
+;;;###autoload
+(defun selector-recentf-source ()
+ "Source for recentf."
+ (selector-source-create
+ "Recent Files"
+ :candidates
+ (-map #'selector-candidate-create recentf-list)
+ :actions
+ selector-file-actions))
+
+;;;###autoload
+(defun selector-M-x ()
+ "Preconfigured `selector' interface to replace `execute-external-command'."
+ (interactive)
+ (selector (list (selector-extended-command-history-source)
+ (selector-extended-commands-source))))
+
+;;;###autoload
+(defun selector-apropos (&optional initial)
+ "Preconfigured `selector' interface to replace `apropos'.
+INITIAL is the initial text to match."
+ (interactive)
+ (selector (list (selector-apropos-command-source)
+ (selector-apropos-function-source)
+ (selector-apropos-variable-source))
+ :initial (if initial initial (thing-at-point 'symbol t))))
+
+;;;###autoload
+(defun selector-for-buffers ()
+ "Preconfigured `selector' interface for open buffers and recentf."
+ (interactive)
+ (selector (list (selector-buffers-source)
+ (selector-recentf-source)
+ (selector-create-buffer-source))))
+
+;;;###autoload
+(defun selector-for-files ()
+ "Preconfigured `selector' interface for files in the current directory."
+ (interactive)
+ (selector (list (selector-files-source)
+ (selector-create-file-source))))
+
+;;;###autoload
+(defun selector-read-file-name (prompt &optional dir default-filename mustmatch initial predicate)
+ "Replacement for `read-file-name'.
+PROMPT, DIR, DEFAULT-FILENAME, MUSTMATCH, INITIAL and PREDICATE have the same
+meaning as in `read-file-name'."
+ (ignore default-filename mustmatch predicate)
+ (let ((d (if dir dir default-directory)))
+ (concat d (selector (list (selector-source-create
+ "Files"
+ :candidates (-map #'selector-candidate-create (directory-files d)))
+ (selector-source-create
+ "Other"
+ :candidates (list (selector-candidate-create
+ "New file"
+ :type 'dummy
+ :action (lambda (_) (selector-input))))))
+ :prompt prompt
+ :initial initial))))
+
+(defun selector-file-contents-actions (file)
+ "Actions for candidate values corresponding to lines in FILE."
+ (list
+ (lambda (index)
+ (find-file file)
+ (goto-char (point-min))
+ (forward-line index)
+ (pulse-momentary-highlight-one-line (point)))))
+
+(defun selector-file-contents-source (file)
+ "Source for lines in FILE."
+ (selector-source-create
+ file
+ :candidates
+ (-map-indexed
+ (lambda (index l)
+ (selector-candidate-create l :value index))
+ (split-string (f-read-text file) "\n"))
+ :actions
+ (selector-file-contents-actions file)))
+
+
+(provide 'selector)
+;;; selector ends here