-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathmajutsu-selection.el
More file actions
336 lines (298 loc) · 14.1 KB
/
Copy pathmajutsu-selection.el
File metadata and controls
336 lines (298 loc) · 14.1 KB
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
;;; majutsu-selection.el --- Selection control for magit-section -*- lexical-binding: t; -*-
;; Copyright (C) 2026 0WD0
;; Author: 0WD0 <me@0wd0.com>
;; Maintainer: 0WD0 <me@0wd0.com>
;; Keywords: tools, vc
;; URL: https://github.com/0WD0/majutsu
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;;; This library provides magit-section selection for Majutsu.
;;; Code:
(require 'cl-lib)
(require 'crm)
(require 'magit-section)
(require 'seq)
(require 'subr-x)
(require 'transient)
(require 'majutsu-transient)
(defclass majutsu-selection-option (majutsu-transient-key-alias-suffix transient-option)
((selection-label :initarg :selection-label :initform nil)
(selection-face :initarg :selection-face :initform nil)
(locate-fn :initarg :locate-fn :initform nil
:documentation "Resolve a selected value to a section or (START . END) range.")
(targets-fn :initarg :targets-fn :initform nil
:documentation "Return the value(s) to select from point/region when toggling.")
(selection-toggle-key :initarg :selection-toggle-key :initform nil
:documentation "Additional key used to toggle values at point.")
(selection-toggle-if-not :initarg :selection-toggle-if-not :initform nil
:documentation "Disable the toggle key when this predicate is non-nil."))
"Base class for options that control majutsu selection categories.")
(defclass majutsu-revision-selection-option (majutsu-selection-option)
((locate-fn :initform (##majutsu-selection-find-section %))
(targets-fn :initform #'majutsu-revisions-at-point))
"Selection option for JJ revisions and their visible revision sections.")
(cl-defstruct (majutsu-selection-session
(:constructor majutsu-selection-session-create))
buffer
overlays)
(defvar majutsu-selection--overlay-buffers
(make-hash-table :test 'eq :weakness 'key)
"Buffers that may contain `majutsu-selection' overlays.")
(defvar-local majutsu-selection--active-session nil
"Selection session whose overlays are currently rendered in this buffer.")
(defun majutsu-selection--cleanup-overlays-in-buffer (buffer)
(when (buffer-live-p buffer)
(with-current-buffer buffer
(remove-overlays (point-min) (point-max) 'majutsu-selection t)
(setq majutsu-selection--active-session nil))))
(defmacro majutsu-selection--with-session-buffer (session &rest body)
"Run BODY in SESSION's buffer."
(declare (indent 1) (debug (form &rest form)))
(let ((buf (make-symbol "buf")))
`(let ((,buf (and ,session (majutsu-selection-session-buffer ,session))))
(unless (buffer-live-p ,buf)
(user-error "Selection buffer is no longer live"))
(with-current-buffer ,buf
,@body))))
(defun majutsu-selection--transient-setup-buffer ()
"Render selection overlays when a transient menu is being setup."
(let* ((session (transient-scope))
(buf (and (majutsu-selection-session-p session)
(majutsu-selection-session-buffer session))))
;; Clean up old buffers
(dolist (old-buf (hash-table-keys majutsu-selection--overlay-buffers))
(unless (and (eq old-buf buf) (buffer-live-p old-buf))
(majutsu-selection--cleanup-overlays-in-buffer old-buf)
(remhash old-buf majutsu-selection--overlay-buffers)))
;; Setup active buffer
(when (buffer-live-p buf)
(puthash buf t majutsu-selection--overlay-buffers)
(with-current-buffer buf
(unless (eq majutsu-selection--active-session session)
(majutsu-selection--cleanup-overlays-in-buffer buf)
(setq majutsu-selection--active-session session)
(majutsu-selection-render session))))))
(defun majutsu-selection--transient-post-exit ()
"Remove selection overlays when leaving the transient stack."
(dolist (buf (hash-table-keys majutsu-selection--overlay-buffers))
(majutsu-selection--cleanup-overlays-in-buffer buf))
(clrhash majutsu-selection--overlay-buffers))
(defun majutsu-selection-session-end-if-owner ()
"Remove selection overlays in the current buffer."
(remhash (current-buffer) majutsu-selection--overlay-buffers)
(majutsu-selection--cleanup-overlays-in-buffer (current-buffer)))
(defun majutsu-selection--targets-default ()
"Default selection target's value."
(or (magit-region-values nil t)
(when-let* ((section (magit-current-section))
(value (magit-section-ident-value section)))
(list value))))
(defun majutsu-selection--locate-default (value)
"Default locator for selection overlays."
(when-let* ((cur (magit-current-section))
(parent (oref cur parent))
(type (oref cur type)))
(magit-get-section
(append `((,type . ,value)) (magit-section-ident parent)))))
(defun majutsu-selection-find-section (value &optional type root)
"Return the closest section matching VALUE.
When ROOT is non-nil, traverse from that section, otherwise from
`magit-root-section'. TYPE defaults to the current section's type.
Exact matches are preferred; otherwise choose the nearest prefix match."
(let* ((root (or root magit-root-section))
(anchor (or (and-let* ((cur (magit-current-section)))
(oref cur start))
(point)))
(type (or type
(when-let* ((cur (magit-current-section)))
(oref cur type))))
(exact (and root value type
(magit-get-section
(append `((,type . ,value)) (magit-section-ident root)))))
best
best-dist)
(or exact
(progn
(magit-map-sections
(##let ((id (magit-section-value-if type %)))
(when (and (stringp value)
(stringp id)
(or (string-prefix-p id value)
(string-prefix-p value id)))
(let* ((pos (oref % start))
(dist (abs (- pos anchor))))
(when (or (null best-dist) (< dist best-dist))
(setq best %)
(setq best-dist dist)))))
root))
best)))
(defun majutsu-selection-session-begin ()
"Create a transient selection session for the current buffer."
(majutsu-selection-session-create
:buffer (current-buffer)
:overlays (make-hash-table :test 'equal)))
(defun majutsu-selection--selection-id (obj)
"Return selection category identifier for OBJ."
(when (slot-boundp obj 'argument)
(oref obj argument)))
(defun majutsu-selection--selection-multi-p (obj)
"Return non-nil when OBJ's selection category is multi-value."
(oref obj multi-value))
(defun majutsu-selection--find-option (id)
(when (boundp 'transient--suffixes)
(seq-find (lambda (obj)
(and (cl-typep obj 'majutsu-selection-option)
(equal (majutsu-selection--selection-id obj) id)))
transient--suffixes)))
(defun majutsu-selection--toggle-current (current values multi)
(setq values (ensure-list values))
(unless values
(user-error "No selection target at point"))
(if (not multi)
(let ((new (car values))
(old (if (listp current) (car current) current)))
(if (equal old new) nil new))
(unless (listp current)
(setq current (and current (list current))))
(dolist (id values)
(setq current (if (member id current)
(delete id current)
(append current (list id)))))
current))
(defun majutsu-selection-values (id)
"Return selected values for category ID."
(when-let* ((obj (majutsu-selection--find-option id)))
(let ((val (oref obj value)))
(if (listp val) val (list val)))))
(defun majutsu-selection--render-overlay (session id labels locate-fn)
(majutsu-selection--with-session-buffer session
(let* ((overlays (majutsu-selection-session-overlays session))
(range (when labels
(when-let* ((located (funcall locate-fn id)))
(cond
((and (consp located) (car located) (cdr located))
located)
((eieio-object-p located)
(let ((start (oref located start))
(end (or (oref located content) (oref located end))))
(and start end (cons start end)))))))))
(if range
(let* ((existing (gethash id overlays))
(start (car range))
(end (cdr range)))
(unless (and existing
(overlay-buffer existing)
(= (overlay-start existing) start)
(= (overlay-end existing) end))
(when existing (delete-overlay existing))
(setq existing (make-overlay start end nil t))
(overlay-put existing 'evaporate t)
(overlay-put existing 'priority '(nil . 50))
(overlay-put existing 'majutsu-selection t)
(puthash id existing overlays))
(overlay-put existing 'before-string
(concat (mapconcat #'identity (nreverse labels) " ") " ")))
(when-let* ((existing (gethash id overlays)))
(delete-overlay existing)
(remhash id overlays))))))
(defun majutsu-selection-render (&optional session)
"Re-render selection overlays for the current buffer."
(let* ((session (or session (transient-scope)))
(buf (and (majutsu-selection-session-p session)
(majutsu-selection-session-buffer session))))
(when (and buf (buffer-live-p buf) (boundp 'transient--suffixes))
(let ((overlays (majutsu-selection-session-overlays session))
(active-ids (make-hash-table :test 'equal)))
(dolist (obj transient--suffixes)
(when (cl-typep obj 'majutsu-selection-option)
(let ((vals (oref obj value))
(label (oref obj selection-label))
(face (oref obj selection-face)))
(when vals
(unless (listp vals) (setq vals (list vals)))
(dolist (id vals)
(let ((existing (gethash id active-ids)))
(puthash id (cons (cons (propertize (or label "") 'face face)
(car existing))
obj)
active-ids)))))))
(dolist (id (hash-table-keys overlays))
(unless (gethash id active-ids)
(delete-overlay (gethash id overlays))
(remhash id overlays)))
(maphash (lambda (id data)
(let* ((obj (cdr data))
(locate-fn (or (oref obj locate-fn)
#'majutsu-selection--locate-default)))
(majutsu-selection--render-overlay
session id (car data) locate-fn)))
active-ids)))))
(defun majutsu-selection-clear (&optional id)
"Clear selections.
If ID is non-nil, clear only that selection category."
(interactive)
(when (boundp 'transient--suffixes)
(dolist (obj transient--suffixes)
(when (and (cl-typep obj 'majutsu-selection-option)
(or (null id)
(equal (majutsu-selection--selection-id obj) id)))
(oset obj value nil)))
(majutsu-selection-render)))
(cl-defmethod majutsu-transient-key-aliases ((obj majutsu-selection-option))
(append (cl-call-next-method)
(and (majutsu-selection--toggle-active-p obj)
(ensure-list (oref obj selection-toggle-key)))))
(defun majutsu-selection--toggle-active-p (obj)
"Return non-nil when OBJ's toggle key is active."
(if-let* ((predicate (oref obj selection-toggle-if-not)))
(not (funcall predicate))
t))
(defun majutsu-selection--toggle-key-p (obj)
"Return non-nil when OBJ's toggle key invoked the current command."
(and (majutsu-selection--toggle-active-p obj)
(seq-some #'majutsu-transient-key-invoked-p
(ensure-list (oref obj selection-toggle-key)))))
(defun majutsu-selection--read-toggle (obj)
"Return OBJ's value after toggling the target at point."
(let* ((fn (or (oref obj targets-fn)
#'majutsu-selection--targets-default))
(values (funcall fn)))
(majutsu-selection--toggle-current
(oref obj value) values (majutsu-selection--selection-multi-p obj))))
(cl-defmethod transient-init-value :after ((obj majutsu-selection-option))
(when (and (eq (oref obj multi-value) 'repeat)
(slot-boundp obj 'argument)
(listp (oref obj value)))
(let ((argument (oref obj argument)))
(oset obj value
(mapcar (lambda (value)
(or (and (stringp value)
(transient-arg-value argument (list value)))
value))
(oref obj value))))))
(cl-defmethod transient-infix-set ((_obj majutsu-selection-option) _value)
(cl-call-next-method)
(majutsu-selection-render))
(cl-defmethod transient-infix-read :around ((obj majutsu-selection-option))
(let* ((session (transient-scope))
(value (if (majutsu-selection-session-p session)
(majutsu-selection--with-session-buffer session
(if (majutsu-selection--toggle-key-p obj)
(majutsu-selection--read-toggle obj)
(cl-call-next-method)))
(if (majutsu-selection--toggle-key-p obj)
(majutsu-selection--read-toggle obj)
(cl-call-next-method)))))
(if (not (majutsu-selection--selection-multi-p obj))
value
(cond
((null value) nil)
((listp value) value)
((and (eq (oref obj multi-value) 'repeat)
(stringp value))
(split-string value crm-separator t))
(t (list value))))))
(add-hook 'transient-setup-buffer-hook #'majutsu-selection--transient-setup-buffer)
(add-hook 'transient-post-exit-hook #'majutsu-selection--transient-post-exit)
(provide 'majutsu-selection)
;;; majutsu-selection.el ends here