summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimen Heggestøyl <simenheg@gmail.com>2020-05-28 17:02:17 +0200
committerSimen Heggestøyl <simenheg@gmail.com>2020-05-28 17:02:17 +0200
commitd97f224fd0db2ee13150ec7c4d6311eab48cda9e (patch)
tree06417974e77ae3e27683f08a6508badc87ceb96d
parent2bdb2cd10d08a1d9c9a187c7d967fdc64b8e6743 (diff)
parent9823c66b885c0c310061489bd732f3888a802b01 (diff)
downloademacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.tar.gz
emacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.tar.bz2
emacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.zip
Merge branch 'feature/project-switching'
-rw-r--r--doc/emacs/maintaining.texi35
-rw-r--r--etc/NEWS13
-rw-r--r--lisp/progmodes/project.el157
3 files changed, 195 insertions, 10 deletions
diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi
index ebcdddfcae3..22b7639d349 100644
--- a/doc/emacs/maintaining.texi
+++ b/doc/emacs/maintaining.texi
@@ -1656,8 +1656,16 @@ support additional types of projects.
the project back-end. For example, the VC back-end doesn't consider
``ignored'' files (@pxref{VC Ignore}) to be part of the project.
+@menu
+* Project File Commands:: Commands for handling project files.
+* Switching Projects:: Switching between projects.
+@end menu
+
+@node Project File Commands
+@subsection Project File Commands
+
Emacs provides commands for handling project files conveniently.
-This section describes these commands.
+This subsection describes these commands.
@cindex current project
All of the commands described here share the notion of the
@@ -1705,6 +1713,31 @@ Replace}), and continues to the next match after you respond. If your
response causes Emacs to exit the query-replace loop, you can later
continue with @w{@kbd{M-x fileloop-continue @key{RET}}}.
+@findex project-dired
+ The command @code{project-dired} opens a Dired buffer
+(@pxref{Dired}) listing the files in the current project's root
+directory.
+
+@findex project-eshell
+ The command @code{project-eshell} starts an Eshell session in a new
+buffer with the current project's root as the working directory.
+@xref{Top,Eshell,Eshell, eshell, Eshell: The Emacs Shell}.
+
+@node Switching Projects
+@subsection Switching Projects
+
+ Commands that operate on project files (@pxref{Project File
+Commands}) will conveniently prompt you for a project directory when
+no project is current. When you are inside a project but you want to
+operate on a different project, the command
+@code{project-switch-project} can be used.
+
+ This command prompts you to choose a directory among known project
+roots, and then displays the menu of available commands to operate on
+the chosen project. The variable @code{project-switch-commands}
+controls which commands are available in the menu, and by which keys
+they are invoked.
+
@node Change Log
@section Change Logs
diff --git a/etc/NEWS b/etc/NEWS
index ac65b320041..32b4435fdac 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -395,6 +395,19 @@ information, see the related entry about 'shr-browse-url' above.
*** New user option 'project-vc-merge-submodules'.
+*** Previously used project directories are now suggested by
+all commands that prompt for a project directory.
+
++++
+*** New commands 'project-dired' and 'project-eshell'.
+These commands run Dired and Eshell in a project's root directory,
+respectively.
+
++++
+*** New command 'project-switch-project'.
+This command lets you "switch" to another project and run a project
+command chosen from a dispatch menu.
+
** json.el
---
diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el
index 88f73e4fb31..a3e81d4d3aa 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -93,6 +93,7 @@
;;; Code:
(require 'cl-generic)
+(eval-when-compile (require 'subr-x))
(defvar project-find-functions (list #'project-try-vc)
"Special hook to find the project containing a given directory.
@@ -100,23 +101,26 @@ Each functions on this hook is called in turn with one
argument (the directory) and should return either nil to mean
that it is not applicable, or a project instance.")
+(defvar project-current-inhibit-prompt nil
+ "Non-nil to skip prompting the user in `project-current'.")
+
;;;###autoload
(defun project-current (&optional maybe-prompt dir)
"Return the project instance in DIR or `default-directory'.
When no project found in DIR, and MAYBE-PROMPT is non-nil, ask
-the user for a different directory to look in. If that directory
-is not a part of a detectable project either, return a
-`transient' project instance rooted in it."
+the user for a different project to look in."
(unless dir (setq dir default-directory))
(let ((pr (project--find-in-directory dir)))
(cond
(pr)
- (maybe-prompt
- (setq dir (read-directory-name "Choose the project directory: " dir nil t)
- pr (project--find-in-directory dir))
- (unless pr
- (message "Using `%s' as a transient project root" dir)
- (setq pr (cons 'transient dir)))))
+ ((unless project-current-inhibit-prompt
+ maybe-prompt)
+ (setq dir (project-prompt-project-dir)
+ pr (project--find-in-directory dir))))
+ (if pr
+ (project--add-to-project-list-front pr)
+ (project--remove-from-project-list dir)
+ (setq pr (cons 'transient dir)))
pr))
(defun project--find-in-directory (dir)
@@ -662,6 +666,19 @@ PREDICATE, HIST, and DEFAULT have the same meaning as in
collection predicate t res hist nil)))
res))
+;;;###autoload
+(defun project-dired ()
+ "Open Dired in the current project."
+ (interactive)
+ (dired (project-root (project-current t))))
+
+;;;###autoload
+(defun project-eshell ()
+ "Open Eshell in the current project."
+ (interactive)
+ (let ((default-directory (project-root (project-current t))))
+ (eshell t)))
+
(declare-function fileloop-continue "fileloop" ())
;;;###autoload
@@ -697,5 +714,127 @@ loop using the command \\[fileloop-continue]."
(default-directory (project-root pr)))
(call-interactively 'compile)))
+
+;;; Project list
+
+(defvar project--list 'unset
+ "List of known project directories.")
+
+(defun project--ensure-file-exists (filename)
+ "Create an empty file FILENAME if it doesn't exist."
+ (unless (file-exists-p filename)
+ (with-temp-buffer
+ (write-file filename))))
+
+(defun project--read-project-list ()
+ "Initialize `project--list' from the project list file."
+ (let ((filename (locate-user-emacs-file "project-list")))
+ (project--ensure-file-exists filename)
+ (with-temp-buffer
+ (insert-file-contents filename)
+ (let ((dirs (split-string (buffer-string) "\n" t))
+ (project-list '()))
+ (dolist (dir dirs)
+ (cl-pushnew (file-name-as-directory dir)
+ project-list
+ :test #'equal))
+ (setq project--list (reverse project-list))))))
+
+(defun project--ensure-read-project-list ()
+ "Initialize `project--list' if it hasn't already been."
+ (when (eq project--list 'unset)
+ (project--read-project-list)))
+
+(defun project--write-project-list ()
+ "Persist `project--list' to the project list file."
+ (let ((filename (locate-user-emacs-file "project-list")))
+ (with-temp-buffer
+ (insert (string-join project--list "\n"))
+ (write-region nil nil filename nil 'silent))))
+
+(defun project--add-to-project-list-front (pr)
+ "Add project PR to the front of the project list and save it.
+Return PR."
+ (project--ensure-read-project-list)
+ (let ((dir (project-root pr)))
+ (setq project--list (delete dir project--list))
+ (push dir project--list))
+ (project--write-project-list)
+ pr)
+
+(defun project--remove-from-project-list (pr-dir)
+ "Remove directory PR-DIR from the project list.
+If the directory was in the list before the removal, save the
+result to disk."
+ (project--ensure-read-project-list)
+ ;; XXX: This hardcodes that the number of roots = 1.
+ ;; It's fine, though.
+ (when (member pr-dir project--list)
+ (setq project--list (delete pr-dir project--list))
+ (message "Project `%s' not found; removed from list" pr-dir)
+ (project--write-project-list)))
+
+(defun project-prompt-project-dir ()
+ "Prompt the user for a directory from known project roots.
+The project is chosen among projects known from the project list.
+It's also possible to enter an arbitrary directory."
+ (project--ensure-read-project-list)
+ (let* ((dir-choice "... (choose a dir)")
+ (choices
+ ;; XXX: Just using this for the category (for the substring
+ ;; completion style).
+ (project--file-completion-table
+ (append project--list `(,dir-choice))))
+ (pr-dir (completing-read "Project: " choices nil t)))
+ (if (equal pr-dir dir-choice)
+ (read-directory-name "Choose directory: " default-directory nil t)
+ pr-dir)))
+
+
+;;; Project switching
+
+;;;###autoload
+(defvar project-switch-commands
+ '(("f" "Find file" project-find-file)
+ ("s" "Find regexp" project-find-regexp)
+ ("d" "Dired" project-dired)
+ ("e" "Eshell" project-eshell))
+ "Alist mapping keys to project switching menu entries.
+Used by `project-switch-project' to construct a dispatch menu of
+commands available upon \"switching\" to another project.
+
+Each element looks like (KEY LABEL COMMAND), where COMMAND is the
+command to run when KEY is pressed. LABEL is used to distinguish
+the choice in the dispatch menu.")
+
+(defun project--keymap-prompt ()
+ "Return a prompt for the project swithing dispatch menu."
+ (mapconcat
+ (pcase-lambda (`(,key ,label))
+ (format "[%s] %s"
+ (propertize (key-description `(,key)) 'face 'bold)
+ label))
+ project-switch-commands
+ " "))
+
+;;;###autoload
+(defun project-switch-project ()
+ "\"Switch\" to another project by running a chosen command.
+The available commands are picked from `project-switch-commands'
+and presented in a dispatch menu."
+ (interactive)
+ (let ((dir (project-prompt-project-dir))
+ (choice nil))
+ (while (not (and choice
+ (or (equal choice (kbd "C-g"))
+ (assoc choice project-switch-commands))))
+ (setq choice (read-key-sequence (project--keymap-prompt))))
+ (if (equal choice (kbd "C-g"))
+ (message "Quit")
+ (let ((default-directory dir)
+ (project-current-inhibit-prompt t))
+ (call-interactively
+ (nth 2 (assoc choice project-switch-commands)))))))
+
(provide 'project)
;;; project.el ends here