diff options
| author | Simen Heggestøyl | 2020-05-28 17:02:17 +0200 |
|---|---|---|
| committer | Simen Heggestøyl | 2020-05-28 17:02:17 +0200 |
| commit | d97f224fd0db2ee13150ec7c4d6311eab48cda9e (patch) | |
| tree | 06417974e77ae3e27683f08a6508badc87ceb96d | |
| parent | 2bdb2cd10d08a1d9c9a187c7d967fdc64b8e6743 (diff) | |
| parent | 9823c66b885c0c310061489bd732f3888a802b01 (diff) | |
| download | emacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.tar.gz emacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.zip | |
Merge branch 'feature/project-switching'
| -rw-r--r-- | doc/emacs/maintaining.texi | 35 | ||||
| -rw-r--r-- | etc/NEWS | 13 | ||||
| -rw-r--r-- | lisp/progmodes/project.el | 157 |
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. | |||
| 1656 | the project back-end. For example, the VC back-end doesn't consider | 1656 | the 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. |
| 1660 | This section describes these commands. | 1668 | This 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 | |||
| 1705 | response causes Emacs to exit the query-replace loop, you can later | 1713 | response causes Emacs to exit the query-replace loop, you can later |
| 1706 | continue with @w{@kbd{M-x fileloop-continue @key{RET}}}. | 1714 | continue 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 | ||
| 1719 | directory. | ||
| 1720 | |||
| 1721 | @findex project-eshell | ||
| 1722 | The command @code{project-eshell} starts an Eshell session in a new | ||
| 1723 | buffer 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 | ||
| 1730 | Commands}) will conveniently prompt you for a project directory when | ||
| 1731 | no project is current. When you are inside a project but you want to | ||
| 1732 | operate 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 | ||
| 1736 | roots, and then displays the menu of available commands to operate on | ||
| 1737 | the chosen project. The variable @code{project-switch-commands} | ||
| 1738 | controls which commands are available in the menu, and by which keys | ||
| 1739 | they are invoked. | ||
| 1740 | |||
| 1708 | @node Change Log | 1741 | @node Change Log |
| 1709 | @section Change Logs | 1742 | @section Change Logs |
| 1710 | 1743 | ||
| @@ -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 | ||
| 399 | all commands that prompt for a project directory. | ||
| 400 | |||
| 401 | +++ | ||
| 402 | *** New commands 'project-dired' and 'project-eshell'. | ||
| 403 | These commands run Dired and Eshell in a project's root directory, | ||
| 404 | respectively. | ||
| 405 | |||
| 406 | +++ | ||
| 407 | *** New command 'project-switch-project'. | ||
| 408 | This command lets you "switch" to another project and run a project | ||
| 409 | command 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 | |||
| 100 | argument (the directory) and should return either nil to mean | 101 | argument (the directory) and should return either nil to mean |
| 101 | that it is not applicable, or a project instance.") | 102 | that 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'. |
| 106 | When no project found in DIR, and MAYBE-PROMPT is non-nil, ask | 110 | When no project found in DIR, and MAYBE-PROMPT is non-nil, ask |
| 107 | the user for a different directory to look in. If that directory | 111 | the user for a different project to look in." |
| 108 | is 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. | ||
| 757 | Return 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. | ||
| 767 | If the directory was in the list before the removal, save the | ||
| 768 | result 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. | ||
| 779 | The project is chosen among projects known from the project list. | ||
| 780 | It'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. | ||
| 803 | Used by `project-switch-project' to construct a dispatch menu of | ||
| 804 | commands available upon \"switching\" to another project. | ||
| 805 | |||
| 806 | Each element looks like (KEY LABEL COMMAND), where COMMAND is the | ||
| 807 | command to run when KEY is pressed. LABEL is used to distinguish | ||
| 808 | the 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. | ||
| 823 | The available commands are picked from `project-switch-commands' | ||
| 824 | and 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 |