Merge branch 'feature/project-switching'

This commit is contained in:
Simen Heggestøyl 2020-05-28 17:02:17 +02:00
commit d97f224fd0
3 changed files with 195 additions and 10 deletions

View file

@ -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

View file

@ -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
---

View file

@ -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