-
Notifications
You must be signed in to change notification settings - Fork 3
/
bufferlo.el
1269 lines (1163 loc) · 56.3 KB
/
bufferlo.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
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; bufferlo.el --- Manage frame/tab-local buffer lists -*- lexical-binding: t -*-
;; Copyright (C) 2021-2024 Free Software Foundation, Inc.
;; Author: Florian Rommel <[email protected]>
;; Maintainer: Florian Rommel <[email protected]>
;; Url: https://github.com/florommel/bufferlo
;; Created: 2021-09-15
;; Version: 0.8
;; Package-Requires: ((emacs "27.1"))
;; Keywords: buffer frame tabs local
;; 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 gives you separate buffer lists per frame and per (tab-bar) tab.
;; Bufferlo is a lightweight wrapper around Emacs's buffer-list frame
;; parameter. In contrast to similar solutions, it integrates
;; seamlessly with the standard frame and tab management facilities,
;; including undeletion of frame and tabs, tab duplication and moving,
;; frame cloning, and persisting sessions (via desktop.el).
;; With bufferlo, every frame or tab (if you use tab-bar tabs) has an
;; additional manageable local buffer list. A buffer is added to the
;; local buffer list when displayed in the frame/tab (e.g., by opening
;; a new file in the tab or by switching to the buffer from the global
;; buffer list). Bufferlo provides extensive management functions for
;; the local list and frame/tab-local variants of the switch-buffer
;; function, buffer menu, and Ibuffer. In addition, you can configure
;; any command that selects a buffer to use the local buffer list
;; (bufferlo anyhwere). Bufferlo also allows you to bookmark and
;; persist the state of individual frames or tabs.
;;; Code:
(require 'seq)
(require 'tab-bar)
(require 'desktop)
(require 'bookmark)
(defgroup bufferlo nil
"Manage frame/tab-local buffer lists."
:group 'convenience)
(defcustom bufferlo-prefer-local-buffers t
"Use a frame predicate to prefer local buffers over global ones.
This means that a local buffer will be preferred to be displayed
when the current buffer disappears (buried or killed).
This is also required to make `next-buffer' and `previous-buffer'
work as expected.
Changes to this variable must be made before enabling
`bufferlo-mode' in order to take effect."
:type 'boolean)
(defcustom bufferlo-include-buried-buffers t
"Include buried buffers in the local list (`bufferlo-buffer-list').
Use `bufferlo-bury' to remove and bury a buffer if this is set to t."
:type 'boolean)
(defcustom bufferlo-include-buffer-filters nil
"Buffers that should always get included in a new tab or frame.
This is a list of regular expressions that match buffer names.
This is applied on frame and tab creation. Included buffers can be
explicitly removed later.
This overrides buffers excluded by `bufferlo-exclude-buffer-filters.'"
:type '(repeat string))
(defcustom bufferlo-exclude-buffer-filters '(".*")
"Buffers that should always get excluded in a new tab or frame.
This is a list of regular expressions that match buffer names.
This is applied on frame and tab creation. Excluded buffers can be
added explicitly later. Use `bufferlo-hidden-buffers' to permanently
hide buffers from the local list.
Buffers included by `bufferlo-include-buffer-filters' take precedence."
:type '(repeat string))
(defcustom bufferlo-hidden-buffers nil
"List of regexps matching names of buffers to hide in the local buffer lists.
Matching buffers are hidden even if displayed in the current frame or tab."
:type '(repeat string))
(defcustom bufferlo-kill-buffers-exclude-filters
'("\\` " "\\`\\*Messages\\*\\'" "\\`\\*scratch\\*\\'")
"Buffers that should not be killed by `bufferlo-kill-buffers'.
This is a list of regular expressions that match buffer names."
:type '(repeat string))
(defcustom bufferlo-ibuffer-bind-local-buffer-filter t
"If non-nil, bind the local buffer filter and the orphan filter in ibuffer.
The local buffer filter is bound to \"/ l\" and the orphan filter to \"/ L\"."
:type 'boolean)
(defcustom bufferlo-local-scratch-buffer-name "*local scratch*"
"Base name of the local scratch buffer.
Multiple frame/tabs will use `generate-new-buffer-name' (which
appends \"<N>\" to the name) in order to get a unique buffer.
Local scratch buffers are optional and not used by default.
Use the following functions to create and work with them:
`bufferlo-create-local-scratch-buffer',
`bufferlo-get-local-scratch-buffer',
`bufferlo-switch-to-local-scratch-buffer',
and `bufferlo-toggle-local-scratch-buffer'.
For example, create a dedicated local scratch buffer for all tabs and frames:
(setq \\='tab-bar-new-tab-choice #\\='bufferlo-create-local-scratch-buffer)
(add-hook \\='after-make-frame-functions
#\\='bufferlo-switch-to-local-scratch-buffer)
You can set this to \"*scratch*\"."
:type 'string)
(defcustom bufferlo-local-scratch-buffer-initial-major-mode nil
"The initial major mode for local scratch buffers.
If nil, the local scratch buffers' major mode is set to `initial-major-mode'."
:type 'function)
(defcustom bufferlo-anywhere-filter '(switch-to-buffer
bufferlo-switch-to-buffer
bufferlo-find-buffer
bufferlo-find-buffer-switch)
"The functions that use the local buffer list in `bufferlo-anywhere-mode'.
If `bufferlo-anywhere-filter-type' is set to `exclude', this is an exclude
filter (i.e., determines the functions that do not use the local buffer list).
If `bufferlo-anywhere-filter-type' is set to `include' (or any other value),
this is an include filter.
The value can either be a list of functions, or t (for all functions),
or a custom filter function that takes a function symbol as its argument and
returns whether the probed function should be filtered (non-nil) or
not-filtered (nil)."
:type '(choice (repeat :tag "Filter specific functions" function)
(const :tag "All functions" t)
(function :tag "Custom filter function")))
(defcustom bufferlo-anywhere-filter-type 'exclude
"Determines whether `bufferlo-anywhere-filter' is an include or exclude filter.
Set this to `include' or `exclude'."
:type '(radio (const :tag "Include filter" include)
(const :tag "Exclude filter" exclude)))
(defcustom bufferlo-bookmark-map-functions nil
"Functions to call for every local buffer when making a tab bookmark.
Each function takes a bookmark record as its argument. The corresponding
buffer is set as current buffer. Every function should return a valid
bookmark record or nil. The first function gets the buffer's default
bookmark record or nil if it is not bookmarkable. Subsequent functions
receive the bookmark record that the previous function returned as their
argument. The bookmark record of the last function is used as the
effective record. If the last function returns nil, no record for the
respective buffer is included in the frame or tab bookmark.
These functions are also called when creating a frame bookmark, since a
frame bookmark is a collection of tab bookmarks."
:type 'hook)
(defvar bufferlo--desktop-advice-active nil)
(defvar bufferlo--desktop-advice-active-force nil)
(defvar bufferlo--clear-buffer-lists-active nil)
;;;###autoload
(define-minor-mode bufferlo-mode
"Manage frame/tab-local buffers."
:global t
:require 'bufferlo
:init-value nil
:lighter nil
:keymap nil
(if bufferlo-mode
(progn
;; Prefer local buffers
(when bufferlo-prefer-local-buffers
(dolist (frame (frame-list))
(bufferlo--set-buffer-predicate frame))
(add-hook 'after-make-frame-functions #'bufferlo--set-buffer-predicate))
;; Include/exclude buffers
(add-hook 'after-make-frame-functions #'bufferlo--include-exclude-buffers)
(add-hook 'tab-bar-tab-post-open-functions #'bufferlo--tab-include-exclude-buffers)
;; Save/restore local buffer list
(advice-add #'window-state-get :around #'bufferlo--window-state-get)
(advice-add #'window-state-put :after #'bufferlo--window-state-put)
;; Desktop support
(advice-add #'frameset--restore-frame :around #'bufferlo--activate)
;; Duplicate/move tabs
(advice-add #'tab-bar-select-tab :around #'bufferlo--activate-force)
;; Clone & undelete frame
(when (>= emacs-major-version 28)
(advice-add #'clone-frame :around #'bufferlo--activate-force))
(when (>= emacs-major-version 29)
(advice-add #'undelete-frame :around #'bufferlo--activate-force))
;; Switch-tab workaround
(advice-add #'tab-bar-select-tab :around #'bufferlo--clear-buffer-lists-activate)
(advice-add #'tab-bar--tab :after #'bufferlo--clear-buffer-lists))
;; Prefer local buffers
(dolist (frame (frame-list))
(bufferlo--reset-buffer-predicate frame))
(remove-hook 'after-make-frame-functions #'bufferlo--set-buffer-predicate)
;; Include/exclude buffers
(remove-hook 'after-make-frame-functions #'bufferlo--include-exclude-buffers)
(remove-hook 'tab-bar-tab-post-open-functions #'bufferlo--tab-include-exclude-buffers)
;; Save/restore local buffer list
(advice-remove #'window-state-get #'bufferlo--window-state-get)
(advice-remove #'window-state-put #'bufferlo--window-state-put)
;; Desktop support
(advice-remove #'frameset--restore-frame #'bufferlo--activate)
;; Duplicate/move tabs
(advice-remove #'tab-bar-select-tab #'bufferlo--activate-force)
;; Clone & undelete frame
(when (>= emacs-major-version 28)
(advice-remove #'clone-frame #'bufferlo--activate-force))
(when (>= emacs-major-version 29)
(advice-remove #'undelete-frame #'bufferlo--activate-force))
;; Switch-tab workaround
(advice-remove #'tab-bar-select-tab #'bufferlo--clear-buffer-lists-activate)
(advice-remove #'tab-bar--tab #'bufferlo--clear-buffer-lists)))
(defun bufferlo-local-buffer-p (buffer &optional frame tabnum include-hidden)
"Return non-nil if BUFFER is in the list of local buffers.
A non-nil value of FRAME selects a specific frame instead of the current one.
If TABNUM is nil, the current tab is used. If it is non-nil, it specifies
a tab index in the given frame. If INCLUDE-HIDDEN is set, include hidden
buffers, see `bufferlo-hidden-buffers'."
(memq buffer (bufferlo-buffer-list frame tabnum include-hidden)))
(defun bufferlo-non-local-buffer-p (buffer &optional frame tabnum include-hidden)
"Return non-nil if BUFFER is not in the list of local buffers.
A non-nil value of FRAME selects a specific frame instead of the current one.
If TABNUM is nil, the current tab is used. If it is non-nil, it specifies
a tab index in the given frame. If INCLUDE-HIDDEN is set, include hidden
buffers, see `bufferlo-hidden-buffers'."
(not (bufferlo-local-buffer-p buffer frame tabnum include-hidden)))
(defun bufferlo--clear-buffer-lists (&optional frame)
"This is a workaround advice function to fix tab-bar's tab switching behavior.
On `tab-bar-select-tab', when `wc-bl' or `wc-bbl' is nil, the function does not
set the correspoinding `buffer-list' / `buried-buffer-list' frame parameters.
As a result the previous tab's values remain active.
To mitigate this, this functions clears `buffer-list' and `buried-buffer-list'.
It should be set up as an advice after `tab-bar--tab' and takes its FRAME
parameter. In addition, `bufferlo--clear-buffer-lists-activate' must be
set up as a advice around `tab-bar-select-tab' to activate this function
when `tab-bar--tab' is called from `tab-bar-select-tab."
(when bufferlo--clear-buffer-lists-active
(set-frame-parameter frame 'buffer-list nil)
(set-frame-parameter frame 'buried-buffer-list nil)))
(defun bufferlo--clear-buffer-lists-activate (oldfn &rest args)
"This should be set up as a advice around `tab-bar-select-tab'.
It actiavtes clearing the buffer lists for `tab-bar--tab'
before calling OLDFN with ARGS. See `bufferlo--clear-buffer-lists'."
(let* ((bufferlo--clear-buffer-lists-active t)
(result (apply oldfn args)))
;; Occasionally it happens that a non-local buffer is shown in the tab,
;; after switching frames, primarily with empty tabs.
;; This workaround selects a buffer that is in the local list in such a case.
(unless (bufferlo-local-buffer-p (current-buffer) nil nil t)
(let ((buffer (or
(seq-find (lambda (b) (not (minibufferp b)))
(frame-parameter nil 'buffer-list))
(seq-find (lambda (b) (not (minibufferp b)))
(frame-parameter nil 'buried-buffer-list)))))
(switch-to-buffer buffer t t)))
result))
(defun bufferlo--buffer-predicate (buffer)
"Return whether BUFFER is local to the current fram/tab.
Includes hidden buffers."
(bufferlo-local-buffer-p buffer nil nil t))
(defun bufferlo--set-buffer-predicate (frame)
"Set the buffer predicate of FRAME to `bufferlo--buffer-predicate'."
(set-frame-parameter frame 'buffer-predicate #'bufferlo--buffer-predicate))
(defun bufferlo--reset-buffer-predicate (frame)
"Reset the buffer predicate of FRAME if it is `bufferlo--buffer-predicate'."
(when (eq (frame-parameter frame 'buffer-predicate) #'bufferlo--buffer-predicate)
(set-frame-parameter frame 'buffer-predicate nil)))
(defun bufferlo--merge-regexp-list (regexp-list)
"Merge a list of regular expressions REGEXP-LIST."
(mapconcat (lambda (x)
(concat "\\(?:" x "\\)"))
regexp-list "\\|"))
(defun bufferlo--include-exclude-buffers (frame)
"Include and exclude buffers from the local buffer list of FRAME."
(let* ((include (bufferlo--merge-regexp-list
(append '("a^") bufferlo-include-buffer-filters)))
(exclude (bufferlo--merge-regexp-list
(append '("a^") bufferlo-exclude-buffer-filters)))
(buffers (bufferlo--current-buffers frame))
(buffers (seq-filter (lambda (b)
(not (string-match-p exclude (buffer-name b))))
buffers))
(incl-buffers (seq-filter (lambda (b)
(string-match-p include (buffer-name b)))
(buffer-list frame)))
(buffers (delete-dups (append buffers incl-buffers))))
;; FIXME: Currently all the included buffers are put into the 'buffer-list,
;; even if they were in the 'buried-buffer-list before.
(set-frame-parameter frame 'buffer-list
;; The current buffer must be always on the list,
;; otherwise the buffer list gets replaced later.
(push (if frame
(with-selected-frame frame (current-buffer))
(current-buffer))
buffers))
(set-frame-parameter frame 'buried-buffer-list nil)))
(defun bufferlo--tab-include-exclude-buffers (ignore)
"Include and exclude buffers from the buffer list of the current tab's frame.
Argument IGNORE is for compatibility with `tab-bar-tab-post-open-functions'."
(ignore ignore)
;; Reset the local buffer list unless we clone the tab (tab-duplicate).
(unless (or (eq tab-bar-new-tab-choice 'clone)
(and (< emacs-major-version 29)
(not tab-bar-new-tab-choice)))
(bufferlo--include-exclude-buffers nil)))
(defun bufferlo--current-buffers (frame)
"Get the buffers of the current tab in FRAME."
(if bufferlo-include-buried-buffers
(append
(frame-parameter frame 'buffer-list)
(frame-parameter frame 'buried-buffer-list))
(frame-parameter frame 'buffer-list)))
(defun bufferlo--get-tab-buffers (tab)
"Extract buffers from the given TAB structure."
(or
(if bufferlo-include-buried-buffers
(append
(cdr (assq 'wc-bl tab))
(cdr (assq 'wc-bbl tab)))
(cdr (assq 'wc-bl tab)))
;; fallback to bufferlo-buffer-list, managed by bufferlo--window-state-*
(mapcar 'get-buffer
(car (cdr (assq 'bufferlo-buffer-list (assq 'ws tab)))))))
(defun bufferlo--get-buffers (&optional frame tabnum)
"Get the buffers of tab TABNUM in FRAME.
If FRAME is nil, the current frame is selected.
If TABNUM is nil, the current tab is selected.
If TABNUM is \\='all, all tabs of the frame are selected."
(cond ((eq tabnum 'all)
(seq-uniq (seq-mapcat (lambda (tb)
(if (eq 'current-tab (car tb))
(bufferlo--current-buffers frame)
(bufferlo--get-tab-buffers tb)))
(funcall tab-bar-tabs-function frame))))
(tabnum
(let ((tab (nth tabnum (funcall tab-bar-tabs-function frame))))
(if (eq 'current-tab (car tab))
(bufferlo--current-buffers frame)
(bufferlo--get-tab-buffers tab))))
(t
(bufferlo--current-buffers frame))))
(defun bufferlo-buffer-list (&optional frame tabnum include-hidden)
"Return a list of all live buffers associated with the current frame and tab.
A non-nil value of FRAME selects a specific frame instead of the current one.
If TABNUM is nil, the current tab is used. If it is non-nil, it specifies
a tab index in the given frame. If INCLUDE-HIDDEN is set, include hidden
buffers, see `bufferlo-hidden-buffers'."
(let ((list (bufferlo--get-buffers frame tabnum)))
(if include-hidden
(seq-filter #'buffer-live-p list)
(seq-filter (lambda (buffer)
(let ((hidden (bufferlo--merge-regexp-list
(append '("a^") bufferlo-hidden-buffers))))
(and
(buffer-live-p buffer)
(not (string-match-p hidden (buffer-name buffer))))))
list))))
(defun bufferlo--window-state-get (oldfn &optional window writable)
"Save the frame's buffer list to the window state.
Used as advice around `window-state-get'. OLDFN is the original
function. WINDOW and WRITABLE are passed to the function."
(let ((ws (apply oldfn (list window writable))))
(let* ((buffers (bufferlo--current-buffers (window-frame window)))
(names (mapcar #'buffer-name buffers)))
(if names (append ws (list (list 'bufferlo-buffer-list names))) ws))))
(defun bufferlo--window-state-put (state &optional window ignore)
"Restore the frame's buffer list from the window state.
Used as advice after `window-state-put'. STATE is the window state.
WINDOW is the window in question. IGNORE is not used and exists for
compatibility with the adviced function."
(ignore ignore)
;; We have to make sure that the window is live at this point.
;; `frameset-restore' may pass a window with a non-existing buffer
;; to `window-state-put', which in turn will delete that window
;; before the advice calls us.
;; This is not the case when we are called from `tab-bar-select-tab'.
(when (or bufferlo--desktop-advice-active-force
(and bufferlo--desktop-advice-active (window-live-p window)))
;; FIXME: Currently there is no distinction between buffers and
;; buried buffers for dektop.el.
(let ((bl (car (cdr (assq 'bufferlo-buffer-list state)))))
(set-frame-parameter (window-frame window) 'buffer-list
;; The current buffer must be always on the list,
;; otherwise the buffer list gets replaced later.
(cons (window-buffer window)
(mapcar #'get-buffer bl)))
(set-frame-parameter (window-frame window) 'buried-buffer-list
(list (window-buffer window))))))
(defun bufferlo--activate (oldfn &rest args)
"Activate the advice for `bufferlo--window-state-{get,put}'.
OLDFN is the original function. ARGS is for compatibility with
the adviced functions."
(let ((bufferlo--desktop-advice-active t))
(apply oldfn args)))
(defun bufferlo--activate-force (oldfn &rest args)
"Activate the advice for `bufferlo--window-state-{get,put}'.
OLDFN is the original function. ARGS is for compatibility with
the adviced functions."
(let ((bufferlo--desktop-advice-active t)
(bufferlo--desktop-advice-active-force t))
(apply oldfn args)))
(defsubst bufferlo--warn ()
"Warn if `bufferlo-mode' is not enabled."
(defvar bufferlo--warn-current-command nil)
(when (and (not bufferlo-mode)
(not (eq this-command bufferlo--warn-current-command)))
(setq bufferlo--warn-current-command this-command)
(message (format "%s: bufferlo-mode should be enabled"
this-command))))
(defun bufferlo-clear (&optional frame)
"Clear the frame/tab's buffer list, except for the current buffer.
If FRAME is nil, use the current frame."
(interactive)
(bufferlo--warn)
(set-frame-parameter frame 'buffer-list
(list (if frame
(with-selected-frame frame
(current-buffer))
(current-buffer))))
(set-frame-parameter frame 'buried-buffer-list nil))
(defun bufferlo-remove (buffer)
"Remove BUFFER from the frame/tab's buffer list."
(interactive
(list
(let ((lbs (mapcar (lambda (b) (buffer-name b))
(bufferlo-buffer-list))))
(read-buffer "Remove buffer: " nil t
(lambda (b) (member (car b) lbs))))))
(bufferlo--warn)
(let ((bl (frame-parameter nil 'buffer-list))
(bbl (frame-parameter nil 'buried-buffer-list))
(buffer (get-buffer buffer)))
(set-frame-parameter nil 'buffer-list (delq buffer bl))
(set-frame-parameter nil 'buried-buffer-list (delq buffer bbl))
;; Adapted from bury-buffer:
(cond
((or (not (eq buffer (window-buffer)))
;; Don't try to delete the minibuffer window, undedicate it
;; or switch to a previous buffer in it.
(window-minibuffer-p)))
((window--delete nil t))
(t
;; Switch to another buffer in window.
(set-window-dedicated-p nil nil)
(switch-to-prev-buffer nil 'bury)))
nil))
(defun bufferlo-remove-non-exclusive-buffers ()
"Remove all buffers from the local buffer list that are not exclusive to it."
(interactive)
(bufferlo--warn)
(dolist (buffer (bufferlo--get-exclusive-buffers nil nil t))
(bufferlo-remove buffer)))
(defun bufferlo-bury (&optional buffer-or-name)
"Bury and remove the buffer specified by BUFFER-OR-NAME from the local list.
If `bufferlo-include-buried-buffers' is set to nil then this has the same
effect as a simple `bury-buffer'."
(interactive)
(bufferlo--warn)
(let ((buffer (window-normalize-buffer buffer-or-name)))
(bury-buffer-internal buffer)
(bufferlo-remove buffer)))
(defun bufferlo--get-captured-buffers (&optional exclude-frame exclude-tabnum)
"Get all buffers that are in a local list of at least one frame or tab.
If EXCLUDE-FRAME is a frame, exclude the local buffer list of the tab with
the number EXCLUSIVE-TABNUM of this frame.
If EXCLUSIVE-TABNUM is nil, select the default tab.
If EXCLUSIVE-TABNUM is \\='all, select all tabs of the frame.
If EXCLUDE-FRAME is nil, do not exclude a local buffer list
and ignore EXCLUDE-TABNUM."
(let* ((exclude-tab (when (and exclude-tabnum (not (eq exclude-tabnum 'all)))
(nth exclude-tabnum
(funcall tab-bar-tabs-function exclude-frame))))
(get-inactive-tabs-buffers
(lambda (f)
(seq-mapcat
(lambda (tb)
(unless (and (eq f exclude-frame)
(or (eq exclude-tabnum 'all)
(eq tb exclude-tab)))
(bufferlo--get-tab-buffers tb)))
(funcall tab-bar-tabs-function f))))
(get-frames-buffers
(lambda ()
(seq-mapcat
(lambda (f)
(unless (and (eq f exclude-frame)
(or (eq exclude-tabnum 'all)
(not exclude-tab)
(eq 'current-tab (car exclude-tab))))
(bufferlo--current-buffers f)))
(frame-list)))))
(seq-uniq
(append (seq-mapcat get-inactive-tabs-buffers (frame-list))
(funcall get-frames-buffers)))))
(defun bufferlo--get-orphan-buffers ()
"Get all buffers that are not in any local list of a frame or tab."
(seq-filter (lambda (b)
(not (memq b (bufferlo--get-captured-buffers))))
(buffer-list)))
(defun bufferlo--get-exclusive-buffers (&optional frame tabnum invert)
"Get all buffers that are exclusive to this frame and tab.
If FRAME is nil, use the current frame.
If TABNUM is nil, use the current tab.
If TABNUM is \\='all, kill all tabs of the frame.
If INVERT is non-nil, return the non-exclusive buffers instead."
(let ((other-bufs (bufferlo--get-captured-buffers (or frame (selected-frame))
tabnum))
(this-bufs (bufferlo--get-buffers frame tabnum)))
(seq-filter (if invert
(lambda (b) (memq b other-bufs))
(lambda (b) (not (memq b other-bufs))))
this-bufs)))
(defun bufferlo-kill-buffers (&optional killall frame tabnum internal-too)
"Kill the buffers of the frame/tab-local buffer list.
By default, this will only kill buffers that are exclusive to the frame/tab.
If KILLALL (prefix argument) is given then buffers that are also present in the
local lists of other frames and tabs are killed too.
Buffers matching `bufferlo-kill-buffers-exclude-filters' are never killed.
If FRAME is nil, use the current frame.
If TABNUM is nil, use the current tab.
If TABNUM is \\='all, kill all tabs of the frame.
Ignores buffers whose names start with a space, unless optional
argument INTERNAL-TOO is non-nil."
(interactive "P")
(bufferlo--warn)
(let* ((exclude (bufferlo--merge-regexp-list
(append '("a^") bufferlo-kill-buffers-exclude-filters)))
(kill-list (if killall
(bufferlo--get-buffers frame tabnum)
(bufferlo--get-exclusive-buffers frame tabnum)))
(buffers (seq-filter
(lambda (b)
(not (and
(or internal-too (/= (aref (buffer-name b) 0) ?\s))
(string-match-p exclude (buffer-name b)))))
kill-list)))
(dolist (b buffers)
(kill-buffer b))))
(defun bufferlo-kill-orphan-buffers (&optional internal-too)
"Kill all buffers that are not in any local list of a frame or tab.
Ignores buffers whose names start with a space, unless optional
argument INTERNAL-TOO is non-nil.
Buffers matching `bufferlo-kill-buffers-exclude-filters' are never killed."
(interactive)
(bufferlo--warn)
(let* ((exclude (bufferlo--merge-regexp-list
(append '("a^") bufferlo-kill-buffers-exclude-filters)))
(buffers (seq-filter
(lambda (b)
(not (and
(or internal-too (/= (aref (buffer-name b) 0) ?\s))
(string-match-p exclude (buffer-name b)))))
(bufferlo--get-orphan-buffers))))
(dolist (b buffers)
(kill-buffer b))))
(defun bufferlo-delete-frame-kill-buffers (&optional frame internal-too)
"Delete a frame and kill the local buffers of its tabs.
If FRAME is nil, kill the current frame.
Ignores buffers whose names start with a space, unless optional
argument INTERNAL-TOO is non-nil."
(interactive)
(bufferlo--warn)
(bufferlo-kill-buffers nil frame 'all internal-too)
(delete-frame))
(defun bufferlo-tab-close-kill-buffers (&optional killall internal-too)
"Close the current tab and kill the local buffers.
The optional arguments KILLALL and INTERNAL-TOO are passed to
`bufferlo-kill-buffers'."
(interactive "P")
(bufferlo--warn)
(bufferlo-kill-buffers killall nil nil internal-too)
(tab-bar-close-tab))
(defun bufferlo-isolate-project (&optional file-buffers-only)
"Isolate a project in the frame or tab.
Remove all buffers that do not belong to the current project from
the local buffer list. When FILE-BUFFERS-ONLY is non-nil or the
prefix argument is given, remove only buffers that visit a file.
Buffers matching `bufferlo-include-buffer-filters' are not removed."
(interactive "P")
(bufferlo--warn)
(let ((curr-project (project-current))
(include (bufferlo--merge-regexp-list
(append '("a^") bufferlo-include-buffer-filters))))
(if curr-project
(dolist (buffer (bufferlo-buffer-list))
(when (and (not (string-match-p include (buffer-name buffer)))
(not (equal curr-project
(with-current-buffer buffer (project-current))))
(or (not file-buffers-only) (buffer-file-name buffer)))
(bufferlo-remove buffer)))
(message "Current buffer is not part of a project"))))
(defun bufferlo-find-buffer (buffer-or-name)
"Switch to the frame/tab containing BUFFER-OR-NAME in its local list.
If multiple frame or tabs contain the buffer, interactively prompt
for the to-be-selected frame and tab.
This does not select the buffer -- just the containing frame and tab."
(interactive "b")
(bufferlo--warn)
(let* ((buffer (get-buffer buffer-or-name))
(search-tabs (lambda (f)
(let ((i 0))
(mapcar
(lambda (tab)
(setq i (1+ i))
(when (bufferlo-local-buffer-p buffer f (1- i) t)
(list f (frame-parameter f 'name)
(eq f (selected-frame))
i (cdr (assq 'name tab)))))
(funcall tab-bar-tabs-function f)))))
(search-frames (lambda (f)
(unless (frame-parameter f 'no-accept-focus)
(if (funcall tab-bar-tabs-function f)
;; has tabs
(funcall search-tabs f)
;; has no tabs
(when (bufferlo-local-buffer-p buffer f nil t)
(list (list f (frame-parameter f 'name)
(eq f (selected-frame))
nil nil)))))))
(candidates (seq-filter 'identity
(seq-mapcat
(lambda (f)
(funcall search-frames f))
(frame-list))))
(candidates (mapcar
(lambda (c)
(let ((sel (if (nth 2 c) " [this]" ""))
(frame-name (nth 1 c))
(frame-obj (nth 0 c))
(tab-index (nth 3 c))
(tab-name (nth 4 c)))
(if tab-index
(cons (format "Frame: %s (%s)%s Tab %s: %s"
frame-name frame-obj sel
tab-index tab-name)
c)
(cons (format "Frame: %s (%s)%s"
frame-name frame-obj sel)
c))))
candidates))
(selected (if (cdr candidates)
(completing-read
"Select frame/tab: "
candidates
nil t)
(caar candidates)))
(selected (assoc selected candidates)))
(if (not selected)
(message "Orphan: No frame/tab contains buffer '%s'" (buffer-name buffer))
(let ((frame (nth 1 selected))
(tab-index (nth 4 selected)))
(select-frame-set-input-focus frame)
(when tab-index
(tab-bar-select-tab tab-index))
frame))))
(defun bufferlo-find-buffer-switch (buffer-or-name)
"Switch to the frame/tab containig BUFFER-OR-NAME and select the buffer.
This is like `bufferlo-find-buffer' but additionally selects the buffer.
If the buffer is already visible in a non-selected window, select it."
(interactive "b")
(bufferlo--warn)
(when (bufferlo-find-buffer buffer-or-name)
(if-let (w (seq-find
(lambda (w)
(eq (get-buffer buffer-or-name) (window-buffer w)))
(window-list)))
(select-window w)
(switch-to-buffer buffer-or-name))))
(defun bufferlo-get-local-scratch-buffer ()
"Get the local scratch buffer or create it if not already existent and return it."
(let ((buffer (seq-find (lambda (b)
(string-match-p
(concat
"^"
(regexp-quote bufferlo-local-scratch-buffer-name)
"\\(<[0-9]*>\\)?$")
(buffer-name b)))
(bufferlo-buffer-list nil nil t))))
(unless buffer
(setq buffer (get-buffer-create
(generate-new-buffer-name bufferlo-local-scratch-buffer-name)))
(with-current-buffer buffer
(when (eq major-mode 'fundamental-mode)
(funcall (or bufferlo-local-scratch-buffer-initial-major-mode
initial-major-mode
#'ignore)))))
buffer))
(defun bufferlo-create-local-scratch-buffer ()
"Create a local scratch buffer and return it."
(get-buffer-create (generate-new-buffer-name bufferlo-local-scratch-buffer-name)))
(defun bufferlo-switch-to-scratch-buffer (&optional frame)
"Switch to the scratch buffer.
When FRAME is non-nil, switch to the scratch buffer in the specified frame
instead of the current one."
(interactive)
(if frame
(with-selected-frame frame
(switch-to-buffer "*scratch*"))
(switch-to-buffer "*scratch*")))
(defun bufferlo-switch-to-local-scratch-buffer (&optional frame)
"Switch to the local scratch buffer.
When FRAME is non-nil, switch to the local scratch buffer in the specified frame
instead of the current one."
(interactive)
(if frame
(with-selected-frame frame
(switch-to-buffer (bufferlo-get-local-scratch-buffer)))
(switch-to-buffer (bufferlo-get-local-scratch-buffer))))
(defun bufferlo-toggle-local-scratch-buffer ()
"Switch to the local scratch buffer or bury it if it is already selected.
Creates a new local scratch buffer if none exists for this frame/tab."
(interactive)
(let ((buffer (bufferlo-get-local-scratch-buffer)))
(if (eq buffer (current-buffer))
(bury-buffer)
(switch-to-buffer buffer))))
(defun bufferlo-switch-to-buffer (buffer &optional norecord force-same-window)
"Display the BUFFER in the selected window.
Completion includes only local buffers.
This is the frame/tab-local equivilant to `switch-to-buffer'.
The arguments NORECORD and FORCE-SAME-WINDOW are passed to `switch-to-buffer'.
If the prefix arument is given, include all buffers."
(interactive
(list
(if current-prefix-arg
(read-buffer "Switch to buffer: " (other-buffer (current-buffer)) nil)
(let ((lbs (mapcar #'buffer-name (bufferlo-buffer-list))))
(read-buffer
"Switch to local buffer: " (other-buffer (current-buffer)) nil
(lambda (b) (member (if (stringp b) b (car b)) lbs)))))))
(bufferlo--warn)
(switch-to-buffer buffer norecord force-same-window))
(defvar-local bufferlo--buffer-menu-this-frame nil)
(defun bufferlo--local-buffer-list-this-frame ()
"Return the local buffer list of the buffer's frame."
(bufferlo-buffer-list bufferlo--buffer-menu-this-frame))
(defun bufferlo-list-buffers ()
"Display a list of local buffers."
(interactive)
(bufferlo--warn)
(display-buffer
(let* ((old-buffer (current-buffer))
(name (or
(seq-find (lambda (b)
(string-match-p
"\\`\\*Local Buffer List\\*\\(<[0-9]*>\\)?\\'"
(buffer-name b)))
(bufferlo-buffer-list))
(generate-new-buffer-name "*Local Buffer List*")))
(buffer (get-buffer-create name)))
(with-current-buffer buffer
(Buffer-menu-mode)
(setq bufferlo--buffer-menu-this-frame (selected-frame))
(setq Buffer-menu-files-only nil)
(setq Buffer-menu-buffer-list #'bufferlo--local-buffer-list-this-frame)
(setq Buffer-menu-filter-predicate nil)
(list-buffers--refresh #'bufferlo--local-buffer-list-this-frame old-buffer)
(tabulated-list-print)
(revert-buffer))
buffer)))
(defun bufferlo-list-orphan-buffers ()
"Display a list of orphan buffers."
(interactive)
(bufferlo--warn)
(display-buffer
(let* ((old-buffer (current-buffer))
(name "*Orphan Buffer List*")
(buffer (get-buffer-create name)))
(with-current-buffer buffer
(Buffer-menu-mode)
(setq bufferlo--buffer-menu-this-frame (selected-frame))
(setq Buffer-menu-files-only nil)
(setq Buffer-menu-buffer-list #'bufferlo--get-orphan-buffers)
(setq Buffer-menu-filter-predicate nil)
(list-buffers--refresh #'bufferlo--get-orphan-buffers old-buffer)
(tabulated-list-print)
(revert-buffer))
buffer)))
(with-eval-after-load 'ibuf-ext
(define-ibuffer-filter bufferlo-local-buffers
"Limit current view to local buffers."
(:description "local buffers" :reader nil)
(bufferlo-local-buffer-p buf))
(define-ibuffer-filter bufferlo-orphan-buffers
"Limit current view to orphan buffers."
(:description "orphan buffers" :reader nil)
(not (memq buf (bufferlo--get-captured-buffers)))))
(with-eval-after-load 'ibuffer
(when bufferlo-ibuffer-bind-local-buffer-filter
(require 'ibuf-ext)
(define-key ibuffer--filter-map (kbd "l")
#'ibuffer-filter-by-bufferlo-local-buffers)
(define-key ibuffer--filter-map (kbd "L")
#'ibuffer-filter-by-bufferlo-orphan-buffers)))
(defun bufferlo-ibuffer (&optional other-window-p noselect shrink)
"Invoke `ibuffer' filtered for local buffers.
Every frame/tab gets its own local bufferlo ibuffer buffer.
The parameters OTHER-WINDOW-P NOSELECT SHRINK are passed to `ibuffer'."
(interactive)
(bufferlo--warn)
(let ((name (or
(seq-find (lambda (b)
(string-match-p
"\\`\\*Bufferlo Ibuffer\\*\\(<[0-9]*>\\)?\\'"
(buffer-name b)))
(bufferlo-buffer-list))
(generate-new-buffer-name "*Bufferlo Ibuffer*"))))
(ibuffer other-window-p name '((bufferlo-local-buffers . nil))
noselect shrink)))
(defun bufferlo-ibuffer-orphans (&optional other-window-p noselect shrink)
"Invoke `ibuffer' filtered for orphan buffers.
The parameters OTHER-WINDOW-P NOSELECT SHRINK are passed to `ibuffer'."
(interactive)
(bufferlo--warn)
(let ((name "*Bufferlo Orphans Ibuffer*"))
(ibuffer other-window-p name '((bufferlo-orphan-buffers . nil))
noselect shrink)))
(define-minor-mode bufferlo-anywhere-mode
"Frame/tab-local buffer lists anywhere you like.
Enables bufferlo's local buffer list for any function that interactively prompts
for buffers via `read-buffer'. By default this enables the local buffer list
for (almost) all functions. Customize `bufferlo-anywhere-filter' and
`bufferlo-anywhere-filter-type' to adapt the behavior.
This minor mode requires `bufferlo-mode' to be enabled.
You can use `bufferlo-anywhere-disable' to disable the local buffer list for
the next command, when the mode is enabled."
:global t
:require 'bufferlo
:init-value nil
:lighter nil
:keymap nil
(if bufferlo-anywhere-mode
(progn
(bufferlo--warn)
(advice-add #'call-interactively
:around #'bufferlo--interactive-advice))
(advice-remove #'call-interactively #'bufferlo--interactive-advice)))
(defvar bufferlo--anywhere-tmp-enabled nil)
(defvar bufferlo--anywhere-tmp-disabled nil)
(defvar bufferlo--anywhere-old-read-buffer-function nil)
(defvar bufferlo--anywhere-nested nil)
(defun bufferlo--interactive-advice (oldfn function &optional record-flags keys)
"Advice function for `call-interactively' for `bufferlo-anywhere-mode'.
Temporarily overrides the `read-buffer-function' to filter the
available buffers to bufferlo's local buffer list.
OLDFN is the original function.
FUNCTION is the interactively called functions.
RECORD-FLAGS and KEYS are passed to `call-interactively'."
(if (or bufferlo--anywhere-tmp-enabled
(and (not bufferlo--anywhere-tmp-disabled)
(xor (eq bufferlo-anywhere-filter-type 'exclude)
(cond
((eq bufferlo-anywhere-filter t) t)
((listp bufferlo-anywhere-filter)
(memq function bufferlo-anywhere-filter))
((functionp bufferlo-anywhere-filter)
(funcall bufferlo-anywhere-filter function))))))
(let* ((bufferlo--anywhere-old-read-buffer-function
;; Preserve the original `read-buffer-function' but not for
;; nested calls; otherwise we would save our own function.
(if bufferlo--anywhere-nested
bufferlo--anywhere-old-read-buffer-function
read-buffer-function))
(bufferlo--anywhere-nested t)
(read-buffer-function
(lambda (prompt &optional def require-match predicate)
(let ((read-buffer-function
bufferlo--anywhere-old-read-buffer-function))
(read-buffer prompt def require-match
(lambda (b)
(and (bufferlo-local-buffer-p
(get-buffer
(if (stringp b) b (car b))))
(or (not predicate)
(funcall predicate b)))))))))
(apply oldfn (list function record-flags keys)))
;; `call-interactively' can be nested, e.g., on M-x invocations.
;; Therefore, we restore the original value of the `read-buffer-function'
;; if we do not use bufferlo's local buffer list for this call.
(let ((read-buffer-function
(if bufferlo--anywhere-nested
bufferlo--anywhere-old-read-buffer-function
read-buffer-function)))
(apply oldfn (list function record-flags keys)))))
(defun bufferlo-anywhere-disable-prefix ()
"Disable `bufferlo-anywhere-mode' only for the next command.
Has no effect if `bufferlo-anywhere-mode' is not enabled.
Has no effect if the next command does not query for a buffer."
(interactive)
(let* ((command this-command)
(minibuffer-depth (minibuffer-depth))
(postfun (make-symbol "bufferlo--anywhere-reenable-next-command")))
(fset postfun
(lambda ()
(unless (or
;; from window.el:display-buffer-override-next-command
(> (minibuffer-depth) minibuffer-depth)
(eq this-command command))
(setq bufferlo--anywhere-tmp-disabled nil)
(remove-hook 'post-command-hook postfun))))
(setq bufferlo--anywhere-tmp-disabled t)
(add-hook 'post-command-hook postfun)))
(defun bufferlo-anywhere-enable-prefix ()
"Use bufferlo's local buffer list for the next command.
Has a similar effect as `bufferlo-anywhere-mode' but only for the next command.
Has no effect if the next command does not query for a buffer.
Can be used with or without `bufferlo-anywhere-mode' enabled.
In contrast to `bufferlo-anywhere-mode', this does not adhere to
`bufferlo-anywhere-filter'. Therefore, you can use it in conjunction with
`bufferlo-anywhere-mode' to temporarily disable the configured filters."
(interactive)
(let* ((command this-command)
(minibuffer-depth (minibuffer-depth))
(postfun (make-symbol "bufferlo--anywhere-disable-next-command")))
(fset postfun
(lambda ()
(unless (or
;; from window.el:display-buffer-override-next-command
(> (minibuffer-depth) minibuffer-depth)
(eq this-command command))
(setq bufferlo--anywhere-tmp-enabled nil)
(unless bufferlo-anywhere-mode
(advice-remove #'call-interactively
#'bufferlo--interactive-advice))
(remove-hook 'post-command-hook postfun))))
(setq bufferlo--anywhere-tmp-enabled t)
(unless bufferlo-anywhere-mode
(advice-add #'call-interactively :around #'bufferlo--interactive-advice))
(add-hook 'post-command-hook postfun)))