💥 Remove :feature category

:feature was a "catch-all" category. Many of its modules fit better in
other categories, so they've been moved:

- feature/debugger -> tools/debugger
- feature/evil -> editor/evil
- feature/eval -> tools/eval
- feature/lookup -> tools/lookup
- feature/snippets -> editor/snippets
- feature/file-templates -> editor/file-templates
- feature/workspaces -> ui/workspaces

More potential changes in the future:

- A new :term category for terminal emulation modules (eshell, term and
  vterm).
- A new :os category for modules dedicated to os-specific functionality.
  The :tools macos module would fit here, but so would modules for nixos
  and arch.
- A new :services category for web-service integration, like wakatime,
  twitter, elfeed, gist and pastebin services.
This commit is contained in:
Henrik Lissner 2019-04-21 19:59:44 -04:00
parent 52eed893fe
commit 77e4cc4d58
No known key found for this signature in database
GPG key ID: 5F6C0EA160557395
193 changed files with 304 additions and 303 deletions

View file

@ -0,0 +1,89 @@
#+TITLE: :ui workspaces
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
* Table of Contents :TOC:
- [[#install][Install]]
- [[#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]]
* Install
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
[[../../private/default/+bindings.el][private/default/+bindings.el]]), and corresponding ex commands (if any -- defined
in [[../../private/default/+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 |
| ~doom/quicksave-load~ | =SPC TAB L= / =:sl[oad]= | Replace current session with a saved one |
| ~+workspace/save~ | =SPC TAB s= | Save the current workspace to a file |
| ~doom/quicksave-save~ | =SPC TAB S= / =:ss[ave]= | Save current session |
| ~+workspace/switch-to~ | =SPC TAB .= | Switch to an open workspace |
| ~+workspace/switch-left~ | =SPC TAB [= / =[ w= / =gT= | Switch to previous workspace |
| ~+workspace/switch-right~ | =SPC TAB [= / =] w= / =gt= | Switch to next 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 "feature/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 "feature/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 "feature/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 "feature/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 "feature/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 "feature/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 "feature/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,540 @@
;;; 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')."
(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."
(cdr persp-names-cache))
;;;###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))
(cl-loop for buf in (buffer-list)
if (+workspace-contains-buffer-p buf persp)
collect buf)))
;;;###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))
(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 current-name)
current-name))))
(condition-case-unless-debug ex
(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"))
((> (length workspaces) 1)
(+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)))
(+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))
;;;###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
(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)
(unless (member index names)
(error "No workspace named %s" index))
(+workspace-switch index))
(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
(defun +workspace/switch-to-last ()
"Switch to the last workspace."
(interactive)
(+workspace/switch-to (car (last (+workspace-list-names)))))
;;;###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))))
(t (+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)
(minibuffer-message "%s" (+workspace--tabline))))
;;
;; Hooks
;;;###autoload
(defun +workspaces|delete-associated-workspace (&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|cleanup-unassociated-buffers ()
"Kill leftover buffers that are unassociated with any perspective."
(when persp-mode
(cl-loop for buf in (buffer-list)
unless (or (persp--buffer-in-persps buf)
(get-buffer-window buf))
if (kill-buffer buf)
sum 1)))
;;;###autoload
(defun +workspaces|associate-frame (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 ()
"A `projectile-switch-project-action' that sets the project directory for
`+workspaces|switch-to-project'."
(setq +workspaces--project-dir default-directory))
;;;###autoload
(defun +workspaces|switch-to-project (&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-p
(persp
(let ((project-name (doom-project-name +workspaces--project-dir)))
(or (setq persp-p (+workspace-get project-name t))
(+workspace-new project-name))))
(new-name (persp-name persp)))
(+workspace-switch new-name)
(unless persp-p
(switch-to-buffer (doom-fallback-buffer)))
(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)))
(unless current-prefix-arg
(funcall +workspaces-switch-project-function +workspaces--project-dir)))
(setq +workspaces--project-dir nil))))
;;
;; Advice
;;;###autoload
(defun +workspaces*autosave-real-buffers (orig-fn &rest args)
"Don't autosave if no real buffers are open."
(when (doom-real-buffer-list)
(apply orig-fn args))
t)
;;;###autoload
(defun +workspaces*switch-project-by-name (orig-fn &rest args)
"Switch to a project and prompt for a file to open.
Ensures the scratch (or dashboard) buffers are CDed into the project's root."
(when persp-mode
(+workspace-switch (car args) t)
(with-current-buffer (switch-to-buffer (doom-fallback-buffer))
(setq default-directory (car args))))
(apply orig-fn args))

View file

@ -0,0 +1,196 @@
;;; 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'.")
;;
;; Packages
(def-package! persp-mode
:commands (persp-switch-to-buffer)
:init
(defun +workspaces|init ()
;; Remove default buffer predicate so persp-mode can put in its own
(setq default-frame-alist
(delq (assq 'buffer-predicate default-frame-alist)
default-frame-alist))
(add-hook 'after-make-frame-functions #'+workspaces|init-frame)
(require 'persp-mode)
(unless (daemonp)
(+workspaces|init-frame (selected-frame))))
(defun +workspaces|init-frame (frame)
"Ensure a main workspace exists and is switched to, if FRAME isn't in any
workspace. Also ensures that the *Warnings* buffer will be visible in main.
Uses `+workspaces-main' to determine the name of the main workspace."
(unless persp-mode
(persp-mode +1)
(unless noninteractive
(let (persp-before-switch-functions)
(with-selected-frame frame
;; 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 aren't auto-loading the last session
(when (and (string= (safe-persp-name (get-current-persp)) persp-nil-name)
(= persp-auto-resume-time -1))
(persp-frame-switch +workspaces-main frame)
;; We want to know where we are in every new daemon frame
(when (daemonp)
(run-at-time 0.1 nil #'+workspace/display))
;; 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 'doom-init-modules-hook #'+workspaces|init t)
: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)
;; Ensure buffers we've opened/switched to are auto-added to the current
;; perspective
(setq persp-add-buffer-on-find-file t
persp-add-buffer-on-after-change-major-mode t)
(add-hook 'persp-add-buffer-on-after-change-major-mode-filter-functions #'doom-unreal-buffer-p)
(defun +workspaces|init-persp-mode ()
(cond (persp-mode
;; `persp-kill-buffer-query-function' must be 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))
((advice-remove #'doom-buffer-list #'+workspace-buffer-list))))
(add-hook 'persp-mode-hook #'+workspaces|init-persp-mode)
(defun +workspaces|leave-nil-perspective (&rest _)
(when (string= (+workspace-current-name) persp-nil-name)
(+workspace-switch (or (if (+workspace-p +workspace--last) +workspace--last)
(car (+workspace-list-names))
+workspaces-main)
'auto-create)))
(add-hook 'persp-after-load-state-functions #'+workspaces|leave-nil-perspective)
;; 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-delete-window] #'+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
persp-emacsclient-init-frame-behaviour-override #'+workspaces|associate-frame)
(add-hook 'delete-frame-functions #'+workspaces|delete-associated-workspace)
;; per-project workspaces, but reuse current workspace if empty
(setq projectile-switch-project-action #'+workspaces|set-project-action
counsel-projectile-switch-project-action
'(1 ("o" +workspaces|switch-to-project "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)
;; In some scenarios, persp-mode throws error when Emacs tries to die,
;; preventing its death and trapping us in Emacs.
(defun +workspaces*ignore-errors-on-kill-emacs (orig-fn)
(ignore-errors (funcall orig-fn)))
(advice-add #'persp-kill-emacs-h :around #'+workspaces*ignore-errors-on-kill-emacs)
;;
;; 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))))
: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)))
(defun +workspaces|reload-indirect-buffers (&rest _)
(dolist (ibc +workspaces--indirect-buffers-to-restore)
(let* ((nbn (car ibc))
(bbn (cdr ibc))
(bb (get-buffer bbn)))
(when bb
(when (get-buffer nbn)
(setq nbn (generate-new-buffer-name nbn)))
(make-indirect-buffer bb nbn t))))
(setq +workspaces--indirect-buffers-to-restore nil))
(add-hook 'persp-after-load-state-functions #'+workspaces|reload-indirect-buffers))

View file

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

View file

@ -0,0 +1,123 @@
;; -*- 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)
(before-all
(delete-other-windows)
(require! :feature workspaces)
(require 'persp-mode))
(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")))))