summaryrefslogtreecommitdiff
path: root/lisp/progmodes/project.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/progmodes/project.el')
-rw-r--r--lisp/progmodes/project.el202
1 files changed, 127 insertions, 75 deletions
diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el
index abe563bec04..4620ea8f47e 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -1,7 +1,7 @@
;;; project.el --- Operations on the current project -*- lexical-binding: t; -*-
;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
-;; Version: 0.5.4
+;; Version: 0.6.1
;; Package-Requires: ((emacs "26.1") (xref "1.0.2"))
;; This is a GNU ELPA :core package. Avoid using functionality that
@@ -106,7 +106,7 @@
;;
;; - Write a new function that will determine the current project
;; based on the directory and add it to `project-find-functions'
-;; (which see) using `add-hook'. It is a good idea to depend on the
+;; (which see) using `add-hook'. It is a good idea to depend on the
;; directory only, and not on the current major mode, for example.
;; Because the usual expectation is that all files in the directory
;; belong to the same project (even if some/most of them are ignored).
@@ -201,20 +201,27 @@ of the project instance object."
(when maybe-prompt
(if pr
(project-remember-project pr)
- (project--remove-from-project-list directory)
+ (project--remove-from-project-list
+ directory "Project `%s' not found; removed from list")
(setq pr (cons 'transient directory))))
pr))
(defun project--find-in-directory (dir)
(run-hook-with-args-until-success 'project-find-functions dir))
+(defvar project--within-roots-fallback nil)
+
(cl-defgeneric project-root (project)
"Return root directory of the current project.
It usually contains the main build file, dependencies
configuration file, etc. Though neither is mandatory.
-The directory name must be absolute."
+The directory name must be absolute.")
+
+(cl-defmethod project-root (project
+ &context (project--within-roots-fallback
+ (eql nil)))
(car (project-roots project)))
(cl-defgeneric project-roots (project)
@@ -226,7 +233,8 @@ and the rest should be possible to express through
;; FIXME: Can we specify project's version here?
;; FIXME: Could we make this affect cl-defmethod calls too?
(declare (obsolete project-root "0.3.0"))
- (list (project-root project)))
+ (let ((project--within-roots-fallback t))
+ (list (project-root project))))
;; FIXME: Add MODE argument, like in `ede-source-paths'?
(cl-defgeneric project-external-roots (_project)
@@ -288,11 +296,11 @@ to find the list of ignores for each directory."
;; Make sure ~/ etc. in local directory name is
;; expanded and not left for the shell command
;; to interpret.
- (localdir (file-local-name (expand-file-name dir)))
- (command (format "%s %s %s -type f %s -print0"
+ (localdir (file-name-unquote (file-local-name (expand-file-name dir))))
+ (command (format "%s -H %s %s -type f %s -print0"
find-program
- ;; In case DIR is a symlink.
- (file-name-as-directory localdir)
+ (shell-quote-argument
+ (directory-file-name localdir)) ; Bug#48471
(xref--find-ignores-arguments ignores localdir)
(if files
(concat (shell-quote-argument "(")
@@ -303,16 +311,25 @@ to find the list of ignores for each directory."
(concat " -o " find-name-arg " "))
" "
(shell-quote-argument ")"))
- ""))))
+ "")))
+ (output (with-output-to-string
+ (with-current-buffer standard-output
+ (let ((status
+ (process-file-shell-command command nil t)))
+ (unless (zerop status)
+ (error "File listing failed: %s" (buffer-string))))))))
(project--remote-file-names
- (sort (split-string (shell-command-to-string command) "\0" t)
+ (sort (split-string output "\0" t)
#'string<))))
(defun project--remote-file-names (local-files)
- "Return LOCAL-FILES as if they were on the system of `default-directory'."
+ "Return LOCAL-FILES as if they were on the system of `default-directory'.
+Also quote LOCAL-FILES if `default-directory' is quoted."
(let ((remote-id (file-remote-p default-directory)))
(if (not remote-id)
- local-files
+ (if (file-name-quoted-p default-directory)
+ (mapcar #'file-name-quote local-files)
+ local-files)
(mapcar (lambda (file)
(concat remote-id file))
local-files))))
@@ -724,13 +741,14 @@ requires quoting, e.g. `\\[quoted-insert]<space>'."
(interactive (list (project--read-regexp)))
(require 'xref)
(require 'grep)
- (let* ((pr (project-current t))
+ (let* ((caller-dir default-directory)
+ (pr (project-current t))
(default-directory (project-root pr))
(files
(if (not current-prefix-arg)
(project-files pr)
(let ((dir (read-directory-name "Base directory: "
- nil default-directory t)))
+ caller-dir nil t)))
(project--files-in-directory dir
nil
(grep-read-files regexp))))))
@@ -774,9 +792,12 @@ pattern to search for."
(user-error "No matches for: %s" regexp))
xrefs))
+(defvar project-regexp-history-variable 'grep-regexp-history)
+
(defun project--read-regexp ()
- (let ((sym (thing-at-point 'symbol)))
- (read-regexp "Find regexp" (and sym (regexp-quote sym)))))
+ (let ((sym (thing-at-point 'symbol t)))
+ (read-regexp "Find regexp" (and sym (regexp-quote sym))
+ project-regexp-history-variable)))
;;;###autoload
(defun project-find-file ()
@@ -858,23 +879,16 @@ PREDICATE, HIST, and DEFAULT have the same meaning as in
(defun project--completing-read-strict (prompt
collection &optional predicate
hist default)
- ;; Tried both expanding the default before showing the prompt, and
- ;; removing it when it has no matches. Neither seems natural
- ;; enough. Removal is confusing; early expansion makes the prompt
- ;; too long.
- (let* ((new-prompt (if (and default (not (string-equal default "")))
- (format "%s (default %s): " prompt default)
- (format "%s: " prompt)))
- (res (completing-read new-prompt
- collection predicate t
- nil ;; initial-input
- hist default)))
- (when (and (equal res default)
- (not (test-completion res collection predicate)))
- (setq res
- (completing-read (format "%s: " prompt)
- collection predicate t res hist nil)))
- res))
+ (minibuffer-with-setup-hook
+ (lambda ()
+ (setq-local minibuffer-default-add-function
+ (lambda ()
+ (let ((minibuffer-default default))
+ (minibuffer-default-add-completions)))))
+ (completing-read (format "%s: " prompt)
+ collection predicate 'confirm
+ nil
+ hist)))
;;;###autoload
(defun project-dired ()
@@ -897,14 +911,10 @@ With \\[universal-argument] prefix arg, create a new inferior shell buffer even
if one already exists."
(interactive)
(let* ((default-directory (project-root (project-current t)))
- (default-project-shell-name
- (concat "*" (file-name-nondirectory
- (directory-file-name
- (file-name-directory default-directory)))
- "-shell*"))
+ (default-project-shell-name (project-prefixed-buffer-name "shell"))
(shell-buffer (get-buffer default-project-shell-name)))
(if (and shell-buffer (not current-prefix-arg))
- (pop-to-buffer shell-buffer)
+ (pop-to-buffer-same-window shell-buffer)
(shell (generate-new-buffer-name default-project-shell-name)))))
;;;###autoload
@@ -917,14 +927,10 @@ if one already exists."
(interactive)
(defvar eshell-buffer-name)
(let* ((default-directory (project-root (project-current t)))
- (eshell-buffer-name
- (concat "*" (file-name-nondirectory
- (directory-file-name
- (file-name-directory default-directory)))
- "-eshell*"))
+ (eshell-buffer-name (project-prefixed-buffer-name "eshell"))
(eshell-buffer (get-buffer eshell-buffer-name)))
(if (and eshell-buffer (not current-prefix-arg))
- (pop-to-buffer eshell-buffer)
+ (pop-to-buffer-same-window eshell-buffer)
(eshell t))))
;;;###autoload
@@ -973,12 +979,34 @@ loop using the command \\[fileloop-continue]."
(defvar compilation-read-command)
(declare-function compilation-read-command "compile")
+(defun project-prefixed-buffer-name (mode)
+ (concat "*"
+ (file-name-nondirectory
+ (directory-file-name default-directory))
+ "-"
+ (downcase mode)
+ "*"))
+
+(defcustom project-compilation-buffer-name-function nil
+ "Function to compute the name of a project compilation buffer.
+If non-nil, it overrides `compilation-buffer-name-function' for
+`project-compile'."
+ :version "28.1"
+ :group 'project
+ :type '(choice (const :tag "Default" nil)
+ (const :tag "Prefixed with root directory name"
+ project-prefixed-buffer-name)
+ (function :tag "Custom function")))
+
;;;###autoload
(defun project-compile ()
"Run `compile' in the project root."
(declare (interactive-only compile))
(interactive)
- (let ((default-directory (project-root (project-current t))))
+ (let ((default-directory (project-root (project-current t)))
+ (compilation-buffer-name-function
+ (or project-compilation-buffer-name-function
+ compilation-buffer-name-function)))
(call-interactively #'compile)))
(defun project--read-project-buffer ()
@@ -1085,11 +1113,16 @@ current project, it will be killed."
(defun project--buffer-list (pr)
"Return the list of all buffers in project PR."
- (let (bufs)
+ (let ((conn (file-remote-p (project-root pr)))
+ bufs)
(dolist (buf (buffer-list))
- (when (equal pr
- (with-current-buffer buf
- (project-current)))
+ ;; For now we go with the assumption that a project must reside
+ ;; entirely on one host. We might relax that in the future.
+ (when (and (equal conn
+ (file-remote-p (buffer-local-value 'default-directory buf)))
+ (equal pr
+ (with-current-buffer buf
+ (project-current))))
(push buf bufs)))
(nreverse bufs)))
@@ -1210,17 +1243,27 @@ Save the result in `project-list-file' if the list of projects has changed."
(push (list dir) project--list)
(project--write-project-list))))
-(defun project--remove-from-project-list (pr-dir)
- "Remove directory PR-DIR of a missing project from the project list.
+(defun project--remove-from-project-list (project-root report-message)
+ "Remove directory PROJECT-ROOT of a missing project from the project list.
If the directory was in the list before the removal, save the
result in `project-list-file'. Announce the project's removal
-from the list."
+from the list using REPORT-MESSAGE, which is a format string
+passed to `message' as its first argument."
(project--ensure-read-project-list)
- (when-let ((ent (assoc pr-dir project--list)))
+ (when-let ((ent (assoc project-root project--list)))
(setq project--list (delq ent project--list))
- (message "Project `%s' not found; removed from list" pr-dir)
+ (message report-message project-root)
(project--write-project-list)))
+;;;###autoload
+(defun project-remove-known-project (project-root)
+ "Remove directory PROJECT-ROOT from the project list.
+PROJECT-ROOT is the root directory of a known project listed in
+the project list."
+ (interactive (list (project-prompt-project-dir)))
+ (project--remove-from-project-list
+ project-root "Project `%s' removed from known projects"))
+
(defun project-prompt-project-dir ()
"Prompt the user for a directory that is one of the known project roots.
The project is chosen among projects known from the project list,
@@ -1255,7 +1298,6 @@ It's also possible to enter an arbitrary directory not in the list."
;;; Project switching
-;;;###autoload
(defcustom project-switch-commands
'((project-find-file "Find file")
(project-find-regexp "Find regexp")
@@ -1272,6 +1314,7 @@ to distinguish the menu entries in the dispatch menu. If KEY is
absent, COMMAND must be bound in `project-prefix-map', and the
key is looked up in that map."
:version "28.1"
+ :group 'project
:package-version '(project . "0.6.0")
:type '(repeat
(list
@@ -1288,6 +1331,7 @@ listed in `project-switch-commands' and signal an error when
others are invoked. Otherwise, all keys in `project-prefix-map'
are legal even if they aren't listed in the dispatch menu."
:type 'boolean
+ :group 'project
:version "28.1")
(defun project--keymap-prompt ()
@@ -1301,7 +1345,7 @@ are legal even if they aren't listed in the dispatch menu."
key tmp)))
(let ((key (if key
(vector key)
- (where-is-internal cmd project-prefix-map t))))
+ (where-is-internal cmd (list project-prefix-map) t))))
(format "[%s] %s"
(propertize (key-description key) 'face 'bold)
label)))
@@ -1317,28 +1361,36 @@ made from `project-switch-commands'.
When called in a program, it will use the project corresponding
to directory DIR."
(interactive (list (project-prompt-project-dir)))
- (let ((commands-menu
- (mapcar
- (lambda (row)
- (if (characterp (car row))
- ;; Deprecated format.
- ;; XXX: Add a warning about it?
- (reverse row)
- row))
- project-switch-commands))
- command)
+ (let* ((commands-menu
+ (mapcar
+ (lambda (row)
+ (if (characterp (car row))
+ ;; Deprecated format.
+ ;; XXX: Add a warning about it?
+ (reverse row)
+ row))
+ project-switch-commands))
+ (commands-map
+ (let ((temp-map (make-sparse-keymap)))
+ (set-keymap-parent temp-map project-prefix-map)
+ (dolist (row commands-menu temp-map)
+ (when-let ((cmd (nth 0 row))
+ (keychar (nth 2 row)))
+ (define-key temp-map (vector keychar) cmd)))))
+ command)
(while (not command)
- (let ((choice (read-event (project--keymap-prompt))))
- (when (setq command
- (or (car
- (seq-find (lambda (row) (equal choice (nth 2 row)))
- commands-menu))
- (lookup-key project-prefix-map (vector choice))))
+ (let* ((overriding-local-map commands-map)
+ (choice (read-key-sequence (project--keymap-prompt))))
+ (when (setq command (lookup-key commands-map choice))
(unless (or project-switch-use-entire-map
(assq command commands-menu))
;; TODO: Add some hint to the prompt, like "key not
;; recognized" or something.
- (setq command nil)))))
+ (setq command nil)))
+ (let ((global-command (lookup-key (current-global-map) choice)))
+ (when (memq global-command
+ '(keyboard-quit keyboard-escape-quit))
+ (call-interactively global-command)))))
(let ((default-directory dir)
(project-current-inhibit-prompt t))
(call-interactively command))))