fedora-csb-system-manager
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