diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/jumper.el | 252 | 
1 files changed, 176 insertions, 76 deletions
| diff --git a/modules/jumper.el b/modules/jumper.el index 7a3991d0..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,30 +222,39 @@ 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.") +      (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." @@ -172,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. | 
