-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathbufler-workspace.el
435 lines (375 loc) · 18.6 KB
/
bufler-workspace.el
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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
;;; bufler-workspace.el --- Bufler workspaces -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Adam Porter
;; Author: Adam Porter <[email protected]>
;; URL: https://github.com/alphapapa/bufler.el
;;; License:
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This file implements workspace features using Bufler.
;;; Code:
;;;; Requirements
(require 'burly)
(require 'bufler)
;;;; Variables
(defvar bufler-mode)
(defvar burly-tabs-mode)
(declare-function burly-tabs-mode "burly-tabs")
;;;; Customization
(defgroup bufler-workspace nil
"Options for Bufler's workspaces."
:group 'bufler)
(defcustom bufler-workspace-ignore-case t
"Ignore case when completing buffer paths and names."
:type 'boolean)
(defcustom bufler-workspace-prefix "Workspace: "
"Prefix for workspace names.
Applied when saving a workspace."
:type 'string)
(defcustom bufler-workspace-prefix-abbreviation (cons (rx bos "Workspace: ") "𝕎 ")
"How to abbreviate workspace names.
Applied to tab/frame names. The regular expression is replaced
with the string."
:type '(choice (cons (regexp :tag "Removal regexp" "\\`Workspace: ")
(string :tag "Replacement string" "𝕎: "))
(const :tag "Don't abbreviate" nil)))
(defcustom bufler-workspace-set-hook
(list #'bufler-workspace-set-frame-name)
"Functions called when the workspace is set.
Functions are called with one argument, the workspace's name or
path."
:type 'hook)
(defcustom bufler-workspace-format-path-fn #'bufler-format-path
"Function to format group paths for display in mode line and frame title.
May be customized to, e.g. only return the last element of a path."
:type '(choice (const :tag "Whole path" bufler-format-path)
(const :tag "Last element" (lambda (path)
(car (last (bufler-faceify-path path)))))
(function :tag "Custom function")))
(defcustom bufler-workspace-switch-buffer-filter-fns
'(bufler--buffer-hidden-p bufler--buffer-mode-filtered-p bufler--buffer-name-filtered-p)
"Buffers matching these functions are hidden when offering buffers for switching."
:type '(repeat
(choice (function-item bufler--buffer-hidden-p)
(function-item bufler--buffer-mode-filtered-p)
(function-item bufler--buffer-name-filtered-p)
(function-item bufler--buffer-special-p)
(function :tag "Custom function"))))
(defcustom bufler-switch-buffer-include-recent-buffers t
"Include recently shown buffers when offering buffers for switching.
Includes buffers from `window-prev-buffers' at the top of the
list of buffers in `bufler-switch-buffer'."
:type 'boolean)
(defcustom bufler-workspace-mode-lighter "Bflr:"
"Lighter used in mode-line for `bufler-workspace-mode'."
:type 'string)
;;;; Variables
(defvar burly-buffer-local-variables)
;;;; Macros
(defmacro bufler-without-mode (mode &rest body)
"Evaluate BODY without MODE enabled.
Re-enable MODE afterward if it was already enabled."
(declare (indent defun))
`(let (was-enabled-p)
(when ,mode
(setf was-enabled-p t)
(,mode -1))
,@body
(when was-enabled-p
(,mode 1))))
;; These follow the examples in `tab-bar'.
(defsubst bufler-workspace--tab-parameter (parameter tab)
"Return PARAMETER's value in TAB."
(alist-get parameter (cdr tab)))
(defsubst bufler-workspace--set-tab-parameter (parameter tab value)
"Set PARAMETER in TAB to VALUE and return it."
(setf (alist-get parameter (cdr tab)) value))
(gv-define-simple-setter bufler-workspace--tab-parameter bufler-workspace--set-tab-parameter)
;;;; Commands
;;;###autoload
(defun bufler-workspace-frame-set ()
"Call `bufler-workspace-set' with `tab-bar-mode bound to nil."
(interactive)
;; Bind `tab-bar-mode' nil to trick `bufler-workspace-set' into
;; thinking it's not active and setting the frame parameter instead.
;; (Hopefully this doesn't hurt...)
(let ((tab-bar-mode nil))
(call-interactively #'bufler-workspace-set)))
;;;###autoload
(cl-defun bufler-workspace-set (&optional path &key title)
"Set workspace for the current tab or frame to the one at PATH.
Interactively, choose workspace path with completion. If PATH is
nil (interactively, with prefix), unset the frame's workspace.
Sets tab's workspace if `tab-bar-mode' is active, otherwise the
frame's. If TITLE, use it as the tab's/frame's name (note that
this is not the same as using a named workspace). Return the
workspace path."
(interactive
(list
(unless current-prefix-arg
(let* ((bufler-vc-state nil)
(grouped-buffers (bufler-buffers))
(buffer-paths (bufler-group-tree-paths grouped-buffers))
group-paths alist)
(cl-labels ((push-subpaths (path)
(when path
(push path group-paths)
(push-subpaths (butlast path))))
(path-cons (path)
(cons (bufler-format-path path) path)))
(thread-last buffer-paths
(mapcar #'butlast)
(mapc #'push-subpaths))
(setf group-paths (seq-uniq group-paths)
alist (mapcar #'path-cons group-paths))
(when (string-empty-p (caar alist))
;; HACK: Remove empty-string/nil group that somehow ends up being first.
(setf alist (cdr alist)))
(bufler-read-from-alist "Group: " alist))))))
(if tab-bar-mode
(let ((current-tab (tab-bar--current-tab-find)))
(setf (bufler-workspace--tab-parameter 'bufler-workspace-path current-tab) path
(bufler-workspace--tab-parameter 'bufler-workspace-path-formatted current-tab)
(funcall bufler-workspace-format-path-fn path)))
(set-frame-parameter nil 'bufler-workspace-path path)
(set-frame-parameter nil 'bufler-workspace-path-formatted (funcall bufler-workspace-format-path-fn path)))
(run-hook-with-args 'bufler-workspace-set-hook
(or title
(format "Workspace: %s" (funcall bufler-workspace-format-path-fn path))))
(force-mode-line-update 'all)
path)
;;;###autoload
(cl-defun bufler-workspace-focus-buffer (buffer &key title)
"Set current tab's or frame's workspace to BUFFER's workspace.
If TITLE, pass it to `bufler-workspace-set'. If `tab-bar-mode'
is active, set the tab's; otherwise, the frame's. Interactively,
use current buffer."
(interactive (list (current-buffer)))
(bufler-workspace-set (bufler-buffer-workspace-path buffer) :title title))
;;;###autoload
(cl-defun bufler-workspace-switch-buffer
(&key all-p no-filter (include-recent-buffers bufler-switch-buffer-include-recent-buffers) (switch-workspace-p t))
"Switch to another buffer in the current group.
Without any input, switch to the previous buffer, like
`switch-to-buffer'.
If ALL-P (interactively, with universal prefix) or if the frame
has no workspace, select from all buffers.
If SWITCH-WORKSPACE-P (disable with two universal prefixes),
select from all buffers and switch to that buffer's workspace.
If NO-FILTER (with three universal prefixes), include buffers
that would otherwise be filtered by
`bufler-workspace-switch-buffer-filter-fns'.
If INCLUDE-RECENT-BUFFERS, include recently shown buffers in the
selected window at the top of the list of buffers (see option
`bufler-switch-buffer-include-recent-buffers').
If `bufler-workspace-switch-buffer-sets-workspace' is non-nil,
act as if SET-WORKSPACE-P is non-nil. And if
`bufler-workspace-switch-buffer-and-tab' is non-nil,
automatically switch to the buffer's workspace's tab, if it has
one."
(interactive
(list :all-p current-prefix-arg
:no-filter (and current-prefix-arg
(>= (car current-prefix-arg) 64))
:switch-workspace-p (not (and current-prefix-arg
(>= (car current-prefix-arg) 16)))))
(let* ((bufler-vc-state nil)
(completion-ignore-case bufler-workspace-ignore-case)
(path (unless all-p
(or (when tab-bar-mode
(bufler-workspace--tab-parameter 'bufler-workspace-path (tab-bar--current-tab-find)))
(frame-parameter nil 'bufler-workspace-path))))
(buffers (bufler-buffer-alist-at
path :filter-fns (unless no-filter
bufler-workspace-switch-buffer-filter-fns)))
(other-buffer-path (bufler-group-tree-leaf-path
(bufler-buffers) (other-buffer (current-buffer))))
(other-buffer-cons (cons (buffer-name (-last-item other-buffer-path))
(other-buffer (current-buffer))))
(recent-buffers (when include-recent-buffers
(cl-loop for (buffer _ _) in (window-prev-buffers)
collect (cons (buffer-name buffer) buffer))))
(buffers (cons other-buffer-cons (append recent-buffers buffers)))
(buffer-name (completing-read "Buffer: " (mapcar #'car buffers)
nil nil nil nil other-buffer-cons))
(selected-buffer (alist-get buffer-name buffers nil nil #'string=)))
;; TODO: If selected-buffer has no associated workspace tab, try
;; to use a tab that has a window that most recently displayed it.
(when-let ((switch-workspace-p)
(workspace-path (bufler-buffer-workspace-path selected-buffer))
(workspace-tab (cl-find workspace-path (tab-bar-tabs) :test #'equal
:key (lambda (tab)
(bufler-workspace--tab-parameter 'bufler-workspace-path tab))))
(tab-name (bufler-workspace--tab-parameter 'name workspace-tab)))
;; TODO: Try to switch to a frame when not using tab-bar-mode
;; (or just ignore frames and focus on supporting tab-bar best).
(tab-bar-switch-to-tab tab-name))
(if-let ((selected-buffer)
(window (get-buffer-window selected-buffer)))
(select-window window)
(switch-to-buffer (or selected-buffer buffer-name)))))
;;;###autoload
(defun bufler-workspace-buffer-name-workspace (&optional name)
"Set current buffer's workspace to NAME.
If NAME is nil (interactively, with prefix), unset the buffer's
workspace name. This sets the buffer-local variable
`bufler-workspace-name'. Note that, in order for a buffer to
appear in a named workspace, the buffer must be matched by an
`auto-workspace' group before any other group."
(interactive (list (unless current-prefix-arg
(completing-read "Named workspace: "
(seq-uniq
(cl-loop for buffer in (buffer-list)
when (buffer-local-value 'bufler-workspace-name buffer)
collect it))))))
(setf bufler-cache nil)
(setq-local bufler-workspace-name name))
;;;###autoload
(define-minor-mode bufler-workspace-mode
"When active, set the frame title according to current Bufler group."
:global t
(let ((lighter '(bufler-workspace-mode (:eval (bufler-workspace-mode-lighter)))))
(if bufler-workspace-mode
;; Avoid adding the lighter multiple times if the mode is activated again.
(cl-pushnew (list lighter) mode-line-misc-info :test #'equal)
(setf mode-line-misc-info
(delete lighter mode-line-misc-info)))))
;;;###autoload
(defun bufler-workspace-save (name)
"Save current Bufler workspace as NAME.
Also sets current tab/frame's workspace to the current buffer's."
(interactive (list (completing-read "Save workspace: " (bufler-workspace-names)
nil nil bufler-workspace-prefix)))
(let ((burly-buffer-local-variables '(bufler-workspace-name)))
(let ((record (list (cons 'url (burly-windows-url))
(cons 'handler #'bufler-workspace-bookmark-handler)
(cons 'bufler-workspace-name name))))
(bookmark-store name record nil)))
(setf (bufler-workspace--tab-parameter
;; FIXME: This doesn't seem to work: after saving a new workspace, it can't be reset until it's opened again.
'bufler-workspace-bookmark-name (tab-bar--current-tab-find))
name)
(bufler-workspace-set (bufler-buffer-workspace-path (current-buffer))
:title name))
;;;###autoload
(defun bufler-workspace-open (name)
"Open the workspace NAME.
NAME should be the name of a bookmark (this just calls
`bookmark-jump'). Interactively, prompt for a Bufler workspace."
(interactive (list (completing-read "Open workspace: " (bufler-workspace-names :active nil))))
(bookmark-jump name))
(defun bufler-workspace-reset ()
"Reset the current tab's workspace."
(interactive)
(cl-assert tab-bar-mode nil "Only supported for `tab-bar-mode'")
(if-let ((name (bufler-workspace--tab-parameter 'bufler-workspace-bookmark-name (tab-bar--current-tab-find))))
(bufler-workspace-open name)
(error "Current tab has no Bufler workspace name")))
;;;; Functions
;;;###autoload
(defun bufler-workspace-bookmark-handler (bookmark)
"Handler function for `bufler-workspace' BOOKMARK."
(bufler-without-mode burly-tabs-mode
(let ((name (bufler-workspace--abbreviate-name (car bookmark))))
(when tab-bar-mode
(if-let ((tab (cl-find name (tab-bar-tabs) :test #'equal
:key (apply-partially #'bufler-workspace--tab-parameter 'name))))
(tab-bar-select-tab-by-name name)
(tab-new)))
;; TODO: Also do this for frames when not using tab-bar?
(setf (bufler-workspace--tab-parameter
'bufler-workspace-bookmark-name (tab-bar--current-tab-find))
(car bookmark))
(burly-bookmark-handler bookmark)))
;; HACK: Use an immediate timer for this so that, e.g. the
;; `burly-tabs-mode' advice has a chance to run first, otherwise the
;; newly opened tab won't be active when this happens.
(let ((name (bookmark-prop-get bookmark 'bufler-workspace-name)))
(run-at-time nil nil
(lambda ()
(bufler-workspace-set (bufler-buffer-workspace-path (current-buffer)) :title name)))))
(cl-defun bufler-workspace-names (&key (saved t) (active t))
"Return list of workspace names.
When SAVED, include names of saved workspaces. When ACTIVE,
include names of active ones."
(bookmark-maybe-load-default-file)
(delete-dups
(append (when saved
(cl-loop for bookmark in bookmark-alist
for (_name . params) = bookmark
when (and (equal #'bufler-workspace-bookmark-handler (alist-get 'handler params))
(alist-get 'bufler-workspace-name params))
collect (car bookmark)))
(when active
(cl-loop for buffer in (buffer-list)
when (buffer-local-value 'bufler-workspace-name buffer)
collect it)))))
(cl-defun bufler-workspace-buffers (&optional (frame (selected-frame)))
"Return list of buffers for FRAME's workspace.
Works as `tab-line-tabs-function'."
;; This is specifically for `bufler-workspace-workspaces-as-tabs-mode',
;; but it needn't be only for that, so it probably belongs here.
(let (buffers)
(--tree-map-nodes (bufferp it)
(push it buffers)
(bufler-buffers :path (frame-parameter frame 'bufler-workspace-path)
;; Like the default function, `tab-line-tabs-buffer-list',
;; we remove hidden buffers.
:filter-fns (list (lambda (buffer)
(= (aref (buffer-name buffer) 0) ?\s)))))
(cl-sort buffers #'string< :key #'buffer-name)))
(defun bufler-workspace-mode-lighter ()
"Return lighter string for mode line."
(concat bufler-workspace-mode-lighter
(if tab-bar-mode
(bufler-workspace--tab-parameter 'bufler-workspace-path-formatted (tab-bar--current-tab-find))
(frame-parameter nil 'bufler-workspace-path-formatted))))
(defun bufler-workspace-set-frame-name (name)
"Set current frame's name according to NAME.
But if `tab-bar-mode' is active, do nothing. Abbreviates NAME
according to `bufler-workspace-prefix-abbreviation'."
;; TODO: Rename this function?
(when bufler-workspace-prefix-abbreviation
(setf name (bufler-workspace--abbreviate-name name)))
(if tab-bar-mode
(tab-rename (or name ""))
(set-frame-name name)))
(cl-defun bufler-workspace-read-item (tree &key (leaf-key #'identity))
"Return a leaf read from TREE with completion.
LEAF-KEY is applied to each leaf in TREE. Completion is done in
steps when descending into branches."
(cl-labels ((read-item (tree)
(cl-typecase (car tree)
(list (let ((key (completing-read "Group: " (mapcar #'car tree))))
(read-item (alist-get key tree nil nil #'string=))))
(atom (completing-read "Buffer: " (mapcar leaf-key tree))))))
(read-item tree)))
(defun bufler-workspace-read-group-path (groups)
"Return a path to a group in GROUPS read with completion."
(cl-labels ((read-path (items &optional last-key)
(cl-typecase (car items)
(list (list last-key
(let ((key (completing-read "Group: " (mapcar #'car items))))
(read-path (alist-get key items nil nil #'string=) key))))
(atom last-key))))
(let ((path (cadr (read-path groups))))
(cl-typecase path
(list path)
(atom (list path))))))
(defun bufler-workspace--abbreviate-name (name)
"Return NAME having been abbreviated.
Abbreviates according to `bufler-workspace-prefix-abbreviation'."
(replace-regexp-in-string
(car bufler-workspace-prefix-abbreviation) (cdr bufler-workspace-prefix-abbreviation)
name))
;;;; Footer
(provide 'bufler-workspace)
;;; bufler-workspace.el ends here