├── README.org ├── project-switching-2.gif ├── project-switching.gif └── project-x.el /README.org: -------------------------------------------------------------------------------- 1 | #+title: Project-X 2 | #+author: Karthik Chikmagalur 3 | #+STARTUP: nofold 4 | 5 | - /New in v 0.1.6:/ =project-x-local-identifier= can now be a list of file names 6 | - /New in v 0.1.5:/ Autosave the state of the active project periodically. 7 | 8 | Project-X adds a couple of convenience features for Emacs' =project.el= library. 9 | 10 | - Recognize any directory with a =.project= file as a project (name customizable). Also works if any parent directory has this file. 11 | 12 | - Save and restore project files and window configurations across sessions. Project-X will load all /saved project/ files and directories (as =dired= buffers) and try to recreate the window configuration at the time of saving. Project-X can also auto-save your current project's window configuration regularly. 13 | 14 | + Enhanced project switching: When you switch projects you switch window configurations too, (re)opening files as needed. 15 | #+ATTR_ORG: :width 500 16 | #+ATTR_HTML: :width 1000px 17 | [[file:project-switching.gif]] 18 | 19 | + Restoring project state in a new Emacs instance. Note the new option ("Restore windows") in the project dispatch menu: 20 | #+ATTR_ORG: :width 500 21 | #+ATTR_HTML: :width 1000px 22 | [[file:project-switching-2.gif]] 23 | 24 | 25 | While Emacs has many built-in features to save and restore state (bookmarks, desktop, window-configurations and more) none of them allow you to bookmark and switch to, across Emacs sessions, a collection of files, buffers and windows together as a unit... or project. Hence project-x. 26 | 27 | More features are planned, but note that Emacs' project library is a young project. As it is developed, some or all of the features in project-x might become obsolete. (In fact, that would be great.) 28 | 29 | ** Setup and customization 30 | Load =project-x.el=, then run =(project-x-mode +1)=. 31 | 32 | OR, with =use-package=: 33 | #+begin_src emacs-lisp 34 | (use-package project-x 35 | :load-path "~/path/to/project-x/" 36 | :after project 37 | :config 38 | (setq project-x-save-interval 600) ;Save project state every 10 min 39 | (project-x-mode 1)) 40 | #+end_src 41 | 42 | OR, if you do not want to use the (opinionated) minor-mode =project-x-mode=, 43 | 44 | #+begin_src emacs-lisp 45 | (use-package project-x 46 | :load-path "~/path/to/project-x/" 47 | :after project 48 | :config 49 | (add-hook 'project-find-functions 'project-x-try-local 90) 50 | (add-hook 'kill-emacs-hook 'project-x--window-state-write) 51 | (add-to-list 'project-switch-commands 52 | '(?j "Restore windows" project-x-windows) t) 53 | :bind (("C-x p w" . project-x-window-state-save) 54 | ("C-x p j" . project-x-window-state-load))) 55 | #+end_src 56 | 57 | OR, to automatically reload the windows when switching to a project, 58 | 59 | #+begin_src emacs-lisp 60 | (use-package project-x 61 | :load-path "~/path/to/project-x/" 62 | :after project 63 | :config 64 | (add-hook 'project-find-functions 'project-x-try-local 90) 65 | (add-hook 'kill-emacs-hook 'project-x--window-state-write) 66 | (setq project-switch-commands #'project-x-windows) 67 | :bind (("C-x p w" . project-x-window-state-save) 68 | ("C-x p j" . project-x-window-state-load)))) 69 | #+end_src 70 | 71 | There are three customization options right now: 72 | - =project-x-window-list-file=: File to store project window configurations. Defaults to your emacs config directory. 73 | - =project-x-local-identifier=: String matched against file names to decide if a directory (or some parent thereof) is a project. Defaults to =.project=. You can also supply a list of strings instead. For example, node projects use a file named =package.json= to denote the root of a project, Elixir uses =mix.exs= and Julia uses =Project.toml=. You can use project-x to identify all of the above as project directories by setting 74 | #+BEGIN_SRC emacs-lisp 75 | (setq project-x-local-identifier '("package.json" "mix.exs" "Project.toml" ".project")) 76 | #+END_SRC 77 | - =project-x-save-interval=: Number of seconds between autosaves of the current project window configuration. Defaults to nil (autosave disabled). This requires =project-x-mode= to be turned on. 78 | 79 | ** Usage 80 | 81 | *** Session management 82 | The =project-x-mode= minor-mode is provided for convenience. It enables these features: 83 | 84 | | Keybinding | Command | Effect | 85 | |-------------+-----------------------------+-----------------------------------------| 86 | | =C-x p w= | =project-x-window-state-save= | Save your current project session | 87 | | =C-x p j= | =project-x-window-state-load= | Load session from a project | 88 | | =C-x p p j= | =project-x-windows= | Load session from project dispatch menu | 89 | 90 | Save a project session with =C-x p w= and you should be able to load it any time across Emacs sessions. 91 | 92 | You can go back to your previous window configuration with =winner-undo=. 93 | 94 | *** 'Local' projects 95 | To recognize 'local' projects with a ".project" file somewhere in the path, turn on =project-x-mode= OR run 96 | #+begin_src emacs-lisp 97 | (add-hook 'project-find-functions 'project-x-try-local 90) 98 | #+end_src 99 | 100 | All =project.el= features should work as expected. 101 | 102 | ** Limitations 103 | :PROPERTIES: 104 | :ID: c1326cad-5dd9-4789-8e5e-74f5b012b546 105 | :END: 106 | - This is currently limited to storing only the current frame configuration. 107 | 108 | - The only state saved is your project files, project =dired= buffers and the current frame configuration. No minor modes, registers or special buffers (shells, help buffers etc) are recorded. For complete recall you can look into the Desktop library for Emacs. 109 | 110 | - If you use multiple Emacs instances the project states saved to disk can get overwritten. 111 | 112 | - /Implemented in v0.1.5/: +Your project state needs to be manually saved to be restored. I'm looking into auto-saving the state any time a project buffer is opened or window displayed, or when switching projects.+ 113 | 114 | ** Alternatives 115 | *** How does this compare with... 116 | **** ...just using window-configurations? 117 | Package-X does use window configurations under the hood. However, it has a few advantages: 118 | - Your project state remains persistent across sessions, and any files or dired buffers are reopened if necessary. 119 | - Your project state is associated with the project instead of with registers or data structures from other packages. 120 | 121 | **** ...Tabs/Workspaces/persp etc? 122 | If you think in terms of projects, you may find it more convenient to use =project-x= through the project dispatch menu (=C-x p p=) to continue working from where you left off. This is a helper library to define and handle projects, not an overarching modification to your Emacs usage pattern. 123 | 124 | **** ...Burly? 125 | [[https://github.com/alphapapa/burly.el][Burly]] provides a more universal method to save and restore frames and window configurations as Emacs bookmarks (thus persistent across sessions) that is not limited to the project metaphor. If you are looking for this feature but in a more general Emacs context you might be better served by it. 126 | 127 | **** ...the Desktop library? 128 | See [[id:c1326cad-5dd9-4789-8e5e-74f5b012b546][Limitations]]. Desktop restores your entire session, this is a much diminished version of the same for individual projects. But desktop being an all-or-nothing affair (without extensive customization) is also a disadvantage. Here each project gets its own desktop state. 129 | 130 | **** ..Projectile? 131 | =project.el= is still very basic in its features and =project-x= is a small addition to it. However, as far as I know Projectile does not offer the ability to save and restore your project sessions (including window configurations). 132 | 133 | ** Planned features 134 | - [X] Autosave the current project configuration when opening project files or switching projects. 135 | - Save the window configuration across frames and tabs instead of only the current one. 136 | 137 | ** Technical notes 138 | Since this library uses the built in Emacs API to store the state, it is very compact. The machinery to maintain and recreate project states is only four short =defun='s. Likewise implementing a 'local' project backend is fewer than ten lines of code. 139 | 140 | =project-x= creates entries containing project state information for a project in the data structure it uses (an associative list) only when you save its state. Thus it should remain fast even if you have thousands of projects so long as you actively work on a few at a time. If you experience slow down please raise an issue and I will consider reimplementing it as a hash table. 141 | 142 | 143 | -------------------------------------------------------------------------------- /project-switching-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karthink/project-x/eafc9828f54dddd594887bb28a7249cf1584230c/project-switching-2.gif -------------------------------------------------------------------------------- /project-switching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karthink/project-x/eafc9828f54dddd594887bb28a7249cf1584230c/project-switching.gif -------------------------------------------------------------------------------- /project-x.el: -------------------------------------------------------------------------------- 1 | ;;; project-x.el --- Extra convenience features for project.el -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2021 Karthik Chikmagalur 4 | 5 | ;; Author: Karthik Chikmagalur 6 | ;; URL: https://github.com/karthink/project-x 7 | ;; Version: 0.1.5 8 | ;; Package-Requires: ((emacs "27.1")) 9 | 10 | ;; This file is NOT part of GNU Emacs. 11 | 12 | ;; This file is free software; you can redistribute it and/or modify 13 | ;; it under the terms of the GNU General Public License as published by 14 | ;; the Free Software Foundation; either version 3, or (at your option) 15 | ;; any later version. 16 | ;; 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | ;; 22 | ;; For a full copy of the GNU General Public License 23 | ;; see . 24 | ;; 25 | ;;; Commentary: 26 | ;; 27 | ;; project-x provides some convenience features for project.el: 28 | ;; - Recognize any directory with a `.project' file as a project. 29 | ;; - Save and restore project files and window configurations across sessions 30 | ;; 31 | ;; COMMANDS: 32 | ;; 33 | ;; project-x-window-state-save : Save the window configuration of currently open project buffers 34 | ;; project-x-window-state-load : Load a previously saved project window configuration 35 | ;; 36 | ;; CUSTOMIZATION: 37 | ;; 38 | ;; `project-x-window-list-file': File to store project window configurations 39 | ;; `project-x-local-identifier': String matched against file names to decide if a 40 | ;; directory is a project 41 | ;; `project-x-save-interval': Interval in seconds between autosaves of the 42 | ;; current project. 43 | ;; 44 | ;; by Karthik Chikmagalur 45 | ;; 46 | 47 | ;;; Code: 48 | 49 | (require 'project) 50 | (eval-when-compile (require 'subr-x)) 51 | (eval-when-compile (require 'seq)) 52 | (defvar project-prefix-map) 53 | (defvar project-switch-commands) 54 | (declare-function project-prompt-project-dir "project") 55 | (declare-function project--buffer-list "project") 56 | (declare-function project-buffers "project") 57 | 58 | (defgroup project-x nil 59 | "Convenience features for the Project library." 60 | :group 'project) 61 | 62 | ;; Persistent project sessions 63 | ;; ------------------------------------- 64 | (defcustom project-x-window-list-file 65 | (locate-user-emacs-file "project-window-list") 66 | "File in which to save project window configurations by default." 67 | :type 'file 68 | :group 'project-x) 69 | 70 | (defcustom project-x-save-interval nil 71 | "Saves the current project state with this interval. 72 | 73 | When set to nil auto-save is disabled." 74 | :type '(choice (const :tag "Disabled" nil) 75 | integer) 76 | :group 'project-x) 77 | 78 | (defvar project-x-window-alist nil 79 | "Alist of window configurations associated with known projects.") 80 | 81 | (defvar project-x-save-timer nil 82 | "Timer for auto-saving project state.") 83 | 84 | (defun project-x--window-state-write (&optional file) 85 | "Write project window states to `project-x-window-list-file'. 86 | If FILE is specified, write to it instead." 87 | (when project-x-window-alist 88 | (require 'pp) 89 | (unless file (make-directory (file-name-directory project-x-window-list-file) t)) 90 | (with-temp-file (or file project-x-window-list-file) 91 | (insert ";;; -*- lisp-data -*-\n") 92 | (let ((print-level nil) (print-length nil)) 93 | (pp project-x-window-alist (current-buffer)))) 94 | (message (format "Wrote project window state to %s" project-x-window-list-file)))) 95 | 96 | (defun project-x--window-state-read (&optional file) 97 | "Read project window states from `project-x-window-list-file'. 98 | If FILE is specified, read from it instead." 99 | (and (or file 100 | (file-exists-p project-x-window-list-file)) 101 | (with-temp-buffer 102 | (insert-file-contents (or file project-x-window-list-file)) 103 | (condition-case nil 104 | (if-let ((win-state-alist (read (current-buffer)))) 105 | (setq project-x-window-alist win-state-alist) 106 | (message (format "Could not read %s" project-x-window-list-file))) 107 | (error (message (format "Could not read %s" project-x-window-list-file))))))) 108 | 109 | (defun project-x-window-state-save (&optional arg) 110 | "Save current window state of project. 111 | With optional prefix argument ARG, query for project." 112 | (interactive "P") 113 | (when-let* ((dir (cond (arg (project-prompt-project-dir)) 114 | ((project-current) 115 | (project-root (project-current))))) 116 | (default-directory dir)) 117 | (unless project-x-window-alist (project-x--window-state-read)) 118 | (let ((file-list)) 119 | ;; Collect file-list of all the open project buffers 120 | (dolist (buf 121 | (funcall (if (fboundp 'project--buffers-list) 122 | #'project--buffers-list 123 | #'project-buffers) 124 | (project-current)) 125 | file-list) 126 | (if-let ((file-name (or (buffer-file-name buf) 127 | (with-current-buffer buf 128 | (and (derived-mode-p 'dired-mode) 129 | dired-directory))))) 130 | (push file-name file-list))) 131 | (setf (alist-get dir project-x-window-alist nil nil 'equal) 132 | (list (cons 'files file-list) 133 | (cons 'windows (window-state-get nil t))))) 134 | (message (format "Saved project state for %s" dir)))) 135 | 136 | (defun project-x-window-state-load (dir) 137 | "Load the saved window state for project with directory DIR. 138 | If DIR is unspecified query the user for a project instead." 139 | (interactive (list (project-prompt-project-dir))) 140 | (unless project-x-window-alist (project-x--window-state-read)) 141 | (if-let* ((project-x-window-alist) 142 | (project-state (alist-get dir project-x-window-alist 143 | nil nil 'equal))) 144 | (let ((file-list (alist-get 'files project-state)) 145 | (window-config (alist-get 'windows project-state))) 146 | (dolist (file-name file-list nil) 147 | (find-file file-name)) 148 | (window-state-put window-config nil 'safe) 149 | (message (format "Restored project state for %s" dir))) 150 | (message (format "No saved window state for project %s" dir)))) 151 | 152 | (defun project-x-windows () 153 | "Restore the last saved window state of the chosen project." 154 | (interactive) 155 | (project-x-window-state-load (project-root (project-current)))) 156 | 157 | ;; Recognize directories as projects by defining a new project backend `local' 158 | ;; ------------------------------------- 159 | (defcustom project-x-local-identifier ".project" 160 | "Filename(s) that identifies a directory as a project. 161 | 162 | You can specify a single filename or a list of names." 163 | :type '(choice (string :tag "Single file") 164 | (repeat (string :tag "Filename"))) 165 | :group 'project-x) 166 | 167 | (cl-defmethod project-root ((project (head local))) 168 | "Return root directory of current PROJECT." 169 | (cdr project)) 170 | 171 | (defun project-x-try-local (dir) 172 | "Determine if DIR is a non-VC project. 173 | DIR must include a .project file to be considered a project." 174 | (if-let ((root (if (listp project-x-local-identifier) 175 | (seq-some (lambda (n) 176 | (locate-dominating-file dir n)) 177 | project-x-local-identifier) 178 | (locate-dominating-file dir project-x-local-identifier)))) 179 | (cons 'local root))) 180 | 181 | ;;;###autoload 182 | (define-minor-mode project-x-mode 183 | "Minor mode to enable extra convenience features for project.el. 184 | When enabled, save and load project window states. 185 | Recognize any directory that contains (or whose parent 186 | contains) a special file as a project." 187 | :global t 188 | :version "0.10" 189 | :lighter "" 190 | :group 'project-x 191 | (if project-x-mode 192 | ;;Turning the mode ON 193 | (progn 194 | (add-hook 'project-find-functions 'project-x-try-local 90) 195 | (add-hook 'kill-emacs-hook 'project-x--window-state-write) 196 | (project-x--window-state-read) 197 | (define-key project-prefix-map (kbd "w") 'project-x-window-state-save) 198 | (define-key project-prefix-map (kbd "j") 'project-x-window-state-load) 199 | (if (listp project-switch-commands) 200 | (add-to-list 'project-switch-commands 201 | '(?j "Restore windows" project-x-windows) t) 202 | (message "`project-switch-commands` is not a list, not adding 'restore windows' command")) 203 | (when project-x-save-interval 204 | (setq project-x-save-timer 205 | (run-with-timer 0 (max project-x-save-interval 5) 206 | #'project-x-window-state-save)))) 207 | (remove-hook 'project-find-functions 'project-x-try-local 90) 208 | (remove-hook 'kill-emacs-hook 'project-x--window-state-write) 209 | (define-key project-prefix-map (kbd "w") nil) 210 | (define-key project-prefix-map (kbd "j") nil) 211 | (when (listp project-switch-commands) 212 | (delete '(?j "Restore windows" project-x-windows) project-switch-commands)) 213 | (when (timerp project-x-save-timer) 214 | (cancel-timer project-x-save-timer)))) 215 | 216 | (provide 'project-x) 217 | ;;; project-x.el ends here 218 | --------------------------------------------------------------------------------