-
Notifications
You must be signed in to change notification settings - Fork 5
/
project-x.el
217 lines (194 loc) · 8.77 KB
/
project-x.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
;;; project-x.el --- Extra convenience features for project.el -*- lexical-binding: t -*-
;; Copyright (C) 2021 Karthik Chikmagalur
;; Author: Karthik Chikmagalur <[email protected]>
;; URL: https://github.com/karthink/project-x
;; Version: 0.1.5
;; Package-Requires: ((emacs "27.1"))
;; This file is NOT part of GNU Emacs.
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; For a full copy of the GNU General Public License
;; see <http://www.gnu.org/licenses/>.
;;
;;; Commentary:
;;
;; project-x provides some convenience features for project.el:
;; - Recognize any directory with a `.project' file as a project.
;; - Save and restore project files and window configurations across sessions
;;
;; COMMANDS:
;;
;; project-x-window-state-save : Save the window configuration of currently open project buffers
;; project-x-window-state-load : Load a previously saved project window configuration
;;
;; CUSTOMIZATION:
;;
;; `project-x-window-list-file': File to store project window configurations
;; `project-x-local-identifier': String matched against file names to decide if a
;; directory is a project
;; `project-x-save-interval': Interval in seconds between autosaves of the
;; current project.
;;
;; by Karthik Chikmagalur
;; <[email protected]>
;;; Code:
(require 'project)
(eval-when-compile (require 'subr-x))
(eval-when-compile (require 'seq))
(defvar project-prefix-map)
(defvar project-switch-commands)
(declare-function project-prompt-project-dir "project")
(declare-function project--buffer-list "project")
(declare-function project-buffers "project")
(defgroup project-x nil
"Convenience features for the Project library."
:group 'project)
;; Persistent project sessions
;; -------------------------------------
(defcustom project-x-window-list-file
(locate-user-emacs-file "project-window-list")
"File in which to save project window configurations by default."
:type 'file
:group 'project-x)
(defcustom project-x-save-interval nil
"Saves the current project state with this interval.
When set to nil auto-save is disabled."
:type '(choice (const :tag "Disabled" nil)
integer)
:group 'project-x)
(defvar project-x-window-alist nil
"Alist of window configurations associated with known projects.")
(defvar project-x-save-timer nil
"Timer for auto-saving project state.")
(defun project-x--window-state-write (&optional file)
"Write project window states to `project-x-window-list-file'.
If FILE is specified, write to it instead."
(when project-x-window-alist
(require 'pp)
(unless file (make-directory (file-name-directory project-x-window-list-file) t))
(with-temp-file (or file project-x-window-list-file)
(insert ";;; -*- lisp-data -*-\n")
(let ((print-level nil) (print-length nil))
(pp project-x-window-alist (current-buffer))))
(message (format "Wrote project window state to %s" project-x-window-list-file))))
(defun project-x--window-state-read (&optional file)
"Read project window states from `project-x-window-list-file'.
If FILE is specified, read from it instead."
(and (or file
(file-exists-p project-x-window-list-file))
(with-temp-buffer
(insert-file-contents (or file project-x-window-list-file))
(condition-case nil
(if-let ((win-state-alist (read (current-buffer))))
(setq project-x-window-alist win-state-alist)
(message (format "Could not read %s" project-x-window-list-file)))
(error (message (format "Could not read %s" project-x-window-list-file)))))))
(defun project-x-window-state-save (&optional arg)
"Save current window state of project.
With optional prefix argument ARG, query for project."
(interactive "P")
(when-let* ((dir (cond (arg (project-prompt-project-dir))
((project-current)
(project-root (project-current)))))
(default-directory dir))
(unless project-x-window-alist (project-x--window-state-read))
(let ((file-list))
;; Collect file-list of all the open project buffers
(dolist (buf
(funcall (if (fboundp 'project--buffers-list)
#'project--buffers-list
#'project-buffers)
(project-current))
file-list)
(if-let ((file-name (or (buffer-file-name buf)
(with-current-buffer buf
(and (derived-mode-p 'dired-mode)
dired-directory)))))
(push file-name file-list)))
(setf (alist-get dir project-x-window-alist nil nil 'equal)
(list (cons 'files file-list)
(cons 'windows (window-state-get nil t)))))
(message (format "Saved project state for %s" dir))))
(defun project-x-window-state-load (dir)
"Load the saved window state for project with directory DIR.
If DIR is unspecified query the user for a project instead."
(interactive (list (project-prompt-project-dir)))
(unless project-x-window-alist (project-x--window-state-read))
(if-let* ((project-x-window-alist)
(project-state (alist-get dir project-x-window-alist
nil nil 'equal)))
(let ((file-list (alist-get 'files project-state))
(window-config (alist-get 'windows project-state)))
(dolist (file-name file-list nil)
(find-file file-name))
(window-state-put window-config nil 'safe)
(message (format "Restored project state for %s" dir)))
(message (format "No saved window state for project %s" dir))))
(defun project-x-windows ()
"Restore the last saved window state of the chosen project."
(interactive)
(project-x-window-state-load (project-root (project-current))))
;; Recognize directories as projects by defining a new project backend `local'
;; -------------------------------------
(defcustom project-x-local-identifier ".project"
"Filename(s) that identifies a directory as a project.
You can specify a single filename or a list of names."
:type '(choice (string :tag "Single file")
(repeat (string :tag "Filename")))
:group 'project-x)
(cl-defmethod project-root ((project (head local)))
"Return root directory of current PROJECT."
(cdr project))
(defun project-x-try-local (dir)
"Determine if DIR is a non-VC project.
DIR must include a .project file to be considered a project."
(if-let ((root (if (listp project-x-local-identifier)
(seq-some (lambda (n)
(locate-dominating-file dir n))
project-x-local-identifier)
(locate-dominating-file dir project-x-local-identifier))))
(cons 'local root)))
;;;###autoload
(define-minor-mode project-x-mode
"Minor mode to enable extra convenience features for project.el.
When enabled, save and load project window states.
Recognize any directory that contains (or whose parent
contains) a special file as a project."
:global t
:version "0.10"
:lighter ""
:group 'project-x
(if project-x-mode
;;Turning the mode ON
(progn
(add-hook 'project-find-functions 'project-x-try-local 90)
(add-hook 'kill-emacs-hook 'project-x--window-state-write)
(project-x--window-state-read)
(define-key project-prefix-map (kbd "w") 'project-x-window-state-save)
(define-key project-prefix-map (kbd "j") 'project-x-window-state-load)
(if (listp project-switch-commands)
(add-to-list 'project-switch-commands
'(?j "Restore windows" project-x-windows) t)
(message "`project-switch-commands` is not a list, not adding 'restore windows' command"))
(when project-x-save-interval
(setq project-x-save-timer
(run-with-timer 0 (max project-x-save-interval 5)
#'project-x-window-state-save))))
(remove-hook 'project-find-functions 'project-x-try-local 90)
(remove-hook 'kill-emacs-hook 'project-x--window-state-write)
(define-key project-prefix-map (kbd "w") nil)
(define-key project-prefix-map (kbd "j") nil)
(when (listp project-switch-commands)
(delete '(?j "Restore windows" project-x-windows) project-switch-commands))
(when (timerp project-x-save-timer)
(cancel-timer project-x-save-timer))))
(provide 'project-x)
;;; project-x.el ends here