summaryrefslogtreecommitdiff
path: root/modules/jumper.el
blob: e1025472d6f3d6873f5b32d72afe05796f23e077 (plain)
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
;;; 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.

;;; Code:

(defgroup jumper nil
  "Quick navigation between stored locations."
  :group 'convenience)

(defcustom jumper-prefix-key "M-SPC"
  "Prefix key for jumper commands.

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)

;; 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))
			 (pos (get-register reg))
			 (marker (and pos (registerv-data pos))))
		(when 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))
		 (pos (get-register reg))
		 (marker (and pos (registerv-data pos))))
	(when 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-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!"))))

(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")))))

(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-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
(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)

(provide 'jumper)
;;; jumper.el ends here.