aboutsummaryrefslogtreecommitdiffstats
path: root/elpa/evil-20220503.1314/evil-jumps.el
blob: fea2e431df094129a77e98bd1f57925814774ca4 (plain) (blame)
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
;;; evil-jumps.el --- Jump list implementation -*- lexical-binding: t -*-

;; Author: Bailey Ling <bling at live.ca>

;; Version: 1.15.0

;;
;; This file is NOT part of GNU Emacs.

;;; License:

;; This file is part of Evil.
;;
;; Evil is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; Evil is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with Evil.  If not, see <http://www.gnu.org/licenses/>.

(require 'cl-lib)
(require 'evil-core)
(require 'evil-states)

;;; Code:

(defgroup evil-jumps nil
  "Evil jump list configuration options."
  :prefix "evil-jumps"
  :group 'evil)

(defcustom evil-jumps-cross-buffers t
  "When non-nil, the jump commands can cross borders between buffers, otherwise the jump commands act only within the current buffer."
  :type 'boolean
  :group 'evil-jumps)

(defcustom evil-jumps-max-length 100
  "The maximum number of jumps to keep track of."
  :type 'integer
  :group 'evil-jumps)

(defcustom evil-jumps-pre-jump-hook nil
  "Hooks to run just before jumping to a location in the jump list."
  :type 'hook
  :group 'evil-jumps)

(defcustom evil-jumps-post-jump-hook nil
  "Hooks to run just after jumping to a location in the jump list."
  :type 'hook
  :group 'evil-jumps)

(defcustom evil-jumps-ignored-file-patterns '("COMMIT_EDITMSG$" "TAGS$")
  "A list of pattern regexps to match on the file path to exclude from being included in the jump list."
  :type '(repeat string)
  :group 'evil-jumps)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defvar savehist-additional-variables)

(defvar evil--jumps-jumping nil)

(defvar evil--jumps-jumping-backward nil
  "Set by `evil--jump-backward', used and cleared in the
`post-command-hook' by `evil--jump-handle-buffer-crossing'")

(eval-when-compile (defvar evil--jumps-debug nil))

(defvar evil--jumps-buffer-targets "\\*\\(new\\|scratch\\)\\*"
  "Regexp to match against `buffer-name' to determine whether it's a valid jump target.")

(defvar evil--jumps-window-jumps (make-hash-table)
  "Hashtable which stores all jumps on a per window basis.")

(defvar evil-jumps-history nil
  "History of `evil-mode' jumps that are persisted with `savehist'.")

(cl-defstruct evil-jumps-struct
  ring
  (idx -1)
  previous-pos)

;; Is inlining this really worth it?
(defsubst evil--jumps-message (format &rest args)
  (when (eval-when-compile evil--jumps-debug)
    (with-current-buffer (get-buffer-create "*evil-jumps*")
       (goto-char (point-max))
       (insert (apply #'format format args) "\n"))))

(defun evil--jumps-get-current (&optional window)
  (unless window
    (setq window (frame-selected-window)))
  (let* ((jump-struct (gethash window evil--jumps-window-jumps)))
    (unless jump-struct
      (setq jump-struct (make-evil-jumps-struct))
      (puthash window jump-struct evil--jumps-window-jumps))
    jump-struct))

(defun evil--jumps-get-jumps (struct)
  (let ((ring (evil-jumps-struct-ring struct)))
    (unless ring
      (setq ring (make-ring evil-jumps-max-length))
      (setf (evil-jumps-struct-ring struct) ring))
    ring))

(defun evil--jumps-get-window-jump-list ()
  (let ((struct (evil--jumps-get-current)))
    (evil--jumps-get-jumps struct)))

(defun evil--jumps-savehist-load ()
  (add-to-list 'savehist-additional-variables 'evil-jumps-history)
  (let ((ring (make-ring evil-jumps-max-length)))
    (cl-loop for jump in (reverse evil-jumps-history)
             do (ring-insert ring jump))
    (setf (evil-jumps-struct-ring (evil--jumps-get-current)) ring))
  (add-hook 'savehist-save-hook #'evil--jumps-savehist-sync)
  (remove-hook 'savehist-mode-hook #'evil--jumps-savehist-load))

(defun evil--jumps-savehist-sync ()
  "Updates the printable value of window jumps for `savehist'."
  (setq evil-jumps-history
        (delq nil (mapcar #'(lambda (jump)
                              (let* ((mark (car jump))
                                     (pos (if (markerp mark)
                                              (marker-position mark)
                                            mark))
                                     (file-name (cadr jump)))
                                (when (and (not (file-remote-p file-name))
                                           (file-exists-p file-name)
                                           pos)
                                  (list pos file-name))))
                          (ring-elements (evil--jumps-get-window-jump-list))))))

(defun evil--jumps-jump (idx shift)
  (let ((target-list (evil--jumps-get-window-jump-list)))
    (evil--jumps-message "jumping from %s by %s" idx shift)
    (evil--jumps-message "target list = %s" target-list)
    (setq idx (+ idx shift))
    (let* ((current-file-name (or (buffer-file-name) (buffer-name)))
           (size (ring-length target-list)))
      (unless evil-jumps-cross-buffers
        ;; skip jump marks pointing to other buffers
        (while (and (< idx size) (>= idx 0)
                    (not (string= current-file-name (cadr (ring-ref target-list idx)))))
          (setq idx (+ idx shift))))
      (when (and (< idx size) (>= idx 0))
        ;; actual jump
        (run-hooks 'evil-jumps-pre-jump-hook)
        (let* ((place (ring-ref target-list idx))
               (pos (car place))
               (file-name (cadr place)))
          (setq evil--jumps-jumping t)
          (unless (string= current-file-name file-name)
            (if (string-match-p evil--jumps-buffer-targets file-name)
                (switch-to-buffer file-name)
              (find-file file-name)))
          (setq evil--jumps-jumping nil)
          (goto-char pos)
          (setf (evil-jumps-struct-idx (evil--jumps-get-current)) idx)
          (run-hooks 'evil-jumps-post-jump-hook))))))

(defun evil--jumps-push ()
  "Pushes the current cursor/file position to the jump list."
  (let ((target-list (evil--jumps-get-window-jump-list)))
    (let ((file-name (buffer-file-name))
          (buffer-name (buffer-name))
          (current-pos (point-marker))
          (first-pos nil)
          (first-file-name nil)
          (excluded nil))
      (when (and (not file-name)
                 (string-match-p evil--jumps-buffer-targets buffer-name))
        (setq file-name buffer-name))
      (when file-name
        (dolist (pattern evil-jumps-ignored-file-patterns)
          (when (string-match-p pattern file-name)
            (setq excluded t)))
        (unless excluded
          (unless (ring-empty-p target-list)
            (setq first-pos (car (ring-ref target-list 0)))
            (setq first-file-name (car (cdr (ring-ref target-list 0)))))
          (unless (and (equal first-pos current-pos)
                       (equal first-file-name file-name))
            (evil--jumps-message "pushing %s on %s" current-pos file-name)
            (ring-insert target-list `(,current-pos ,file-name))))))
    (evil--jumps-message "%s %s"
                         (selected-window)
                         (and (not (ring-empty-p target-list))
                              (ring-ref target-list 0)))))

(evil-define-command evil-show-jumps ()
  "Display the contents of the jump list."
  :repeat nil
  (evil-with-view-list
    :name "evil-jumps"
    :mode "Evil Jump List"
    :format [("Jump" 5 nil)
             ("Marker" 8 nil)
             ("File/text" 1000 t)]
    :entries (let* ((jumps (evil--jumps-savehist-sync))
                    (count 0))
               (cl-loop for jump in jumps
                        collect `(nil [,(number-to-string (cl-incf count))
                                       ,(number-to-string (car jump))
                                       (,(cadr jump))])))
    :select-action #'evil--show-jumps-select-action))

(defun evil--show-jumps-select-action (jump)
  (let ((position (string-to-number (elt jump 1)))
        (file (car (elt jump 2))))
    (kill-buffer)
    (switch-to-buffer (find-file file))
    (goto-char position)))

(defun evil-set-jump (&optional pos)
  "Set jump point at POS.
POS defaults to point."
  (save-excursion
    (when (markerp pos)
      (set-buffer (marker-buffer pos)))

    (unless (or (region-active-p) (evil-visual-state-p))
      (push-mark pos t))

    (unless evil--jumps-jumping
      ;; clear out intermediary jumps when a new one is set
      (let* ((struct (evil--jumps-get-current))
             (target-list (evil--jumps-get-jumps struct))
             (idx (evil-jumps-struct-idx struct)))
        (cl-loop repeat idx
                 do (ring-remove target-list))
        (setf (evil-jumps-struct-idx struct) -1))
      (when pos
        (goto-char pos))
      (evil--jumps-push))))

(defun evil--jump-backward (count)
  (setq evil--jumps-jumping-backward t)
  (let ((count (or count 1)))
    (evil-motion-loop (nil count)
      (let* ((struct (evil--jumps-get-current))
             (idx (evil-jumps-struct-idx struct)))
        (evil--jumps-message "jumping back %s" idx)
        (when (= idx -1)
          (setq idx 0)
          (setf (evil-jumps-struct-idx struct) 0)
          (evil--jumps-push))
        (evil--jumps-jump idx 1)))))

(defun evil--jump-forward (count)
  (let ((count (or count 1)))
    (evil-motion-loop (nil count)
      (let* ((struct (evil--jumps-get-current))
             (idx (evil-jumps-struct-idx struct)))
        (when (= idx -1)
          (setq idx 0)
          (setf (evil-jumps-struct-idx struct) 0)
          (evil--jumps-push))
        (evil--jumps-jump idx -1)))))

(defun evil--jumps-window-configuration-hook (&rest _args)
  (let* ((window-list (window-list-1 nil nil t))
         (existing-window (selected-window))
         (new-window (previous-window)))
    (when (and (not (eq existing-window new-window))
               (> (length window-list) 1))
      (let* ((target-jump-struct (evil--jumps-get-current new-window)))
        (if (not (ring-empty-p (evil--jumps-get-jumps target-jump-struct)))
            (evil--jumps-message "target window %s already has %s jumps" new-window
                                 (ring-length (evil--jumps-get-jumps target-jump-struct)))
          (evil--jumps-message "new target window detected; copying %s to %s" existing-window new-window)
          (let* ((source-jump-struct (evil--jumps-get-current existing-window))
                 (source-list (evil--jumps-get-jumps source-jump-struct)))
            (when (= (ring-length (evil--jumps-get-jumps target-jump-struct)) 0)
              (setf (evil-jumps-struct-previous-pos target-jump-struct) (evil-jumps-struct-previous-pos source-jump-struct))
              (setf (evil-jumps-struct-idx target-jump-struct) (evil-jumps-struct-idx source-jump-struct))
              (setf (evil-jumps-struct-ring target-jump-struct) (ring-copy source-list)))))))
    ;; delete obsolete windows
    (maphash (lambda (key _val)
               (unless (member key window-list)
                 (evil--jumps-message "removing %s" key)
                 (remhash key evil--jumps-window-jumps)))
             evil--jumps-window-jumps)))

(defun evil--jump-hook (&optional command)
  "`pre-command-hook' for evil-jumps.
Set jump point if COMMAND has a non-nil `:jump' property. Otherwise,
save the current position in case the command being executed will
change the current buffer."
  (setq command (or command this-command))
  (if (evil-get-command-property command :jump)
      (evil-set-jump)
    (setf (evil-jumps-struct-previous-pos (evil--jumps-get-current))
          (point-marker))))

(defun evil--jump-handle-buffer-crossing ()
  (let ((jumping-backward evil--jumps-jumping-backward))
    (setq evil--jumps-jumping-backward nil)
    (dolist (frame (frame-list))
      (dolist (window (window-list frame))
        (let* ((struct (evil--jumps-get-current window))
               (previous-pos (evil-jumps-struct-previous-pos struct)))
          (when previous-pos
            (setf (evil-jumps-struct-previous-pos struct) nil)
            (if (and
                 ;; `evil-jump-backward' (and other backward jumping
                 ;; commands) needs to be handled specially. When
                 ;; jumping backward multiple times, calling
                 ;; `evil-set-jump' is always wrong: If you jump back
                 ;; twice and we call `evil-set-jump' after the second
                 ;; time, we clear the forward jump list and
                 ;; `evil--jump-forward' won't work.

                 ;; The first time you jump backward, setting a jump
                 ;; point is sometimes correct. But we don't do it
                 ;; here because this function is called after
                 ;; `evil--jump-backward' has updated our position in
                 ;; the jump list so, again, `evil-set-jump' would
                 ;; break `evil--jump-forward'.
                 (not jumping-backward)
                 (let ((previous-buffer (marker-buffer previous-pos)))
                   (and previous-buffer
                        (not (eq previous-buffer (window-buffer window))))))
                (evil-set-jump previous-pos)
              (set-marker previous-pos nil))))))))

(if (bound-and-true-p savehist-loaded)
    (evil--jumps-savehist-load)
  (add-hook 'savehist-mode-hook #'evil--jumps-savehist-load))

(defun evil--jumps-install-or-uninstall ()
  (if evil-local-mode
      (progn
        (add-hook 'pre-command-hook #'evil--jump-hook nil t)
        (add-hook 'post-command-hook #'evil--jump-handle-buffer-crossing nil t)
        (add-hook 'next-error-hook #'evil-set-jump nil t)
        (add-hook 'window-configuration-change-hook #'evil--jumps-window-configuration-hook nil t))
    (remove-hook 'pre-command-hook #'evil--jump-hook t)
    (remove-hook 'post-command-hook #'evil--jump-handle-buffer-crossing t)
    (remove-hook 'next-error-hook #'evil-set-jump t)
    (remove-hook 'window-configuration-change-hook #'evil--jumps-window-configuration-hook t)
    (evil--jump-handle-buffer-crossing)))

(add-hook 'evil-local-mode-hook #'evil--jumps-install-or-uninstall)

(provide 'evil-jumps)

;;; evil-jumps.el ends here