Moving to Doom Emacs!

This commit is contained in:
Derek Taylor
2019-12-16 20:21:19 -06:00
parent d9f2f456f1
commit d4b4c33550
683 changed files with 51877 additions and 100 deletions

View File

@@ -0,0 +1,104 @@
#+TITLE: ui/workspaces
#+DATE: February 4, 2017
#+SINCE: v1.3
#+STARTUP: inlineimages
* Table of Contents :TOC:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#packages][Packages]]
- [[#prerequisites][Prerequisites]]
- [[#features][Features]]
- [[#isolated-buffer-list][Isolated buffer-list]]
- [[#automatic-workspaces][Automatic workspaces]]
- [[#session-persistence][Session persistence]]
- [[#workspace-persistence][Workspace persistence]]
- [[#appendix][Appendix]]
- [[#commands--keybindings][Commands & Keybindings]]
- [[#api][API]]
* Description
This module adds support for workspaces, powered by persp_mode, as well as a API
for manipulating them.
#+begin_quote
There are many ways to use workspaces. I spawn a workspace per task. Say I'm
working in the main workspace, when I realize there is a bug in another part of
my project. I open a new workspace and deal with it in there. In the meantime, I
need to check my email, so mu4e gets its own workspace.
Once I've completed the task, I close the workspace and return to main.
#+end_quote
** Module Flags
This module provides no flags.
** Packages
+ [[https://github.com/Bad-ptr/persp-mode.el][persp-mode]]
* Prerequisites
This module has no additional dependencies.
* Features
** Isolated buffer-list
When persp-mode is active, ~doom-buffer-list~ becomes workspace-restricted. You
can overcome this by using ~buffer-list~.
** Automatic workspaces
A workspace is automatically created (and switched to) when you:
+ Create a new frame (with =make-frame=; bound to =M-N= by default).
+ Switch to a project using ~projectile-switch-project~.
** Session persistence
By default, your session is autosaved when you quit Emacs (or disable
~persp-mode~). You can load a previous session with ~M-x
+workspace/load-session~ or ~:sl[oad]~ (ex command).
You can supply either a name to load a specific session to replace your current
one.
** Workspace persistence
If you'd like to save a specific workspace, use ~M-x +workspace/save~, which can
be loaded into the current session (as another workspace) with ~M-x
+workspace/load~.
* Appendix
** Commands & Keybindings
Here is a list of available commands, their default keybindings (defined in
[[../../config/default/+evil-bindings.el][private/default/+bindings.el]]), and corresponding ex commands (if any -- defined
in [[../../editor/evil/+commands.el][private/default/+evil-commands.el]]).
| command | key / ex command | description |
|-----------------------------------+----------------------------+------------------------------------------------------------|
| ~+workspace/new~ | =SPC TAB n= | Create a new, blank workspace |
| ~+workspace/display~ | =SPC TAB TAB= | Display open workspaces in the mode-line |
| ~+workspace/load~ | =SPC TAB l= | Load a saved workspace into the current session |
| ~+workspace/restore-last-session~ | =SPC TAB R= | Restore last session |
| ~+workspace/rename~ | =SPC TAB r= | Rename the current workspace |
| ~+workspace/save~ | =SPC TAB s= | Save the current workspace to a file |
| ~+workspace/switch-to~ | =SPC TAB .= | Switch to an open workspace |
| ~+workspace/other~ | =SPC TAB `= | Switch to last workspace |
| ~+workspace/switch-left~ | =SPC TAB [= / =[ w= / =gT= | Switch to previous workspace |
| ~+workspace/switch-right~ | =SPC TAB ]= / =] w= / =gt= | Switch to next workspace |
| ~+workspace/delete~ | =SPC TAB d= | Delete the current workspace |
| ~+workspace/kill-session~ | =SPC TAB x= / =:sclear= | Clears the current session (kills all windows and buffers) |
** API
+ ~+workspace-list~ -> list<Struct>
+ ~+workspace-list-names~ -> list<string>
+ ~+workspace-buffer-list &optional PERSP~ -> bool
+ ~+workspace-p OBJ~ -> bool
+ ~+workspace-exists-p NAME~ -> bool
+ ~+workspace-get NAME &optional NOERROR~ -> Struct
+ ~+workspace-current &optional FRAME WINDOW~ -> Struct
+ ~+workspace-current-name~ -> string
+ ~+workspace-load NAME~
+ ~+workspace-load-session NAME~
+ ~+workspace-save NAME~
+ ~+workspace-save-session NAME~
+ ~+workspace-new NAME~
+ ~+workspace-rename NAME NEW-NAME~
+ ~+workspace-delete NAME &optional INHIBIT-KILL-P~
+ ~+workspace-switch NAME &optional AUTO-CREATE-P~
+ ~+workspace-protected-p NAME~ -> bool

View File

@@ -0,0 +1,39 @@
;;; ui/workspaces/autoload/evil.el -*- lexical-binding: t; -*-
;;;###if (featurep! :editor evil)
;;;###autoload (autoload '+workspace:save "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:save (&optional name)
"Ex wrapper around `+workspace/save-session'."
(interactive "<a>") (+workspace/save name))
;;;###autoload (autoload '+workspace:load "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:load (&optional name)
"Ex wrapper around `+workspace/load-session'."
(interactive "<a>") (+workspace/load name))
;;;###autoload (autoload '+workspace:new "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:new (bang name)
"Ex wrapper around `+workspace/new'. If BANG, clone the current workspace."
(interactive "<!><a>") (+workspace/new name bang))
;;;###autoload (autoload '+workspace:rename "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:rename (new-name)
"Ex wrapper around `+workspace/rename'."
(interactive "<a>") (+workspace/rename new-name))
;;;###autoload (autoload '+workspace:delete "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:delete ()
"Ex wrapper around `+workspace/delete'."
(interactive) (+workspace/delete (+workspace-current-name)))
;;;###autoload (autoload '+workspace:switch-next "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:switch-next (&optional count)
"Switch to next workspace. If COUNT, switch to COUNT-th workspace."
(interactive "<c>")
(if count (+workspace/switch-to count) (+workspace/cycle +1)))
;;;###autoload (autoload '+workspace:switch-previous "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:switch-previous (&optional count)
"Switch to previous workspace. If COUNT, switch to COUNT-th workspace."
(interactive "<c>")
(if count (+workspace/switch-to count) (+workspace/cycle -1)))

View File

@@ -0,0 +1,531 @@
;;; feature/workspaces/autoload/workspaces.el -*- lexical-binding: t; -*-
(defvar +workspace--last nil)
(defvar +workspace--index 0)
;;;###autoload
(defface +workspace-tab-selected-face '((t (:inherit highlight)))
"The face for selected tabs displayed by `+workspace/display'"
:group 'persp-mode)
;;;###autoload
(defface +workspace-tab-face '((t (:inherit default)))
"The face for selected tabs displayed by `+workspace/display'"
:group 'persp-mode)
;;
;;; Library
(defun +workspace--protected-p (name)
(equal name persp-nil-name))
(defun +workspace--generate-id ()
(or (cl-loop for name in (+workspace-list-names)
when (string-match-p "^#[0-9]+$" name)
maximize (string-to-number (substring name 1)) into max
finally return (if max (1+ max)))
1))
;;; Predicates
;;;###autoload
(defalias #'+workspace-p #'perspective-p
"Return t if OBJ is a perspective hash table.")
;;;###autoload
(defun +workspace-exists-p (name)
"Returns t if NAME is the name of an existing workspace."
(member name (+workspace-list-names)))
;;;###autoload
(defalias #'+workspace-contains-buffer-p #'persp-contain-buffer-p
"Return non-nil if BUFFER is in WORKSPACE (defaults to current workspace).")
;;; Getters
;;;###autoload
(defalias #'+workspace-current #'get-current-persp
"Return the currently active workspace.")
;;;###autoload
(defun +workspace-get (name &optional noerror)
"Return a workspace named NAME. Unless NOERROR is non-nil, this throws an
error if NAME doesn't exist."
(cl-check-type name string)
(when-let (persp (persp-get-by-name name))
(cond ((+workspace-p persp) persp)
((not noerror)
(error "No workspace called '%s' was found" name)))))
;;;###autoload
(defun +workspace-current-name ()
"Get the name of the current workspace."
(safe-persp-name (+workspace-current)))
;;;###autoload
(defun +workspace-list ()
"Return a list of workspace structs (satisifes `+workspace-p')."
;; We don't use `hash-table-values' because it doesn't ensure order in older
;; versions of Emacs
(cdr (cl-loop for persp being the hash-values of *persp-hash*
collect persp)))
;;;###autoload
(defun +workspace-list-names ()
"Return the list of names of open workspaces."
(mapcar #'safe-persp-name (+workspace-list)))
;;;###autoload
(defun +workspace-buffer-list (&optional persp)
"Return a list of buffers in PERSP.
The buffer list is ordered by recency (same as `buffer-list').
PERSP can be a string (name of a workspace) or a workspace (satisfies
`+workspace-p'). If nil or omitted, it defaults to the current workspace."
(let ((persp (or persp (+workspace-current))))
(unless (+workspace-p persp)
(user-error "Not in a valid workspace (%s)" persp))
(persp-buffers persp)))
;;;###autoload
(defun +workspace-orphaned-buffer-list ()
"Return a list of buffers that aren't associated with any perspective."
(cl-remove-if #'persp--buffer-in-persps (buffer-list)))
;;; Actions
;;;###autoload
(defun +workspace-load (name)
"Loads a single workspace (named NAME) into the current session. Can only
retrieve perspectives that were explicitly saved with `+workspace-save'.
Returns t if successful, nil otherwise."
(when (+workspace-exists-p name)
(user-error "A workspace named '%s' already exists." name))
(persp-load-from-file-by-names
(expand-file-name +workspaces-data-file persp-save-dir)
*persp-hash* (list name))
(+workspace-exists-p name))
;;;###autoload
(defun +workspace-save (name)
"Saves a single workspace (NAME) from the current session. Can be loaded again
with `+workspace-load'. NAME can be the string name of a workspace or its
perspective hash table.
Returns t on success, nil otherwise."
(unless (+workspace-exists-p name)
(error "'%s' is an invalid workspace" name))
(let ((fname (expand-file-name +workspaces-data-file persp-save-dir)))
(persp-save-to-file-by-names fname *persp-hash* (list name))
(and (member name (persp-list-persp-names-in-file fname))
t)))
;;;###autoload
(defun +workspace-new (name)
"Create a new workspace named NAME. If one already exists, return nil.
Otherwise return t on success, nil otherwise."
(when (+workspace--protected-p name)
(error "Can't create a new '%s' workspace" name))
(when (+workspace-exists-p name)
(error "A workspace named '%s' already exists" name))
(let ((persp (persp-add-new name))
(+popup--inhibit-transient t))
(save-window-excursion
(let ((ignore-window-parameters t)
(+popup--inhibit-transient t))
(persp-delete-other-windows))
(switch-to-buffer (doom-fallback-buffer))
(setf (persp-window-conf persp)
(funcall persp-window-state-get-function (selected-frame))))
persp))
;;;###autoload
(defun +workspace-rename (name new-name)
"Rename the current workspace named NAME to NEW-NAME. Returns old name on
success, nil otherwise."
(when (+workspace--protected-p name)
(error "Can't rename '%s' workspace" name))
(persp-rename new-name (+workspace-get name)))
;;;###autoload
(defun +workspace-delete (workspace &optional inhibit-kill-p)
"Delete the workspace denoted by WORKSPACE, which can be the name of a perspective
or its hash table. If INHIBIT-KILL-P is non-nil, don't kill this workspace's
buffers."
(unless (stringp workspace)
(setq workspace (persp-name workspace)))
(when (+workspace--protected-p workspace)
(error "Can't delete '%s' workspace" workspace))
(+workspace-get workspace) ; error checking
(persp-kill workspace inhibit-kill-p)
(not (+workspace-exists-p workspace)))
;;;###autoload
(defun +workspace-switch (name &optional auto-create-p)
"Switch to another workspace named NAME (a string).
If AUTO-CREATE-P is non-nil, create the workspace if it doesn't exist, otherwise
throws an error."
(unless (+workspace-exists-p name)
(if auto-create-p
(+workspace-new name)
(error "%s is not an available workspace" name)))
(let ((old-name (+workspace-current-name)))
(setq +workspace--last
(or (and (not (string= old-name persp-nil-name))
old-name)
+workspaces-main))
(persp-frame-switch name)
(equal (+workspace-current-name) name)))
;;
;;; Commands
;;;###autoload
(defalias '+workspace/restore-last-session #'doom/quickload-session)
;;;###autoload
(defun +workspace/load (name)
"Load a workspace and switch to it. If called with C-u, try to reload the
current workspace (by name) from session files."
(interactive
(list
(if current-prefix-arg
(+workspace-current-name)
(completing-read
"Workspace to load: "
(persp-list-persp-names-in-file
(expand-file-name +workspaces-data-file persp-save-dir))))))
(if (not (+workspace-load name))
(+workspace-error (format "Couldn't load workspace %s" name))
(+workspace/switch-to name)
(+workspace/display)))
;;;###autoload
(defun +workspace/save (name)
"Save the current workspace. If called with C-u, autosave the current
workspace."
(interactive
(list
(if current-prefix-arg
(+workspace-current-name)
(completing-read "Workspace to save: " (+workspace-list-names)))))
(if (+workspace-save name)
(+workspace-message (format "'%s' workspace saved" name) 'success)
(+workspace-error (format "Couldn't save workspace %s" name))))
;;;###autoload
(defun +workspace/rename (new-name)
"Rename the current workspace."
(interactive (list (read-from-minibuffer "New workspace name: ")))
(condition-case-unless-debug ex
(let* ((current-name (+workspace-current-name))
(old-name (+workspace-rename current-name new-name)))
(unless old-name
(error "Failed to rename %s" current-name))
(+workspace-message (format "Renamed '%s'->'%s'" old-name new-name) 'success))
('error (+workspace-error ex t))))
;;;###autoload
(defun +workspace/delete (name)
"Delete this workspace. If called with C-u, prompts you for the name of the
workspace to delete."
(interactive
(let ((current-name (+workspace-current-name)))
(list
(if current-prefix-arg
(completing-read (format "Delete workspace (default: %s): " current-name)
(+workspace-list-names)
nil nil nil nil current-name)
current-name))))
(condition-case-unless-debug ex
;; REVIEW refactor me
(let ((workspaces (+workspace-list-names)))
(if (not (member name workspaces))
(+workspace-message (format "'%s' workspace doesn't exist" name) 'warn)
(cond ((delq (selected-frame) (persp-frames-with-persp (get-frame-persp)))
(user-error "Can't close workspace, it's visible in another frame"))
((not (equal (+workspace-current-name) name))
(+workspace-delete name))
((cdr workspaces)
(+workspace-delete name)
(+workspace-switch
(if (+workspace-exists-p +workspace--last)
+workspace--last
(car (+workspace-list-names))))
(unless (doom-buffer-frame-predicate (window-buffer))
(switch-to-buffer (doom-fallback-buffer))))
(t
(+workspace-switch +workspaces-main t)
(unless (string= (car workspaces) +workspaces-main)
(+workspace-delete name))
(doom/kill-all-buffers (doom-buffer-list))))
(+workspace-message (format "Deleted '%s' workspace" name) 'success)))
('error (+workspace-error ex t))))
;;;###autoload
(defun +workspace/kill-session ()
"Delete the current session, all workspaces, windows and their buffers."
(interactive)
(unless (cl-every #'+workspace-delete (+workspace-list-names))
(+workspace-error "Could not clear session"))
(+workspace-switch +workspaces-main t)
(doom/kill-all-buffers (buffer-list)))
;;;###autoload
(defun +workspace/kill-session-and-quit ()
"Kill emacs without saving anything."
(interactive)
(let ((persp-auto-save-opt 0))
(kill-emacs)))
;;;###autoload
(defun +workspace/new (&optional name clone-p)
"Create a new workspace named NAME. If CLONE-P is non-nil, clone the current
workspace, otherwise the new workspace is blank."
(interactive "iP")
(unless name
(setq name (format "#%s" (+workspace--generate-id))))
(condition-case e
(cond ((+workspace-exists-p name)
(error "%s already exists" name))
(clone-p (persp-copy name t))
(t
(+workspace-switch name t)
(+workspace/display)))
((debug error) (+workspace-error (cadr e) t))))
;;;###autoload
(defun +workspace/switch-to (index)
"Switch to a workspace at a given INDEX. A negative number will start from the
end of the workspace list."
(interactive
(list (or current-prefix-arg
(if (featurep! :completion ivy)
(ivy-read "Switch to workspace: "
(+workspace-list-names)
:caller #'+workspace/switch-to
:preselect (+workspace-current-name))
(completing-read "Switch to workspace: " (+workspace-list-names))))))
(when (and (stringp index)
(string-match-p "^[0-9]+$" index))
(setq index (string-to-number index)))
(condition-case-unless-debug ex
(let ((names (+workspace-list-names))
(old-name (+workspace-current-name)))
(cond ((numberp index)
(let ((dest (nth index names)))
(unless dest
(error "No workspace at #%s" (1+ index)))
(+workspace-switch dest)))
((stringp index)
(+workspace-switch index t))
(t
(error "Not a valid index: %s" index)))
(unless (called-interactively-p 'interactive)
(if (equal (+workspace-current-name) old-name)
(+workspace-message (format "Already in %s" old-name) 'warn)
(+workspace/display))))
('error (+workspace-error (cadr ex) t))))
;;;###autoload
(dotimes (i 9)
(defalias (intern (format "+workspace/switch-to-%d" i))
(lambda () (interactive) (+workspace/switch-to i))))
;;;###autoload
(defun +workspace/switch-to-final ()
"Switch to the final workspace in open workspaces."
(interactive)
(+workspace/switch-to (car (last (+workspace-list-names)))))
;;;###autoload
(defun +workspace/other ()
"Switch to the last activated workspace."
(interactive)
(+workspace/switch-to +workspace--last))
;;;###autoload
(defun +workspace/cycle (n)
"Cycle n workspaces to the right (default) or left."
(interactive (list 1))
(let ((current-name (+workspace-current-name)))
(if (equal current-name persp-nil-name)
(+workspace-switch +workspaces-main t)
(condition-case-unless-debug ex
(let* ((persps (+workspace-list-names))
(perspc (length persps))
(index (cl-position current-name persps)))
(when (= perspc 1)
(user-error "No other workspaces"))
(+workspace/switch-to (% (+ index n perspc) perspc))
(unless (called-interactively-p 'interactive)
(+workspace/display)))
('user-error (+workspace-error (cadr ex) t))
('error (+workspace-error ex t))))))
;;;###autoload
(defun +workspace/switch-left () (interactive) (+workspace/cycle -1))
;;;###autoload
(defun +workspace/switch-right () (interactive) (+workspace/cycle +1))
;;;###autoload
(defun +workspace/close-window-or-workspace ()
"Close the selected window. If it's the last window in the workspace, either
close the workspace (as well as its associated frame, if one exists) and move to
the next."
(interactive)
(let ((delete-window-fn (if (featurep 'evil) #'evil-window-delete #'delete-window)))
(if (window-dedicated-p)
(funcall delete-window-fn)
(let ((current-persp-name (+workspace-current-name)))
(cond ((or (+workspace--protected-p current-persp-name)
(cdr (doom-visible-windows)))
(funcall delete-window-fn))
((cdr (+workspace-list-names))
(let ((frame-persp (frame-parameter nil 'workspace)))
(if (string= frame-persp (+workspace-current-name))
(delete-frame)
(+workspace/delete current-persp-name))))
((+workspace-error "Can't delete last workspace" t)))))))
;;
;;; Tabs display in minibuffer
(defun +workspace--tabline (&optional names)
(let ((names (or names (+workspace-list-names)))
(current-name (+workspace-current-name)))
(mapconcat
#'identity
(cl-loop for name in names
for i to (length names)
collect
(propertize (format " [%d] %s " (1+ i) name)
'face (if (equal current-name name)
'+workspace-tab-selected-face
'+workspace-tab-face)))
" ")))
(defun +workspace--message-body (message &optional type)
(concat (+workspace--tabline)
(propertize " | " 'face 'font-lock-comment-face)
(propertize (format "%s" message)
'face (pcase type
('error 'error)
('warn 'warning)
('success 'success)
('info 'font-lock-comment-face)))))
;;;###autoload
(defun +workspace-message (message &optional type)
"Show an 'elegant' message in the echo area next to a listing of workspaces."
(message "%s" (+workspace--message-body message type)))
;;;###autoload
(defun +workspace-error (message &optional noerror)
"Show an 'elegant' error in the echo area next to a listing of workspaces."
(funcall (if noerror #'message #'error)
"%s" (+workspace--message-body message 'error)))
;;;###autoload
(defun +workspace/display ()
"Display a list of workspaces (like tabs) in the echo area."
(interactive)
(let (message-log-max)
(message "%s" (+workspace--tabline))))
;;
;;; Hooks
;;;###autoload
(defun +workspaces-delete-associated-workspace-h (&optional frame)
"Delete workspace associated with current frame.
A workspace gets associated with a frame when a new frame is interactively
created."
(when persp-mode
(unless frame
(setq frame (selected-frame)))
(let ((frame-persp (frame-parameter frame 'workspace)))
(when (string= frame-persp (+workspace-current-name))
(+workspace/delete frame-persp)))))
;;;###autoload
(defun +workspaces-associate-frame-fn (frame &optional _new-frame-p)
"Create a blank, new perspective and associate it with FRAME."
(when persp-mode
(if (not (persp-frame-list-without-daemon))
(+workspace-switch +workspaces-main t)
(with-selected-frame frame
(+workspace-switch (format "#%s" (+workspace--generate-id)) t)
(unless (doom-real-buffer-p (current-buffer))
(switch-to-buffer (doom-fallback-buffer)))
(set-frame-parameter frame 'workspace (+workspace-current-name))
;; ensure every buffer has a buffer-predicate
(persp-set-frame-buffer-predicate frame))
(run-at-time 0.1 nil #'+workspace/display))))
(defvar +workspaces--project-dir nil)
;;;###autoload
(defun +workspaces-set-project-action-fn ()
"A `projectile-switch-project-action' that sets the project directory for
`+workspaces-switch-to-project-h'."
(setq +workspaces--project-dir default-directory))
;;;###autoload
(defun +workspaces-switch-to-project-h (&optional dir)
"Creates a workspace dedicated to a new project. If one already exists, switch
to it. If in the main workspace and it's empty, recycle that workspace, without
renaming it.
Afterwords, runs `+workspaces-switch-project-function'. By default, this prompts
the user to open a file in the new project.
This be hooked to `projectile-after-switch-project-hook'."
(when dir
(setq +workspaces--project-dir dir))
(when (and persp-mode +workspaces--project-dir)
(unwind-protect
(if (and (not (null +workspaces-on-switch-project-behavior))
(or (eq +workspaces-on-switch-project-behavior t)
(+workspace-buffer-list)))
(let* ((persp
(let ((project-name (doom-project-name +workspaces--project-dir)))
(or (+workspace-get project-name t)
(+workspace-new project-name))))
(new-name (persp-name persp)))
(+workspace-switch new-name)
(with-current-buffer (doom-fallback-buffer)
(setq default-directory +workspaces--project-dir))
(unless current-prefix-arg
(funcall +workspaces-switch-project-function +workspaces--project-dir))
(+workspace-message
(format "Switched to '%s' in new workspace" new-name)
'success))
(with-current-buffer (doom-fallback-buffer)
(setq default-directory +workspaces--project-dir)
(message "Switched to '%s'" (doom-project-name +workspaces--project-dir)))
(with-demoted-errors "Workspace error: %s"
(+workspace-rename (+workspace-current-name) (doom-project-name +workspaces--project-dir)))
(unless current-prefix-arg
(funcall +workspaces-switch-project-function +workspaces--project-dir)))
(setq +workspaces--project-dir nil))))
;;
;;; Advice
;;;###autoload
(defun +workspaces-autosave-real-buffers-a (orig-fn &rest args)
"Don't autosave if no real buffers are open."
(when (doom-real-buffer-list)
(apply orig-fn args))
t)

View File

@@ -0,0 +1,222 @@
;;; 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
:commands persp-switch-to-buffer
:init
(add-hook! 'doom-init-modules-hook
(defun +workspaces-init-h ()
(unless noninteractive
;; Remove default buffer predicate so persp-mode can put in its own
(delq! 'buffer-predicate default-frame-alist 'assq)
(require 'persp-mode)
(if (daemonp)
(add-hook 'after-make-frame-functions #'persp-mode-start-and-remove-from-make-frame-hook)
(persp-mode +1)))))
:config
(setq persp-autokill-buffer-on-remove 'kill-weak
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-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
(advice-add #'persp-asave-on-exit :around #'+workspaces-autosave-real-buffers-a)
(add-hook! '(persp-mode-hook persp-after-load-state-functions)
(defun +workspaces-ensure-main-workspace-h (&rest _)
"Ensure the main workspace exists and the nil workspace is never active."
(when persp-mode
(let (persp-before-switch-functions)
;; The default perspective persp-mode creates (`persp-nil-name') is
;; special and doesn't represent a real persp object, so buffers can't
;; really be assigned to it, among other quirks. We create a *real* main
;; workspace to fill this role.
(unless (persp-get-by-name +workspaces-main)
(persp-add-new +workspaces-main))
;; Switch to it if we're in the nil perspective
(dolist (frame (frame-list))
(when (string= (safe-persp-name (get-current-persp frame)) persp-nil-name)
(persp-frame-switch +workspaces-main frame)
;; Fix #319: the warnings buffer gets swallowed by creating
;; `+workspaces-main', so we display it manually, 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)))))))))))
(add-hook! 'persp-mode-hook
(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)))))
;; We don't rely on the built-in mechanism for auto-registering a buffer to
;; the current workspace; some buffers slip through the cracks. Instead, we
;; add buffers when they are switched to.
(setq persp-add-buffer-on-find-file nil
persp-add-buffer-on-after-change-major-mode nil)
(add-hook! '(doom-switch-buffer-hook server-visit-hook)
(defun +workspaces-add-current-buffer-h ()
"Add current buffer to focused perspective."
(and persp-mode
(not (persp-buffer-filtered-out-p
(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)))
;; 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)
;; per-project workspaces, but reuse current workspace if empty
(setq projectile-switch-project-action #'+workspaces-set-project-action-fn
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")
("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")))
(add-hook 'projectile-after-switch-project-hook #'+workspaces-switch-to-project-h)
;; 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))))
;; Fix #1525: Ignore dead buffers in PERSP's buffer list
(defun +workspaces-dead-buffer-p (buf)
(not (buffer-live-p buf)))
(add-hook 'persp-filter-save-buffers-functions #'+workspaces-dead-buffer-p)
;;
;; 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))
;; 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
(when (buffer-live-p (get-buffer base-buffer-name))
(when (get-buffer buffer-name)
(setq buffer-name (generate-new-buffer-name buffer-name)))
(make-indirect-buffer bb buffer-name t))))
(setq +workspaces--indirect-buffers-to-restore nil))))

View File

@@ -0,0 +1,5 @@
;; -*- no-byte-compile: t; -*-
;;; ui/workspaces/packages.el
(package! persp-mode)

View File

@@ -0,0 +1,124 @@
;; -*- no-byte-compile: t; -*-
;;; ui/workspaces/test/test-workspaces.el
(describe "ui/workspaces"
:var (persp-auto-resume-time
persp-auto-save-opt
persp-switch-to-added-buffer
persp-autokill-persp-when-removed-last-buffer
persp-autokill-buffer-on-remove
in1 in2 out1 out2
persp1 persp1-name persp2 persp2-name
wconf)
(require! :ui workspaces)
(require 'persp-mode)
(before-all
(delete-other-windows))
(before-each
(switch-to-buffer "*scratch*")
(setq wconf (current-window-configuration)
persp-auto-resume-time -1
persp-auto-save-opt 0
persp-switch-to-added-buffer nil
persp-autokill-persp-when-removed-last-buffer nil
persp-autokill-buffer-on-remove nil
in1 (get-buffer-create "in1")
in2 (get-buffer-create "in2")
out1 (get-buffer-create "out1")
out2 (get-buffer-create "out2"))
(doom-set-buffer-real in1 t)
(doom-set-buffer-real out1 t)
(let (noninteractive)
(persp-mode +1)
(let (persp-before-switch-functions persp-activated-functions)
(setq persp1-name +workspaces-main
persp1 (persp-add-new persp1-name)
persp2-name "test"
persp2 (persp-add-new persp2-name))
(persp-switch persp1-name)
(persp-add-buffer (list in1 in2) persp1))))
(after-each
(let (kill-buffer-query-functions kill-buffer-hook)
(let (noninteractive ignore-window-parameters)
(dolist (persp (persp-names))
(ignore-errors (persp-kill persp)))
(persp-mode -1))
(set-window-configuration wconf)
(mapc #'kill-buffer (list in1 in2 out1 out2))))
;;
(describe "switch"
(it "throws an error when switching to a non-existent workspace"
(expect (+workspace-switch "non-existent") :to-throw))
(it "switches to a valid workspace"
(+workspace-switch persp2-name)
(expect (+workspace-current-name) :to-equal persp2-name)))
(describe "current"
(it "returns the current workspace persp"
(expect (+workspace-p (+workspace-current)))
(expect (+workspace-current) :to-equal (get-current-persp)))
(it "returns the current workspace's name"
(expect (+workspace-current-name) :to-equal persp1-name)
(persp-switch (persp-name persp2))
(expect (+workspace-current-name) :to-equal persp2-name)))
(describe "exists-p"
(it "returns t for valid workspaces"
(expect (+workspace-exists-p persp1-name)))
(it "returns t for non-current (but valid) workspaces"
(expect (+workspace-exists-p persp2-name)))
(it "returns nil for non-existent workspaces"
(expect (+workspace-exists-p "non-existent") :to-be nil)))
(describe "buffer membership"
(it "returns t for buffers in current workspace"
(expect (+workspace-contains-buffer-p in1)))
(it "returns nil for buffers outside of current workspace"
(expect (+workspace-contains-buffer-p out1) :to-be nil))
(xit "returns a list of orphaned buffers"
(expect (+workspace-orphaned-buffer-list) :to-contain out2)))
(describe "list"
(it "returns a list of names"
(expect (+workspace-list-names)
:to-have-same-items-as (list persp1-name persp2-name)))
(it "returns a list of perspective structs"
(expect (+workspace-list)
:to-have-same-items-as (list persp1 persp2))))
(describe "CRUD"
(it "creates new workspaces"
(+workspace-new "X")
(expect (+workspace-list-names) :to-contain "X"))
(it "renames an existing workspace"
(+workspace-rename persp2-name "X")
(expect (persp-name persp2) :to-equal "X")
(expect (+workspace-list-names)
:to-have-same-items-as (list persp1-name "X")))
(it "deletes a live workspace"
(+workspace-delete persp2-name)
(expect (+workspace-list-names) :not :to-contain persp2-name)))
(describe "command"
(describe "close-window-or-workspace"
(before-each
(+workspace-switch persp2-name)
(split-window)
(expect (length (doom-visible-windows)) :to-be 2))
(it "kills window if more than one window"
(quiet! (+workspace/close-window-or-workspace))
(expect (length (doom-visible-windows)) :to-be 1))
(it "kills workspace on last window"
(quiet! (+workspace/close-window-or-workspace)
(+workspace/close-window-or-workspace))
(expect (+workspace-current-name) :to-equal persp1-name)))
(describe "rename"
(it "renames the current workspace"
(quiet! (+workspace/rename "X"))
(expect (+workspace-current-name) :to-equal "X")))))