aboutsummaryrefslogtreecommitdiffstats
path: root/lisp
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 /lisp
parent2bdb2cd10d08a1d9c9a187c7d967fdc64b8e6743 (diff)
parent9823c66b885c0c310061489bd732f3888a802b01 (diff)
downloademacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.tar.gz
emacs-d97f224fd0db2ee13150ec7c4d6311eab48cda9e.zip
Merge branch 'feature/project-switching'
Diffstat (limited to 'lisp')
-rw-r--r--lisp/progmodes/project.el157
1 files changed, 148 insertions, 9 deletions
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