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.el278
1 files changed, 196 insertions, 82 deletions
diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el
index 07093d61474..30f51704dca 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2015-2022 Free Software Foundation, Inc.
;; Version: 0.8.1
-;; Package-Requires: ((emacs "26.1") (xref "1.0.2"))
+;; Package-Requires: ((emacs "26.1") (xref "1.4.0"))
;; This is a GNU ELPA :core package. Avoid using functionality that
;; not compatible with the version of Emacs recorded above.
@@ -322,7 +322,15 @@ to find the list of ignores for each directory."
(process-file-shell-command command nil t))
(pt (point-min)))
(unless (zerop status)
- (error "File listing failed: %s" (buffer-string)))
+ (goto-char (point-min))
+ (if (and
+ (not (eql status 127))
+ (search-forward "Permission denied\n" nil t))
+ (let ((end (1- (point))))
+ (re-search-backward "\\`\\|\0")
+ (error "File listing failed: %s"
+ (buffer-substring (1+ (point)) end)))
+ (error "File listing failed: %s" (buffer-string))))
(goto-char pt)
(while (search-forward "\0" nil t)
(push (buffer-substring-no-properties (1+ pt) (1- (point)))
@@ -374,6 +382,12 @@ you might have to restart Emacs to see the effect."
:package-version '(project . "0.2.0")
:safe #'booleanp)
+(defcustom project-vc-include-untracked t
+ "When non-nil, the VC project backend includes untracked files."
+ :type 'boolean
+ :version "29.1"
+ :safe #'booleanp)
+
;; FIXME: Using the current approach, major modes are supposed to set
;; this variable to a buffer-local value. So we don't have access to
;; the "external roots" of language A from buffers of language B, which
@@ -410,30 +424,33 @@ The directory names should be absolute. Used in the VC project
backend implementation of `project-external-roots'.")
(defun project-try-vc (dir)
- (let* ((backend
- ;; FIXME: This is slow. Cache it.
- (ignore-errors (vc-responsible-backend dir)))
- (root
- (pcase backend
- ('Git
- ;; Don't stop at submodule boundary.
- ;; FIXME: Cache for a shorter time.
- (or (vc-file-getprop dir 'project-git-root)
- (let ((root (vc-call-backend backend 'root dir)))
- (vc-file-setprop
- dir 'project-git-root
- (if (and
- ;; FIXME: Invalidate the cache when the value
- ;; of this variable changes.
- (project--vc-merge-submodules-p root)
- (project--submodule-p root))
- (let* ((parent (file-name-directory
- (directory-file-name root))))
- (vc-call-backend backend 'root parent))
- root)))))
- ('nil nil)
- (_ (ignore-errors (vc-call-backend backend 'root dir))))))
- (and root (cons 'vc root))))
+ (or (vc-file-getprop dir 'project-vc)
+ (let* ((backend (ignore-errors (vc-responsible-backend dir)))
+ (root
+ (pcase backend
+ ('Git
+ ;; Don't stop at submodule boundary.
+ (or (vc-file-getprop dir 'project-git-root)
+ (let ((root (vc-call-backend backend 'root dir)))
+ (vc-file-setprop
+ dir 'project-git-root
+ (if (and
+ ;; FIXME: Invalidate the cache when the value
+ ;; of this variable changes.
+ (project--vc-merge-submodules-p root)
+ (project--submodule-p root))
+ (let* ((parent (file-name-directory
+ (directory-file-name root))))
+ (vc-call-backend backend 'root parent))
+ root)))))
+ ('nil nil)
+ (_ (ignore-errors (vc-call-backend backend 'root dir)))))
+ project)
+ (when root
+ (setq project (list 'vc backend root))
+ ;; FIXME: Cache for a shorter time.
+ (vc-file-setprop dir 'project-vc project)
+ project))))
(defun project--submodule-p (root)
;; XXX: We only support Git submodules for now.
@@ -459,7 +476,7 @@ backend implementation of `project-external-roots'.")
(t nil))))
(cl-defmethod project-root ((project (head vc)))
- (cdr project))
+ (nth 2 project))
(cl-defmethod project-external-roots ((project (head vc)))
(project-subtract-directories
@@ -474,8 +491,8 @@ backend implementation of `project-external-roots'.")
(lambda (dir)
(let ((ignores (project--value-in-dir 'project-vc-ignores dir))
backend)
- (if (and (file-equal-p dir (cdr project))
- (setq backend (vc-responsible-backend dir))
+ (if (and (file-equal-p dir (nth 2 project))
+ (setq backend (cadr project))
(cond
((eq backend 'Hg))
((and (eq backend 'Git)
@@ -501,8 +518,9 @@ backend implementation of `project-external-roots'.")
(args '("-z"))
(vc-git-use-literal-pathspecs nil)
files)
- ;; Include unregistered.
- (setq args (append args '("-c" "-o" "--exclude-standard")))
+ (setq args (append args
+ '("-c" "--exclude-standard")
+ (and project-vc-include-untracked '("-o"))))
(when extra-ignores
(setq args (append args
(cons "--"
@@ -554,9 +572,9 @@ backend implementation of `project-external-roots'.")
(delete-consecutive-dups files)))
(`Hg
(let ((default-directory (expand-file-name (file-name-as-directory dir)))
- args)
- ;; Include unregistered.
- (setq args (nconc args '("-mcardu" "--no-status" "-0")))
+ (args (list (concat "-mcard" (and project-vc-include-untracked "u"))
+ "--no-status"
+ "-0")))
(when extra-ignores
(setq args (nconc args
(mapcan
@@ -581,17 +599,17 @@ backend implementation of `project-external-roots'.")
(insert-file-contents ".gitmodules")
(let (res)
(goto-char (point-min))
- (while (re-search-forward "path *= *\\(.+\\)" nil t)
+ (while (re-search-forward "^[ \t]*path *= *\\(.+\\)" nil t)
(push (match-string 1) res))
(nreverse res)))
(file-missing nil)))
(cl-defmethod project-ignores ((project (head vc)) dir)
- (let* ((root (cdr project))
+ (let* ((root (nth 2 project))
backend)
(append
(when (file-equal-p dir root)
- (setq backend (vc-responsible-backend root))
+ (setq backend (cadr project))
(delq
nil
(mapcar
@@ -768,7 +786,6 @@ The following commands are available:
(define-key tab-prefix-map "p" #'project-other-tab-command))
(declare-function grep-read-files "grep")
-(declare-function xref--show-xrefs "xref")
(declare-function xref--find-ignores-arguments "xref")
;;;###autoload
@@ -794,7 +811,7 @@ requires quoting, e.g. `\\[quoted-insert]<space>'."
(project--files-in-directory dir
nil
(grep-read-files regexp))))))
- (xref--show-xrefs
+ (xref-show-xrefs
(apply-partially #'project--find-regexp-in-files regexp files)
nil)))
@@ -822,7 +839,7 @@ pattern to search for."
(project-files pr (cons
(project-root pr)
(project-external-roots pr)))))
- (xref--show-xrefs
+ (xref-show-xrefs
(apply-partially #'project--find-regexp-in-files regexp files)
nil)))
@@ -842,28 +859,40 @@ pattern to search for."
project-regexp-history-variable)))
;;;###autoload
-(defun project-find-file ()
+(defun project-find-file (&optional include-all)
"Visit a file (with completion) in the current project.
The filename at point (determined by `thing-at-point'), if any,
-is available as part of \"future history\"."
- (interactive)
+is available as part of \"future history\".
+
+If INCLUDE-ALL is non-nil, or with prefix argument when called
+interactively, include all files under the project root, except
+for VCS directories listed in `vc-directory-exclusion-list'."
+ (interactive "P")
(let* ((pr (project-current t))
- (dirs (list (project-root pr))))
- (project-find-file-in (thing-at-point 'filename) dirs pr)))
+ (root (project-root pr))
+ (dirs (list root)))
+ (project-find-file-in
+ (or (thing-at-point 'filename)
+ (and buffer-file-name (file-relative-name buffer-file-name root)))
+ dirs pr include-all)))
;;;###autoload
-(defun project-or-external-find-file ()
+(defun project-or-external-find-file (&optional include-all)
"Visit a file (with completion) in the current project or external roots.
The filename at point (determined by `thing-at-point'), if any,
-is available as part of \"future history\"."
- (interactive)
+is available as part of \"future history\".
+
+If INCLUDE-ALL is non-nil, or with prefix argument when called
+interactively, include all files under the project root, except
+for VCS directories listed in `vc-directory-exclusion-list'."
+ (interactive "P")
(let* ((pr (project-current t))
(dirs (cons
(project-root pr)
(project-external-roots pr))))
- (project-find-file-in (thing-at-point 'filename) dirs pr)))
+ (project-find-file-in (thing-at-point 'filename) dirs pr include-all)))
(defcustom project-read-file-name-function #'project--read-file-cpd-relative
"Function to call to read a file name from a list.
@@ -916,15 +945,28 @@ by the user at will."
predicate
hist mb-default))
-(defun project-find-file-in (suggested-filename dirs project)
+(defun project-find-file-in (suggested-filename dirs project &optional include-all)
"Complete a file name in DIRS in PROJECT and visit the result.
SUGGESTED-FILENAME is a relative file name, or part of it, which
-is used as part of \"future history\"."
- (let* ((all-files (project-files project dirs))
+is used as part of \"future history\".
+
+If INCLUDE-ALL is non-nil, or with prefix argument when called
+interactively, include all files from DIRS, except for VCS
+directories listed in `vc-directory-exclusion-list'."
+ (let* ((vc-dirs-ignores (mapcar
+ (lambda (dir)
+ (concat dir "/"))
+ vc-directory-exclusion-list))
+ (all-files
+ (if include-all
+ (mapcan
+ (lambda (dir) (project--files-in-directory dir vc-dirs-ignores))
+ dirs)
+ (project-files project dirs)))
(completion-ignore-case read-file-name-completion-ignore-case)
(file (funcall project-read-file-name-function
- "Find file" all-files nil nil
+ "Find file" all-files nil 'file-name-history
suggested-filename)))
(if (string= file "")
(user-error "You didn't specify the file")
@@ -961,7 +1003,7 @@ is used as part of \"future history\"."
"Dired"
;; Some completion UIs show duplicates.
(delete-dups all-dirs)
- nil nil)))
+ nil 'file-name-history)))
(dired dir)))
;;;###autoload
@@ -976,6 +1018,8 @@ is used as part of \"future history\"."
(interactive)
(vc-dir (project-root (project-current t))))
+(declare-function comint-check-proc "comint")
+
;;;###autoload
(defun project-shell ()
"Start an inferior shell in the current project's root directory.
@@ -984,11 +1028,14 @@ switch to it. Otherwise, create a new shell buffer.
With \\[universal-argument] prefix arg, create a new inferior shell buffer even
if one already exists."
(interactive)
+ (require 'comint)
(let* ((default-directory (project-root (project-current t)))
(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-same-window shell-buffer)
+ (if (comint-check-proc shell-buffer)
+ (pop-to-buffer shell-buffer (bound-and-true-p display-comint-buffer-action))
+ (shell shell-buffer))
(shell (generate-new-buffer-name default-project-shell-name)))))
;;;###autoload
@@ -1004,7 +1051,7 @@ if one already exists."
(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-same-window eshell-buffer)
+ (pop-to-buffer eshell-buffer (bound-and-true-p display-comint-buffer-action))
(eshell t))))
;;;###autoload
@@ -1047,11 +1094,17 @@ type \\[help-command] at that time.
If you exit the `query-replace', you can later continue the
`query-replace' loop using the command \\[fileloop-continue]."
(interactive
- (pcase-let ((`(,from ,to)
- (query-replace-read-args "Query replace (regexp)" t t)))
- (list from to)))
+ (let ((query-replace-read-from-regexp-default 'find-tag-default-as-regexp))
+ (pcase-let ((`(,from ,to)
+ (query-replace-read-args "Query replace (regexp)" t t)))
+ (list from to))))
(fileloop-initialize-replace
- from to (project-files (project-current t)) 'default)
+ from to
+ ;; XXX: Filter out Git submodules, which are not regular files.
+ ;; `project-files' can return those, which is arguably suboptimal,
+ ;; but removing them eagerly has performance cost.
+ (cl-delete-if-not #'file-regular-p (project-files (project-current t)))
+ 'default)
(fileloop-continue))
(defvar compilation-read-command)
@@ -1087,6 +1140,29 @@ If non-nil, it overrides `compilation-buffer-name-function' for
compilation-buffer-name-function)))
(call-interactively #'compile)))
+(defcustom project-ignore-buffer-conditions nil
+ "List of conditions to filter the buffers to be switched to.
+If any of these conditions are satisfied for a buffer in the
+current project, `project-switch-to-buffer',
+`project-display-buffer' and `project-display-buffer-other-frame'
+ignore it.
+See the doc string of `project-kill-buffer-conditions' for the
+general form of conditions."
+ :type '(repeat (choice regexp function symbol
+ (cons :tag "Major mode"
+ (const major-mode) symbol)
+ (cons :tag "Derived mode"
+ (const derived-mode) symbol)
+ (cons :tag "Negation"
+ (const not) sexp)
+ (cons :tag "Conjunction"
+ (const and) sexp)
+ (cons :tag "Disjunction"
+ (const or) sexp)))
+ :version "29.1"
+ :group 'project
+ :package-version '(project . "0.8.2"))
+
(defun project--read-project-buffer ()
(let* ((pr (project-current t))
(current-buffer (current-buffer))
@@ -1096,7 +1172,10 @@ If non-nil, it overrides `compilation-buffer-name-function' for
(predicate
(lambda (buffer)
;; BUFFER is an entry (BUF-NAME . BUF-OBJ) of Vbuffer_alist.
- (memq (cdr buffer) buffers))))
+ (and (memq (cdr buffer) buffers)
+ (not
+ (project--buffer-check
+ (cdr buffer) project-ignore-buffer-conditions))))))
(read-buffer
"Switch to buffer: "
(when (funcall predicate (cons other-name other-buffer))
@@ -1150,7 +1229,10 @@ displayed."
(not (major-mode . help-mode)))
(derived-mode . compilation-mode)
(derived-mode . dired-mode)
- (derived-mode . diff-mode))
+ (derived-mode . diff-mode)
+ (derived-mode . comint-mode)
+ (derived-mode . eshell-mode)
+ (derived-mode . change-log-mode))
"List of conditions to kill buffers related to a project.
This list is used by `project-kill-buffers'.
Each condition is either:
@@ -1160,10 +1242,9 @@ Each condition is either:
- a cons-cell, where the car describes how to interpret the cdr.
The car can be one of the following:
* `major-mode': the buffer is killed if the buffer's major
- mode is eq to the cons-cell's cdr
+ mode is eq to the cons-cell's cdr.
* `derived-mode': the buffer is killed if the buffer's major
- mode is derived from the major mode denoted by the cons-cell's
- cdr
+ mode is derived from the major mode in the cons-cell's cdr.
* `not': the cdr is interpreted as a negation of a condition.
* `and': the cdr is a list of recursive conditions, that all have
to be met.
@@ -1183,9 +1264,18 @@ current project, it will be killed."
(const and) sexp)
(cons :tag "Disjunction"
(const or) sexp)))
- :version "28.1"
+ :version "29.1"
+ :group 'project
+ :package-version '(project . "0.8.2"))
+
+(defcustom project-kill-buffers-display-buffer-list nil
+ "Non-nil to display list of buffers to kill before killing project buffers.
+Used by `project-kill-buffers'."
+ :type 'boolean
+ :version "29.1"
:group 'project
- :package-version '(project . "0.6.0"))
+ :package-version '(project . "0.8.2")
+ :safe #'booleanp)
(defun project--buffer-list (pr)
"Return the list of all buffers in project PR."
@@ -1202,16 +1292,17 @@ current project, it will be killed."
(push buf bufs)))
(nreverse bufs)))
-(defun project--kill-buffer-check (buf conditions)
+(defun project--buffer-check (buf conditions)
"Check if buffer BUF matches any element of the list CONDITIONS.
-See `project-kill-buffer-conditions' for more details on the form
-of CONDITIONS."
- (catch 'kill
+See `project-kill-buffer-conditions' or
+`project-ignore-buffer-conditions' for more details on the
+form of CONDITIONS."
+ (catch 'match
(dolist (c conditions)
(when (cond
((stringp c)
(string-match-p c (buffer-name buf)))
- ((symbolp c)
+ ((functionp c)
(funcall c buf))
((eq (car-safe c) 'major-mode)
(eq (buffer-local-value 'major-mode buf)
@@ -1221,15 +1312,15 @@ of CONDITIONS."
(buffer-local-value 'major-mode buf)
(cdr c)))
((eq (car-safe c) 'not)
- (not (project--kill-buffer-check buf (cdr c))))
+ (not (project--buffer-check buf (cdr c))))
((eq (car-safe c) 'or)
- (project--kill-buffer-check buf (cdr c)))
+ (project--buffer-check buf (cdr c)))
((eq (car-safe c) 'and)
(seq-every-p
- (apply-partially #'project--kill-buffer-check
+ (apply-partially #'project--buffer-check
buf)
(mapcar #'list (cdr c)))))
- (throw 'kill t)))))
+ (throw 'match t)))))
(defun project--buffers-to-kill (pr)
"Return list of buffers in project PR to kill.
@@ -1237,7 +1328,7 @@ What buffers should or should not be killed is described
in `project-kill-buffer-conditions'."
(let (bufs)
(dolist (buf (project-buffers pr))
- (when (project--kill-buffer-check buf project-kill-buffer-conditions)
+ (when (project--buffer-check buf project-kill-buffer-conditions)
(push buf bufs)))
bufs))
@@ -1250,17 +1341,40 @@ identical. Only the buffers that match a condition in
`project-kill-buffer-conditions' will be killed. If NO-CONFIRM
is non-nil, the command will not ask the user for confirmation.
NO-CONFIRM is always nil when the command is invoked
-interactively."
+interactively.
+
+Also see the `project-kill-buffers-display-buffer-list' variable."
(interactive)
(let* ((pr (project-current t))
- (bufs (project--buffers-to-kill pr)))
+ (bufs (project--buffers-to-kill pr))
+ (query-user (lambda ()
+ (yes-or-no-p
+ (format "Kill %d buffers in %s? "
+ (length bufs)
+ (project-root pr))))))
(cond (no-confirm
(mapc #'kill-buffer bufs))
((null bufs)
(message "No buffers to kill"))
- ((yes-or-no-p (format "Kill %d buffers in %s? "
- (length bufs)
- (project-root pr)))
+ (project-kill-buffers-display-buffer-list
+ (when
+ (with-current-buffer-window
+ (get-buffer-create "*Buffer List*")
+ `(display-buffer--maybe-at-bottom
+ (dedicated . t)
+ (window-height . (fit-window-to-buffer))
+ (preserve-size . (nil . t))
+ (body-function
+ . ,#'(lambda (_window)
+ (list-buffers-noselect nil bufs))))
+ #'(lambda (window _value)
+ (with-selected-window window
+ (unwind-protect
+ (funcall query-user)
+ (when (window-live-p window)
+ (quit-restore-window window 'kill))))))
+ (mapc #'kill-buffer bufs)))
+ ((funcall query-user)
(mapc #'kill-buffer bufs)))))