├── .gitignore ├── .gitmodules ├── Cask ├── Makefile ├── README.md ├── lib ├── project-persist-pkg.el └── project-persist.el ├── project-persist.el └── test └── project-persist-test.el /.gitignore: -------------------------------------------------------------------------------- 1 | .cask 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "el.mk"] 2 | path = el.mk 3 | url = https://github.com/rdallasgray/el.mk.git 4 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source melpa) 2 | 3 | (package "project-persist" "@VERSION" "A minor mode to allow loading and saving of project settings.") 4 | 5 | (development 6 | (depends-on "ert-runner")) 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_LCNAME=project-persist 2 | include el.mk/el.mk 3 | 4 | .PHONY : test 5 | 6 | test: 7 | @cask exec ert-runner 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is project-persist? 2 | Project-persist is a simple, extensible Emacs package to allow persistence of a 3 | list of projects with relevant settings. 4 | 5 | # What does it do? 6 | It allows you to create, open, save, close and delete simple projects based on 7 | root directories. 8 | 9 | # Is that it? 10 | Pretty much. It also provides hooks around each of these functions so that you 11 | could, for example, load and save an Emacs desktop in tandem with a project, 12 | create and save a tags file, or load another project-management solution such as 13 | [Projectile](https://github.com/bbatsov/projectile). 14 | 15 | By default, only a project's name and root directory are saved, but you can 16 | easily add other settings like this: 17 | 18 | ```lisp 19 | (add-to-list 'project-persist-additional-settings 20 | '(my-setting . (lambda () (read-from-minibuffer "My setting: ")))) 21 | ``` 22 | 23 | Each element of the list is a cons cell with car a symbol naming the new setting 24 | and cdr a function to obtain the value of the setting. The function will be 25 | called during project creation and the setting's value saved as normal. 26 | 27 | The setting can be retrieved once a project is loaded by invoking: 28 | 29 | ```lisp 30 | (pp/settings-get 'my-setting) 31 | ``` 32 | 33 | Project-persist is intentionally lightweight, in the spirit of Emacs, so that it 34 | can be used to build a more complex project-management infrastructure tailored 35 | to your needs. Other packages, like the aforementioned Projectile, handle things 36 | like searching within a project, so there's no need to duplicate such 37 | functionality. 38 | 39 | It can be required and enabled as follows: 40 | 41 | ```lisp 42 | (require 'project-persist) 43 | (project-persist-mode t) 44 | ``` 45 | -------------------------------------------------------------------------------- /lib/project-persist-pkg.el: -------------------------------------------------------------------------------- 1 | (define-package "project-persist" "1.0.1" "A minor mode to allow loading and saving of project settings." 'nil) 2 | -------------------------------------------------------------------------------- /lib/project-persist.el: -------------------------------------------------------------------------------- 1 | ;;; project-persist.el --- A minor mode to allow loading and saving of project settings. 2 | 3 | ;; Copyright (C) 2018 Robert Dallas Gray 4 | 5 | ;; Author: Robert Dallas Gray 6 | ;; URL: https://github.com/rdallasgray/project-persist 7 | ;; Version: 1.0.1 8 | ;; Created: 2012-09-13 9 | ;; Keywords: project, persistence 10 | 11 | ;; This file is NOT part of GNU Emacs. 12 | 13 | ;;; License: 14 | 15 | ;; This program is free software; you can redistribute it and/or modify 16 | ;; it under the terms of the GNU General Public License as published by 17 | ;; the Free Software Foundation; either version 3, or (at your option) 18 | ;; any later version. 19 | ;; 20 | ;; This program is distributed in the hope that it will be useful, 21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | ;; GNU General Public License for more details. 24 | ;; 25 | ;; You should have received a copy of the GNU General Public License 26 | ;; along with GNU Emacs; see the file COPYING. If not, write to the 27 | ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 28 | ;; Boston, MA 02110-1301, USA. 29 | 30 | ;;; Commentary: 31 | ;; 32 | ;; # What is project-persist? 33 | ;; Project-persist is a simple, extensible Emacs package to allow persistence of a 34 | ;; list of projects with relevant settings. 35 | ;; 36 | ;; # What does it do? 37 | ;; It allows you to create, open, save, close and delete simple projects based on 38 | ;; root directories. 39 | ;; 40 | ;; # Is that it? 41 | ;; Pretty much. It also provides hooks around each of these functions so that you 42 | ;; could, for example, load and save an Emacs desktop in tandem with a project, 43 | ;; create and save a tags file, or load another project-management solution such as 44 | ;; [Projectile](https://github.com/bbatsov/projectile). 45 | ;; 46 | ;; By default, only a project's name and root directory are saved, but you can 47 | ;; easily add other settings like this: 48 | ;; 49 | ;; ```lisp 50 | ;; (add-to-list 'project-persist-additional-settings 51 | ;; '(my-setting . (lambda () (read-from-minibuffer "My setting: ")))) 52 | ;; ``` 53 | ;; 54 | ;; Each element of the list is a cons cell with car a symbol naming the new setting 55 | ;; and cdr a function to obtain the value of the setting. The function will be 56 | ;; called during project creation and the setting's value saved as normal. 57 | ;; 58 | ;; The setting can be retrieved once a project is loaded by invoking: 59 | ;; 60 | ;; ```lisp 61 | ;; (pp/settings-get 'my-setting) 62 | ;; ``` 63 | ;; 64 | ;; Project-persist is intentionally lightweight, in the spirit of Emacs, so that it 65 | ;; can be used to build a more complex project-management infrastructure tailored 66 | ;; to your needs. Other packages, like the aforementioned Projectile, handle things 67 | ;; like searching within a project, so there's no need to duplicate such 68 | ;; functionality. 69 | ;; 70 | ;; It can be required and enabled as follows: 71 | ;; 72 | ;; ```lisp 73 | ;; (require 'project-persist) 74 | ;; (project-persist-mode t) 75 | ;; ``` 76 | ;; 77 | ;;; Code: 78 | 79 | ;; Customize options 80 | (defgroup project-persist nil 81 | "Settings related to project-persist, a package to enable simple persistence of project settings." 82 | :group 'tools) 83 | 84 | (defcustom project-persist-settings-dir (concat user-emacs-directory "project-persist") 85 | "The directory in which project-persist will save project settings files." 86 | :type 'directory 87 | :group 'project-persist) 88 | 89 | (defcustom project-persist-keymap-prefix (kbd "C-c P") 90 | "Project-persist keymap prefix." 91 | :type 'sexp 92 | :group 'project-persist) 93 | 94 | (defcustom project-persist-auto-save-global t 95 | "If non-nil, automatically save projects without prompting. 96 | 97 | Can be overridden on a project-basis with 98 | \(project-persist--settings-set 'auto-save VALUE), where VALUE is t or 'prompt 99 | 100 | If the project setting `auto-save' is t or if the value of 101 | variable `project-persist-auto-save-global' is non-nil, save the 102 | project without prompting 103 | 104 | If the project setting `auto-save' is 'prompt, always prompt before saving" 105 | :type 'boolean 106 | :group 'project-persist) 107 | 108 | 109 | ;; Hooks 110 | (defvar project-persist-mode-hook nil 111 | "Run when entering project-persist-mode.") 112 | 113 | (defvar project-persist-before-create-hook nil 114 | "A hook to be run before project-persist creates a project.") 115 | 116 | (defvar project-persist-after-create-hook nil 117 | "A hook to be run after project-persist creates a project.") 118 | 119 | (defvar project-persist-before-save-hook nil 120 | "A hook to be run before project-persist saves a project.") 121 | 122 | (defvar project-persist-after-save-hook nil 123 | "A hook to be run after project-persist saves a project.") 124 | 125 | (defvar project-persist-before-load-hook nil 126 | "A hook to be run before project-persist loads a project.") 127 | 128 | (defvar project-persist-after-load-hook nil 129 | "A hook to be run after project-persist loads a project.") 130 | 131 | (defvar project-persist-before-close-hook nil 132 | "A hook to be run before project-persist closes a project.") 133 | 134 | (defvar project-persist-after-close-hook nil 135 | "A hook to be run after project-persist closes a project.") 136 | 137 | (defvar project-persist-mode-map 138 | (let ((map (make-sparse-keymap))) 139 | (let ((prefix-map (make-sparse-keymap))) 140 | (define-key prefix-map (kbd "f") 'project-persist-find) 141 | (define-key prefix-map (kbd "s") 'project-persist-save) 142 | (define-key prefix-map (kbd "k") 'project-persist-close) 143 | (define-key prefix-map (kbd "d") 'project-persist-delete) 144 | (define-key prefix-map (kbd "n") 'project-persist-create) 145 | (define-key map project-persist-keymap-prefix prefix-map)) 146 | map) 147 | "Keymap for project-persist-mode.") 148 | 149 | 150 | ;; Global variables 151 | (defvar project-persist-current-project-name nil 152 | "The name of the project currently loaded by project-persist.") 153 | 154 | (defvar project-persist-current-project-root-dir nil 155 | "The root directory of the project currently loaded by project-persist.") 156 | 157 | (defvar project-persist-current-project-settings-dir nil 158 | "The directory in which settings for the current project are stored.") 159 | 160 | (defvar project-persist-additional-settings '() 161 | "A list of additional keys to store in the project settings file. 162 | The defaults are 'name and 'root-dir. The format should be a cons cell: 163 | \('key . read-function); e.g. ('name . (lambda () (read-from-buffer \"Project name: \"))).") 164 | 165 | ;; Internal variables 166 | (defvar project-persist--lighter nil 167 | "Modeline lighter for minor mode.") 168 | 169 | (defvar project-persist--project-list-cache '() 170 | "Cached list of projects.") 171 | 172 | (defvar project-persist--project-list-cache-valid nil 173 | "Whether the cached project list is currently valid.") 174 | 175 | (defvar project-persist--settings-file-name "pp-settings.txt" 176 | "Name of the default settings file to write in each project's settings directory.") 177 | 178 | (defvar project-persist--settings-hash (make-hash-table :test 'equal) 179 | "Settings hashtable to be written to the project settings file.") 180 | 181 | ;; Interactive functions 182 | (defun project-persist-find () 183 | "Find and load the given project name." 184 | (interactive) 185 | (project-persist--project-open (project-persist--read-project-name))) 186 | 187 | (defun project-persist-create () 188 | "Create a new project-persist project, giving a project name and root directory." 189 | (interactive) 190 | (project-persist--offer-save-if-open-project) 191 | (let ((root-dir (read-directory-name "Project root directory: "))) 192 | (let ((name 193 | (read-from-minibuffer 194 | "Project name: " 195 | (file-name-nondirectory (directory-file-name root-dir))))) 196 | (condition-case err 197 | (progn 198 | (project-persist--project-setup root-dir name) 199 | (project-persist--project-open name)) 200 | (error (project-persist--signal-error err)))))) 201 | 202 | (defun project-persist-delete () 203 | "Delete the given project name." 204 | (interactive) 205 | (let ((name (project-persist--read-project-name))) 206 | (when (eq name project-persist-current-project-name) 207 | (error "Can't delete the currently open project. Please close the project first.")) 208 | (let ((confirm (yes-or-no-p (format "Are you sure you want to delete project %s?" name)))) 209 | (when confirm 210 | (project-persist--project-destroy name))))) 211 | 212 | (defun project-persist-save () 213 | "Save the project settings and run relevant hooks." 214 | (interactive) 215 | (when (not (project-persist--has-open-project)) (error "No project is currently open.")) 216 | (let ((settings-dir (project-persist--settings-dir-from-name project-persist-current-project-name))) 217 | (project-persist--project-write settings-dir))) 218 | 219 | (defun project-persist-close () 220 | "Close the currently open project." 221 | (interactive) 222 | (when (not (project-persist--has-open-project)) (error "No project is currently open.")) 223 | (project-persist--offer-save-if-open-project) 224 | (project-persist--close-current-project)) 225 | 226 | 227 | ;; Internal functions 228 | (defun project-persist--offer-save-if-open-project () 229 | "Offer to save the open project. 230 | Depending on the value of the variable` project-persist-auto-save-global' 231 | and the project setting `auto-save', save the project without asking." 232 | (when (project-persist--has-open-project) 233 | (let ((auto-save (project-persist--auto-save-value))) 234 | (when (or auto-save (y-or-n-p (format "Save project %s?" project-persist-current-project-name))) 235 | (project-persist-save))))) 236 | 237 | (defun project-persist--auto-save-value () 238 | "Get the auto-save setting; if set locally, use that, otherwise use the global setting." 239 | (let ((local-setting (project-persist--settings-get 'auto-save))) 240 | (if local-setting 241 | (not (eq local-setting 'prompt)) 242 | project-persist-auto-save-global))) 243 | 244 | (defun project-persist--disable-hooks () 245 | "Disable all project-persist hooks (normally on disabling the minor mode)." 246 | (let ((hooks '(project-persist-before-create-hook 247 | project-persist-after-create-hook 248 | project-persist-before-load-hook 249 | project-persist-after-load-hook 250 | project-persist-before-save-hook 251 | project-persist-after-save-hook))) 252 | (mapc (lambda (hook) (set hook nil)) hooks)) 253 | (remove-hook 'kill-emacs-hook 'project-persist--offer-save-if-open-project)) 254 | 255 | (defun project-persist--reset-hashtable () 256 | "Empty the hashtable containing project settings." 257 | (clrhash project-persist--settings-hash)) 258 | 259 | (defun project-persist--settings-get (key) 260 | "Get the value of setting KEY." 261 | (gethash key project-persist--settings-hash)) 262 | 263 | (defun project-persist--settings-set (key value) 264 | "Set project setting KEY to VALUE." 265 | (puthash key value project-persist--settings-hash)) 266 | 267 | (defun project-persist--read-project-name () 268 | "Read the project name from user input using a choice of `completing-read' or `ido-completing-read'." 269 | (let ((func 'completing-read)) 270 | (cond ((featurep 'ivy) (setq func 'ivy-completing-read)) 271 | ((featurep 'ido) (setq func 'ido-completing-read))) 272 | (funcall func "Project name: " (project-persist--project-list) nil t))) 273 | 274 | (defun project-persist--signal-error (err &optional func) 275 | "Ding and message the error string, optionally continuing with a given function." 276 | (ding) 277 | (message "%s" (error-message-string err)) 278 | (sit-for 1) 279 | (when func (funcall func))) 280 | 281 | (defun project-persist--project-destroy (name) 282 | "Delete the settings directory for the given project NAME." 283 | (let ((settings-dir (project-persist--settings-dir-from-name name))) 284 | (delete-directory settings-dir t t) 285 | (project-persist--invalidate-project-list-cache))) 286 | 287 | (defun project-persist--close-current-project () 288 | "Close the current project, setting relevant vars to nil." 289 | (run-hooks 'project-persist-before-close-hook) 290 | (project-persist--reset-hashtable) 291 | (project-persist--clear-project-vars) 292 | (setq project-persist--lighter nil) 293 | (run-hooks 'project-persist-after-close-hook)) 294 | 295 | (defun project-persist--clear-project-vars () 296 | "Clear standard project variables." 297 | (let ((vars '(project-persist-current-project-name 298 | project-persist-current-project-root-dir 299 | project-persist-current-project-settings-dir))) 300 | (mapc (lambda (var) (set var nil)) vars))) 301 | 302 | (defun project-persist--project-list () 303 | "Get a list of names of existing projects." 304 | (when (not project-persist--project-list-cache-valid) 305 | (let ((settings-dir project-persist-settings-dir)(project-list '())) 306 | (let ((dirs (directory-files settings-dir))) 307 | (while dirs 308 | (let ((dir (car dirs))) 309 | (when (not (or (eq dir ".") (eq dir ".."))) 310 | (let ((settings (project-persist--get-settings-in-dirname dir))) 311 | (when settings 312 | (add-to-list 'project-list (gethash 'name settings))))) 313 | (setq dirs (cdr dirs))))) 314 | (project-persist--set-project-list-cache project-list))) 315 | project-persist--project-list-cache) 316 | 317 | (defun project-persist--set-project-list-cache (project-list) 318 | "Set the cached project list to PROJECT-LIST and make it valid." 319 | (setq project-persist--project-list-cache project-list) 320 | (setq project-persist--project-list-cache-valid t)) 321 | 322 | (defun project-persist--invalidate-project-list-cache () 323 | "Make the cached project list invalid." 324 | (setq project-persist--project-list-cache-valid nil)) 325 | 326 | (defun project-persist--has-open-project () 327 | "Whether a project is currently open." 328 | (not (null project-persist-current-project-name))) 329 | 330 | (defun project-persist--project-exists (name) 331 | "Whether a project with the given NAME already exists. 332 | \(I.e., an appropriately-named directory exists in the project settings 333 | directory, and a valid settings file exists within that directory)." 334 | (let ((settings-dir (project-persist--settings-dir-from-name name))) 335 | (let ((settings-file (expand-file-name project-persist--settings-file-name settings-dir))) 336 | (file-exists-p settings-file)))) 337 | 338 | (defun project-persist--get-settings-in-dirname (dirname) 339 | "Return the settings from the settings file in the given DIRNAME, or nil." 340 | (let ((dir (expand-file-name dirname project-persist-settings-dir))(settings nil)) 341 | (if (file-directory-p dir) 342 | (let ((settings-file (expand-file-name project-persist--settings-file-name dir))) 343 | (if (file-exists-p settings-file) 344 | (let ((settings-string (project-persist--get-settings-file-contents settings-file))) 345 | (setq settings (project-persist--read-settings-from-string settings-string)))))) 346 | settings)) 347 | 348 | (defun project-persist--project-setup (root-dir name) 349 | "Set up a project with root directory ROOT-DIR and name NAME." 350 | (if (string= name "") (error "Project name is empty")) 351 | (if (project-persist--project-exists name) (error "Project %s already exists." name)) 352 | (run-hooks 'project-persist-before-create-hook) 353 | (let ((settings-dir (project-persist--settings-dir-from-name name))) 354 | (project-persist--make-settings-dir settings-dir) 355 | (project-persist--reset-hashtable) 356 | (project-persist--settings-set 'root-dir root-dir) 357 | (project-persist--settings-set 'name name) 358 | (project-persist--set-additional-settings) 359 | (setq project-persist-current-project-settings-dir settings-dir) 360 | (project-persist--project-write settings-dir) 361 | (project-persist--invalidate-project-list-cache) 362 | (run-hooks 'project-persist-after-create-hook))) 363 | 364 | (defun project-persist--set-additional-settings () 365 | "Set any values given in `project-persist-additional-settings'." 366 | (let ((settings-keys project-persist-additional-settings)) 367 | (while settings-keys 368 | (let ((setting (car settings-keys))) 369 | (let ((setting-key (car setting))(setting-value (funcall (cdr setting)))) 370 | (project-persist--settings-set setting-key setting-value) 371 | (setq settings-keys (cdr settings-keys))))))) 372 | 373 | (defun project-persist--project-open (name) 374 | "Open the project named NAME." 375 | (let ((settings-file 376 | (expand-file-name 377 | project-persist--settings-file-name (project-persist--settings-dir-from-name name)))) 378 | (let ((settings (project-persist--read-settings-from-string 379 | (project-persist--get-settings-file-contents settings-file)))) 380 | (project-persist--offer-save-if-open-project) 381 | (project-persist--apply-project-settings settings)))) 382 | 383 | (defun project-persist--apply-project-settings (settings) 384 | "Make the SETTINGS read from the project settings file current." 385 | (run-hooks 'project-persist-before-load-hook) 386 | (setq project-persist--settings-hash settings) 387 | (setq project-persist-current-project-name (gethash 'name settings)) 388 | (setq project-persist-current-project-root-dir (gethash 'root-dir settings)) 389 | (setq project-persist--lighter (format " pp:%s" project-persist-current-project-name)) 390 | (setq project-persist-current-project-settings-dir (project-persist--settings-dir-from-name project-persist-current-project-name)) 391 | (add-hook 'kill-emacs-hook 'project-persist--offer-save-if-open-project) 392 | (run-hooks 'project-persist-after-load-hook)) 393 | 394 | (defun project-persist--get-settings-file-contents (settings-file) 395 | "Read and return contents of SETTINGS-FILE." 396 | (with-temp-buffer 397 | (insert-file-contents settings-file) 398 | (buffer-string))) 399 | 400 | (defun project-persist--read-settings-from-string (settings-string) 401 | "Read and return the project settings hash from the given SETTINGS-STRING." 402 | (read settings-string)) 403 | 404 | (defun project-persist--project-write (settings-dir) 405 | "Write project settings to the given SETTINGS-DIR." 406 | (let ((settings-file (expand-file-name project-persist--settings-file-name settings-dir))) 407 | (with-temp-buffer 408 | (print project-persist--settings-hash (current-buffer)) 409 | (project-persist--write-to-settings settings-file (buffer-string))))) 410 | 411 | (defun project-persist--write-to-settings (settings-file settings-string) 412 | "Write to SETTINGS-FILE with the given SETTINGS-STRING." 413 | (run-hooks 'project-persist-before-save-hook) 414 | (with-temp-file settings-file 415 | (insert settings-string)) 416 | (run-hooks 'project-persist-after-save-hook)) 417 | 418 | (defun project-persist--settings-dir-from-name (name) 419 | "Return the settings directory for the project based on its NAME." 420 | (concat (expand-file-name name project-persist-settings-dir))) 421 | 422 | (defun project-persist--make-settings-dir (settings-dir) 423 | "Create the project SETTINGS-DIR if it doesn't already exist. 424 | Create the project-persist root settings directory if necessary." 425 | (unless (file-exists-p project-persist-settings-dir) 426 | (make-directory project-persist-settings-dir)) 427 | (unless (file-exists-p settings-dir) 428 | (make-directory settings-dir))) 429 | 430 | ;;;###autoload 431 | (define-minor-mode project-persist-mode 432 | "A minor mode to allow loading and saving of project settings." 433 | :global t 434 | :lighter project-persist--lighter 435 | :keymap project-persist-mode-map 436 | :group 'project-persist 437 | (unless project-persist-mode 438 | (project-persist--disable-hooks) 439 | (project-persist--close-current-project))) 440 | 441 | (provide 'project-persist) 442 | ;;; project-persist.el ends here 443 | -------------------------------------------------------------------------------- /project-persist.el: -------------------------------------------------------------------------------- 1 | ;;; project-persist.el --- A minor mode to allow loading and saving of project settings. 2 | 3 | ;; Copyright (C) @YEAR Robert Dallas Gray 4 | 5 | ;; Author: Robert Dallas Gray 6 | ;; URL: https://github.com/rdallasgray/project-persist 7 | ;; Version: @VERSION 8 | ;; Created: 2012-09-13 9 | ;; Keywords: project, persistence 10 | 11 | ;; This file is NOT part of GNU Emacs. 12 | 13 | ;;; License: 14 | 15 | ;; This program is free software; you can redistribute it and/or modify 16 | ;; it under the terms of the GNU General Public License as published by 17 | ;; the Free Software Foundation; either version 3, or (at your option) 18 | ;; any later version. 19 | ;; 20 | ;; This program is distributed in the hope that it will be useful, 21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | ;; GNU General Public License for more details. 24 | ;; 25 | ;; You should have received a copy of the GNU General Public License 26 | ;; along with GNU Emacs; see the file COPYING. If not, write to the 27 | ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 28 | ;; Boston, MA 02110-1301, USA. 29 | 30 | ;;; Commentary: 31 | ;; 32 | ;@COMMENTARY 33 | ;; 34 | ;;; Code: 35 | 36 | ;; Customize options 37 | (defgroup project-persist nil 38 | "Settings related to project-persist, a package to enable simple persistence of project settings." 39 | :group 'tools) 40 | 41 | (defcustom project-persist-settings-dir (concat user-emacs-directory "project-persist") 42 | "The directory in which project-persist will save project settings files." 43 | :type 'directory 44 | :group 'project-persist) 45 | 46 | (defcustom project-persist-keymap-prefix (kbd "C-c P") 47 | "Project-persist keymap prefix." 48 | :type 'sexp 49 | :group 'project-persist) 50 | 51 | (defcustom project-persist-auto-save-global t 52 | "If non-nil, automatically save projects without prompting. 53 | 54 | Can be overridden on a project-basis with 55 | \(project-persist--settings-set 'auto-save VALUE), where VALUE is t or 'prompt 56 | 57 | If the project setting `auto-save' is t or if the value of 58 | variable `project-persist-auto-save-global' is non-nil, save the 59 | project without prompting 60 | 61 | If the project setting `auto-save' is 'prompt, always prompt before saving" 62 | :type 'boolean 63 | :group 'project-persist) 64 | 65 | 66 | ;; Hooks 67 | (defvar project-persist-mode-hook nil 68 | "Run when entering project-persist-mode.") 69 | 70 | (defvar project-persist-before-create-hook nil 71 | "A hook to be run before project-persist creates a project.") 72 | 73 | (defvar project-persist-after-create-hook nil 74 | "A hook to be run after project-persist creates a project.") 75 | 76 | (defvar project-persist-before-save-hook nil 77 | "A hook to be run before project-persist saves a project.") 78 | 79 | (defvar project-persist-after-save-hook nil 80 | "A hook to be run after project-persist saves a project.") 81 | 82 | (defvar project-persist-before-load-hook nil 83 | "A hook to be run before project-persist loads a project.") 84 | 85 | (defvar project-persist-after-load-hook nil 86 | "A hook to be run after project-persist loads a project.") 87 | 88 | (defvar project-persist-before-close-hook nil 89 | "A hook to be run before project-persist closes a project.") 90 | 91 | (defvar project-persist-after-close-hook nil 92 | "A hook to be run after project-persist closes a project.") 93 | 94 | (defvar project-persist-mode-map 95 | (let ((map (make-sparse-keymap))) 96 | (let ((prefix-map (make-sparse-keymap))) 97 | (define-key prefix-map (kbd "f") 'project-persist-find) 98 | (define-key prefix-map (kbd "s") 'project-persist-save) 99 | (define-key prefix-map (kbd "k") 'project-persist-close) 100 | (define-key prefix-map (kbd "d") 'project-persist-delete) 101 | (define-key prefix-map (kbd "n") 'project-persist-create) 102 | (define-key map project-persist-keymap-prefix prefix-map)) 103 | map) 104 | "Keymap for project-persist-mode.") 105 | 106 | 107 | ;; Global variables 108 | (defvar project-persist-current-project-name nil 109 | "The name of the project currently loaded by project-persist.") 110 | 111 | (defvar project-persist-current-project-root-dir nil 112 | "The root directory of the project currently loaded by project-persist.") 113 | 114 | (defvar project-persist-current-project-settings-dir nil 115 | "The directory in which settings for the current project are stored.") 116 | 117 | (defvar project-persist-additional-settings '() 118 | "A list of additional keys to store in the project settings file. 119 | The defaults are 'name and 'root-dir. The format should be a cons cell: 120 | \('key . read-function); e.g. ('name . (lambda () (read-from-buffer \"Project name: \"))).") 121 | 122 | ;; Internal variables 123 | (defvar project-persist--lighter nil 124 | "Modeline lighter for minor mode.") 125 | 126 | (defvar project-persist--project-list-cache '() 127 | "Cached list of projects.") 128 | 129 | (defvar project-persist--project-list-cache-valid nil 130 | "Whether the cached project list is currently valid.") 131 | 132 | (defvar project-persist--settings-file-name "pp-settings.txt" 133 | "Name of the default settings file to write in each project's settings directory.") 134 | 135 | (defvar project-persist--settings-hash (make-hash-table :test 'equal) 136 | "Settings hashtable to be written to the project settings file.") 137 | 138 | ;; Interactive functions 139 | (defun project-persist-find () 140 | "Find and load the given project name." 141 | (interactive) 142 | (project-persist--project-open (project-persist--read-project-name))) 143 | 144 | (defun project-persist-create () 145 | "Create a new project-persist project, giving a project name and root directory." 146 | (interactive) 147 | (project-persist--offer-save-if-open-project) 148 | (let ((root-dir (read-directory-name "Project root directory: "))) 149 | (let ((name 150 | (read-from-minibuffer 151 | "Project name: " 152 | (file-name-nondirectory (directory-file-name root-dir))))) 153 | (condition-case err 154 | (progn 155 | (project-persist--project-setup root-dir name) 156 | (project-persist--project-open name)) 157 | (error (project-persist--signal-error err)))))) 158 | 159 | (defun project-persist-delete () 160 | "Delete the given project name." 161 | (interactive) 162 | (let ((name (project-persist--read-project-name))) 163 | (when (eq name project-persist-current-project-name) 164 | (error "Can't delete the currently open project. Please close the project first.")) 165 | (let ((confirm (yes-or-no-p (format "Are you sure you want to delete project %s?" name)))) 166 | (when confirm 167 | (project-persist--project-destroy name))))) 168 | 169 | (defun project-persist-save () 170 | "Save the project settings and run relevant hooks." 171 | (interactive) 172 | (when (not (project-persist--has-open-project)) (error "No project is currently open.")) 173 | (let ((settings-dir (project-persist--settings-dir-from-name project-persist-current-project-name))) 174 | (project-persist--project-write settings-dir))) 175 | 176 | (defun project-persist-close () 177 | "Close the currently open project." 178 | (interactive) 179 | (when (not (project-persist--has-open-project)) (error "No project is currently open.")) 180 | (project-persist--offer-save-if-open-project) 181 | (project-persist--close-current-project)) 182 | 183 | 184 | ;; Internal functions 185 | (defun project-persist--offer-save-if-open-project () 186 | "Offer to save the open project. 187 | Depending on the value of the variable` project-persist-auto-save-global' 188 | and the project setting `auto-save', save the project without asking." 189 | (when (project-persist--has-open-project) 190 | (let ((auto-save (project-persist--auto-save-value))) 191 | (when (or auto-save (y-or-n-p (format "Save project %s?" project-persist-current-project-name))) 192 | (project-persist-save))))) 193 | 194 | (defun project-persist--auto-save-value () 195 | "Get the auto-save setting; if set locally, use that, otherwise use the global setting." 196 | (let ((local-setting (project-persist--settings-get 'auto-save))) 197 | (if local-setting 198 | (not (eq local-setting 'prompt)) 199 | project-persist-auto-save-global))) 200 | 201 | (defun project-persist--disable-hooks () 202 | "Disable all project-persist hooks (normally on disabling the minor mode)." 203 | (let ((hooks '(project-persist-before-create-hook 204 | project-persist-after-create-hook 205 | project-persist-before-load-hook 206 | project-persist-after-load-hook 207 | project-persist-before-save-hook 208 | project-persist-after-save-hook))) 209 | (mapc (lambda (hook) (set hook nil)) hooks)) 210 | (remove-hook 'kill-emacs-hook 'project-persist--offer-save-if-open-project)) 211 | 212 | (defun project-persist--reset-hashtable () 213 | "Empty the hashtable containing project settings." 214 | (clrhash project-persist--settings-hash)) 215 | 216 | (defun project-persist--settings-get (key) 217 | "Get the value of setting KEY." 218 | (gethash key project-persist--settings-hash)) 219 | 220 | (defun project-persist--settings-set (key value) 221 | "Set project setting KEY to VALUE." 222 | (puthash key value project-persist--settings-hash)) 223 | 224 | (defun project-persist--read-project-name () 225 | "Read the project name from user input using a choice of `completing-read' or `ido-completing-read'." 226 | (let ((func 'completing-read)) 227 | (cond ((featurep 'ivy) (setq func 'ivy-completing-read)) 228 | ((featurep 'ido) (setq func 'ido-completing-read))) 229 | (funcall func "Project name: " (project-persist--project-list) nil t))) 230 | 231 | (defun project-persist--signal-error (err &optional func) 232 | "Ding and message the error string, optionally continuing with a given function." 233 | (ding) 234 | (message "%s" (error-message-string err)) 235 | (sit-for 1) 236 | (when func (funcall func))) 237 | 238 | (defun project-persist--project-destroy (name) 239 | "Delete the settings directory for the given project NAME." 240 | (let ((settings-dir (project-persist--settings-dir-from-name name))) 241 | (delete-directory settings-dir t t) 242 | (project-persist--invalidate-project-list-cache))) 243 | 244 | (defun project-persist--close-current-project () 245 | "Close the current project, setting relevant vars to nil." 246 | (run-hooks 'project-persist-before-close-hook) 247 | (project-persist--reset-hashtable) 248 | (project-persist--clear-project-vars) 249 | (setq project-persist--lighter nil) 250 | (run-hooks 'project-persist-after-close-hook)) 251 | 252 | (defun project-persist--clear-project-vars () 253 | "Clear standard project variables." 254 | (let ((vars '(project-persist-current-project-name 255 | project-persist-current-project-root-dir 256 | project-persist-current-project-settings-dir))) 257 | (mapc (lambda (var) (set var nil)) vars))) 258 | 259 | (defun project-persist--project-list () 260 | "Get a list of names of existing projects." 261 | (when (not project-persist--project-list-cache-valid) 262 | (let ((settings-dir project-persist-settings-dir)(project-list '())) 263 | (let ((dirs (directory-files settings-dir))) 264 | (while dirs 265 | (let ((dir (car dirs))) 266 | (when (not (or (eq dir ".") (eq dir ".."))) 267 | (let ((settings (project-persist--get-settings-in-dirname dir))) 268 | (when settings 269 | (add-to-list 'project-list (gethash 'name settings))))) 270 | (setq dirs (cdr dirs))))) 271 | (project-persist--set-project-list-cache project-list))) 272 | project-persist--project-list-cache) 273 | 274 | (defun project-persist--set-project-list-cache (project-list) 275 | "Set the cached project list to PROJECT-LIST and make it valid." 276 | (setq project-persist--project-list-cache project-list) 277 | (setq project-persist--project-list-cache-valid t)) 278 | 279 | (defun project-persist--invalidate-project-list-cache () 280 | "Make the cached project list invalid." 281 | (setq project-persist--project-list-cache-valid nil)) 282 | 283 | (defun project-persist--has-open-project () 284 | "Whether a project is currently open." 285 | (not (null project-persist-current-project-name))) 286 | 287 | (defun project-persist--project-exists (name) 288 | "Whether a project with the given NAME already exists. 289 | \(I.e., an appropriately-named directory exists in the project settings 290 | directory, and a valid settings file exists within that directory)." 291 | (let ((settings-dir (project-persist--settings-dir-from-name name))) 292 | (let ((settings-file (expand-file-name project-persist--settings-file-name settings-dir))) 293 | (file-exists-p settings-file)))) 294 | 295 | (defun project-persist--get-settings-in-dirname (dirname) 296 | "Return the settings from the settings file in the given DIRNAME, or nil." 297 | (let ((dir (expand-file-name dirname project-persist-settings-dir))(settings nil)) 298 | (if (file-directory-p dir) 299 | (let ((settings-file (expand-file-name project-persist--settings-file-name dir))) 300 | (if (file-exists-p settings-file) 301 | (let ((settings-string (project-persist--get-settings-file-contents settings-file))) 302 | (setq settings (project-persist--read-settings-from-string settings-string)))))) 303 | settings)) 304 | 305 | (defun project-persist--project-setup (root-dir name) 306 | "Set up a project with root directory ROOT-DIR and name NAME." 307 | (if (string= name "") (error "Project name is empty")) 308 | (if (project-persist--project-exists name) (error "Project %s already exists." name)) 309 | (run-hooks 'project-persist-before-create-hook) 310 | (let ((settings-dir (project-persist--settings-dir-from-name name))) 311 | (project-persist--make-settings-dir settings-dir) 312 | (project-persist--reset-hashtable) 313 | (project-persist--settings-set 'root-dir root-dir) 314 | (project-persist--settings-set 'name name) 315 | (project-persist--set-additional-settings) 316 | (setq project-persist-current-project-settings-dir settings-dir) 317 | (project-persist--project-write settings-dir) 318 | (project-persist--invalidate-project-list-cache) 319 | (run-hooks 'project-persist-after-create-hook))) 320 | 321 | (defun project-persist--set-additional-settings () 322 | "Set any values given in `project-persist-additional-settings'." 323 | (let ((settings-keys project-persist-additional-settings)) 324 | (while settings-keys 325 | (let ((setting (car settings-keys))) 326 | (let ((setting-key (car setting))(setting-value (funcall (cdr setting)))) 327 | (project-persist--settings-set setting-key setting-value) 328 | (setq settings-keys (cdr settings-keys))))))) 329 | 330 | (defun project-persist--project-open (name) 331 | "Open the project named NAME." 332 | (let ((settings-file 333 | (expand-file-name 334 | project-persist--settings-file-name (project-persist--settings-dir-from-name name)))) 335 | (let ((settings (project-persist--read-settings-from-string 336 | (project-persist--get-settings-file-contents settings-file)))) 337 | (project-persist--offer-save-if-open-project) 338 | (project-persist--apply-project-settings settings)))) 339 | 340 | (defun project-persist--apply-project-settings (settings) 341 | "Make the SETTINGS read from the project settings file current." 342 | (run-hooks 'project-persist-before-load-hook) 343 | (setq project-persist--settings-hash settings) 344 | (setq project-persist-current-project-name (gethash 'name settings)) 345 | (setq project-persist-current-project-root-dir (gethash 'root-dir settings)) 346 | (setq project-persist--lighter (format " pp:%s" project-persist-current-project-name)) 347 | (setq project-persist-current-project-settings-dir (project-persist--settings-dir-from-name project-persist-current-project-name)) 348 | (add-hook 'kill-emacs-hook 'project-persist--offer-save-if-open-project) 349 | (run-hooks 'project-persist-after-load-hook)) 350 | 351 | (defun project-persist--get-settings-file-contents (settings-file) 352 | "Read and return contents of SETTINGS-FILE." 353 | (with-temp-buffer 354 | (insert-file-contents settings-file) 355 | (buffer-string))) 356 | 357 | (defun project-persist--read-settings-from-string (settings-string) 358 | "Read and return the project settings hash from the given SETTINGS-STRING." 359 | (read settings-string)) 360 | 361 | (defun project-persist--project-write (settings-dir) 362 | "Write project settings to the given SETTINGS-DIR." 363 | (let ((settings-file (expand-file-name project-persist--settings-file-name settings-dir))) 364 | (with-temp-buffer 365 | (print project-persist--settings-hash (current-buffer)) 366 | (project-persist--write-to-settings settings-file (buffer-string))))) 367 | 368 | (defun project-persist--write-to-settings (settings-file settings-string) 369 | "Write to SETTINGS-FILE with the given SETTINGS-STRING." 370 | (run-hooks 'project-persist-before-save-hook) 371 | (with-temp-file settings-file 372 | (insert settings-string)) 373 | (run-hooks 'project-persist-after-save-hook)) 374 | 375 | (defun project-persist--settings-dir-from-name (name) 376 | "Return the settings directory for the project based on its NAME." 377 | (concat (expand-file-name name project-persist-settings-dir))) 378 | 379 | (defun project-persist--make-settings-dir (settings-dir) 380 | "Create the project SETTINGS-DIR if it doesn't already exist. 381 | Create the project-persist root settings directory if necessary." 382 | (unless (file-exists-p project-persist-settings-dir) 383 | (make-directory project-persist-settings-dir)) 384 | (unless (file-exists-p settings-dir) 385 | (make-directory settings-dir))) 386 | 387 | ;;;###autoload 388 | (define-minor-mode project-persist-mode 389 | "A minor mode to allow loading and saving of project settings." 390 | :global t 391 | :lighter project-persist--lighter 392 | :keymap project-persist-mode-map 393 | :group 'project-persist 394 | (unless project-persist-mode 395 | (project-persist--disable-hooks) 396 | (project-persist--close-current-project))) 397 | 398 | (provide 'project-persist) 399 | ;;; project-persist.el ends here 400 | -------------------------------------------------------------------------------- /test/project-persist-test.el: -------------------------------------------------------------------------------- 1 | (let ((current-directory (file-name-directory (if load-file-name load-file-name buffer-file-name)))) 2 | (setq project-persist--test-path (expand-file-name "." current-directory)) 3 | (setq project-persist--root-path (expand-file-name ".." current-directory))) 4 | 5 | (add-to-list 'load-path project-persist--root-path) 6 | 7 | (require 'project-persist) 8 | (require 'cl) 9 | 10 | (ert-deftest pp-test/empty-project-name-signals-error () 11 | "Test that attemp 12 | ting to create a project with an empty name signals an error." 13 | (project-persist-mode 1) 14 | (flet ((project-persist--make-settings-dir (sd) t) (project-persist--project-write (n rd sd) t)) 15 | (should-error (project-persist--project-setup "/test" "")))) 16 | 17 | (ert-deftest pp-test/existing-project-name-signals-error () 18 | "Test that attempting to create a project with an existing name signals an error." 19 | (project-persist-mode 1) 20 | (flet ((project-persist--project-exists (name) t) (project-persist--make-settings-dir (sd) t) (project-persist--project-write (n rd sd) t)) 21 | (should-error (project-persist--project-setup "/test" "test")))) 22 | 23 | (ert-deftest pp-test/correct-settings-dir-from-name () 24 | "Test that the correct directory name for the settings file is returned." 25 | (project-persist-mode 1) 26 | (setq project-persist-settings-dir "/test/settings-dir") 27 | (should (equal (project-persist--settings-dir-from-name "name") "/test/settings-dir/name"))) 28 | 29 | (ert-deftest pp-test/settings-written-to-correct-file () 30 | "Test that project-persist--project-write writes to the correct file and directory." 31 | (project-persist-mode 1) 32 | (let 33 | ((settings-dir "/test/settings-dir") 34 | (project-name "test-project-name") 35 | (project-root-dir "/test/project-root-dir")) 36 | (flet ((project-persist--write-to-settings (settings-file settings-string) 37 | (should (equal settings-file "/test/settings-dir/pp-settings.txt")))) 38 | (project-persist--project-write settings-dir)))) 39 | 40 | (ert-deftest pp-test/settings-written-correctly () 41 | "Test that project settings are stored correctly." 42 | (project-persist-mode 1) 43 | (setq project-persist-additional-settings nil) 44 | (project-persist--reset-hashtable) 45 | (let 46 | ((project-name "test-project-name") 47 | (project-root-dir "/test/project-root-dir") 48 | (settings-text "(root-dir \"/test/project-root-dir\" name \"test-project-name\")")) 49 | (flet ((project-persist--write-to-settings (settings-file settings-string) 50 | (with-temp-buffer 51 | (insert settings-string) 52 | (should (string-match-p settings-text (buffer-string)))))) 53 | (project-persist--settings-set 'root-dir project-root-dir) 54 | (project-persist--settings-set 'name project-name) 55 | (project-persist--project-write "/test-settings-dir")))) 56 | 57 | (ert-deftest pp-test/additional-settings-written-correctly () 58 | "Test that additional project settings are stored correctly." 59 | (project-persist-mode 1) 60 | (setq project-persist-additional-settings nil) 61 | (project-persist--reset-hashtable) 62 | (let 63 | ((project-name "test-project-name") 64 | (project-root-dir "/test/project-root-dir") 65 | (settings-text "(root-dir \"/test/project-root-dir\" name \"test-project-name\" test \"test setting\")")) 66 | (flet ((project-persist--write-to-settings (settings-file settings-string) 67 | (with-temp-buffer 68 | (insert settings-string) 69 | (should (string-match-p settings-text (buffer-string)))))) 70 | (project-persist--settings-set 'root-dir project-root-dir) 71 | (project-persist--settings-set 'name project-name) 72 | (add-to-list 'project-persist-additional-settings '(test . (lambda () (concat "test setting")))) 73 | (project-persist--set-additional-settings) 74 | (project-persist--project-write "/test-settings-dir")))) 75 | 76 | (ert-deftest pp-test/settings-read-correctly () 77 | "Test that project settings are read back in correctly." 78 | (project-persist-mode 1) 79 | (let ((settings-string "\n#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data (root-dir \"/test/project-root-dir\" name \"test-project-name\" test \"test setting\"))\n")) 80 | (let ((settings (project-persist--read-settings-from-string settings-string))) 81 | (should (equal (gethash 'name settings) "test-project-name")) 82 | (should (equal (gethash 'root-dir settings) "/test/project-root-dir")) 83 | (should (equal (gethash 'test settings) "test setting"))))) 84 | 85 | (ert-deftest pp-test/settings-applied-correctly () 86 | "Test that project settings are applied correctly." 87 | (project-persist-mode 1) 88 | (let ((settings-string "\n#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data (root-dir \"/test/project-root-dir\" name \"test-project-name\" test \"test setting\"))\n")) 89 | (let ((settings (project-persist--read-settings-from-string settings-string))) 90 | (flet ((add-hook (hook func))) 91 | (project-persist--apply-project-settings settings) 92 | (should (equal project-persist-current-project-name "test-project-name")) 93 | (should (equal project-persist-current-project-root-dir "/test/project-root-dir")))))) 94 | 95 | (ert-deftest pp-test/settings-read-from-hash-correctly () 96 | "Test that settings are stored in the hash and read back correctly via the project-persist--settings-get method." 97 | (project-persist-mode 1) 98 | (let ((settings-string "\n#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data (root-dir \"/test/project-root-dir\" name \"test-project-name\" test \"test setting\"))\n")) 99 | (let ((settings (project-persist--read-settings-from-string settings-string))) 100 | (flet ((add-hook (hook func))) 101 | (project-persist--apply-project-settings settings) 102 | (should (equal (project-persist--settings-get 'name) "test-project-name")) 103 | (should (equal (project-persist--settings-get 'root-dir) "/test/project-root-dir")) 104 | (should (equal (project-persist--settings-get 'test) "test setting")))))) 105 | 106 | (ert-deftest pp-test/project-closed-correctly () 107 | "Test that variables are set to nil when a project is closed." 108 | (project-persist-mode 1) 109 | (let ((settings-string "\n#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data (root-dir \"/test/project-root-dir\" name \"test-project-name\" test \"test setting\"))\n")) 110 | (let ((settings (project-persist--read-settings-from-string settings-string))) 111 | (project-persist--apply-project-settings settings) 112 | (project-persist--close-current-project) 113 | (should (equal nil project-persist-current-project-name)) 114 | (should (equal nil project-persist-current-project-root-dir))))) 115 | 116 | (ert-deftest pp-test/project-mode-off-removes-hooks () 117 | "Test that hooks are set to nil when a project is closed." 118 | (project-persist-mode 1) 119 | (add-hook 'project-persist-before-load-hook (lambda () (concat "test"))) 120 | (project-persist-mode -1) 121 | (should (equal nil project-persist-before-load-hook))) 122 | 123 | (ert-deftest pp-test/global-auto-save-setting-nil () 124 | "Test that the global auto-save setting is honoured when set to nil." 125 | (project-persist-mode 1) 126 | (let ((project-persist-auto-save-global nil) (prompt-called nil) (project-persist-current-project-name "test")) 127 | (flet ((y-or-n-p (prompt) (setq prompt-called t)) (project-persist--write-to-settings (sf ss) t) (project-persist--has-open-project () t)) 128 | (project-persist-close) 129 | (should (equal t prompt-called))))) 130 | 131 | (ert-deftest pp-test/global-auto-save-setting-t () 132 | "Test that the global auto-save setting is honoured when set to t." 133 | (project-persist-mode 1) 134 | (let ((project-persist-auto-save-global t) (prompt-called nil) (project-persist-current-project-name "test")) 135 | (flet ((y-or-n-p (prompt) (setq prompt-called t)) (project-persist--write-to-settings (sf ss) t) (project-persist--has-open-project () t)) 136 | (project-persist-close) 137 | (should (equal nil prompt-called))))) 138 | 139 | (ert-deftest pp-test/local-auto-save-setting-t () 140 | "Test that the local auto-save setting is honoured when set to t." 141 | (project-persist-mode 1) 142 | (let ((prompt-called nil) (project-persist-current-project-name "test")) 143 | (project-persist--settings-set 'auto-save t) 144 | (flet ((y-or-n-p (prompt) (setq prompt-called t)) (project-persist--write-to-settings (sf ss) t) (project-persist--has-open-project () t)) 145 | (project-persist-close) 146 | (should (equal nil prompt-called))))) 147 | 148 | (ert-deftest pp-test/local-auto-save-setting-prompt () 149 | "Test that the local auto-save setting is honoured when set to prompt." 150 | (project-persist-mode 1) 151 | (let ((prompt-called nil) (project-persist-current-project-name "test")) 152 | (project-persist--settings-set 'auto-save 'prompt) 153 | (flet ((y-or-n-p (prompt) (setq prompt-called t)) (project-persist--write-to-settings (sf ss) t) (project-persist--has-open-project () t)) 154 | (project-persist-close) 155 | (should (equal t prompt-called))))) 156 | --------------------------------------------------------------------------------