featurep! will be renamed modulep! in the future, so it's been deprecated. They have identical interfaces, and can be replaced without issue. featurep! was never quite the right name for this macro. It implied that it had some connection to featurep, which it doesn't (only that it was similar in purpose; still, Doom modules are not features). To undo such implications and be consistent with its namespace (and since we're heading into a storm of breaking changes with the v3 release anyway), now was the best opportunity to begin the transition.
279 lines
14 KiB
EmacsLisp
279 lines
14 KiB
EmacsLisp
;;; ui/workspaces/config.el -*- lexical-binding: t; -*-
|
|
|
|
;; `persp-mode' gives me workspaces, a workspace-restricted `buffer-list', and
|
|
;; file-based session persistence. I used workgroups2 before this, but abandoned
|
|
;; it because it was unstable and slow; `persp-mode' is neither (and still
|
|
;; maintained).
|
|
;;
|
|
;; NOTE persp-mode requires `workgroups' for file persistence in Emacs 24.4.
|
|
|
|
(defvar +workspaces-main "main"
|
|
"The name of the primary and initial workspace, which cannot be deleted.")
|
|
|
|
(defvar +workspaces-switch-project-function #'doom-project-find-file
|
|
"The function to run after `projectile-switch-project' or
|
|
`counsel-projectile-switch-project'. This function must take one argument: the
|
|
new project directory.")
|
|
|
|
(defvar +workspaces-on-switch-project-behavior 'non-empty
|
|
"Controls the behavior of workspaces when switching to a new project.
|
|
|
|
Can be one of the following:
|
|
|
|
t Always create a new workspace for the project
|
|
'non-empty Only create a new workspace if the current one already has buffers
|
|
associated with it.
|
|
nil Never create a new workspace on project switch.")
|
|
|
|
;; FIXME actually use this for wconf bookmark system
|
|
(defvar +workspaces-data-file "_workspaces"
|
|
"The basename of the file to store single workspace perspectives. Will be
|
|
stored in `persp-save-dir'.")
|
|
|
|
(defvar +workspace--old-uniquify-style nil)
|
|
|
|
|
|
;;
|
|
;; Packages
|
|
|
|
(use-package! persp-mode
|
|
:unless noninteractive
|
|
:commands persp-switch-to-buffer
|
|
:hook (doom-init-ui . persp-mode)
|
|
:config
|
|
(setq persp-autokill-buffer-on-remove 'kill-weak
|
|
persp-reset-windows-on-nil-window-conf nil
|
|
persp-nil-hidden t
|
|
persp-auto-save-fname "autosave"
|
|
persp-save-dir (concat doom-etc-dir "workspaces/")
|
|
persp-set-last-persp-for-new-frames t
|
|
persp-switch-to-added-buffer nil
|
|
persp-kill-foreign-buffer-behaviour 'kill
|
|
persp-remove-buffers-from-nil-persp-behaviour nil
|
|
persp-auto-resume-time -1 ; Don't auto-load on startup
|
|
persp-auto-save-opt (if noninteractive 0 1)) ; auto-save on kill
|
|
|
|
|
|
;;;; Create main workspace
|
|
;; The default perspective persp-mode creates is special and doesn't represent
|
|
;; a real persp object, so buffers can't really be assigned to it, among other
|
|
;; quirks, so I replace it with a "main" perspective.
|
|
(add-hook! '(persp-mode-hook persp-after-load-state-functions)
|
|
(defun +workspaces-ensure-no-nil-workspaces-h (&rest _)
|
|
(when persp-mode
|
|
(dolist (frame (frame-list))
|
|
(when (string= (safe-persp-name (get-current-persp frame)) persp-nil-name)
|
|
;; Take extra steps to ensure no frame ends up in the nil perspective
|
|
(persp-frame-switch (or (cadr (hash-table-keys *persp-hash*))
|
|
+workspaces-main)
|
|
frame))))))
|
|
|
|
(add-hook! 'persp-mode-hook
|
|
(defun +workspaces-init-first-workspace-h (&rest _)
|
|
"Ensure a main workspace exists."
|
|
(when persp-mode
|
|
(let (persp-before-switch-functions)
|
|
;; Try our best to hide the nil perspective.
|
|
(when (equal (car persp-names-cache) persp-nil-name)
|
|
(pop persp-names-cache))
|
|
;; ...and create a *real* main workspace to fill this role.
|
|
(unless (or (persp-get-by-name +workspaces-main)
|
|
;; Start from 2 b/c persp-mode counts the nil workspace
|
|
(> (hash-table-count *persp-hash*) 2))
|
|
(persp-add-new +workspaces-main))
|
|
;; HACK Fix #319: the warnings buffer gets swallowed when creating
|
|
;; `+workspaces-main', so display it ourselves, if it exists.
|
|
(when-let (warnings (get-buffer "*Warnings*"))
|
|
(save-excursion
|
|
(display-buffer-in-side-window
|
|
warnings '((window-height . shrink-window-if-larger-than-buffer))))))))
|
|
(defun +workspaces-init-persp-mode-h ()
|
|
(cond (persp-mode
|
|
;; `uniquify' breaks persp-mode. It renames old buffers, which causes
|
|
;; errors when switching between perspective (their buffers are
|
|
;; serialized by name and persp-mode expects them to have the same
|
|
;; name when restored).
|
|
(when uniquify-buffer-name-style
|
|
(setq +workspace--old-uniquify-style uniquify-buffer-name-style))
|
|
(setq uniquify-buffer-name-style nil)
|
|
;; Ensure `persp-kill-buffer-query-function' is last
|
|
(remove-hook 'kill-buffer-query-functions #'persp-kill-buffer-query-function)
|
|
(add-hook 'kill-buffer-query-functions #'persp-kill-buffer-query-function t)
|
|
;; Restrict buffer list to workspace
|
|
(advice-add #'doom-buffer-list :override #'+workspace-buffer-list))
|
|
(t
|
|
(when +workspace--old-uniquify-style
|
|
(setq uniquify-buffer-name-style +workspace--old-uniquify-style))
|
|
(advice-remove #'doom-buffer-list #'+workspace-buffer-list)))))
|
|
|
|
;; Per-workspace `winner-mode' history
|
|
(add-to-list 'window-persistent-parameters '(winner-ring . t))
|
|
|
|
(add-hook! 'persp-before-deactivate-functions
|
|
(defun +workspaces-save-winner-data-h (_)
|
|
(when (and (bound-and-true-p winner-mode)
|
|
(get-current-persp))
|
|
(set-persp-parameter
|
|
'winner-ring (list winner-currents
|
|
winner-ring-alist
|
|
winner-pending-undo-ring)))))
|
|
|
|
(add-hook! 'persp-activated-functions
|
|
(defun +workspaces-load-winner-data-h (_)
|
|
(when (bound-and-true-p winner-mode)
|
|
(cl-destructuring-bind
|
|
(currents alist pending-undo-ring)
|
|
(or (persp-parameter 'winner-ring) (list nil nil nil))
|
|
(setq winner-undo-frame nil
|
|
winner-currents currents
|
|
winner-ring-alist alist
|
|
winner-pending-undo-ring pending-undo-ring)))))
|
|
|
|
;;;; Registering buffers to perspectives
|
|
(add-hook! 'doom-switch-buffer-hook
|
|
(defun +workspaces-add-current-buffer-h ()
|
|
"Add current buffer to focused perspective."
|
|
(or (not persp-mode)
|
|
(persp-buffer-filtered-out-p
|
|
(or (buffer-base-buffer (current-buffer))
|
|
(current-buffer))
|
|
persp-add-buffer-on-after-change-major-mode-filter-functions)
|
|
(persp-add-buffer (current-buffer) (get-current-persp) nil nil))))
|
|
|
|
(add-hook 'persp-add-buffer-on-after-change-major-mode-filter-functions
|
|
#'doom-unreal-buffer-p)
|
|
|
|
(defadvice! +workspaces--evil-alternate-buffer-a (&optional window)
|
|
"Make `evil-alternate-buffer' ignore buffers outside the current workspace."
|
|
:override #'evil-alternate-buffer
|
|
(let* ((prev-buffers
|
|
(if persp-mode
|
|
(cl-remove-if-not #'persp-contain-buffer-p (window-prev-buffers)
|
|
:key #'car)
|
|
(window-prev-buffers)))
|
|
(head (car prev-buffers)))
|
|
(if (eq (car head) (window-buffer window))
|
|
(cadr prev-buffers)
|
|
head)))
|
|
|
|
;; HACK Fixes #4196, #1525: selecting deleted buffer error when quitting Emacs
|
|
;; or on some buffer listing ops.
|
|
(defadvice! +workspaces-remove-dead-buffers-a (persp)
|
|
:before #'persp-buffers-to-savelist
|
|
(when (perspective-p persp)
|
|
;; HACK Can't use `persp-buffers' because of a race condition with its gv
|
|
;; getter/setter not being defined in time.
|
|
(setf (aref persp 2)
|
|
(cl-delete-if-not #'persp-get-buffer-or-null (persp-buffers persp)))))
|
|
|
|
;; Delete the current workspace if closing the last open window
|
|
(define-key! persp-mode-map
|
|
[remap delete-window] #'+workspace/close-window-or-workspace
|
|
[remap evil-window-delete] #'+workspace/close-window-or-workspace)
|
|
|
|
;; per-frame workspaces
|
|
(setq persp-init-frame-behaviour t
|
|
persp-init-new-frame-behaviour-override nil
|
|
persp-interactive-init-frame-behaviour-override #'+workspaces-associate-frame-fn
|
|
persp-emacsclient-init-frame-behaviour-override #'+workspaces-associate-frame-fn)
|
|
(add-hook 'delete-frame-functions #'+workspaces-delete-associated-workspace-h)
|
|
(add-hook 'server-done-hook #'+workspaces-delete-associated-workspace-h)
|
|
|
|
;; per-project workspaces, but reuse current workspace if empty
|
|
;; HACK?? needs review
|
|
(setq projectile-switch-project-action (lambda () (+workspaces-set-project-action-fn) (+workspaces-switch-to-project-h))
|
|
counsel-projectile-switch-project-action
|
|
'(1 ("o" +workspaces-switch-to-project-h "open project in new workspace")
|
|
("O" counsel-projectile-switch-project-action "jump to a project buffer or file")
|
|
("f" counsel-projectile-switch-project-action-find-file "jump to a project file")
|
|
("d" counsel-projectile-switch-project-action-find-dir "jump to a project directory")
|
|
("D" counsel-projectile-switch-project-action-dired "open project in dired")
|
|
("b" counsel-projectile-switch-project-action-switch-to-buffer "jump to a project buffer")
|
|
("m" counsel-projectile-switch-project-action-find-file-manually "find file manually from project root")
|
|
("w" counsel-projectile-switch-project-action-save-all-buffers "save all project buffers")
|
|
("k" counsel-projectile-switch-project-action-kill-buffers "kill all project buffers")
|
|
("r" counsel-projectile-switch-project-action-remove-known-project "remove project from known projects")
|
|
("c" counsel-projectile-switch-project-action-compile "run project compilation command")
|
|
("C" counsel-projectile-switch-project-action-configure "run project configure command")
|
|
("e" counsel-projectile-switch-project-action-edit-dir-locals "edit project dir-locals")
|
|
("v" counsel-projectile-switch-project-action-vc "open project in vc-dir / magit / monky")
|
|
("s" (lambda (project)
|
|
(let ((projectile-switch-project-action
|
|
(lambda () (call-interactively #'+ivy/project-search))))
|
|
(counsel-projectile-switch-project-by-name project))) "search project")
|
|
("xs" counsel-projectile-switch-project-action-run-shell "invoke shell from project root")
|
|
("xe" counsel-projectile-switch-project-action-run-eshell "invoke eshell from project root")
|
|
("xt" counsel-projectile-switch-project-action-run-term "invoke term from project root")
|
|
("X" counsel-projectile-switch-project-action-org-capture "org-capture into project")))
|
|
|
|
(when (modulep! :completion helm)
|
|
(after! helm-projectile
|
|
(setcar helm-source-projectile-projects-actions
|
|
'("Switch to Project" . +workspaces-switch-to-project-h))))
|
|
|
|
;; Don't bother auto-saving the session if no real buffers are open.
|
|
(advice-add #'persp-asave-on-exit :around #'+workspaces-autosave-real-buffers-a)
|
|
|
|
;; Fix #1973: visual selection surviving workspace changes
|
|
(add-hook 'persp-before-deactivate-functions #'deactivate-mark)
|
|
|
|
;; Fix #1017: stop session persistence from restoring a broken posframe
|
|
(after! posframe
|
|
(add-hook! 'persp-after-load-state-functions
|
|
(defun +workspaces-delete-all-posframes-h (&rest _)
|
|
(posframe-delete-all))))
|
|
|
|
;; Don't try to persist dead/remote buffers. They cause errors.
|
|
(add-hook! 'persp-filter-save-buffers-functions
|
|
(defun +workspaces-dead-buffer-p (buf)
|
|
;; Fix #1525: Ignore dead buffers in PERSP's buffer list
|
|
(not (buffer-live-p buf)))
|
|
(defun +workspaces-remote-buffer-p (buf)
|
|
;; And don't save TRAMP buffers; they're super slow to restore
|
|
(let ((dir (buffer-local-value 'default-directory buf)))
|
|
(ignore-errors (file-remote-p dir)))))
|
|
|
|
;; Otherwise, buffers opened via bookmarks aren't treated as "real" and are
|
|
;; excluded from the buffer list.
|
|
(add-hook 'bookmark-after-jump-hook #'+workspaces-add-current-buffer-h)
|
|
|
|
;;; eshell
|
|
(persp-def-buffer-save/load
|
|
:mode 'eshell-mode :tag-symbol 'def-eshell-buffer
|
|
:save-vars '(major-mode default-directory))
|
|
;; compile
|
|
(persp-def-buffer-save/load
|
|
:mode 'compilation-mode :tag-symbol 'def-compilation-buffer
|
|
:save-vars '(major-mode default-directory compilation-directory
|
|
compilation-environment compilation-arguments))
|
|
;; magit
|
|
(persp-def-buffer-save/load
|
|
:mode 'magit-status-mode :tag-symbol 'def-magit-status-buffer
|
|
:save-vars '(default-directory)
|
|
:load-function (lambda (savelist &rest _)
|
|
(cl-destructuring-bind (buffer-name vars &rest _rest) (cdr savelist)
|
|
(magit-status (alist-get 'default-directory vars)))))
|
|
;; Restore indirect buffers
|
|
(defvar +workspaces--indirect-buffers-to-restore nil)
|
|
(persp-def-buffer-save/load
|
|
:tag-symbol 'def-indirect-buffer
|
|
:predicate #'buffer-base-buffer
|
|
:save-function (lambda (buf tag vars)
|
|
(list tag (buffer-name buf) vars
|
|
(buffer-name (buffer-base-buffer buf))))
|
|
:load-function (lambda (savelist &rest _rest)
|
|
(cl-destructuring-bind (buf-name _vars base-buf-name &rest _)
|
|
(cdr savelist)
|
|
(push (cons buf-name base-buf-name)
|
|
+workspaces--indirect-buffers-to-restore)
|
|
nil)))
|
|
(add-hook! 'persp-after-load-state-functions
|
|
(defun +workspaces-reload-indirect-buffers-h (&rest _)
|
|
(dolist (ibc +workspaces--indirect-buffers-to-restore)
|
|
(cl-destructuring-bind (buffer-name . base-buffer-name) ibc
|
|
(let ((base-buffer (get-buffer base-buffer-name)))
|
|
(when (buffer-live-p base-buffer)
|
|
(when (get-buffer buffer-name)
|
|
(setq buffer-name (generate-new-buffer-name buffer-name)))
|
|
(make-indirect-buffer base-buffer buffer-name t)))))
|
|
(setq +workspaces--indirect-buffers-to-restore nil))))
|