project-x.el (8980B)
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 <karthik.chikmagalur@gmail.com> 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 <http://www.gnu.org/licenses/>. 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 ;; <karthik.chikmagalur@gmail.com> 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