summaryrefslogtreecommitdiff
path: root/modules/jumper.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/jumper.el')
-rw-r--r--modules/jumper.el255
1 files changed, 177 insertions, 78 deletions
diff --git a/modules/jumper.el b/modules/jumper.el
index e1025472..67d930aa 100644
--- a/modules/jumper.el
+++ b/modules/jumper.el
@@ -10,24 +10,76 @@
;; Jumper provides a simple way to store and jump between locations
;; in your codebase without needing to remember register assignments.
+;;
+;; PURPOSE:
+;;
+;; When working on large codebases, you often need to jump between
+;; multiple related locations: a function definition, its tests, its
+;; callers, configuration files, etc. Emacs registers are perfect for
+;; this, but require you to remember which register you assigned to
+;; which location. Jumper automates register management, letting you
+;; focus on your work instead of bookkeeping.
+;;
+;; WORKFLOW:
+;;
+;; 1. Navigate to an important location in your code
+;; 2. Press M-SPC SPC to store it (automatically assigned to register 0)
+;; 3. Continue working, storing more locations as needed (registers 1-9)
+;; 4. Press M-SPC j to jump back to any stored location
+;; 5. Select from the list using completion (shows file, line, context)
+;; 6. Press M-SPC d to remove locations you no longer need
+;;
+;; RECOMMENDED USAGE:
+;;
+;; Store locations temporarily while working on a feature:
+;; - Store the main function you're implementing
+;; - Store the test file where you're writing tests
+;; - Store the caller that needs updating
+;; - Store the documentation that needs changes
+;; - Jump between them freely as you work
+;; - Clear them when done with the feature
+;;
+;; SPECIAL BEHAVIORS:
+;;
+;; - Duplicate prevention: Storing the same location twice shows a message
+;; instead of wasting a register slot.
+;;
+;; - Single location toggle: When only one location is stored, M-SPC j
+;; toggles between that location and your current position. Perfect for
+;; rapid back-and-forth between two related files.
+;;
+;; - Last location tracking: The last position before each jump is saved
+;; in register 'z', allowing quick "undo" of navigation.
+;;
+;; - Smart selection: With multiple locations, completing-read shows
+;; helpful context: "[0] filename.el:42 - function definition..."
+;;
+;; KEYBINDINGS:
+;;
+;; M-SPC SPC Store current location in next available register
+;; M-SPC j Jump to a stored location (with completion)
+;; M-SPC d Delete a stored location from the list
+;;
+;; CONFIGURATION:
+;;
+;; You can customize the prefix key and maximum locations:
+;;
+;; (setq jumper-prefix-key "C-c j") ; Change prefix key
+;; (setq jumper-max-locations 20) ; Store up to 20 locations
+;;
+;; Note: Changing jumper-max-locations requires restarting Emacs or
+;; manually reinitializing jumper--registers.
;;; Code:
-(defgroup jumper nil
- "Quick navigation between stored locations."
- :group 'convenience)
+(require 'cl-lib)
-(defcustom jumper-prefix-key "M-SPC"
+(defvar jumper-prefix-key "M-SPC"
"Prefix key for jumper commands.
+Note that using M-SPC will override the default binding to just-one-space.")
-Note that using M-SPC will override the default binding to just-one-space."
- :type 'string
- :group 'jumper)
-
-(defcustom jumper-max-locations 10
- "Maximum number of locations to store."
- :type 'integer
- :group 'jumper)
+(defvar jumper-max-locations 10
+ "Maximum number of locations to store.")
;; Internal variables
(defvar jumper--registers (make-vector jumper-max-locations nil)
@@ -50,12 +102,10 @@ Note that using M-SPC will override the default binding to just-one-space."
"Check if current location is already stored."
(let ((key (jumper--location-key))
(found nil))
- (dotimes (i
- jumper--next-index found)
+ (dotimes (i jumper--next-index found)
(let* ((reg (aref jumper--registers i))
- (pos (get-register reg))
- (marker (and pos (registerv-data pos))))
- (when marker
+ (marker (get-register reg)))
+ (when (and marker (markerp marker))
(save-current-buffer
(set-buffer (marker-buffer marker))
(save-excursion
@@ -70,9 +120,8 @@ Note that using M-SPC will override the default binding to just-one-space."
(defun jumper--format-location (index)
"Format location at INDEX for display."
(let* ((reg (aref jumper--registers index))
- (pos (get-register reg))
- (marker (and pos (registerv-data pos))))
- (when marker
+ (marker (get-register reg)))
+ (when (and marker (markerp marker))
(save-current-buffer
(set-buffer (marker-buffer marker))
(save-excursion
@@ -86,49 +135,83 @@ Note that using M-SPC will override the default binding to just-one-space."
(min (+ (line-beginning-position) 40)
(line-end-position)))))))))
+(defun jumper--do-store-location ()
+ "Store current location in the next free register.
+Returns: \\='already-exists if location is already stored,
+ \\='no-space if all registers are full,
+ register character if successfully stored."
+ (cond
+ ((jumper--location-exists-p) 'already-exists)
+ ((not (jumper--register-available-p)) 'no-space)
+ (t
+ (let ((reg (+ ?0 jumper--next-index)))
+ (point-to-register reg)
+ (aset jumper--registers jumper--next-index reg)
+ (setq jumper--next-index (1+ jumper--next-index))
+ reg))))
+
(defun jumper-store-location ()
"Store current location in the next free register."
(interactive)
- (if (jumper--location-exists-p)
- (message "Location already stored")
- (if (jumper--register-available-p)
- (let ((reg (+ ?0 jumper--next-index)))
- (point-to-register reg)
- (aset jumper--registers jumper--next-index reg)
- (setq jumper--next-index (1+ jumper--next-index))
- (message "Location stored in register %c" reg))
- (message "Sorry - all jump locations are filled!"))))
+ (pcase (jumper--do-store-location)
+ ('already-exists (message "Location already stored"))
+ ('no-space (message "Sorry - all jump locations are filled!"))
+ (reg (message "Location stored in register %c" reg))))
+
+(defun jumper--do-jump-to-location (target-idx)
+ "Jump to location at TARGET-IDX.
+TARGET-IDX: -1 for last location, 0-9 for stored locations, nil for toggle.
+Returns: \\='no-locations if no locations stored,
+ \\='already-there if at the only location (toggle case),
+ \\='jumped if successfully jumped."
+ (cond
+ ((= jumper--next-index 0) 'no-locations)
+ ;; Toggle behavior when target-idx is nil and only 1 location
+ ((and (null target-idx) (= jumper--next-index 1))
+ (if (jumper--location-exists-p)
+ 'already-there
+ (let ((reg (aref jumper--registers 0)))
+ (point-to-register jumper--last-location-register)
+ (jump-to-register reg)
+ 'jumped)))
+ ;; Jump to specific target
+ (t
+ (if (= target-idx -1)
+ ;; Jumping to last location - don't overwrite it
+ (jump-to-register jumper--last-location-register)
+ ;; Jumping to stored location - save current for "last"
+ (progn
+ (point-to-register jumper--last-location-register)
+ (jump-to-register (aref jumper--registers target-idx))))
+ 'jumped)))
(defun jumper-jump-to-location ()
"Jump to a stored location."
(interactive)
- (if (= jumper--next-index 0)
- (message "No locations stored")
- (if (= jumper--next-index 1)
- ;; Special case for one location - toggle behavior
- (let ((reg (aref jumper--registers 0)))
- (if (jumper--location-exists-p)
- (message "You're already at the stored location")
- (point-to-register jumper--last-location-register)
- (jump-to-register reg)
- (message "Jumped to location")))
- ;; Multiple locations - use completing-read
- (let* ((locations
- (cl-loop for i from 0 below jumper--next-index
- for fmt = (jumper--format-location i)
- when fmt collect (cons fmt i)))
- ;; Add last location if available
- (last-pos (get-register jumper--last-location-register))
- (locations (if last-pos
- (cons (cons "[z] Last location" -1) locations)
- locations))
- (choice (completing-read "Jump to: " locations nil t))
- (idx (cdr (assoc choice locations))))
- (point-to-register jumper--last-location-register)
- (if (= idx -1)
- (jump-to-register jumper--last-location-register)
- (jump-to-register (aref jumper--registers idx)))
- (message "Jumped to location")))))
+ (cond
+ ;; No locations
+ ((= jumper--next-index 0)
+ (message "No locations stored"))
+ ;; Single location - toggle
+ ((= jumper--next-index 1)
+ (pcase (jumper--do-jump-to-location nil)
+ ('already-there (message "You're already at the stored location"))
+ ('jumped (message "Jumped to location"))))
+ ;; Multiple locations - prompt user
+ (t
+ (let* ((locations
+ (cl-loop for i from 0 below jumper--next-index
+ for fmt = (jumper--format-location i)
+ when fmt collect (cons fmt i)))
+ ;; Add last location if available
+ (last-pos (get-register jumper--last-location-register))
+ (locations (if last-pos
+ (cons (cons "[z] Last location" -1) locations)
+ locations))
+ (choice (completing-read "Jump to: " locations nil t))
+ (idx (cdr (assoc choice locations))))
+ (jumper--do-jump-to-location idx)
+ (message "Jumped to location")))))
(defun jumper--reorder-registers (removed-idx)
"Reorder registers after removing the one at REMOVED-IDX."
@@ -139,32 +222,40 @@ Note that using M-SPC will override the default binding to just-one-space."
(aset jumper--registers i next-reg))))
(setq jumper--next-index (1- jumper--next-index)))
+(defun jumper--do-remove-location (index)
+ "Remove location at INDEX.
+Returns: \\='no-locations if no locations stored,
+ \\='cancelled if index is -1,
+ t if successfully removed."
+ (cond
+ ((= jumper--next-index 0) 'no-locations)
+ ((= index -1) 'cancelled)
+ (t
+ (jumper--reorder-registers index)
+ t)))
+
(defun jumper-remove-location ()
"Remove a stored location."
(interactive)
(if (= jumper--next-index 0)
- (message "No locations stored")
- (let* ((locations
- (cl-loop for i from 0 below jumper--next-index
- for fmt = (jumper--format-location i)
- when fmt collect (cons fmt i)))
- (locations (cons (cons "Cancel" -1) locations))
- (choice (completing-read "Remove location: " locations nil t))
- (idx (cdr (assoc choice locations))))
- (if (= idx -1)
- (message "Operation cancelled")
- (jumper--reorder-registers idx)
- (message "Location removed")))))
-
-(defvar jumper-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "SPC") #'jumper-store-location)
- (define-key map (kbd "j") #'jumper-jump-to-location)
- (define-key map (kbd "d") #'jumper-remove-location)
- map)
- "Keymap for jumper commands.")
-
-;;;###autoload
+ (message "No locations stored")
+ (let* ((locations
+ (cl-loop for i from 0 below jumper--next-index
+ for fmt = (jumper--format-location i)
+ when fmt collect (cons fmt i)))
+ (locations (cons (cons "Cancel" -1) locations))
+ (choice (completing-read "Remove location: " locations nil t))
+ (idx (cdr (assoc choice locations))))
+ (pcase (jumper--do-remove-location idx)
+ ('cancelled (message "Operation cancelled"))
+ ('t (message "Location removed"))))))
+
+(defvar-keymap jumper-map
+ :doc "Keymap for jumper commands"
+ "SPC" #'jumper-store-location
+ "j" #'jumper-jump-to-location
+ "d" #'jumper-remove-location)
+
(defun jumper-setup-keys ()
"Setup default keybindings for jumper."
(interactive)
@@ -173,5 +264,13 @@ Note that using M-SPC will override the default binding to just-one-space."
;; Call jumper-setup-keys when the package is loaded
(jumper-setup-keys)
+;; which-key integration
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "M-SPC" "jumper menu"
+ "M-SPC SPC" "store location"
+ "M-SPC j" "jump to location"
+ "M-SPC d" "remove location"))
+
(provide 'jumper)
;;; jumper.el ends here.