mirror of
https://github.com/doomemacs/doomemacs.git
synced 2025-12-07 19:10:28 -08:00
349 lines
12 KiB
EmacsLisp
349 lines
12 KiB
EmacsLisp
;;; ui/workspaces/autoload/workspaces.el -*- lexical-binding: t; -*-
|
|
|
|
|
|
;;
|
|
;;; Public
|
|
|
|
;;;###autoload
|
|
(defmacro with-current-workspace (ws &rest body)
|
|
"Evaluate BODY while WS is the active workspace."
|
|
(declare (indent defun))
|
|
`(let ((--tab-- ,ws)
|
|
(--last-tab-- (+workspaces-current-name)))
|
|
(tab-bar-select-tab-by-name (alist-get 'name --tab--))
|
|
(unwind-protect
|
|
(progn
|
|
,@body)
|
|
(tab-bar-select-tab-by-name --last-tab--))))
|
|
|
|
;;;###autoload
|
|
(defmacro with-workspace-buffers (&rest body)
|
|
"Evaluate BODY with the buffer list scoped to the current workspace."
|
|
`(if (bound-and-true-p tabspaces-mode)
|
|
(letf! ((#'buffer-list #'tabspaces--buffer-list))
|
|
,@body)
|
|
,@body))
|
|
|
|
;;;###autoload
|
|
(cl-defun +workspaces-list (&optional (frames t))
|
|
"Return all open tabs in FRAMES (defaults to current frame)."
|
|
(let (workspaces)
|
|
(dolist (fr (if (eq frames t) (list (selected-frame)) frames))
|
|
(dolist (tab (tab-bar-tabs fr))
|
|
(push (if (eq (car tab) 'current-tab)
|
|
(tab-bar--tab fr)
|
|
tab)
|
|
workspaces)))
|
|
(nreverse workspaces)))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-buffer-list (&rest tabs)
|
|
"Return a list of buffers associated with TAB."
|
|
(seq-filter
|
|
#'buffer-live-p
|
|
(if tabs
|
|
(cl-remove-duplicates
|
|
(cl-loop for tab in (delq nil tabs)
|
|
for ws = (if (numberp tab) (nth tab (tab-bar-tabs)) tab)
|
|
if (eq (car-safe ws) 'current-tab)
|
|
append (append (frame-parameter nil 'buffer-list)
|
|
(frame-parameter nil 'bured-buffer-list))
|
|
else
|
|
append (append (alist-get 'wc-bl (cdr ws))
|
|
(alist-get 'wc-bbl (cdr ws))))
|
|
:test #'eq)
|
|
(append (frame-parameter nil 'buffer-list)
|
|
(frame-parameter nil 'buried-buffer-list)
|
|
nil))))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-other-buffer-list ()
|
|
"Return a list of buffers in all other workspaces except the current one."
|
|
(let ((buffers (buffer-list)))
|
|
(dolist (b (+workspaces-buffer-list))
|
|
(cl-callf2 delete b buffers))
|
|
buffers))
|
|
|
|
;;;###autoload
|
|
(cl-defun +workspaces-contain-buffer-p
|
|
(buffer &optional (tab t) (buffer-list (+workspaces-buffer-list tab)))
|
|
"Return non-nil if BUFFEr is in TAB."
|
|
(memq buffer buffer-list))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-get (name &optional frame noerror?)
|
|
"Return a full tab data structure for NAME in FRAME.
|
|
|
|
NAME can be a string representing a named tab or a number representing it's
|
|
index. If NOERROR? is omitted, throws an error if NAME doesn't exist."
|
|
(if-let* ((idx (if (numberp name)
|
|
name
|
|
(tab-bar--tab-index-by-name name nil frame)))
|
|
(tab (nth idx (funcall tab-bar-tabs-function frame))))
|
|
(if (eq (car tab) 'current-tab)
|
|
(let (tab-bar-select-restore-context)
|
|
(tab-bar--tab frame))
|
|
tab)
|
|
(unless noerror?
|
|
(user-error "No workspace found: %s" name))))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-get-by-id (id)
|
|
"Return the tab by unique ID."
|
|
(catch 'result
|
|
(dolist (fr (frame-list))
|
|
(dolist (tab (tab-bar-tabs fr))
|
|
(when (equal id (alist-get 'id tab))
|
|
(throw 'result tab))))))
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces-exists-p #'tab-bar--tab-index-by-name)
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-current ()
|
|
"Return the current workspace."
|
|
(tab-bar--current-tab-find (tab-bar-tabs)))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-current-name ()
|
|
"Return the name of the current workspace."
|
|
(alist-get 'name (+workspaces-current)))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-switch (name &optional required?)
|
|
"Switch to a workspace named NAME or create it.
|
|
|
|
If REQUIRED? is non-nil, throw an error instead of auto-creating a non-existent
|
|
workspace."
|
|
(let ((workspaces (mapcar (lambda (tab)
|
|
(alist-get 'name tab))
|
|
(tab-bar-tabs))))
|
|
(unless (member name workspaces)
|
|
(when required?
|
|
(user-error "No workspace: %s" name)))
|
|
(tab-bar-select-tab-by-name name)))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-add-buffer-to-tab (buffer tab)
|
|
"Add BUFFER to TAB."
|
|
(cl-check-type buffer buffer)
|
|
(let ((tab-names (mapcar
|
|
(lambda (tab) (alist-get 'name tab))
|
|
(funcall tab-bar-tabs-function))))
|
|
;; add buffer to default tabspace
|
|
(tab-bar-select-tab-by-name (alist-get 'name tab))
|
|
(display-buffer buffer)
|
|
(switch-to-buffer buffer t nil)
|
|
(if (one-window-p t)
|
|
(previous-buffer)
|
|
(delete-window))
|
|
(tab-bar-switch-to-recent-tab)))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-remove-buffer-from-tab (buffer &optional tab)
|
|
"Remove BUFFER from TAB."
|
|
(cl-check-type buffer buffer)
|
|
(if tab
|
|
(with-current-workspace tab
|
|
(tabspaces-remove-buffer buffer))
|
|
(tabspaces-remove-buffer buffer)))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-parameter (workspace param &optional default)
|
|
"Return a WORKSPACE's PARAM, otherwise DEFAULT."
|
|
(if-let* ((table (gethash (alist-get 'id (or workspace (+workspaces-current)))
|
|
+workspaces--parameters)))
|
|
(gethash param table default)
|
|
default))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-parameter-set (workspace param val)
|
|
"Set a WORKSPACE's PARAM to VAL"
|
|
(let* ((workspace (or workspace (+workspaces-current)))
|
|
(id (or (alist-get 'id workspace)
|
|
(error "Workspace has no id attribute: %S" workspace))))
|
|
(puthash param val (or (gethash id +workspaces--parameters)
|
|
(error "Invalid workspace ID: %s" id)))))
|
|
|
|
|
|
;;
|
|
;;; Hooks
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-init-parameters-h (tab)
|
|
"Initialize the current workspace's parameters."
|
|
(puthash (alist-get 'id tab) (make-hash-table :test 'eq)
|
|
+workspaces--parameters))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces-cleanup-parameters-h (_tab _last-tab?)
|
|
"Cleanup workspace parameters for forgotten workspaces."
|
|
(dolist (id (hash-table-keys +workspaces--parameters))
|
|
(or (cl-loop for tab in tab-bar-closed-tabs
|
|
for tid = (map-nested-elt tab '(tab id))
|
|
unless (eq (gethash tid +workspaces--parameters t) t)
|
|
return t)
|
|
(remhash id +workspaces--parameters))))
|
|
|
|
|
|
;;
|
|
;;; Interactive commands
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/new #'tab-bar-new-tab)
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/new-named #'tabspaces-switch-or-create-workspace)
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/save-session #'tabspaces-save-session)
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/restore-last-session #'tabspaces-restore-session)
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/kill-other #'tab-bar-close-other-tabs)
|
|
|
|
;;;###autoload
|
|
(defun +workspaces/kill (tab-index)
|
|
"Kill workspace a TAB-INDEX (zero-based) and kill its exclusive buffers."
|
|
(interactive (list (tab-bar--current-tab-index)))
|
|
(unless tab-index
|
|
(user-error "No tab number specified"))
|
|
(let* ((current-idx (tab-bar--current-tab-index))
|
|
(tab-idx (or tab-index current-idx))
|
|
(tab-buffers (+workspaces-buffer-list tab-idx))
|
|
(other-buffers
|
|
(delete-dups
|
|
(cl-loop for ws in (tab-bar-tabs)
|
|
unless (eq (car ws) 'current-tab)
|
|
append (+workspaces-buffer-list ws)))))
|
|
(unwind-protect
|
|
(cl-loop for b in tab-buffers
|
|
unless (member b other-buffers) ; only kill if not open elsewhere
|
|
when (buffer-live-p b)
|
|
do (kill-buffer b))
|
|
(tab-bar-close-tab (1+ tab-idx)))))
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/rename #'tab-bar-rename-tab)
|
|
|
|
;;;###autoload
|
|
(defun +workspaces/kill-session ()
|
|
"Delete the current session, all workspaces, windows and their buffers."
|
|
(interactive)
|
|
(tab-bar-close-other-tabs)
|
|
(doom/kill-all-buffers (buffer-list))
|
|
(tabspaces-reset-buffer-list)
|
|
(switch-to-buffer (doom-fallback-buffer)))
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/undo-close #'tab-bar-undo-close-tab)
|
|
|
|
;;;###autoload
|
|
(defun +workspaces/switch-to (index-or-name)
|
|
"Switch to a workspace at a given INDEX-OR-NAME.
|
|
A negative number will start from the end of the workspace list."
|
|
(interactive
|
|
(list (or current-prefix-arg
|
|
(completing-read "Switch to workspace: " (tabspaces--list-tabspaces)))))
|
|
(if (numberp index-or-name)
|
|
(tab-bar-select-tab (1+ index-or-name))
|
|
(tabspaces-switch-or-create-workspace
|
|
(or (cl-loop for tab in (tab-bar-tabs)
|
|
for name = (alist-get 'name tab)
|
|
if (equal name index-or-name)
|
|
return name)
|
|
(user-error "No workspace with name: %s" name)))))
|
|
|
|
;;;###autoload
|
|
(dotimes (i 9)
|
|
(defalias (intern (format "+workspaces/switch-to-%d" i))
|
|
(cmd! (+workspaces/switch-to i))))
|
|
|
|
;;;###autoload
|
|
(defalias '+workspaces/switch-to-final #'tab-bar-switch-to-last-tab)
|
|
|
|
;;;###autoload
|
|
(defun +workspaces/close-window-or-workspace ()
|
|
"Close the selected window, tab, or frame.
|
|
|
|
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)
|
|
(cond ((or (window-parent)
|
|
(window-dedicated-p)
|
|
(not (bound-and-true-p tabspaces-mode)))
|
|
(funcall (if (featurep 'evil) #'evil-window-delete #'delete-window)))
|
|
((cdr (tab-bar-tabs))
|
|
(call-interactively #'+workspaces/kill))
|
|
((user-error "Can't delete last workspace"))))
|
|
|
|
;;;###autoload
|
|
(defun +workspaces/open-in-project (project &optional force?)
|
|
"Open an existing or new workspace for PROJECT.
|
|
|
|
Afterwards, executes `+workspaces-switch-project-function', if set.
|
|
|
|
If FORCE?, always create a workspace, even if it already exists."
|
|
(interactive
|
|
(list (if-let* ((projects (projectile-relevant-known-projects)))
|
|
(projectile-completing-read "Switch to project: " projects)
|
|
(user-error "There are no known projects"))
|
|
current-prefix-arg))
|
|
(let* ((project (expand-file-name project))
|
|
(existing-tab-names (tabspaces--list-tabspaces))
|
|
(original-tab-name (or (cdr (assoc project tabspaces-project-tab-map))
|
|
(tabspaces-generate-descriptive-tab-name project existing-tab-names)))
|
|
(tab-name original-tab-name)
|
|
(session (tabspaces--get-project-session-file-for-restore project))
|
|
(project-directory project) ; Use the full path as the project directory
|
|
(project-exists (cl-member project projectile-known-projects :test #'file-equal-p))
|
|
(create-new-tab (or force? (not (member tab-name existing-tab-names)))))
|
|
|
|
(with-current-buffer (doom-fallback-buffer)
|
|
(setq-local default-directory project-directory))
|
|
|
|
(cond
|
|
;; If there is no tab nor project, create both
|
|
((not project-exists)
|
|
(message "Creating new workspace for project...")
|
|
(tab-bar-new-tab)
|
|
(tab-bar-rename-tab tab-name)
|
|
(projectile-add-known-project project-directory)
|
|
(switch-to-buffer (doom-fallback-buffer)))
|
|
|
|
;; If project and tab exist, but we want a new tab
|
|
((and project-exists
|
|
(member tab-name existing-tab-names)
|
|
create-new-tab)
|
|
(message "Creating new workspace for known project...")
|
|
(let ((new-tab-name (generate-unique-numbered-tab-name tab-name existing-tab-names)))
|
|
(tab-bar-new-tab)
|
|
(tab-bar-rename-tab new-tab-name)
|
|
(setq tab-name new-tab-name)
|
|
(switch-to-buffer (doom-fallback-buffer))))
|
|
|
|
;; If project and tab exist, switch to it
|
|
((and project-exists
|
|
(member tab-name existing-tab-names))
|
|
(message "Switching to existing project workspace...")
|
|
(tab-bar-switch-to-tab tab-name))
|
|
|
|
;; If project exists, but no corresponding tab, open a new tab
|
|
(project-exists
|
|
(message "Creating new workspace for existing project...")
|
|
(tab-bar-new-tab)
|
|
(tab-bar-rename-tab tab-name)
|
|
(if (file-exists-p session)
|
|
(tabspaces-restore-session session)
|
|
(switch-to-buffer (doom-fallback-buffer))))
|
|
|
|
((user-error "Failed to find the project or create a workspace")))
|
|
|
|
;; Update tabspaces-project-tab-map (only for the main tab, not numbered duplicates)
|
|
(unless (string-match-p "<[0-9]+>$" tab-name)
|
|
(setf (alist-get project-directory tabspaces-project-tab-map nil nil #'equal)
|
|
tab-name))))
|
|
|
|
;;; workspaces.el ends here
|