From afb7602a24cdb38c02998cc1f3c538b31981b255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Sat, 9 May 2020 17:27:06 +0200 Subject: [PATCH 01/17] Add project switching functionality * lisp/progmodes/project.el: Require subr-x. (project--transient-p, project--ensure-file-exists) (project--read-project-list, project--ensure-read-project-list) (project--write-project-list) (project--add-to-project-list-front) (project--remove-from-project-list, project-find-project) (project-switch-project-find-file, project-switch-project-dired) (project-switch-project-eshell, project-add-switch-command) (project--keymap-prompt, project-switch-project): New functions. (project--list, project-switch-keymap): New variables. (project-current): Call 'project-find-project' when no project is current. --- lisp/progmodes/project.el | 172 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 88f73e4fb31..e77416397bc 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,23 @@ 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.") +(defun project--transient-p (pr) + "Return non-nil if PR is a transient project." + (eq (car pr) 'transient)) + ;;;###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))))) + (setq pr (project-find-project)))) + (when (and pr (not (project--transient-p pr))) + (project--add-to-project-list-front pr)) pr)) (defun project--find-in-directory (dir) @@ -697,5 +698,160 @@ 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 (string-trim (buffer-string)) "\n")) + (project-list '())) + (dolist (dir dirs) + (cl-pushnew (list (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 (mapcar #'car 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 ((dirs (project-roots pr))) + (setq project--list (delete dirs project--list)) + (push dirs project--list)) + (project--write-project-list) + pr) + +(defun project--remove-from-project-list (pr-dir) + "Remove directory PR-DIR from the project list and save it." + (project--ensure-read-project-list) + (setq project--list (delete (list pr-dir) project--list)) + (project--write-project-list)) + +(defun project-find-project () + "Prompt the user for a project and return it. +The project is chosen among projects known from the project list. +It's also possible to enter an arbitrary directory, in which case +a project for that directory is returned (possibly a transient +one). Return nil if no project or directory was chosen." + (project--ensure-read-project-list) + (let* ((dir-choice "... (choose a dir)") + (choices (append project--list `(,dir-choice))) + (pr-dir (completing-read "Project: " choices))) + (if (equal pr-dir dir-choice) + (let ((dir (read-directory-name + "Choose directory: " default-directory nil t))) + (if-let (pr (project--find-in-directory dir)) + (project--add-to-project-list-front pr) + (message "Using `%s' as a transient project root" dir) + (cons 'transient dir))) + (if-let (pr (project--find-in-directory pr-dir)) + (project--add-to-project-list-front pr) + (project--remove-from-project-list pr-dir) + (message "Project `%s' not found; removed from list" pr-dir) + nil)))) + + +;;; Project switching + +(defvar project-switch-keymap (make-sparse-keymap) + "Keymap of commands for \"switching\" to a project. +Used by `project-switch-project' to construct a dispatch menu of +commands available for \"switching\" to another project.") + +;;;###autoload +(defun project-switch-project-find-file (&optional pr) + "\"Switch\" to project PR by finding a file in it. +If PR is nil, prompt for a project." + (interactive) + (setq pr (or pr (project-find-project))) + (let ((dirs (project-roots pr))) + (project-find-file-in nil dirs pr))) + +;;;###autoload +(defun project-switch-project-dired (&optional pr) + "\"Switch\" to project PR by visiting its root with Dired. +If PR is nil, prompt for a project." + (interactive) + (let ((dirs (project-roots (or pr (project-find-project))))) + (dired (car dirs)))) + +;;;###autoload +(defun project-switch-project-eshell (&optional pr) + "\"Switch\" to project PR by launching Eshell in its root. +If PR is nil, prompt for a project." + (interactive) + (let* ((dirs (project-roots (or pr (project-find-project)))) + (default-directory (car dirs))) + (eshell t))) + +;;;###autoload +(defun project-add-switch-command (symbol key label) + "Add a function to the project switching dispatch menu. +SYMBOL should stand for a function to be invoked by the key KEY. +LABEL is used to distinguish the function in the dispatch menu." + (function-put symbol 'dispatch-label label) + (define-key project-switch-keymap key symbol)) + +(project-add-switch-command + 'project-switch-project-find-file "f" "Find file") + +(project-add-switch-command + 'project-switch-project-dired "d" "Dired") + +(project-add-switch-command + 'project-switch-project-eshell "e" "Eshell") + +(defun project--keymap-prompt () + "Return a prompt for the project swithing dispatch menu." + (let ((prompt "")) + (map-keymap + (lambda (event value) + (let ((key (propertize (key-description `(,event)) 'face 'bold)) + (desc (function-get value 'dispatch-label))) + (setq prompt (concat (format "[%s] %s " key desc) prompt)))) + project-switch-keymap) + prompt)) + +;;;###autoload +(defun project-switch-project () + "\"Switch\" to another project by running a chosen command. +The available commands are picked from `project-switch-keymap' +and presented in a dispatch menu." + (interactive) + (let ((pr (project-find-project)) + (choice nil)) + (while (not (and choice + (or (equal choice (kbd "C-g")) + (lookup-key project-switch-keymap choice)))) + (setq choice (read-key-sequence (project--keymap-prompt)))) + (if (equal choice (kbd "C-g")) + (message "Quit") + (funcall (lookup-key project-switch-keymap choice) pr)))) + (provide 'project) ;;; project.el ends here From 9f88356b6770fb710c47e277dbdb4ba31b463d08 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 12 May 2020 03:22:30 +0300 Subject: [PATCH 02/17] Simplify a little, and avoid duplicate commands * lisp/progmodes/project.el: (project--transient-p) Remove, not needed. (project-current): Move project-find based on the directory here. (project--remove-from-project-list): Only write if the list changed. (project-find-project): Rename to project-prompt-project-dir. Simply return the directory selected by the user. (project-switch-project-find-file): Remove. (project-switch-project-dired): Rename to project-dired and make it follow the convention of existing projec tcommands. (project-switch-project-eshell): Ditto. (project-switch-project): Instead of passing the project instance to the command, just bind default-directory. --- lisp/progmodes/project.el | 82 ++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 49 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index e77416397bc..fd691be5599 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -101,10 +101,6 @@ 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.") -(defun project--transient-p (pr) - "Return non-nil if PR is a transient project." - (eq (car pr) 'transient)) - ;;;###autoload (defun project-current (&optional maybe-prompt dir) "Return the project instance in DIR or `default-directory'. @@ -115,9 +111,12 @@ the user for a different project to look in." (cond (pr) (maybe-prompt - (setq pr (project-find-project)))) - (when (and pr (not (project--transient-p pr))) - (project--add-to-project-list-front pr)) + (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) @@ -747,33 +746,28 @@ Return PR." pr) (defun project--remove-from-project-list (pr-dir) - "Remove directory PR-DIR from the project list and save it." + "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) - (setq project--list (delete (list pr-dir) project--list)) - (project--write-project-list)) + ;; XXX: This hardcodes that the number of roots = 1. + ;; It's fine, though. + (when (member (list pr-dir) project--list) + (setq project--list (delete (list pr-dir) project--list)) + (message "Project `%s' not found; removed from list" pr-dir) + (project--write-project-list))) -(defun project-find-project () - "Prompt the user for a project and return it. +(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, in which case -a project for that directory is returned (possibly a transient -one). Return nil if no project or directory was chosen." +It's also possible to enter an arbitrary directory." (project--ensure-read-project-list) (let* ((dir-choice "... (choose a dir)") (choices (append project--list `(,dir-choice))) (pr-dir (completing-read "Project: " choices))) (if (equal pr-dir dir-choice) - (let ((dir (read-directory-name - "Choose directory: " default-directory nil t))) - (if-let (pr (project--find-in-directory dir)) - (project--add-to-project-list-front pr) - (message "Using `%s' as a transient project root" dir) - (cons 'transient dir))) - (if-let (pr (project--find-in-directory pr-dir)) - (project--add-to-project-list-front pr) - (project--remove-from-project-list pr-dir) - (message "Project `%s' not found; removed from list" pr-dir) - nil)))) + (read-directory-name "Choose directory: " default-directory nil t) + pr-dir))) ;;; Project switching @@ -784,28 +778,17 @@ Used by `project-switch-project' to construct a dispatch menu of commands available for \"switching\" to another project.") ;;;###autoload -(defun project-switch-project-find-file (&optional pr) - "\"Switch\" to project PR by finding a file in it. -If PR is nil, prompt for a project." +(defun project-dired () + "Open Dired in the current project." (interactive) - (setq pr (or pr (project-find-project))) - (let ((dirs (project-roots pr))) - (project-find-file-in nil dirs pr))) - -;;;###autoload -(defun project-switch-project-dired (&optional pr) - "\"Switch\" to project PR by visiting its root with Dired. -If PR is nil, prompt for a project." - (interactive) - (let ((dirs (project-roots (or pr (project-find-project))))) + (let ((dirs (project-roots (project-current t)))) (dired (car dirs)))) ;;;###autoload -(defun project-switch-project-eshell (&optional pr) - "\"Switch\" to project PR by launching Eshell in its root. -If PR is nil, prompt for a project." +(defun project-eshell () + "Open Eshell in the current project." (interactive) - (let* ((dirs (project-roots (or pr (project-find-project)))) + (let* ((dirs (project-roots (project-current t))) (default-directory (car dirs))) (eshell t))) @@ -818,13 +801,13 @@ LABEL is used to distinguish the function in the dispatch menu." (define-key project-switch-keymap key symbol)) (project-add-switch-command - 'project-switch-project-find-file "f" "Find file") + 'project-find-file "f" "Find file") (project-add-switch-command - 'project-switch-project-dired "d" "Dired") + 'project-dired "d" "Dired") (project-add-switch-command - 'project-switch-project-eshell "e" "Eshell") + 'project-eshell "e" "Eshell") (defun project--keymap-prompt () "Return a prompt for the project swithing dispatch menu." @@ -843,15 +826,16 @@ LABEL is used to distinguish the function in the dispatch menu." The available commands are picked from `project-switch-keymap' and presented in a dispatch menu." (interactive) - (let ((pr (project-find-project)) - (choice nil)) + (let* ((dir (project-prompt-project-dir)) + (choice nil)) (while (not (and choice (or (equal choice (kbd "C-g")) (lookup-key project-switch-keymap choice)))) (setq choice (read-key-sequence (project--keymap-prompt)))) (if (equal choice (kbd "C-g")) (message "Quit") - (funcall (lookup-key project-switch-keymap choice) pr)))) + (let ((default-directory dir)) + (funcall (lookup-key project-switch-keymap choice)))))) (provide 'project) ;;; project.el ends here From 9422fb5e686a66898c2de76226f8a404ab253136 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 12 May 2020 03:32:05 +0300 Subject: [PATCH 03/17] Improve project name completion * lisp/progmodes/project.el: (project-prompt-project-dir): Use REQUIRE-MATCH=t. Make sure the 'substring' completion style is used by default. --- lisp/progmodes/project.el | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index fd691be5599..3e943ca0533 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -763,8 +763,12 @@ 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 (append project--list `(,dir-choice))) - (pr-dir (completing-read "Project: " choices))) + (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))) From c8cca68751ac3ebd702ab809bc2bb2cc352a190e Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 12 May 2020 04:13:48 +0300 Subject: [PATCH 04/17] Use an alist instead of a keymap * lisp/progmodes/project.el: (project--switch-alist): New variable to use instead of project-switch-keymap, which remove. Update all references. --- lisp/progmodes/project.el | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 3e943ca0533..7209246c228 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -776,10 +776,10 @@ It's also possible to enter an arbitrary directory." ;;; Project switching -(defvar project-switch-keymap (make-sparse-keymap) - "Keymap of commands for \"switching\" to a project. +(defvar project--switch-alist nil + "Association list mapping characters to commands. Used by `project-switch-project' to construct a dispatch menu of -commands available for \"switching\" to another project.") +commands available upon \"switching\" to another project.") ;;;###autoload (defun project-dired () @@ -802,7 +802,8 @@ commands available for \"switching\" to another project.") SYMBOL should stand for a function to be invoked by the key KEY. LABEL is used to distinguish the function in the dispatch menu." (function-put symbol 'dispatch-label label) - (define-key project-switch-keymap key symbol)) + ;; XXX: It could host the label as well now. + (add-to-list 'project--switch-alist `(,key . ,symbol))) (project-add-switch-command 'project-find-file "f" "Find file") @@ -816,12 +817,13 @@ LABEL is used to distinguish the function in the dispatch menu." (defun project--keymap-prompt () "Return a prompt for the project swithing dispatch menu." (let ((prompt "")) - (map-keymap - (lambda (event value) - (let ((key (propertize (key-description `(,event)) 'face 'bold)) - (desc (function-get value 'dispatch-label))) + (mapc + (lambda (entry) + (pcase-let* ((`(,char . ,symbol) entry) + (key (propertize (key-description `(,char)) 'face 'bold)) + (desc (function-get symbol 'dispatch-label))) (setq prompt (concat (format "[%s] %s " key desc) prompt)))) - project-switch-keymap) + project--switch-alist) prompt)) ;;;###autoload @@ -834,12 +836,12 @@ and presented in a dispatch menu." (choice nil)) (while (not (and choice (or (equal choice (kbd "C-g")) - (lookup-key project-switch-keymap choice)))) + (assoc choice project--switch-alist)))) (setq choice (read-key-sequence (project--keymap-prompt)))) (if (equal choice (kbd "C-g")) (message "Quit") (let ((default-directory dir)) - (funcall (lookup-key project-switch-keymap choice)))))) + (funcall (assoc-default choice project--switch-alist)))))) (provide 'project) ;;; project.el ends here From afb96da6111ed0f9c26b6b0ff91aaddfec6277a5 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 12 May 2020 04:17:23 +0300 Subject: [PATCH 05/17] Move project-dired and project-eshell higher * lisp/progmodes/project.el: (project-dired, project-eshell): Move higher in the file, according to their universal utility. --- lisp/progmodes/project.el | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 7209246c228..12b7e3fdb93 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -662,6 +662,21 @@ 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) + (let ((dirs (project-roots (project-current t)))) + (dired (car dirs)))) + +;;;###autoload +(defun project-eshell () + "Open Eshell in the current project." + (interactive) + (let* ((dirs (project-roots (project-current t))) + (default-directory (car dirs))) + (eshell t))) + (declare-function fileloop-continue "fileloop" ()) ;;;###autoload @@ -781,21 +796,6 @@ It's also possible to enter an arbitrary directory." Used by `project-switch-project' to construct a dispatch menu of commands available upon \"switching\" to another project.") -;;;###autoload -(defun project-dired () - "Open Dired in the current project." - (interactive) - (let ((dirs (project-roots (project-current t)))) - (dired (car dirs)))) - -;;;###autoload -(defun project-eshell () - "Open Eshell in the current project." - (interactive) - (let* ((dirs (project-roots (project-current t))) - (default-directory (car dirs))) - (eshell t))) - ;;;###autoload (defun project-add-switch-command (symbol key label) "Add a function to the project switching dispatch menu. From 02e1ee95a8d6459f455f1bfb5953b8798f069645 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 12 May 2020 04:20:26 +0300 Subject: [PATCH 06/17] Integrate project-switch-project with project-find-regexp * lisp/progmodes/project.el: (project-find-regexp): Add to the list of 'switch' commands. (project-switch-project): Use call-interactively so that the former can read its arguments. --- lisp/progmodes/project.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 12b7e3fdb93..da865255e36 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -808,6 +808,9 @@ LABEL is used to distinguish the function in the dispatch menu." (project-add-switch-command 'project-find-file "f" "Find file") +(project-add-switch-command + 'project-find-regexp "g" "Find regexp") + (project-add-switch-command 'project-dired "d" "Dired") @@ -841,7 +844,7 @@ and presented in a dispatch menu." (if (equal choice (kbd "C-g")) (message "Quit") (let ((default-directory dir)) - (funcall (assoc-default choice project--switch-alist)))))) + (call-interactively (assoc-default choice project--switch-alist)))))) (provide 'project) ;;; project.el ends here From 46bb2cbd00eb29eb6bb68f2bd8e47c94365d4e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Sat, 16 May 2020 09:00:49 +0200 Subject: [PATCH 07/17] Change dispatch binding of 'project-find-regexp' * lisp/progmodes/project.el: Change default dispatch binding of 'project-find-regexp' to 's'. --- lisp/progmodes/project.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index da865255e36..edf690cdf9c 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -809,7 +809,7 @@ LABEL is used to distinguish the function in the dispatch menu." 'project-find-file "f" "Find file") (project-add-switch-command - 'project-find-regexp "g" "Find regexp") + 'project-find-regexp "s" "Find regexp") (project-add-switch-command 'project-dired "d" "Dired") From c6f56bd279bf466450fe9174b3b09201c844eca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Sat, 16 May 2020 09:53:43 +0200 Subject: [PATCH 08/17] Turn project switch menu var into a public alist * lisp/progmodes/project.el: Require seq. (project--switch-alist): Remove in favor of the public 'project-switch-menu'. (project-add-switch-command): Remove; not needed now that 'project-switch-menu' is a public alist. (project-switch-menu): New variable mapping keys to project switching menu entries. (project--keymap-prompt, project-switch-project): Adjust to the new 'project-switch-menu' format. --- lisp/progmodes/project.el | 64 ++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index edf690cdf9c..a00bb703814 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -93,6 +93,7 @@ ;;; Code: (require 'cl-generic) +(require 'seq) (eval-when-compile (require 'subr-x)) (defvar project-find-functions (list #'project-try-vc) @@ -791,60 +792,47 @@ It's also possible to enter an arbitrary directory." ;;; Project switching -(defvar project--switch-alist nil - "Association list mapping characters to commands. -Used by `project-switch-project' to construct a dispatch menu of -commands available upon \"switching\" to another project.") - ;;;###autoload -(defun project-add-switch-command (symbol key label) - "Add a function to the project switching dispatch menu. -SYMBOL should stand for a function to be invoked by the key KEY. -LABEL is used to distinguish the function in the dispatch menu." - (function-put symbol 'dispatch-label label) - ;; XXX: It could host the label as well now. - (add-to-list 'project--switch-alist `(,key . ,symbol))) +(defvar project-switch-menu + '(("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. -(project-add-switch-command - 'project-find-file "f" "Find file") - -(project-add-switch-command - 'project-find-regexp "s" "Find regexp") - -(project-add-switch-command - 'project-dired "d" "Dired") - -(project-add-switch-command - 'project-eshell "e" "Eshell") +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." - (let ((prompt "")) - (mapc - (lambda (entry) - (pcase-let* ((`(,char . ,symbol) entry) - (key (propertize (key-description `(,char)) 'face 'bold)) - (desc (function-get symbol 'dispatch-label))) - (setq prompt (concat (format "[%s] %s " key desc) prompt)))) - project--switch-alist) - prompt)) + (string-trim + (seq-mapcat + (pcase-lambda (`(,key ,label)) + (format "[%s] %s " + (propertize (key-description `(,key)) 'face 'bold) + label)) + project-switch-menu 'string))) ;;;###autoload (defun project-switch-project () "\"Switch\" to another project by running a chosen command. -The available commands are picked from `project-switch-keymap' -and presented in a dispatch menu." +The available commands are picked from `project-switch-menu' and +presented in a dispatch menu." (interactive) - (let* ((dir (project-prompt-project-dir)) - (choice nil)) + (let ((dir (project-prompt-project-dir)) + (choice nil)) (while (not (and choice (or (equal choice (kbd "C-g")) - (assoc choice project--switch-alist)))) + (assoc choice project-switch-menu)))) (setq choice (read-key-sequence (project--keymap-prompt)))) (if (equal choice (kbd "C-g")) (message "Quit") (let ((default-directory dir)) - (call-interactively (assoc-default choice project--switch-alist)))))) + (call-interactively + (nth 2 (assoc choice project-switch-menu))))))) (provide 'project) ;;; project.el ends here From c6e80fdb65ca425d4826f48c348cfd9e30f8eb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Sun, 17 May 2020 08:49:08 +0200 Subject: [PATCH 09/17] Simplify 'project--keymap-prompt' a bit * lisp/progmodes/project.el: Remove seq requirement. (project--keymap-prompt): Simplify with 'mapconcat'. --- lisp/progmodes/project.el | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index a00bb703814..7f765e628eb 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -93,7 +93,6 @@ ;;; Code: (require 'cl-generic) -(require 'seq) (eval-when-compile (require 'subr-x)) (defvar project-find-functions (list #'project-try-vc) @@ -808,13 +807,13 @@ the choice in the dispatch menu.") (defun project--keymap-prompt () "Return a prompt for the project swithing dispatch menu." - (string-trim - (seq-mapcat - (pcase-lambda (`(,key ,label)) - (format "[%s] %s " - (propertize (key-description `(,key)) 'face 'bold) - label)) - project-switch-menu 'string))) + (mapconcat + (pcase-lambda (`(,key ,label)) + (format "[%s] %s" + (propertize (key-description `(,key)) 'face 'bold) + label)) + project-switch-menu + " ")) ;;;###autoload (defun project-switch-project () From 70824683fd996a78d417e2d12b9f6bf162c364d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Sun, 17 May 2020 13:54:32 +0200 Subject: [PATCH 10/17] Rename 'project-switch-menu' to 'project-switch-commands' * lisp/progmodes/project.el (project-switch-commands): Rename from 'project-switch-menu'. (project--keymap-prompt, project-switch-project): Update after the renaming. --- lisp/progmodes/project.el | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 7f765e628eb..956a4b8a345 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -792,7 +792,7 @@ It's also possible to enter an arbitrary directory." ;;; Project switching ;;;###autoload -(defvar project-switch-menu +(defvar project-switch-commands '(("f" "Find file" project-find-file) ("s" "Find regexp" project-find-regexp) ("d" "Dired" project-dired) @@ -812,26 +812,26 @@ the choice in the dispatch menu.") (format "[%s] %s" (propertize (key-description `(,key)) 'face 'bold) label)) - project-switch-menu + 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-menu' and -presented in a dispatch menu." +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-menu)))) + (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)) (call-interactively - (nth 2 (assoc choice project-switch-menu))))))) + (nth 2 (assoc choice project-switch-commands))))))) (provide 'project) ;;; project.el ends here From 0db801a3adf8e8584c9b071764200259336a8434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Mon, 18 May 2020 18:17:10 +0200 Subject: [PATCH 11/17] Update the Emacs manual with recent project.el changes * doc/emacs/maintaining.texi (Projects): Add a menu. (Project File Commands): New subsection describing project file commands (moved here from 'Working with Projects'). Describe the new commands 'project-dired' and 'project-eshell'. (Switching Projects): New subsection. * etc/NEWS: Mention project.el changes. --- doc/emacs/maintaining.texi | 34 +++++++++++++++++++++++++++++++++- etc/NEWS | 13 +++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index ebcdddfcae3..adaafdbeccd 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,30 @@ 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 a project is current but you want to +operate on a different project, the command +@code{project-switch-project} can be used. + + This command prompts you for a new project directory, and then +displays a menu of commands avilable for operating on the chosen +project. The variable @code{project-switch-commands} controls which +commands are avilable 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 e97755a4541..7424507e82f 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -389,6 +389,19 @@ information, see the related entry about 'shr-browse-url' above. *** New user option 'project-vc-merge-submodules'. +*** Previously used project directories are now are now completed 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 by running a project +command chosen from a dispatch menu. + ** json.el --- From 0b057ca9bcfa2bbef57f7e5a1da47d62f4f1f15a Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 19 May 2020 01:00:43 +0300 Subject: [PATCH 12/17] Teach project-current to inhibit the prompt * lisp/progmodes/project.el: (project-current-inhibit-prompt): New variable. (project-current, project-switch-project): Use it. --- lisp/progmodes/project.el | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 956a4b8a345..132d172a08e 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -101,6 +101,9 @@ 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'. @@ -110,7 +113,8 @@ the user for a different project to look in." (let ((pr (project--find-in-directory dir))) (cond (pr) - (maybe-prompt + ((unless project-current-inhibit-prompt + maybe-prompt) (setq dir (project-prompt-project-dir) pr (project--find-in-directory dir)))) (if pr @@ -829,7 +833,8 @@ and presented in a dispatch menu." (setq choice (read-key-sequence (project--keymap-prompt)))) (if (equal choice (kbd "C-g")) (message "Quit") - (let ((default-directory dir)) + (let ((default-directory dir) + (project-current-inhibit-prompt t)) (call-interactively (nth 2 (assoc choice project-switch-commands))))))) From e37e6c8d91c1028fbba52466f40dca2d82b95feb Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 19 May 2020 01:27:37 +0300 Subject: [PATCH 13/17] Some copy edits * doc/emacs/maintaining.texi (Switching Projects) (Project File Commands): Copy edits. * etc/NEWS: Same. --- doc/emacs/maintaining.texi | 15 ++++++++------- etc/NEWS | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index adaafdbeccd..5388b13f437 100644 --- a/doc/emacs/maintaining.texi +++ b/doc/emacs/maintaining.texi @@ -1719,23 +1719,24 @@ continue with @w{@kbd{M-x fileloop-continue @key{RET}}}. directory. @findex project-eshell - The command @code{project-eshell} starts an Eshell session in a new + 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 that operate on project files (@pxref{Project File Commands}) will conveniently prompt you for a project directory when -no project is current. When a project is current but you want to +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 for a new project directory, and then -displays a menu of commands avilable for operating on the chosen -project. The variable @code{project-switch-commands} controls which -commands are avilable in the menu, and by which keys they are invoked. + 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 avilable 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 7424507e82f..90aea8796de 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -389,7 +389,7 @@ information, see the related entry about 'shr-browse-url' above. *** New user option 'project-vc-merge-submodules'. -*** Previously used project directories are now are now completed by +*** Previously used project directories are now suggested by all commands that prompt for a project directory. +++ @@ -399,7 +399,7 @@ respectively. +++ *** New command 'project-switch-project'. -This command lets you "switch" to another project by running a project +This command lets you "switch" to another project and run a project command chosen from a dispatch menu. ** json.el From 5a48ede3acb1f21a8dd6d56ebbc675b7225d28e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Mon, 25 May 2020 21:10:03 +0200 Subject: [PATCH 14/17] Adapt project functions to the new 'project-root' * lisp/progmodes/project.el (project-dired, project-eshell) (project--read-project-list, project--write-project-list) (project--add-to-project-list-front) (project--remove-from-project-list): Adapt to the new 'project-root'. --- lisp/progmodes/project.el | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 132d172a08e..f00aca83d2d 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -670,15 +670,13 @@ PREDICATE, HIST, and DEFAULT have the same meaning as in (defun project-dired () "Open Dired in the current project." (interactive) - (let ((dirs (project-roots (project-current t)))) - (dired (car dirs)))) + (dired (project-root (project-current t)))) ;;;###autoload (defun project-eshell () "Open Eshell in the current project." (interactive) - (let* ((dirs (project-roots (project-current t))) - (default-directory (car dirs))) + (let ((default-directory (project-root (project-current t)))) (eshell t))) (declare-function fileloop-continue "fileloop" ()) @@ -737,7 +735,7 @@ loop using the command \\[fileloop-continue]." (let ((dirs (split-string (string-trim (buffer-string)) "\n")) (project-list '())) (dolist (dir dirs) - (cl-pushnew (list (file-name-as-directory dir)) + (cl-pushnew (file-name-as-directory dir) project-list :test #'equal)) (setq project--list (reverse project-list)))))) @@ -751,16 +749,16 @@ loop using the command \\[fileloop-continue]." "Persist `project--list' to the project list file." (let ((filename (locate-user-emacs-file "project-list"))) (with-temp-buffer - (insert (string-join (mapcar #'car project--list) "\n")) + (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 ((dirs (project-roots pr))) - (setq project--list (delete dirs project--list)) - (push dirs project--list)) + (let ((dir (project-root pr))) + (setq project--list (delete dir project--list)) + (push dir project--list)) (project--write-project-list) pr) @@ -771,8 +769,8 @@ result to disk." (project--ensure-read-project-list) ;; XXX: This hardcodes that the number of roots = 1. ;; It's fine, though. - (when (member (list pr-dir) project--list) - (setq project--list (delete (list pr-dir) project--list)) + (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))) From 449810bbe99d6f6ad6f1e0dfa1edc3c8997e5465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Mon, 25 May 2020 21:29:02 +0200 Subject: [PATCH 15/17] Avoid adding the empty string to the project list * lisp/progmodes/project.el (project--read-project-list): Avoid adding the empty string to the project list. --- lisp/progmodes/project.el | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index f00aca83d2d..be0b2e4d5f5 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -735,9 +735,10 @@ loop using the command \\[fileloop-continue]." (let ((dirs (split-string (string-trim (buffer-string)) "\n")) (project-list '())) (dolist (dir dirs) - (cl-pushnew (file-name-as-directory dir) - project-list - :test #'equal)) + (unless (string-empty-p dir) + (cl-pushnew (file-name-as-directory dir) + project-list + :test #'equal))) (setq project--list (reverse project-list)))))) (defun project--ensure-read-project-list () From b7dffcb501d30297e2fa8184b587da18f458ca66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Wed, 27 May 2020 17:17:15 +0200 Subject: [PATCH 16/17] Simplify the previous commit * lisp/progmodes/project.el (project--read-project-list): Simplify the previous commit by utilizing the optional OMIT-NULLS argument to 'split-string'. --- lisp/progmodes/project.el | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index be0b2e4d5f5..a3e81d4d3aa 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -732,13 +732,12 @@ loop using the command \\[fileloop-continue]." (project--ensure-file-exists filename) (with-temp-buffer (insert-file-contents filename) - (let ((dirs (split-string (string-trim (buffer-string)) "\n")) + (let ((dirs (split-string (buffer-string) "\n" t)) (project-list '())) (dolist (dir dirs) - (unless (string-empty-p dir) - (cl-pushnew (file-name-as-directory dir) - project-list - :test #'equal))) + (cl-pushnew (file-name-as-directory dir) + project-list + :test #'equal)) (setq project--list (reverse project-list)))))) (defun project--ensure-read-project-list () From 9823c66b885c0c310061489bd732f3888a802b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Thu, 28 May 2020 16:40:30 +0200 Subject: [PATCH 17/17] ; * doc/emacs/maintaining.texi: Fix typo. --- doc/emacs/maintaining.texi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index 5388b13f437..22b7639d349 100644 --- a/doc/emacs/maintaining.texi +++ b/doc/emacs/maintaining.texi @@ -1735,7 +1735,7 @@ operate on a different project, the command 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 avilable in the menu, and by which keys +controls which commands are available in the menu, and by which keys they are invoked. @node Change Log