1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
|
;;; jumper.el --- Quick jump between locations using registers -*- lexical-binding: t -*-
;; Author: Craig Jennings
;; Version: 0.1
;; Package-Requires: ((emacs "25.1"))
;; Keywords: convenience
;; URL: https://github.com/cjennings/jumper
;;; Commentary:
;; 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:
(require 'cl-lib)
(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.")
(defvar jumper-max-locations 10
"Maximum number of locations to store.")
;; Internal variables
(defvar jumper--registers (make-vector jumper-max-locations nil)
"Vector of used registers.")
(defvar jumper--next-index 0
"Next available index in the jumper--registers vector.")
(defvar jumper--last-location-register ?z
"Register used to store the last location.")
(defun jumper--location-key ()
"Generate a key to identify the current location."
(format "%s:%d:%d"
(or (buffer-file-name) (buffer-name))
(line-number-at-pos)
(current-column)))
(defun jumper--location-exists-p ()
"Check if current location is already stored."
(let ((key (jumper--location-key))
(found nil))
(dotimes (i jumper--next-index found)
(let* ((reg (aref jumper--registers i))
(marker (get-register reg)))
(when (and marker (markerp marker))
(save-current-buffer
(set-buffer (marker-buffer marker))
(save-excursion
(goto-char marker)
(when (string= key (jumper--location-key))
(setq found t)))))))))
(defun jumper--register-available-p ()
"Check if there are registers available."
(< jumper--next-index jumper-max-locations))
(defun jumper--format-location (index)
"Format location at INDEX for display."
(let* ((reg (aref jumper--registers index))
(marker (get-register reg)))
(when (and marker (markerp marker))
(save-current-buffer
(set-buffer (marker-buffer marker))
(save-excursion
(goto-char marker)
(format "[%d] %s:%d - %s"
index
(buffer-name)
(line-number-at-pos)
(buffer-substring-no-properties
(line-beginning-position)
(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)
(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)
(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."
(when (< removed-idx (1- jumper--next-index))
;; Shift all higher registers down
(cl-loop for i from removed-idx below (1- jumper--next-index)
do (let ((next-reg (aref jumper--registers (1+ i))))
(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))))
(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)
(keymap-global-set jumper-prefix-key jumper-map))
;; 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.
|