idk
This commit is contained in:
369
evil-leap.el
369
evil-leap.el
@@ -7,18 +7,26 @@
|
||||
;; Created: October 06, 2024
|
||||
;; Modified: October 06, 2024
|
||||
;; Version: 0.0.1
|
||||
;; Keywords: evil movement
|
||||
;; Keywords: convenience text
|
||||
;; Homepage: https://github.com/crumbtoo/leap.el
|
||||
;; Package-Requires: ((emacs "24.3"))
|
||||
;; Package-Requires: ((emacs "26.1") (evil "1.2.12"))
|
||||
;;
|
||||
;; This file is not part of GNU Emacs.
|
||||
;;
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; Evil leap.
|
||||
;;
|
||||
;;
|
||||
;;; Code:
|
||||
|
||||
(require 'evil)
|
||||
(eval-when-compile (require 'evil))
|
||||
(require 'ht)
|
||||
(require 'cl-lib)
|
||||
|
||||
|
||||
; Options
|
||||
|
||||
(defcustom evil-leap-safe-labels
|
||||
(string-to-list "sfnut/SFNLHMUGTZ?")
|
||||
"TODO A list of characters that you will never use immediately after an evil-leap
|
||||
@@ -34,12 +42,6 @@ jump."
|
||||
:group 'evil-leap
|
||||
:type "List of characters")
|
||||
|
||||
;; (defun evil-leap--collect-keys (&optional forward?)
|
||||
;; (let ((collected-keys nil))
|
||||
;; (dolist (i '(0 1) collected-keys)
|
||||
;; (let ((prompt (format ">%s" (mapconcat #'char-to-string collected-keys))))
|
||||
;; (appendq! collected-keys (list (evil-read-key prompt)))))))
|
||||
|
||||
(defcustom evil-leap-character-aspect-ratio
|
||||
0.3
|
||||
"TODO"
|
||||
@@ -51,16 +53,171 @@ jump."
|
||||
:group 'evil-leap
|
||||
:type "List of lists of characters")
|
||||
|
||||
(defun evil-leap--gather-targets-forward (regexp)
|
||||
(let ((end-of-visible (window-end))
|
||||
targets)
|
||||
(save-excursion
|
||||
(while (re-search-forward regexp end-of-visible t)
|
||||
(save-excursion
|
||||
(goto-char (match-beginning 0))
|
||||
(push (list (point) (current-column) (line-number-at-pos))
|
||||
targets))))
|
||||
(reverse targets)))
|
||||
(defface evil-leap-label-face '((t :inherit isearch))
|
||||
"TODO"
|
||||
:group 'evil-leap)
|
||||
|
||||
(defcustom evil-leap-use-char-fold-table t
|
||||
"TODO"
|
||||
:group 'evil-leap
|
||||
:type "Boolean")
|
||||
|
||||
|
||||
; Core
|
||||
|
||||
(cl-defstruct
|
||||
(evil-leap-target
|
||||
(:constructor evil-leap-make-target))
|
||||
"TODO"
|
||||
pos
|
||||
line
|
||||
column
|
||||
;; TODO: Perhaps we should be tracking /buffers/ rather than windows...
|
||||
(window
|
||||
nil
|
||||
:type "TODO window or nil"
|
||||
:documentation
|
||||
"TODO nil means current window")
|
||||
overlay
|
||||
payload)
|
||||
|
||||
(defun evil-leap--target-at-point ()
|
||||
"TODO"
|
||||
(evil-leap-make-target :pos (point)
|
||||
:line (line-number-at-pos)
|
||||
:column (current-column)
|
||||
:window (or (minibuffer-selected-window)
|
||||
(selected-window))))
|
||||
|
||||
(define-inline evil-leap-jump-to-target (target)
|
||||
(when-let ((window (evil-leap-target-window target)))
|
||||
(select-window window))
|
||||
(goto-char (evil-leap-target-pos target)))
|
||||
|
||||
(defvar evil-leap-action #'evil-leap-jump-to-target)
|
||||
|
||||
(defun evil-leap--remove-all-labels ()
|
||||
(remove-overlays nil nil 'category 'evil-leap))
|
||||
|
||||
;; ₁₂
|
||||
|
||||
(defun evil-leap--add-overlay-target! (target label)
|
||||
"TODO returns overlay and sets overlay slot"
|
||||
;; The label shall be displayed two characters /after/ `pos'. This ensures
|
||||
;; the user is able to see the entirety of the target; that is, the first
|
||||
;; character, located at `pos', and the second, immediately following.
|
||||
(let* ((pos (evil-leap-target-pos target))
|
||||
;; If `target' has an associated window, grab the buffer associated
|
||||
;; thereupon — we need to place the overlay in the correct buffer, of
|
||||
;; course!
|
||||
(buffer (-some-> (evil-leap-target-window target)
|
||||
window-buffer))
|
||||
;; If overlay already exists, just make sure it's in the correct
|
||||
;; position. If it doesn't exist, create it.
|
||||
(overlay (if-let ((ov (evil-leap-target-overlay target)))
|
||||
(move-overlay ov (+ pos 2) (+ pos 3)
|
||||
buffer)
|
||||
(make-overlay (+ pos 2) (+ pos 3)
|
||||
buffer)))
|
||||
(label-string (string label)))
|
||||
(put-text-property 0 1 'face 'evil-leap-label-face label-string)
|
||||
(overlay-put overlay 'category 'evil-leap)
|
||||
(overlay-put overlay 'before-string label-string)
|
||||
;; Normally, we want the labels to cover the buffer's text, rather than
|
||||
;; shifting it to the side; however, doing so unconditionally will cover
|
||||
;; newlines, collapsing multiple visual lines onto one.
|
||||
(unless (memq (char-after (+ pos 2)) '(?\n ?\r))
|
||||
(overlay-put overlay 'invisible t))
|
||||
(setf (evil-leap-target-overlay target) overlay)
|
||||
overlay))
|
||||
|
||||
(defun evil-leap--add-to-labeled-targets! (labeled-targets label target)
|
||||
"TODO Mutate LABELED-TARGETS by inserting value TARGET under key LABEL — with
|
||||
spice!
|
||||
|
||||
Spice: If an entry with key LABEL already exists in LABELED-TARGETS, a fresh
|
||||
hash table will be created and inserted into LABELED-TARGETS under key '?\\s'.
|
||||
This fresh hash table will itself have a key '?\\b' pointing back to
|
||||
LABELED-TARGETS. If this '?\\s' entry already exists in LABELED-TARGETS, it
|
||||
will instead try to insert it into /that/ hash table, continuing until a
|
||||
suitable home is found.
|
||||
|
||||
This all implies that important parts of evil-leap will enter the eldritch world
|
||||
of Undefined Behaviour, should either `evil-leap-safe-labels' or
|
||||
`evil-leap-labels' contain '?\\s' or '?\\b'."
|
||||
;; If the label is already present in `labeled-targets', we insert it into the
|
||||
;; "next" map at key '?\s'.
|
||||
(let ((existing-targets (ht-get labeled-targets label target)))
|
||||
;; REVIEW: This is terribly inefficient to be ran in a tight loop (i.e.
|
||||
;; `evil-leap--label-targets').
|
||||
(if (<= (length evil-leap-labels) (length existing-targets))
|
||||
;; If labeled-targets[?\s] (the "next map") exists, ...
|
||||
(if-let ((next-labeled-targets (ht-get labeled-targets ?\s)))
|
||||
;; ... recurse into it. ...
|
||||
(evil-leap--add-to-labeled-targets!
|
||||
next-labeled-targets label target)
|
||||
;; ... If it doesn't, create a new hash table, stored in
|
||||
;; `labeled-targets' at key '?\s', containing nought but a back-link key
|
||||
;; ('?\b')[1], and the given label/target pair[2].
|
||||
(print "here!")
|
||||
(ht-set! labeled-targets ?\s
|
||||
(ht (?\b labeled-targets) ; [1]
|
||||
(label (list target))))) ; [2]
|
||||
;; The label does not exist, meaning we needn't muck around with the next
|
||||
;; map. Proceed normally with `ht-set!'!
|
||||
(ht-set! labeled-targets label target))))
|
||||
|
||||
(defun evil-leap--label-targets-safe (targets)
|
||||
"TODO returns a hash table mapping labels to targets"
|
||||
;; Nasty! While I'd prefer to iterate over `zip evil-leap-safe-labels
|
||||
;; targets`, this isn't Haskell; each iteration through `targets', we pop
|
||||
;; `labels'.
|
||||
(let ((labels evil-leap-safe-labels)
|
||||
(labeled-targets (ht-create 'eq)))
|
||||
;; The first target is assigned the label `auto-jump', to indicate that it can be
|
||||
;; auto-jumped to. The remaining targets are labeled as you'd expect.
|
||||
(ht-set! labeled-targets 'auto-jump (car targets))
|
||||
(dolist (target (cdr targets) labeled-targets)
|
||||
(let ((lbl (pop labels)))
|
||||
(evil-leap--add-overlay-target! target lbl)
|
||||
(evil-leap--add-to-labeled-targets! labeled-targets lbl target)))))
|
||||
|
||||
(defun evil-leap--label-targets-unsafe (targets)
|
||||
"TODO returns a hash table mapping labels to targets"
|
||||
;; TODO: space labels don't work
|
||||
(let ((labels (-cycle evil-leap-labels))
|
||||
(labeled-targets (ht-create 'eq)))
|
||||
(dolist (target targets labeled-targets)
|
||||
(let ((lbl (pop labels)))
|
||||
(evil-leap--add-overlay-target! target lbl)
|
||||
(evil-leap--add-to-labeled-targets! labeled-targets lbl target)))))
|
||||
|
||||
(defun evil-leap--label-targets (targets)
|
||||
"TODO returns a hash table mapping labels to targets. Labels are
|
||||
represented by characters. Adds overlays for labels"
|
||||
;; Safe labels are preferred, but are not available when there are more
|
||||
;; targets than safe labels.
|
||||
(let ((safe? (<= (length targets) (length evil-leap-safe-labels))))
|
||||
;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
;; FIXME: I think this should be (+ 1 (length evil-leap-safe-labels)) to
|
||||
;; account for the "anonymous label" given to the auto-jump target.
|
||||
(if safe?
|
||||
(evil-leap--label-targets-safe targets)
|
||||
(evil-leap--label-targets-unsafe targets))))
|
||||
|
||||
(defun evil-leap--update-target-overlays! (labeled-targets)
|
||||
"TODO Create or update overlays to visually present a set of targets'
|
||||
associated labels."
|
||||
;; TODO: blah
|
||||
(ht-each (lambda (k v)
|
||||
(cond
|
||||
;; No-op; auto-jump targets are not labeled in the typical sense.
|
||||
((eq k 'auto-jump)
|
||||
nil)
|
||||
|
||||
((eq k ?\s)
|
||||
(error "TODO: Recurse into "))))
|
||||
labeled-targets))
|
||||
|
||||
(defun evil-leap--euclidean-distance (x1 y1 x2 y2)
|
||||
(let* ((width (abs (- x1 x2)))
|
||||
@@ -75,40 +232,168 @@ jump."
|
||||
(point-y (line-number-at-pos)))
|
||||
(sort targets
|
||||
:lessp
|
||||
(lambda (pos1 pos2)
|
||||
(pcase-let ((`(,_ ,pos1-x ,pos1-y) pos1)
|
||||
(`(,_ ,pos2-x ,pos2-y) pos2))
|
||||
(< (evil-leap--euclidean-distance point-x point-y pos1-x pos1-y)
|
||||
(evil-leap--euclidean-distance point-x point-y pos2-x pos2-y)))))))
|
||||
(lambda (target-1 target-2)
|
||||
(< (evil-leap--euclidean-distance
|
||||
point-x
|
||||
point-y
|
||||
(evil-leap-target-column target-1)
|
||||
(evil-leap-target-line target-1))
|
||||
(evil-leap--euclidean-distance
|
||||
point-x
|
||||
point-y
|
||||
(evil-leap-target-column target-2)
|
||||
(evil-leap-target-line target-2)))))))
|
||||
|
||||
;; (defun evil-leap--label-targets (targets)
|
||||
;; (let ((ht (make-hash-table :test 'equal)))
|
||||
;; (dolist (c ))))
|
||||
(defun evil-leap--try-auto-jump (labeled-targets)
|
||||
(when-let ((auto-jump-target (ht-get labeled-targets 'auto-jump)))
|
||||
(funcall evil-leap-action auto-jump-target)))
|
||||
|
||||
;; define-inline?
|
||||
(defun evil-leap-jump-to (target)
|
||||
(goto-char (car target)))
|
||||
(defun evil-leap-select-from-labeled-targets (labeled-targets)
|
||||
"Given LABELED-TARGETS, a hash table mapping labels to targets,
|
||||
- auto-jump to the first target, if possible;
|
||||
- read a key from the user;
|
||||
- if the key corresponds to a label, jump to it;
|
||||
- otherwise, let the input `fall through,' as if Leap were never here."
|
||||
(progn
|
||||
;; REVIEW: Should the return value of `evil-leap-action' be returned by us?
|
||||
(evil-leap--try-auto-jump labeled-targets)
|
||||
(let ((given-key (evil-read-key)))
|
||||
(if-let ((selected-target (ht-get labeled-targets given-key)))
|
||||
(funcall evil-leap-action selected-target)
|
||||
;; If we are given a key that does not correspond to any label, we let
|
||||
;; it 'fall through' to give it a proper run through Emacs' command
|
||||
;; loop. We do this as a formality in the general case, but it is
|
||||
;; essential to the idea behind auto-jump.
|
||||
|
||||
(defun evil-leap (&rest args)
|
||||
;; REVIEW: Should the passthrough behaviour be configurable in the
|
||||
;; non-auto-jump case?
|
||||
(progn (push given-key unread-command-events)
|
||||
nil)))))
|
||||
|
||||
|
||||
; Labels
|
||||
|
||||
(defun evil-leap--label-branch-targets! (branches)
|
||||
"Mutate BRANCHES by calling `evil-leap--label-targets' on each value, and
|
||||
replacing it with the result."
|
||||
(ht-each (lambda (k tgts)
|
||||
(ht-set! branches k (evil-leap--label-targets tgts)))
|
||||
branches))
|
||||
|
||||
(defun evil-leap--unlabel-other-branches (second-char branches)
|
||||
"Delete every overlay referenced by BRANCHES, *except* those under the branch
|
||||
with key SECOND-CHAR."
|
||||
(ht-each
|
||||
(lambda (k labeled-targets)
|
||||
(unless (eq k second-char)
|
||||
(ht-each
|
||||
(lambda (_label target)
|
||||
;; 1. A value in `labeled-targets' isn't always a target (e.g. entries
|
||||
;; at '?\s' and '?\b').
|
||||
;; 2. If a target is an auto-jump candidate, its `overlay' slot is nil.
|
||||
(when-let ((overlay (and (evil-leap-target-p target)
|
||||
(evil-leap-target-overlay target))))
|
||||
(delete-overlay overlay)))
|
||||
labeled-targets)))
|
||||
branches))
|
||||
|
||||
|
||||
; Gathering
|
||||
|
||||
(defun evil-leap--insert-branch-target (table second-char target)
|
||||
(ht-set! table
|
||||
second-char
|
||||
(cons target (ht-get table second-char))))
|
||||
|
||||
(cl-defun evil-leap--gather-branches (first-char &key backward-p)
|
||||
"TODO Searches the scope (for maddy: currently, this just means the visible
|
||||
portion of the buffer) for two-character pairs, consisting of FIRST-CHAR
|
||||
followed by some SECOND-CHAR. For each P = (FIRST-CHAR, SECOND-CHAR) pair
|
||||
found, the returned hash table maps SECOND-CHAR to a list of targets
|
||||
corresponding to each occurence of P."
|
||||
(let* ((start-of-visible (window-start))
|
||||
(end-of-visible (window-end))
|
||||
(pattern (rx-to-string `(seq ,first-char (group anychar))))
|
||||
(table (ht-create 'equal))
|
||||
;; TODO: Clean this up. }:\
|
||||
(search (if backward-p
|
||||
(lambda () (re-search-backward pattern start-of-visible t))
|
||||
(lambda () (re-search-forward pattern end-of-visible t)))))
|
||||
(save-excursion
|
||||
(while (funcall search)
|
||||
;; We are forced to save-excursion a second time, lest we enter a
|
||||
;; feedback loop, where `re-search-forward' moves to a match, and
|
||||
;; `goto-char' jumps to a position *preceding* the match.
|
||||
(save-excursion
|
||||
(goto-char (match-beginning 0))
|
||||
(let ((second-char (aref (match-string 1) 0)) ; string->char
|
||||
(target (evil-leap--target-at-point)))
|
||||
(evil-leap--insert-branch-target table second-char target)))))
|
||||
;; Reverse each list of targets.
|
||||
(ht-each (lambda (k v) (ht-set! table k (reverse v)))
|
||||
table)
|
||||
table))
|
||||
|
||||
|
||||
; Entry point
|
||||
|
||||
(cl-defun evil-leap
|
||||
(&key targets)
|
||||
"TODO Something something something.
|
||||
|
||||
Keyword arguments
|
||||
:backward-p BOOL
|
||||
:action ACTION
|
||||
:targets TARGETS A list of targets, or a Lisp procedure returning such a
|
||||
list. Each target should be a list of form
|
||||
(POS LINE COLUMN . XS)."
|
||||
(let ((backward-p (plist-get args :backward-p))
|
||||
(action (or (plist-get args :action) #'evil-leap-jump-to))
|
||||
(targets (plist-get args :targets)))
|
||||
))
|
||||
:no-overlays BOOL Non-nil if labels shall not be visually marked with
|
||||
overlays.
|
||||
:targets TARGETS A list of targets, or a Lisp procedure returning such a
|
||||
list."
|
||||
(unwind-protect
|
||||
(evil-leap-select-from-labeled-targets
|
||||
(evil-leap--label-targets targets))
|
||||
;; TODO: would it be faster to go through `labeled-targets' and unlabel them
|
||||
;; ourselves?
|
||||
(evil-leap--remove-all-labels))
|
||||
(evil-leap--remove-all-labels))
|
||||
|
||||
(evil-define-motion evil-leap-s (count keys)
|
||||
;; REVIEW: Could we the preview/branch functionality be rewriten in terms of
|
||||
;; higher-level functions? Preferably ones included in the public API?
|
||||
;;;###autoload
|
||||
(evil-define-motion evil-leap-directional (count backward-p)
|
||||
"TODO"
|
||||
:jump t
|
||||
:type 'exclusive
|
||||
(interactive)
|
||||
(evil-leap--collect-keys))
|
||||
;; FIXME: If the search pattern finds zero targets, `labeled-targets' ends up
|
||||
;; being nil, which leads to an error in `evil-leap--select-from-labeled-targets'.
|
||||
(unwind-protect
|
||||
(let* ((first-char (evil-read-key))
|
||||
(branches (evil-leap--gather-branches first-char
|
||||
:backward-p backward-p))
|
||||
(_ (evil-leap--label-branch-targets! branches))
|
||||
(second-char (evil-read-key))
|
||||
(_ (evil-leap--unlabel-other-branches second-char branches))
|
||||
(labeled-targets (ht-get branches second-char)))
|
||||
(pp labeled-targets)
|
||||
(evil-leap-select-from-labeled-targets labeled-targets))
|
||||
;; TODO: would it be faster to go through `labeled-targets' and unlabel them
|
||||
;; ourselves?
|
||||
(evil-leap--remove-all-labels))
|
||||
(evil-leap--remove-all-labels))
|
||||
|
||||
;;;###autoload
|
||||
(evil-define-motion evil-leap-forward (count)
|
||||
"TODO"
|
||||
:jump t
|
||||
:type 'exclusive
|
||||
(evil-leap-directional count nil))
|
||||
|
||||
;;;###autoload
|
||||
(evil-define-motion evil-leap-backward (count)
|
||||
"TODO"
|
||||
:jump t
|
||||
:type 'exclusive
|
||||
(evil-leap-directional count t))
|
||||
|
||||
|
||||
|
||||
(provide 'evil-leap)
|
||||
;;; evil-leap.el ends here
|
||||
|
||||
Reference in New Issue
Block a user