branch: externals/do-at-point commit 7073fc19178ca821362e980dec807d86624ae88c Author: Philip Kaludercic <philip.kaluder...@fau.de> Commit: Philip Kaludercic <philip.kaluder...@fau.de>
Invert event loop to allow movement during selection --- do-at-point.el | 282 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 207 insertions(+), 75 deletions(-) diff --git a/do-at-point.el b/do-at-point.el index 48f1ec9787..8759fe944b 100644 --- a/do-at-point.el +++ b/do-at-point.el @@ -5,7 +5,7 @@ ;; Author: Philip Kaludercic <phil...@posteo.net> ;; Maintainer: Philip Kaludercic <phil...@posteo.net> ;; URL: https://wwwcip.cs.fau.de/~oj14ozun/src+etc/do-at-point.el -;; Version: $Id: do-at-point.el,v 1.5 2023/07/16 11:40:23 oj14ozun Exp oj14ozun $ +;; Version: $Id: do-at-point.el,v 1.6 2023/07/16 11:48:25 oj14ozun Exp $ ;; Package-Version: 1 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: convenience @@ -37,12 +37,14 @@ ;; (global-set-key (kbd "C-'") #'do-at-point) ;; ;; Most of the behaviour is controlled via the user option -;; `do-at-point-actions'. +;; `do-at-point-actions' and `do-at-point-user-actions'. A mode may +;; use `do-at-point-local-actions' to add additional things and/or +;; actions. ;;; Relation to "Embark" ;; A similar package, that served as inspiration for `do-at-point' is -;; called Embark (https://github.com/oantolin/embark), by Omar AntolĂn +;; called Embark (https://github.com/oantolin/embark), by Omar Anatolian ;; Camarena. I'll be honest, I don't get the hype but I find the core ;; functionality nice. This package is a fraction of the size of ;; Embark, but does pretty much exactly what I would want to use @@ -60,8 +62,20 @@ "Generic context-sensitive action dispatcher." :group 'convenience) +(defconst do-at-point-actions-type + '(alist :value-type + (alist :value-type + (list :tag "Action" + (string :tag "Description") function) + :key-type character) + :key-type symbol) + "User option type for `do-at-point' actions.") + (defcustom do-at-point-actions `((region + (?\s "Mark" ,(lambda (start end) + (set-mark start) + (goto-char end))) (?\C-i "Indent" ,#'indent-region) (?s "Isearch" ,(lambda (str) @@ -75,7 +89,7 @@ (?| "Pipe command" ,(lambda (beg end) (let ((cmd (read-shell-command "Command: "))) - (shell-command-on-region beg end cmd)))) + (shell-command-on-region beg end cmd nil t)))) (?! "Shell command" ,#'shell-command)) (email (?m "Compose message" ,(lambda (to) (compose-mail to)))) @@ -85,15 +99,16 @@ (url (?f "Open" ,#'browse-url) (?d "Download" ,#'(lambda (url) - (start-process "*Download*" nil "wget" url)))) + (start-process "*Download*" nil "wget" url))) + (?e "eww" ,#'eww-browse-url)) + (word + (?$ "Spell check" ,(lambda () (ispell-word))) + (?d "Dictionary" ,#'dictionary-search)) (symbol (?. "Xref" ,#'xref-find-definitions) (?o "Occur" ,(lambda (str) (occur (concat "\\_<\\(" (regexp-quote str) "\\)\\_>"))))) - (word - (?$ "Spell check" ,(lambda () (ispell-word))) - (?d "Dictionary" ,#'dictionary-search)) - (string) (sexp) + (string) (sexp) (paragraph (?$)) (defun (?e "Evaluate" ,(lambda () (eval-defun nil))))) "Association of things and their respective actions. @@ -105,74 +120,191 @@ that will be dispatched when KEY is selected. FUNC can take zero, one or two arguments, which `do-at-point' will respectively interpret as function that is invoked without any arguments, or with a buffer substring or the bounds of THING. Actions listed -under the \"thing\" `region' are shared among all \"things\". -This is why a an entry does not require any actions to be -associated with it, if it just serves as a specific kind of -region worth selecting. The order of element in the list -correspond to the order in which `do-at-point' will prompt the -user for possible things at point." - :type '(alist :value-type - (alist :value-type - (list :tag "Action" - (string :tag "Description") function) - :key-type character) - :key-type symbol)) - -(defcustom do-at-point-quick-select '(?\C-m) - "List of keys to quickly select the first action." - :type '(repeat character)) - -(defconst do-at-point--overlay - (let ((ov (make-overlay 0 0))) - (overlay-put ov 'face 'highlight) - (delete-overlay ov) - ov)) - -;;;###autoload -(defun do-at-point () - "Dispatch an action on the thing at point." +under the \"thing\" `region' are shared among all \"things\". An +entry in ACTIONS can omit NAME and FUNC, and it will instead +fallback into the entry for `region'. This is why a an entry +does not require any actions to be associated with it, if it just +serves as a specific kind of region worth selecting. The order +of element in the list correspond to the order in which +`do-at-point' will prompt the user for possible things at point." + :type do-at-point-actions-type) + +(defcustom do-at-point-user-actions '() + "Custom association of things and their respective actions. +Refer to the user option `do-at-point-actions' for details on the +structure of the values of this user option." + :type do-at-point-actions-type) + +(defvar-local do-at-point-local-actions '() + "Actions that can be added by a major or minor mode. +These are prioritised to the user option `do-at-point-actions', +but not `do-at-point-user-actions'. Refer to the user option +`do-at-point-actions' for details on the structure of the values +of this variable.") + +(defvar do-at-point--shortcut-map (make-sparse-keymap)) + +(defun do-at-point--actions (thing) + "Return possible actions for THING. +The function consults `do-at-point-user-actions', +`do-at-point-local-actions' and the user option +`do-at-point-actions' in this order and inherits actions from +more to less specific entries." + (seq-reduce + (lambda (accum ent) + (let ((prev (assq (car ent) accum))) + (cons (list (car ent) + (or (cadr ent) (cadr prev)) + (or (caddr ent) (caddr prev)) + (or (cadddr ent) (cadddr prev))) + (delq prev accum)))) + (reverse (append + (alist-get thing do-at-point-user-actions) + (alist-get 'region do-at-point-user-actions) + (alist-get thing do-at-point-local-actions) + (alist-get 'region do-at-point-local-actions) + (alist-get thing do-at-point-actions) + (alist-get 'region do-at-point-actions))) + '())) + +(defvar-local do-at-point--overlay nil + "Buffer-local overlay object to display the selection overlay. +The overlay is also used to store properties like the current +thing being selected and the key used to invoke `do-at-point'.") + +(defun do-at-point--update () + "Ensure a consistent state for the \"thing\" at point. +This means updating and moving the selection overlay and ensuring +that the repeat key, i.e. the key which was used to initially +invoke `do-at-point' is bound transiently." + (let ((thing (or (overlay-get do-at-point--overlay 'do-at-point-thing) + (do-at-point--next-thing t)))) + (let ((bound (bounds-of-thing-at-point thing))) + (when bound + (move-overlay do-at-point--overlay (car bound) (cdr bound)))) + (set-transient-map + (let ((map (make-sparse-keymap))) + (define-key map (vector (overlay-get do-at-point--overlay 'do-at-point-key)) + #'do-at-point--next-thing) + map)))) + +(defun do-at-point-confirm (&optional quick) + "Dispatch an action on the current \"thing\" being selected. +If the optional argument QUICK is non-nil, the first applicable +action is selected." (interactive) - (unwind-protect - (let ((things (seq-filter #'thing-at-point (mapcar #'car do-at-point-actions))) - (last last-input-event) (key last-input-event) thing) - (when (null things) - (user-error "Nothing actionable at point")) - (setf (cdr (last things)) things) - (while (eq key last) - (setq thing (pop things)) - (let ((bound (bounds-of-thing-at-point thing)) - (default (cadar (or (alist-get thing do-at-point-actions) - (alist-get 'region do-at-point-actions))))) - (move-overlay do-at-point--overlay (car bound) (cdr bound)) - (setq key (read-key (if (and do-at-point-quick-select default) - (format "Act on `%s' (%s by default)?" thing default) - (format "Act on `%s'?" thing)))) - (when (eq key ?\C-g) (keyboard-quit)))) - (let* ((options (append - (and (not (eq thing 'region)) - (alist-get thing do-at-point-actions)) - (alist-get 'region do-at-point-actions))) - (choice - (if (memq key do-at-point-quick-select) - (car options) - (when (assq key options) - (push key unread-post-input-method-events)) - (read-multiple-choice - (format "Action on %s" thing) - (seq-uniq - (mapcar (lambda (ent) - (list (car ent) (cadr ent) (cadddr ent))) - options) - (lambda (a b) (eq (car a) (car b))))))) - (func (cadr (alist-get (car choice) options))) - (bound (bounds-of-thing-at-point thing))) - (message nil) ;clear minibuffer - (pcase (car (func-arity func)) - (0 (funcall func)) - (1 (funcall func (buffer-substring (car bound) (cdr bound)))) - (2 (funcall func (car bound) (cdr bound))) - (_ (error "Unsupported signature: %S" func))))) + (let* ((thing (overlay-get do-at-point--overlay 'do-at-point-thing)) + (options (do-at-point--actions thing)) + (choice (cond + (quick (car options)) + ((assq last-command-event options)) + ((read-multiple-choice + (format "Action on %s" thing) + options)))) + (func (cadr (alist-get (car choice) options))) + (bound (cons (overlay-start do-at-point--overlay) + (overlay-end do-at-point--overlay)))) + (do-at-point--mode -1) + (message nil) ;clear mini buffer + (pcase (car (func-arity func)) + (0 (funcall func)) + (1 (funcall func (buffer-substring (car bound) (cdr bound)))) + (2 (funcall func (car bound) (cdr bound))) + (_ (error "Unsupported signature: %S" func))))) + +(defun do-at-point-confirm-quick () + "Quickly select the first action for the selected \"thing\". +See the function `do-at-point-confirm' for more details." + (interactive) + (do-at-point-confirm t)) + +(defun do-at-point-quit () + "Quit the selection mode and defer to \\[keyboard-quit]." + (interactive) + (do-at-point--mode -1) + (keyboard-quit)) + +(defun do-at-point--next-thing (&optional no-update) + "Select the next possible \"thing\". +If NO-UPDATE is nil, then the selection overlay is also updated. +Otherwise the next \"thing\" is just determined. The return +value of the function is always the new \"thing\"." + (interactive) + (let* ((actions (append do-at-point-user-actions + do-at-point-local-actions + do-at-point-actions)) + (things (seq-filter #'thing-at-point (mapcar #'car actions))) + (thing (overlay-get do-at-point--overlay 'do-at-point-thing))) + (setq thing (or (cadr (memq thing things)) (car things))) + (prog1 (overlay-put do-at-point--overlay + 'do-at-point-thing + thing) + ;; clear and reinitialise the shortcut map + (setcdr do-at-point--shortcut-map nil) + (dolist (key (mapcar #'car (do-at-point--actions thing))) + (define-key do-at-point--shortcut-map (vector key) #'do-at-point-confirm)) + (let ((default (cadar (do-at-point--actions thing)))) + (message "Act on `%s' (%s by default)?" thing default)) + (unless no-update + (do-at-point--update))))) + +(defun do-at-point--lighter () + "Determine the lighter for `do-at-point--mode'. +The lighter depends on the current \"thing\" being selected." + (let ((thing (overlay-get do-at-point--overlay 'do-at-point-thing))) + (and thing (format " Do-At-Point/%s" thing)))) + +(defvar do-at-point--mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map do-at-point--shortcut-map) + (define-key map (kbd "<return>") #'do-at-point-confirm-quick) + (define-key map (kbd "C-<return>") #'do-at-point-confirm) + (define-key map [remap keyboard-quit] #'do-at-point-quit) + (define-key map (kbd "M-n") #'do-at-point-forward) + (define-key map (kbd "M-p") #'do-at-point-backward) + map)) + +(define-minor-mode do-at-point--mode + "Minor mode that implements the selection for `do-at-point'. +This is an internal implementation detail and shouldn't be +invoked or bound directly. Use the command `do-at-point' +instead." + :lighter ((:eval (do-at-point--lighter))) + :interactive nil + (if do-at-point--mode + (let ((ov (or do-at-point--overlay + (let ((ov (make-overlay 0 0))) + (delete-overlay ov) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'face 'highlight) + ov)))) + (overlay-put ov 'do-at-point-key last-command-event) + (add-hook 'post-command-hook #'do-at-point--update 90 t) + (setq do-at-point--overlay ov) + (do-at-point--update)) + (remove-hook 'post-command-hook #'do-at-point--update t) + (overlay-put do-at-point--overlay 'do-at-point-thing nil) (delete-overlay do-at-point--overlay))) +(defun do-at-point-forward (n) + "Move focus N things ahead. +By default, this will move one thing ahead." + (interactive "p") + (forward-thing (overlay-get do-at-point--overlay 'do-at-point-thing) n)) + +(defun do-at-point-backward (n) + "Move focus N things back. +Refer to the command `do-at-point-forward' for more details." + (interactive "p") + (do-at-point-forward (- (or n 1)))) + +(defun do-at-point () + "Focus on a thing at point. +This is the main entry point" + (interactive) + (when do-at-point--mode + (do-at-point--mode -1)) + (do-at-point--mode 1)) + (provide 'do-at-point) ;;; do-at-point.el ends here