system-manager-wakasu
  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
 73When 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'.
 86If 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'.
 98If 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.
111With 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.
138If 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
162You 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.
173DIR 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.
184When enabled, save and load project window states.
185Recognize any directory that contains (or whose parent
186contains) 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