aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimen Heggestøyl2020-05-28 17:02:17 +0200
committerSimen Heggestøyl2020-05-28 17:02:17 +0200
commitd97f224fd0db2ee13150ec7c4d6311eab48cda9e (patch)
tree06417974e77ae3e27683f08a6508badc87ceb96d
parent2bdb2cd10d08a1d9c9a187c7d967fdc64b8e6743 (diff)
parent9823c66b885c0c310061489bd732f3888a802b01 (diff)
downloademacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.tar.gz
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.
1656the project back-end. For example, the VC back-end doesn't consider 1656the project back-end. For example, the VC back-end doesn't consider
1657``ignored'' files (@pxref{VC Ignore}) to be part of the project. 1657``ignored'' files (@pxref{VC Ignore}) to be part of the project.
1658 1658
1659@menu
1660* Project File Commands:: Commands for handling project files.
1661* Switching Projects:: Switching between projects.
1662@end menu
1663
1664@node Project File Commands
1665@subsection Project File Commands
1666
1659 Emacs provides commands for handling project files conveniently. 1667 Emacs provides commands for handling project files conveniently.
1660This section describes these commands. 1668This subsection describes these commands.
1661 1669
1662@cindex current project 1670@cindex current project
1663 All of the commands described here share the notion of the 1671 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
1705response causes Emacs to exit the query-replace loop, you can later 1713response causes Emacs to exit the query-replace loop, you can later
1706continue with @w{@kbd{M-x fileloop-continue @key{RET}}}. 1714continue with @w{@kbd{M-x fileloop-continue @key{RET}}}.
1707 1715
1716@findex project-dired
1717 The command @code{project-dired} opens a Dired buffer
1718(@pxref{Dired}) listing the files in the current project's root
1719directory.
1720
1721@findex project-eshell
1722 The command @code{project-eshell} starts an Eshell session in a new
1723buffer with the current project's root as the working directory.
1724@xref{Top,Eshell,Eshell, eshell, Eshell: The Emacs Shell}.
1725
1726@node Switching Projects
1727@subsection Switching Projects
1728
1729 Commands that operate on project files (@pxref{Project File
1730Commands}) will conveniently prompt you for a project directory when
1731no project is current. When you are inside a project but you want to
1732operate on a different project, the command
1733@code{project-switch-project} can be used.
1734
1735 This command prompts you to choose a directory among known project
1736roots, and then displays the menu of available commands to operate on
1737the chosen project. The variable @code{project-switch-commands}
1738controls which commands are available in the menu, and by which keys
1739they are invoked.
1740
1708@node Change Log 1741@node Change Log
1709@section Change Logs 1742@section Change Logs
1710 1743
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.
395 395
396*** New user option 'project-vc-merge-submodules'. 396*** New user option 'project-vc-merge-submodules'.
397 397
398*** Previously used project directories are now suggested by
399all commands that prompt for a project directory.
400
401+++
402*** New commands 'project-dired' and 'project-eshell'.
403These commands run Dired and Eshell in a project's root directory,
404respectively.
405
406+++
407*** New command 'project-switch-project'.
408This command lets you "switch" to another project and run a project
409command chosen from a dispatch menu.
410
398** json.el 411** json.el
399 412
400--- 413---
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 @@
93;;; Code: 93;;; Code:
94 94
95(require 'cl-generic) 95(require 'cl-generic)
96(eval-when-compile (require 'subr-x))
96 97
97(defvar project-find-functions (list #'project-try-vc) 98(defvar project-find-functions (list #'project-try-vc)
98 "Special hook to find the project containing a given directory. 99 "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
100argument (the directory) and should return either nil to mean 101argument (the directory) and should return either nil to mean
101that it is not applicable, or a project instance.") 102that it is not applicable, or a project instance.")
102 103
104(defvar project-current-inhibit-prompt nil
105 "Non-nil to skip prompting the user in `project-current'.")
106
103;;;###autoload 107;;;###autoload
104(defun project-current (&optional maybe-prompt dir) 108(defun project-current (&optional maybe-prompt dir)
105 "Return the project instance in DIR or `default-directory'. 109 "Return the project instance in DIR or `default-directory'.
106When no project found in DIR, and MAYBE-PROMPT is non-nil, ask 110When no project found in DIR, and MAYBE-PROMPT is non-nil, ask
107the user for a different directory to look in. If that directory 111the user for a different project to look in."
108is not a part of a detectable project either, return a
109`transient' project instance rooted in it."
110 (unless dir (setq dir default-directory)) 112 (unless dir (setq dir default-directory))
111 (let ((pr (project--find-in-directory dir))) 113 (let ((pr (project--find-in-directory dir)))
112 (cond 114 (cond
113 (pr) 115 (pr)
114 (maybe-prompt 116 ((unless project-current-inhibit-prompt
115 (setq dir (read-directory-name "Choose the project directory: " dir nil t) 117 maybe-prompt)
116 pr (project--find-in-directory dir)) 118 (setq dir (project-prompt-project-dir)
117 (unless pr 119 pr (project--find-in-directory dir))))
118 (message "Using `%s' as a transient project root" dir) 120 (if pr
119 (setq pr (cons 'transient dir))))) 121 (project--add-to-project-list-front pr)
122 (project--remove-from-project-list dir)
123 (setq pr (cons 'transient dir)))
120 pr)) 124 pr))
121 125
122(defun project--find-in-directory (dir) 126(defun project--find-in-directory (dir)
@@ -662,6 +666,19 @@ PREDICATE, HIST, and DEFAULT have the same meaning as in
662 collection predicate t res hist nil))) 666 collection predicate t res hist nil)))
663 res)) 667 res))
664 668
669;;;###autoload
670(defun project-dired ()
671 "Open Dired in the current project."
672 (interactive)
673 (dired (project-root (project-current t))))
674
675;;;###autoload
676(defun project-eshell ()
677 "Open Eshell in the current project."
678 (interactive)
679 (let ((default-directory (project-root (project-current t))))
680 (eshell t)))
681
665(declare-function fileloop-continue "fileloop" ()) 682(declare-function fileloop-continue "fileloop" ())
666 683
667;;;###autoload 684;;;###autoload
@@ -697,5 +714,127 @@ loop using the command \\[fileloop-continue]."
697 (default-directory (project-root pr))) 714 (default-directory (project-root pr)))
698 (call-interactively 'compile))) 715 (call-interactively 'compile)))
699 716
717
718;;; Project list
719
720(defvar project--list 'unset
721 "List of known project directories.")
722
723(defun project--ensure-file-exists (filename)
724 "Create an empty file FILENAME if it doesn't exist."
725 (unless (file-exists-p filename)
726 (with-temp-buffer
727 (write-file filename))))
728
729(defun project--read-project-list ()
730 "Initialize `project--list' from the project list file."
731 (let ((filename (locate-user-emacs-file "project-list")))
732 (project--ensure-file-exists filename)
733 (with-temp-buffer
734 (insert-file-contents filename)
735 (let ((dirs (split-string (buffer-string) "\n" t))
736 (project-list '()))
737 (dolist (dir dirs)
738 (cl-pushnew (file-name-as-directory dir)
739 project-list
740 :test #'equal))
741 (setq project--list (reverse project-list))))))
742
743(defun project--ensure-read-project-list ()
744 "Initialize `project--list' if it hasn't already been."
745 (when (eq project--list 'unset)
746 (project--read-project-list)))
747
748(defun project--write-project-list ()
749 "Persist `project--list' to the project list file."
750 (let ((filename (locate-user-emacs-file "project-list")))
751 (with-temp-buffer
752 (insert (string-join project--list "\n"))
753 (write-region nil nil filename nil 'silent))))
754
755(defun project--add-to-project-list-front (pr)
756 "Add project PR to the front of the project list and save it.
757Return PR."
758 (project--ensure-read-project-list)
759 (let ((dir (project-root pr)))
760 (setq project--list (delete dir project--list))
761 (push dir project--list))
762 (project--write-project-list)
763 pr)
764
765(defun project--remove-from-project-list (pr-dir)
766 "Remove directory PR-DIR from the project list.
767If the directory was in the list before the removal, save the
768result to disk."
769 (project--ensure-read-project-list)
770 ;; XXX: This hardcodes that the number of roots = 1.
771 ;; It's fine, though.
772 (when (member pr-dir project--list)
773 (setq project--list (delete pr-dir project--list))
774 (message "Project `%s' not found; removed from list" pr-dir)
775 (project--write-project-list)))
776
777(defun project-prompt-project-dir ()
778 "Prompt the user for a directory from known project roots.
779The project is chosen among projects known from the project list.
780It's also possible to enter an arbitrary directory."
781 (project--ensure-read-project-list)
782 (let* ((dir-choice "... (choose a dir)")
783 (choices
784 ;; XXX: Just using this for the category (for the substring
785 ;; completion style).
786 (project--file-completion-table
787 (append project--list `(,dir-choice))))
788 (pr-dir (completing-read "Project: " choices nil t)))
789 (if (equal pr-dir dir-choice)
790 (read-directory-name "Choose directory: " default-directory nil t)
791 pr-dir)))
792
793
794;;; Project switching
795
796;;;###autoload
797(defvar project-switch-commands
798 '(("f" "Find file" project-find-file)
799 ("s" "Find regexp" project-find-regexp)
800 ("d" "Dired" project-dired)
801 ("e" "Eshell" project-eshell))
802 "Alist mapping keys to project switching menu entries.
803Used by `project-switch-project' to construct a dispatch menu of
804commands available upon \"switching\" to another project.
805
806Each element looks like (KEY LABEL COMMAND), where COMMAND is the
807command to run when KEY is pressed. LABEL is used to distinguish
808the choice in the dispatch menu.")
809
810(defun project--keymap-prompt ()
811 "Return a prompt for the project swithing dispatch menu."
812 (mapconcat
813 (pcase-lambda (`(,key ,label))
814 (format "[%s] %s"
815 (propertize (key-description `(,key)) 'face 'bold)
816 label))
817 project-switch-commands
818 " "))
819
820;;;###autoload
821(defun project-switch-project ()
822 "\"Switch\" to another project by running a chosen command.
823The available commands are picked from `project-switch-commands'
824and presented in a dispatch menu."
825 (interactive)
826 (let ((dir (project-prompt-project-dir))
827 (choice nil))
828 (while (not (and choice
829 (or (equal choice (kbd "C-g"))
830 (assoc choice project-switch-commands))))
831 (setq choice (read-key-sequence (project--keymap-prompt))))
832 (if (equal choice (kbd "C-g"))
833 (message "Quit")
834 (let ((default-directory dir)
835 (project-current-inhibit-prompt t))
836 (call-interactively
837 (nth 2 (assoc choice project-switch-commands)))))))
838
700(provide 'project) 839(provide 'project)
701;;; project.el ends here 840;;; project.el ends here