dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

project.el (73946B)


      1 ;;; project.el --- Operations on the current project  -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2015-2023 Free Software Foundation, Inc.
      4 ;; Version: 0.9.8
      5 ;; Package-Requires: ((emacs "26.1") (xref "1.4.0"))
      6 
      7 ;; This is a GNU ELPA :core package.  Avoid using functionality that
      8 ;; not compatible with the version of Emacs recorded above.
      9 
     10 ;; This file is part of GNU Emacs.
     11 
     12 ;; GNU Emacs is free software: you can redistribute it and/or modify
     13 ;; it under the terms of the GNU General Public License as published by
     14 ;; the Free Software Foundation, either version 3 of the License, or
     15 ;; (at your option) any later version.
     16 
     17 ;; GNU Emacs is distributed in the hope that it will be useful,
     18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     20 ;; GNU General Public License for more details.
     21 
     22 ;; You should have received a copy of the GNU General Public License
     23 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     24 
     25 ;;; Commentary:
     26 
     27 ;; NOTE: The project API is still experimental and can change in major,
     28 ;; backward-incompatible ways.  Everyone is encouraged to try it, and
     29 ;; report to us any problems or use cases we hadn't anticipated, by
     30 ;; sending an email to emacs-devel, or `M-x report-emacs-bug'.
     31 ;;
     32 ;; This file contains generic infrastructure for dealing with
     33 ;; projects, some utility functions, and commands using that
     34 ;; infrastructure.
     35 ;;
     36 ;; The goal is to make it easier for Lisp programs to operate on the
     37 ;; current project, without having to know which package handles
     38 ;; detection of that project type, parsing its config files, etc.
     39 ;;
     40 ;; This file consists of following parts:
     41 ;;
     42 ;; Infrastructure (the public API):
     43 ;;
     44 ;; Function `project-current' that returns the current project
     45 ;; instance based on the value of the hook `project-find-functions',
     46 ;; and several generic functions that act on it.
     47 ;;
     48 ;; `project-root' must be defined for every project.
     49 ;; `project-files' can be overridden for performance purposes.
     50 ;; `project-ignores' and `project-external-roots' describe the project
     51 ;; files and its relations to external directories.  `project-files'
     52 ;; should be consistent with `project-ignores'.
     53 ;;
     54 ;; `project-buffers' can be overridden if the project has some unusual
     55 ;; shape (e.g. it contains files residing outside of its root, or some
     56 ;; files inside the root must not be considered a part of it).  It
     57 ;; should be consistent with `project-files'.
     58 ;;
     59 ;; This list can change in future versions.
     60 ;;
     61 ;; Transient project:
     62 ;;
     63 ;; An instance of this type can be returned by `project-current' if no
     64 ;; project was detected automatically, and the user had to pick a
     65 ;; directory manually.  The fileset it describes is the whole
     66 ;; directory, with the exception of some standard ignored files and
     67 ;; directories.  This type has little purpose otherwise, as the only
     68 ;; generic function it provides an override for is `project-root'.
     69 ;;
     70 ;; VC-aware project:
     71 ;;
     72 ;; Originally conceived as an example implementation, now it's a
     73 ;; relatively fast backend that delegates to 'git ls-files' or 'hg
     74 ;; status' to list the project's files.  It honors the VC ignore
     75 ;; files, but supports additions to the list using the user option
     76 ;; `project-vc-ignores' (usually through .dir-locals.el).  See the
     77 ;; customization group `project-vc' for other options that control its
     78 ;; behavior.
     79 ;;
     80 ;; If the repository is using any other VCS than Git or Hg, the file
     81 ;; listing uses the default mechanism based on `find-program'.
     82 ;;
     83 ;; This project type can also be used for non-VCS controlled
     84 ;; directories, see the variable `project-vc-extra-root-markers'.
     85 ;;
     86 ;; Utils:
     87 ;;
     88 ;; `project-combine-directories' and `project-subtract-directories',
     89 ;; mainly for use in the abovementioned generics' implementations.
     90 ;;
     91 ;; `project-known-project-roots' and `project-remember-project' to
     92 ;; interact with the "known projects" list.
     93 ;;
     94 ;; Commands:
     95 ;;
     96 ;; `project-prefix-map' contains the full list of commands defined in
     97 ;; this package.  This map uses the prefix `C-x p' by default.
     98 ;; Type `C-x p f' to find file in the current project.
     99 ;; Type `C-x p C-h' to see all available commands and bindings.
    100 ;;
    101 ;; All commands defined in this package are implemented using the
    102 ;; public API only.  As a result, they will work with any project
    103 ;; backend that follows the protocol.
    104 ;;
    105 ;; Any third-party code that wants to use this package should likewise
    106 ;; target the public API.  Use any of the built-in commands as the
    107 ;; example.
    108 ;;
    109 ;; How to create a new backend:
    110 ;;
    111 ;; - Consider whether you really should, or whether there are other
    112 ;; ways to reach your goals.  If the backend's performance is
    113 ;; significantly lower than that of the built-in one, and it's first
    114 ;; in the list, it will affect all commands that use it.  Unless you
    115 ;; are going to be using it only yourself or in special circumstances,
    116 ;; you will probably want it to be fast, and it's unlikely to be a
    117 ;; trivial endeavor.  `project-files' is the method to optimize (the
    118 ;; default implementation gets slower the more files the directory
    119 ;; has, and the longer the list of ignores is).
    120 ;;
    121 ;; - Choose the format of the value that represents a project for your
    122 ;; backend (we call it project instance).  Don't use any of the
    123 ;; formats from other backends.  The format can be arbitrary, as long
    124 ;; as the datatype is something `cl-defmethod' can dispatch on.  The
    125 ;; value should be stable (when compared with `equal') across
    126 ;; invocations, meaning calls to that function from buffers belonging
    127 ;; to the same project should return equal values.
    128 ;;
    129 ;; - Write a new function that will determine the current project
    130 ;; based on the directory and add it to `project-find-functions'
    131 ;; (which see) using `add-hook'.  It is a good idea to depend on the
    132 ;; directory only, and not on the current major mode, for example.
    133 ;; Because the usual expectation is that all files in the directory
    134 ;; belong to the same project (even if some/most of them are ignored).
    135 ;;
    136 ;; - Define new methods for some or all generic functions for this
    137 ;; backend using `cl-defmethod'.  A `project-root' method is
    138 ;; mandatory, `project-files' is recommended, the rest are optional.
    139 
    140 ;;; TODO:
    141 
    142 ;; * Reliably cache the list of files in the project, probably using
    143 ;;   filenotify.el (if supported) to invalidate.  And avoiding caching
    144 ;;   if it's not available (manual cache invalidation is not nice).
    145 ;;
    146 ;; * Build tool related functionality.  Start with a `project-build'
    147 ;;   command, which should provide completions on tasks to run, and
    148 ;;   maybe allow entering some additional arguments.  This might
    149 ;;   be handled better with a separate API, though.  Then we won't
    150 ;;   force every project backend to be aware of the build tool(s) the
    151 ;;   project is using.
    152 ;;
    153 ;; * Command to (re)build the tag files in all project roots.  To that
    154 ;;   end, we might need to add a way to provide file whitelist
    155 ;;   wildcards for each root to limit etags to certain files (in
    156 ;;   addition to the blacklist provided by ignores), and/or allow
    157 ;;   specifying additional tag regexps.
    158 ;;
    159 ;; * UI for the user to be able to pick the current project for the
    160 ;;   whole Emacs session, independent of the current directory.  Or,
    161 ;;   in the more advanced case, open a set of projects, and have some
    162 ;;   project-related commands to use them all.  E.g., have a command
    163 ;;   to search for a regexp across all open projects.
    164 ;;
    165 ;; * Support for project-local variables: a UI to edit them, and a
    166 ;;   utility function to retrieve a value.  Probably useless without
    167 ;;   support in various built-in commands.  In the API, we might get
    168 ;;   away with only adding a `project-configuration-directory' method,
    169 ;;   defaulting to the project root the current file/buffer is in.
    170 ;;   And prompting otherwise.  How to best mix that with backends that
    171 ;;   want to set/provide certain variables themselves, is up for
    172 ;;   discussion.
    173 
    174 ;;; Code:
    175 
    176 (require 'cl-generic)
    177 (require 'cl-lib)
    178 (require 'seq)
    179 (eval-when-compile (require 'subr-x))
    180 
    181 (defgroup project nil
    182   "Operations on the current project."
    183   :version "28.1"
    184   :group 'tools)
    185 
    186 (defvar project-find-functions (list #'project-try-vc)
    187   "Special hook to find the project containing a given directory.
    188 Each functions on this hook is called in turn with one
    189 argument, the directory in which to look, and should return
    190 either nil to mean that it is not applicable, or a project instance.
    191 The exact form of the project instance is up to each respective
    192 function; the only practical limitation is to use values that
    193 `cl-defmethod' can dispatch on, like a cons cell, or a list, or a
    194 CL struct.")
    195 
    196 (define-obsolete-variable-alias
    197   'project-current-inhibit-prompt
    198   'project-current-directory-override
    199   "29.1")
    200 
    201 (defvar project-current-directory-override nil
    202   "Value to use instead of `default-directory' when detecting the project.
    203 When it is non-nil, `project-current' will always skip prompting too.")
    204 
    205 ;;;###autoload
    206 (defun project-current (&optional maybe-prompt directory)
    207   "Return the project instance in DIRECTORY, defaulting to `default-directory'.
    208 
    209 When no project is found in that directory, the result depends on
    210 the value of MAYBE-PROMPT: if it is nil or omitted, return nil,
    211 else ask the user for a directory in which to look for the
    212 project, and if no project is found there, return a \"transient\"
    213 project instance.
    214 
    215 The \"transient\" project instance is a special kind of value
    216 which denotes a project rooted in that directory and includes all
    217 the files under the directory except for those that match entries
    218 in `vc-directory-exclusion-list' or `grep-find-ignored-files'.
    219 
    220 See the doc string of `project-find-functions' for the general form
    221 of the project instance object."
    222   (unless directory (setq directory (or project-current-directory-override
    223                                         default-directory)))
    224   (let ((pr (project--find-in-directory directory)))
    225     (cond
    226      (pr)
    227      ((unless project-current-directory-override
    228         maybe-prompt)
    229       (setq directory (project-prompt-project-dir)
    230             pr (project--find-in-directory directory))))
    231     (when maybe-prompt
    232       (if pr
    233           (project-remember-project pr)
    234         (project--remove-from-project-list
    235          directory "Project `%s' not found; removed from list")
    236         (setq pr (cons 'transient directory))))
    237     pr))
    238 
    239 (defun project--find-in-directory (dir)
    240   (run-hook-with-args-until-success 'project-find-functions dir))
    241 
    242 (defvar project--within-roots-fallback nil)
    243 
    244 (cl-defgeneric project-root (project)
    245   "Return root directory of the current project.
    246 
    247 It usually contains the main build file, dependencies
    248 configuration file, etc. Though neither is mandatory.
    249 
    250 The directory name must be absolute.")
    251 
    252 (cl-defmethod project-root (project
    253                             &context (project--within-roots-fallback
    254                                       (eql nil)))
    255   (car (project-roots project)))
    256 
    257 (cl-defgeneric project-roots (project)
    258   "Return the list containing the current project root.
    259 
    260 The function is obsolete, all projects have one main root anyway,
    261 and the rest should be possible to express through
    262 `project-external-roots'."
    263   ;; FIXME: Can we specify project's version here?
    264   ;; FIXME: Could we make this affect cl-defmethod calls too?
    265   (declare (obsolete project-root "0.3.0"))
    266   (let ((project--within-roots-fallback t))
    267     (list (project-root project))))
    268 
    269 ;; FIXME: Add MODE argument, like in `ede-source-paths'?
    270 (cl-defgeneric project-external-roots (_project)
    271   "Return the list of external roots for PROJECT.
    272 
    273 It's the list of directories outside of the project that are
    274 still related to it.  If the project deals with source code then,
    275 depending on the languages used, this list should include the
    276 headers search path, load path, class path, and so on."
    277   nil)
    278 
    279 (cl-defgeneric project-name (project)
    280   "A human-readable name for the project.
    281 Nominally unique, but not enforced."
    282   (file-name-nondirectory (directory-file-name (project-root project))))
    283 
    284 (cl-defgeneric project-ignores (_project _dir)
    285   "Return the list of glob patterns to ignore inside DIR.
    286 Patterns can match both regular files and directories.
    287 To root an entry, start it with `./'.  To match directories only,
    288 end it with `/'.  DIR must be either `project-root' or one of
    289 `project-external-roots'."
    290   ;; TODO: Document and support regexp ignores as used by Hg.
    291   ;; TODO: Support whitelist entries.
    292   (require 'grep)
    293   (defvar grep-find-ignored-files)
    294   (nconc
    295    (mapcar
    296     (lambda (dir)
    297       (concat dir "/"))
    298     vc-directory-exclusion-list)
    299    grep-find-ignored-files))
    300 
    301 (defun project--file-completion-table (all-files)
    302   (lambda (string pred action)
    303     (cond
    304      ((eq action 'metadata)
    305       '(metadata . ((category . project-file))))
    306      (t
    307       (complete-with-action action all-files string pred)))))
    308 
    309 (cl-defmethod project-root ((project (head transient)))
    310   (cdr project))
    311 
    312 (cl-defgeneric project-files (project &optional dirs)
    313   "Return a list of files in directories DIRS in PROJECT.
    314 DIRS is a list of absolute directories; it should be some
    315 subset of the project root and external roots.
    316 
    317 The default implementation uses `find-program'.  PROJECT is used
    318 to find the list of ignores for each directory."
    319   (mapcan
    320    (lambda (dir)
    321      (project--files-in-directory dir
    322                                   (project--dir-ignores project dir)))
    323    (or dirs
    324        (list (project-root project)))))
    325 
    326 (defun project--files-in-directory (dir ignores &optional files)
    327   (require 'find-dired)
    328   (require 'xref)
    329   (let* ((default-directory dir)
    330          ;; Make sure ~/ etc. in local directory name is
    331          ;; expanded and not left for the shell command
    332          ;; to interpret.
    333          (localdir (file-name-unquote (file-local-name (expand-file-name dir))))
    334          (dfn (directory-file-name localdir))
    335          (command (format "%s -H . %s -type f %s -print0"
    336                           find-program
    337                           (xref--find-ignores-arguments ignores "./")
    338                           (if files
    339                               (concat (shell-quote-argument "(")
    340                                       " -name "
    341                                       (mapconcat
    342                                        #'shell-quote-argument
    343                                        (split-string files)
    344                                        (concat " -o -name "))
    345                                       " "
    346                                       (shell-quote-argument ")"))
    347                             "")))
    348          res)
    349     (with-temp-buffer
    350       (let ((status
    351              (process-file-shell-command command nil t))
    352             (pt (point-min)))
    353         (unless (zerop status)
    354           (goto-char (point-min))
    355           (if (and
    356                (not (eql status 127))
    357                (search-forward "Permission denied\n" nil t))
    358               (let ((end (1- (point))))
    359                 (re-search-backward "\\`\\|\0")
    360                 (error "File listing failed: %s"
    361                        (buffer-substring (1+ (point)) end)))
    362             (error "File listing failed: %s" (buffer-string))))
    363         (goto-char pt)
    364         (while (search-forward "\0" nil t)
    365           (push (buffer-substring-no-properties (1+ pt) (1- (point)))
    366                 res)
    367           (setq pt (point)))))
    368     (project--remote-file-names
    369      (mapcar (lambda (s) (concat dfn s))
    370              (sort res #'string<)))))
    371 
    372 (defun project--remote-file-names (local-files)
    373   "Return LOCAL-FILES as if they were on the system of `default-directory'.
    374 Also quote LOCAL-FILES if `default-directory' is quoted."
    375   (let ((remote-id (file-remote-p default-directory)))
    376     (if (not remote-id)
    377         (if (file-name-quoted-p default-directory)
    378             (mapcar #'file-name-quote local-files)
    379           local-files)
    380       (mapcar (lambda (file)
    381                 (concat remote-id file))
    382               local-files))))
    383 
    384 (cl-defgeneric project-buffers (project)
    385   "Return the list of all live buffers that belong to PROJECT.
    386 
    387 The default implementation matches each buffer to PROJECT root using
    388 the buffer's value of `default-directory'."
    389   (let ((root (expand-file-name (file-name-as-directory (project-root project))))
    390         bufs)
    391     (dolist (buf (buffer-list))
    392       (when (string-prefix-p root (expand-file-name
    393                                    (buffer-local-value 'default-directory buf)))
    394         (push buf bufs)))
    395     (nreverse bufs)))
    396 
    397 (defgroup project-vc nil
    398   "VC-aware project implementation."
    399   :version "25.1"
    400   :group 'project)
    401 
    402 (defcustom project-vc-ignores nil
    403   "List of patterns to add to `project-ignores'."
    404   :type '(repeat string)
    405   :safe #'listp)
    406 
    407 (defcustom project-vc-merge-submodules t
    408   "Non-nil to consider submodules part of the parent project.
    409 
    410 After changing this variable (using Customize or .dir-locals.el)
    411 you might have to restart Emacs to see the effect."
    412   :type 'boolean
    413   :version "28.1"
    414   :package-version '(project . "0.2.0")
    415   :safe #'booleanp)
    416 
    417 (defcustom project-vc-include-untracked t
    418   "When non-nil, the VC-aware project backend includes untracked files."
    419   :type 'boolean
    420   :version "29.1"
    421   :safe #'booleanp)
    422 
    423 (defcustom project-vc-name nil
    424   "When non-nil, the name of the current VC-aware project.
    425 
    426 The best way to change the value a VC-aware project reports as
    427 its name, is by setting this in .dir-locals.el."
    428   :type '(choice (const :tag "Default to the base name" nil)
    429                  (string :tag "Custom name"))
    430   :version "29.1"
    431   :package-version '(project . "0.9.0")
    432   :safe #'stringp)
    433 
    434 ;; Not using regexps because these wouldn't work in Git pathspecs, in
    435 ;; case we decide we need to be able to list nested projects.
    436 (defcustom project-vc-extra-root-markers nil
    437   "List of additional markers to signal project roots.
    438 
    439 A marker is either a base file name or a glob pattern for such.
    440 
    441 A directory containing such a marker file or a file matching a
    442 marker pattern will be recognized as the root of a VC-aware
    443 project.
    444 
    445 Example values: \".dir-locals.el\", \"package.json\", \"pom.xml\",
    446 \"requirements.txt\", \"Gemfile\", \"*.gemspec\", \"autogen.sh\".
    447 
    448 These will be used in addition to regular directory markers such
    449 as \".git\", \".hg\", and so on, depending on the value of
    450 `vc-handled-backends'.  It is most useful when a project has
    451 subdirectories inside it that need to be considered as separate
    452 projects.  It can also be used for projects outside of VC
    453 repositories.
    454 
    455 In either case, their behavior will still obey the relevant
    456 variables, such as `project-vc-ignores' or `project-vc-name'."
    457   :type '(repeat string)
    458   :version "29.1"
    459   :package-version '(project . "0.9.0")
    460   :safe (lambda (val) (and (listp val) (cl-every #'stringp val))))
    461 
    462 ;; FIXME: Using the current approach, major modes are supposed to set
    463 ;; this variable to a buffer-local value.  So we don't have access to
    464 ;; the "external roots" of language A from buffers of language B, which
    465 ;; seems desirable in multi-language projects, at least for some
    466 ;; potential uses, like "jump to a file in project or external dirs".
    467 ;;
    468 ;; We could add a second argument to this function: a file extension,
    469 ;; or a language name.  Some projects will know the set of languages
    470 ;; used in them; for others, like the VC-aware type, we'll need
    471 ;; auto-detection.  I see two options:
    472 ;;
    473 ;; - That could be implemented as a separate second hook, with a
    474 ;;   list of functions that return file extensions.
    475 ;;
    476 ;; - This variable will be turned into a hook with "append" semantics,
    477 ;;   and each function in it will perform auto-detection when passed
    478 ;;   nil instead of an actual file extension.  Then this hook will, in
    479 ;;   general, be modified globally, and not from major mode functions.
    480 ;;
    481 ;; The second option seems simpler, but the first one has the
    482 ;; advantage that the user could override the list of languages used
    483 ;; in a project via a directory-local variable, thus skipping
    484 ;; languages they're not working on personally (in a big project), or
    485 ;; working around problems in language detection (the detection logic
    486 ;; might be imperfect for the project in question, or it might work
    487 ;; too slowly for the user's taste).
    488 (defvar project-vc-external-roots-function (lambda () tags-table-list)
    489   "Function that returns a list of external roots.
    490 
    491 It should return a list of directory roots that contain source
    492 files related to the current buffer.
    493 
    494 The directory names should be absolute.  Used in the VC-aware
    495 project backend implementation of `project-external-roots'.")
    496 
    497 (defvar project-vc-backend-markers-alist
    498   `((Git . ".git")
    499     (Hg . ".hg")
    500     (Bzr . ".bzr")
    501     ;; See the comment above `vc-svn-admin-directory' for why we're
    502     ;; duplicating the definition.
    503     (SVN . ,(if (and (memq system-type '(cygwin windows-nt ms-dos))
    504                      (getenv "SVN_ASP_DOT_NET_HACK"))
    505                 "_svn"
    506               ".svn"))
    507     (DARCS . "_darcs")
    508     (Fossil . ".fslckout")
    509     (Got . ".got"))
    510   "Associative list assigning root markers to VC backend symbols.
    511 
    512 See `project-vc-extra-root-markers' for the marker value format.")
    513 
    514 (defun project-try-vc (dir)
    515   ;; FIXME: Learn to invalidate when the value of
    516   ;; `project-vc-merge-submodules' or `project-vc-extra-root-markers'
    517   ;; changes.
    518   (or (vc-file-getprop dir 'project-vc)
    519       (let* ((backend-markers
    520               (delete
    521                nil
    522                (mapcar
    523                 (lambda (b) (assoc-default b project-vc-backend-markers-alist))
    524                 vc-handled-backends)))
    525              (marker-re
    526               (concat
    527                "\\`"
    528                (mapconcat
    529                 (lambda (m) (format "\\(%s\\)" (wildcard-to-regexp m)))
    530                 (append backend-markers
    531                         (project--value-in-dir 'project-vc-extra-root-markers dir))
    532                 "\\|")
    533                "\\'"))
    534              (locate-dominating-stop-dir-regexp
    535               (or vc-ignore-dir-regexp locate-dominating-stop-dir-regexp))
    536              last-matches
    537              (root
    538               (locate-dominating-file
    539                dir
    540                (lambda (d)
    541                  ;; Maybe limit count to 100 when we can drop Emacs < 28.
    542                  (setq last-matches
    543                        (condition-case nil
    544                            (directory-files d nil marker-re t)
    545                          (file-missing nil))))))
    546              (backend
    547               (cl-find-if
    548                (lambda (b)
    549                  (member (assoc-default b project-vc-backend-markers-alist)
    550                          last-matches))
    551                vc-handled-backends))
    552              project)
    553         (when (and
    554                (eq backend 'Git)
    555                (project--vc-merge-submodules-p root)
    556                (project--submodule-p root))
    557           (let* ((parent (file-name-directory (directory-file-name root))))
    558             (setq root (vc-call-backend 'Git 'root parent))))
    559         (when root
    560           (setq project (list 'vc backend root))
    561           ;; FIXME: Cache for a shorter time.
    562           (vc-file-setprop dir 'project-vc project)
    563           project))))
    564 
    565 (defun project--submodule-p (root)
    566   ;; XXX: We only support Git submodules for now.
    567   ;;
    568   ;; For submodules, at least, we expect the users to prefer them to
    569   ;; be considered part of the parent project.  For those who don't,
    570   ;; there is the custom var now.
    571   ;;
    572   ;; Some users may also set up things equivalent to Git submodules
    573   ;; using "git worktree" (for example).  However, we expect that most
    574   ;; of them would prefer to treat those as separate projects anyway.
    575   (let* ((gitfile (expand-file-name ".git" root)))
    576     (cond
    577      ((file-directory-p gitfile)
    578       nil)
    579      ((with-temp-buffer
    580         (insert-file-contents gitfile)
    581         (goto-char (point-min))
    582         ;; Kind of a hack to distinguish a submodule from
    583         ;; other cases of .git files pointing elsewhere.
    584         (looking-at "gitdir: [./]+/\\.git/modules/"))
    585       t)
    586      (t nil))))
    587 
    588 (cl-defmethod project-root ((project (head vc)))
    589   (nth 2 project))
    590 
    591 (cl-defmethod project-external-roots ((project (head vc)))
    592   (project-subtract-directories
    593    (project-combine-directories
    594     (mapcar
    595      #'file-name-as-directory
    596      (funcall project-vc-external-roots-function)))
    597    (list (project-root project))))
    598 
    599 (cl-defmethod project-files ((project (head vc)) &optional dirs)
    600   (mapcan
    601    (lambda (dir)
    602      (let ((ignores (project--value-in-dir 'project-vc-ignores (nth 2 project)))
    603            (backend (cadr project)))
    604        (when backend
    605          (require (intern (concat "vc-" (downcase (symbol-name backend))))))
    606        (if (and (file-equal-p dir (nth 2 project))
    607                 (cond
    608                  ((eq backend 'Hg))
    609                  ((and (eq backend 'Git)
    610                        (or
    611                         (not ignores)
    612                         (version<= "1.9" (vc-git--program-version)))))))
    613            (project--vc-list-files dir backend ignores)
    614          (project--files-in-directory
    615           dir
    616           (project--dir-ignores project dir)))))
    617    (or dirs
    618        (list (project-root project)))))
    619 
    620 (declare-function vc-git--program-version "vc-git")
    621 (declare-function vc-git--run-command-string "vc-git")
    622 (declare-function vc-hg-command "vc-hg")
    623 
    624 (defun project--vc-list-files (dir backend extra-ignores)
    625   (defvar vc-git-use-literal-pathspecs)
    626   (pcase backend
    627     (`Git
    628      (let* ((default-directory (expand-file-name (file-name-as-directory dir)))
    629             (args '("-z"))
    630             (vc-git-use-literal-pathspecs nil)
    631             (include-untracked (project--value-in-dir
    632                                 'project-vc-include-untracked
    633                                 dir))
    634             files)
    635        (setq args (append args
    636                           '("-c" "--exclude-standard")
    637                           (and include-untracked '("-o"))))
    638        (when extra-ignores
    639          (setq args (append args
    640                             (cons "--"
    641                                   (mapcar
    642                                    (lambda (i)
    643                                      (format
    644                                       ":(exclude,glob,top)%s"
    645                                       (if (string-match "\\*\\*" i)
    646                                           ;; Looks like pathspec glob
    647                                           ;; format already.
    648                                           i
    649                                         (if (string-match "\\./" i)
    650                                             ;; ./abc -> abc
    651                                             (setq i (substring i 2))
    652                                           ;; abc -> **/abc
    653                                           (setq i (concat "**/" i))
    654                                           ;; FIXME: '**/abc' should also
    655                                           ;; match a directory with that
    656                                           ;; name, but doesn't (git 2.25.1).
    657                                           ;; Maybe we should replace
    658                                           ;; such entries with two.
    659                                           (if (string-match "/\\'" i)
    660                                               ;; abc/ -> abc/**
    661                                               (setq i (concat i "**"))))
    662                                         i)))
    663                                    extra-ignores)))))
    664        (setq files
    665              (mapcar
    666               (lambda (file) (concat default-directory file))
    667               (split-string
    668                (apply #'vc-git--run-command-string nil "ls-files" args)
    669                "\0" t)))
    670        (when (project--vc-merge-submodules-p default-directory)
    671          ;; Unfortunately, 'ls-files --recurse-submodules' conflicts with '-o'.
    672          (let* ((submodules (project--git-submodules))
    673                 (sub-files
    674                  (mapcar
    675                   (lambda (module)
    676                     (when (file-directory-p module)
    677                       (project--vc-list-files
    678                        (concat default-directory module)
    679                        backend
    680                        extra-ignores)))
    681                   submodules)))
    682            (setq files
    683                  (apply #'nconc files sub-files))))
    684        ;; 'git ls-files' returns duplicate entries for merge conflicts.
    685        ;; XXX: Better solutions welcome, but this seems cheap enough.
    686        (delete-consecutive-dups files)))
    687     (`Hg
    688      (let* ((default-directory (expand-file-name (file-name-as-directory dir)))
    689             (include-untracked (project--value-in-dir
    690                                 'project-vc-include-untracked
    691                                 dir))
    692             (args (list (concat "-mcard" (and include-untracked "u"))
    693                         "--no-status"
    694                         "-0")))
    695        (when extra-ignores
    696          (setq args (nconc args
    697                            (mapcan
    698                             (lambda (i)
    699                               (list "--exclude" i))
    700                             extra-ignores))))
    701        (with-temp-buffer
    702          (apply #'vc-hg-command t 0 "." "status" args)
    703          (mapcar
    704           (lambda (s) (concat default-directory s))
    705           (split-string (buffer-string) "\0" t)))))))
    706 
    707 (defun project--vc-merge-submodules-p (dir)
    708   (project--value-in-dir
    709    'project-vc-merge-submodules
    710    dir))
    711 
    712 (defun project--git-submodules ()
    713   ;; 'git submodule foreach' is much slower.
    714   (condition-case nil
    715       (with-temp-buffer
    716         (insert-file-contents ".gitmodules")
    717         (let (res)
    718           (goto-char (point-min))
    719           (while (re-search-forward "^[ \t]*path *= *\\(.+\\)" nil t)
    720             (push (match-string 1) res))
    721           (nreverse res)))
    722     (file-missing nil)))
    723 
    724 (cl-defmethod project-ignores ((project (head vc)) dir)
    725   (let* ((root (nth 2 project))
    726          backend)
    727     (append
    728      (when (and backend
    729                 (file-equal-p dir root))
    730        (setq backend (cadr project))
    731        (delq
    732         nil
    733         (mapcar
    734          (lambda (entry)
    735            (cond
    736             ((eq ?! (aref entry 0))
    737              ;; No support for whitelisting (yet).
    738              nil)
    739             ((string-match "\\(/\\)[^/]" entry)
    740              ;; FIXME: This seems to be Git-specific.
    741              ;; And / in the entry (start or even the middle) means
    742              ;; the pattern is "rooted".  Or actually it is then
    743              ;; relative to its respective .gitignore (of which there
    744              ;; could be several), but we only support .gitignore at
    745              ;; the root.
    746              (if (= (match-beginning 0) 0)
    747                  (replace-match "./" t t entry 1)
    748                (concat "./" entry)))
    749             (t entry)))
    750          (condition-case nil
    751              (vc-call-backend backend 'ignore-completion-table root)
    752            (vc-not-supported () nil)))))
    753      (project--value-in-dir 'project-vc-ignores root)
    754      (mapcar
    755       (lambda (dir)
    756         (concat dir "/"))
    757       vc-directory-exclusion-list))))
    758 
    759 (defun project-combine-directories (&rest lists-of-dirs)
    760   "Return a sorted and culled list of directory names.
    761 Appends the elements of LISTS-OF-DIRS together, removes
    762 non-existing directories, as well as directories a parent of
    763 whose is already in the list."
    764   (let* ((dirs (sort
    765                 (mapcar
    766                  (lambda (dir)
    767                    (file-name-as-directory (expand-file-name dir)))
    768                  (apply #'append lists-of-dirs))
    769                 #'string<))
    770          (ref dirs))
    771     ;; Delete subdirectories from the list.
    772     (while (cdr ref)
    773       (if (string-prefix-p (car ref) (cadr ref))
    774           (setcdr ref (cddr ref))
    775         (setq ref (cdr ref))))
    776     (cl-delete-if-not #'file-exists-p dirs)))
    777 
    778 (defun project-subtract-directories (files dirs)
    779   "Return a list of elements from FILES that are outside of DIRS.
    780 DIRS must contain directory names."
    781   ;; Sidestep the issue of expanded/abbreviated file names here.
    782   (cl-set-difference files dirs :test #'file-in-directory-p))
    783 
    784 (defun project--value-in-dir (var dir)
    785   (with-temp-buffer
    786     (setq default-directory dir)
    787     (let ((enable-local-variables :all))
    788       (hack-dir-local-variables-non-file-buffer))
    789     (symbol-value var)))
    790 
    791 (cl-defmethod project-buffers ((project (head vc)))
    792   (let* ((root (expand-file-name (file-name-as-directory (project-root project))))
    793          (modules (unless (or (project--vc-merge-submodules-p root)
    794                               (project--submodule-p root))
    795                     (mapcar
    796                      (lambda (m) (format "%s%s/" root m))
    797                      (project--git-submodules))))
    798          dd
    799          bufs)
    800     (dolist (buf (buffer-list))
    801       (setq dd (expand-file-name (buffer-local-value 'default-directory buf)))
    802       (when (and (string-prefix-p root dd)
    803                  (not (cl-find-if (lambda (module) (string-prefix-p module dd))
    804                                   modules)))
    805         (push buf bufs)))
    806     (nreverse bufs)))
    807 
    808 (cl-defmethod project-name ((_project (head vc)))
    809   (or project-vc-name
    810       (cl-call-next-method)))
    811 
    812 
    813 ;;; Project commands
    814 
    815 ;;;###autoload
    816 (defvar project-prefix-map
    817   (let ((map (make-sparse-keymap)))
    818     (define-key map "!" 'project-shell-command)
    819     (define-key map "&" 'project-async-shell-command)
    820     (define-key map "f" 'project-find-file)
    821     (define-key map "F" 'project-or-external-find-file)
    822     (define-key map "b" 'project-switch-to-buffer)
    823     (define-key map "s" 'project-shell)
    824     (define-key map "d" 'project-find-dir)
    825     (define-key map "D" 'project-dired)
    826     (define-key map "v" 'project-vc-dir)
    827     (define-key map "c" 'project-compile)
    828     (define-key map "e" 'project-eshell)
    829     (define-key map "k" 'project-kill-buffers)
    830     (define-key map "p" 'project-switch-project)
    831     (define-key map "g" 'project-find-regexp)
    832     (define-key map "G" 'project-or-external-find-regexp)
    833     (define-key map "r" 'project-query-replace-regexp)
    834     (define-key map "x" 'project-execute-extended-command)
    835     (define-key map "\C-b" 'project-list-buffers)
    836     map)
    837   "Keymap for project commands.")
    838 
    839 ;;;###autoload (define-key ctl-x-map "p" project-prefix-map)
    840 
    841 ;; We can't have these place-specific maps inherit from
    842 ;; project-prefix-map because project--other-place-command needs to
    843 ;; know which map the key binding came from, as if it came from one of
    844 ;; these maps, we don't want to set display-buffer-overriding-action
    845 
    846 (defvar project-other-window-map
    847   (let ((map (make-sparse-keymap)))
    848     (define-key map "\C-o" #'project-display-buffer)
    849     map)
    850   "Keymap for project commands that display buffers in other windows.")
    851 
    852 (defvar project-other-frame-map
    853   (let ((map (make-sparse-keymap)))
    854     (define-key map "\C-o" #'project-display-buffer-other-frame)
    855     map)
    856   "Keymap for project commands that display buffers in other frames.")
    857 
    858 (defun project--other-place-command (action &optional map)
    859   (let* ((key (read-key-sequence-vector nil t))
    860          (place-cmd (lookup-key map key))
    861          (generic-cmd (lookup-key project-prefix-map key))
    862          (switch-to-buffer-obey-display-actions t)
    863          (display-buffer-overriding-action (unless place-cmd action)))
    864     (if-let ((cmd (or place-cmd generic-cmd)))
    865         (call-interactively cmd)
    866       (user-error "%s is undefined" (key-description key)))))
    867 
    868 ;;;###autoload
    869 (defun project-other-window-command ()
    870   "Run project command, displaying resultant buffer in another window.
    871 
    872 The following commands are available:
    873 
    874 \\{project-prefix-map}
    875 \\{project-other-window-map}"
    876   (interactive)
    877   (project--other-place-command '((display-buffer-pop-up-window)
    878                                   (inhibit-same-window . t))
    879                                 project-other-window-map))
    880 
    881 ;;;###autoload (define-key ctl-x-4-map "p" #'project-other-window-command)
    882 
    883 ;;;###autoload
    884 (defun project-other-frame-command ()
    885   "Run project command, displaying resultant buffer in another frame.
    886 
    887 The following commands are available:
    888 
    889 \\{project-prefix-map}
    890 \\{project-other-frame-map}"
    891   (interactive)
    892   (project--other-place-command '((display-buffer-pop-up-frame))
    893                                 project-other-frame-map))
    894 
    895 ;;;###autoload (define-key ctl-x-5-map "p" #'project-other-frame-command)
    896 
    897 ;;;###autoload
    898 (defun project-other-tab-command ()
    899   "Run project command, displaying resultant buffer in a new tab.
    900 
    901 The following commands are available:
    902 
    903 \\{project-prefix-map}"
    904   (interactive)
    905   (project--other-place-command '((display-buffer-in-new-tab))))
    906 
    907 ;;;###autoload
    908 (when (bound-and-true-p tab-prefix-map)
    909   (define-key tab-prefix-map "p" #'project-other-tab-command))
    910 
    911 (declare-function grep-read-files "grep")
    912 (declare-function xref--find-ignores-arguments "xref")
    913 
    914 ;;;###autoload
    915 (defun project-find-regexp (regexp)
    916   "Find all matches for REGEXP in the current project's roots.
    917 With \\[universal-argument] prefix, you can specify the directory
    918 to search in, and the file name pattern to search for.  The
    919 pattern may use abbreviations defined in `grep-files-aliases',
    920 e.g. entering `ch' is equivalent to `*.[ch]'.  As whitespace
    921 triggers completion when entering a pattern, including it
    922 requires quoting, e.g. `\\[quoted-insert]<space>'."
    923   (interactive (list (project--read-regexp)))
    924   (require 'xref)
    925   (require 'grep)
    926   (let* ((caller-dir default-directory)
    927          (pr (project-current t))
    928          (default-directory (project-root pr))
    929          (files
    930           (if (not current-prefix-arg)
    931               (project-files pr)
    932             (let ((dir (read-directory-name "Base directory: "
    933                                             caller-dir nil t)))
    934               (project--files-in-directory dir
    935                                            nil
    936                                            (grep-read-files regexp))))))
    937     (xref-show-xrefs
    938      (apply-partially #'project--find-regexp-in-files regexp files)
    939      nil)))
    940 
    941 (defun project--dir-ignores (project dir)
    942   (let ((root (project-root project)))
    943     (if (not (file-in-directory-p dir root))
    944         (project-ignores nil nil)       ;The defaults.
    945       (let ((ignores (project-ignores project root)))
    946         (if (file-equal-p root dir)
    947             ignores
    948           ;; FIXME: Update the "rooted" ignores to relate to DIR instead.
    949           (cl-delete-if (lambda (str) (string-prefix-p "./" str))
    950                         ignores))))))
    951 
    952 ;;;###autoload
    953 (defun project-or-external-find-regexp (regexp)
    954   "Find all matches for REGEXP in the project roots or external roots.
    955 With \\[universal-argument] prefix, you can specify the file name
    956 pattern to search for."
    957   (interactive (list (project--read-regexp)))
    958   (require 'xref)
    959   (let* ((pr (project-current t))
    960          (default-directory (project-root pr))
    961          (files
    962           (project-files pr (cons
    963                              (project-root pr)
    964                              (project-external-roots pr)))))
    965     (xref-show-xrefs
    966      (apply-partially #'project--find-regexp-in-files regexp files)
    967      nil)))
    968 
    969 (defun project--find-regexp-in-files (regexp files)
    970   (unless files
    971     (user-error "Empty file list"))
    972   (let ((xrefs (xref-matches-in-files regexp files)))
    973     (unless xrefs
    974       (user-error "No matches for: %s" regexp))
    975     xrefs))
    976 
    977 (defvar project-regexp-history-variable 'grep-regexp-history)
    978 
    979 (defun project--read-regexp ()
    980   (let ((sym (thing-at-point 'symbol t)))
    981     (read-regexp "Find regexp" (and sym (regexp-quote sym))
    982                  project-regexp-history-variable)))
    983 
    984 ;;;###autoload
    985 (defun project-find-file (&optional include-all)
    986   "Visit a file (with completion) in the current project.
    987 
    988 The filename at point (determined by `thing-at-point'), if any,
    989 is available as part of \"future history\".
    990 
    991 If INCLUDE-ALL is non-nil, or with prefix argument when called
    992 interactively, include all files under the project root, except
    993 for VCS directories listed in `vc-directory-exclusion-list'."
    994   (interactive "P")
    995   (let* ((pr (project-current t))
    996          (root (project-root pr))
    997          (dirs (list root)))
    998     (project-find-file-in
    999      (or (thing-at-point 'filename)
   1000          (and buffer-file-name (file-relative-name buffer-file-name root)))
   1001      dirs pr include-all)))
   1002 
   1003 ;;;###autoload
   1004 (defun project-or-external-find-file (&optional include-all)
   1005   "Visit a file (with completion) in the current project or external roots.
   1006 
   1007 The filename at point (determined by `thing-at-point'), if any,
   1008 is available as part of \"future history\".
   1009 
   1010 If INCLUDE-ALL is non-nil, or with prefix argument when called
   1011 interactively, include all files under the project root, except
   1012 for VCS directories listed in `vc-directory-exclusion-list'."
   1013   (interactive "P")
   1014   (let* ((pr (project-current t))
   1015          (dirs (cons
   1016                 (project-root pr)
   1017                 (project-external-roots pr))))
   1018     (project-find-file-in (thing-at-point 'filename) dirs pr include-all)))
   1019 
   1020 (defcustom project-read-file-name-function #'project--read-file-cpd-relative
   1021   "Function to call to read a file name from a list.
   1022 For the arguments list, see `project--read-file-cpd-relative'."
   1023   :type '(choice (const :tag "Read with completion from relative names"
   1024                         project--read-file-cpd-relative)
   1025                  (const :tag "Read with completion from absolute names"
   1026                         project--read-file-absolute)
   1027                  (function :tag "Custom function" nil))
   1028   :group 'project
   1029   :version "27.1")
   1030 
   1031 (defun project--read-file-cpd-relative (prompt
   1032                                         all-files &optional predicate
   1033                                         hist mb-default)
   1034   "Read a file name, prompting with PROMPT.
   1035 ALL-FILES is a list of possible file name completions.
   1036 
   1037 PREDICATE and HIST have the same meaning as in `completing-read'.
   1038 
   1039 MB-DEFAULT is used as part of \"future history\", to be inserted
   1040 by the user at will."
   1041   (let* ((common-parent-directory
   1042           (let ((common-prefix (try-completion "" all-files)))
   1043             (if (> (length common-prefix) 0)
   1044                 (file-name-directory common-prefix))))
   1045          (cpd-length (length common-parent-directory))
   1046          (prompt (if (zerop cpd-length)
   1047                      prompt
   1048                    (concat prompt (format " in %s" common-parent-directory))))
   1049          (included-cpd (when (member common-parent-directory all-files)
   1050                          (setq all-files
   1051                                (delete common-parent-directory all-files))
   1052                          t))
   1053          (substrings (mapcar (lambda (s) (substring s cpd-length)) all-files))
   1054          (_ (when included-cpd
   1055               (setq substrings (cons "./" substrings))))
   1056          (new-collection (project--file-completion-table substrings))
   1057          (abbr-cpd (abbreviate-file-name common-parent-directory))
   1058          (abbr-cpd-length (length abbr-cpd))
   1059          (relname (cl-letf ((history-add-new-input nil)
   1060                             ((symbol-value hist)
   1061                              (mapcan
   1062                               (lambda (s)
   1063                                 (and (string-prefix-p abbr-cpd s)
   1064                                      (not (eq abbr-cpd-length (length s)))
   1065                                      (list (substring s abbr-cpd-length))))
   1066                               (symbol-value hist))))
   1067                     (project--completing-read-strict prompt
   1068                                                      new-collection
   1069                                                      predicate
   1070                                                      hist mb-default)))
   1071          (absname (expand-file-name relname common-parent-directory)))
   1072     (when (and hist history-add-new-input)
   1073       (add-to-history hist (abbreviate-file-name absname)))
   1074     absname))
   1075 
   1076 (defun project--read-file-absolute (prompt
   1077                                     all-files &optional predicate
   1078                                     hist mb-default)
   1079   (project--completing-read-strict prompt
   1080                                    (project--file-completion-table all-files)
   1081                                    predicate
   1082                                    hist mb-default))
   1083 
   1084 (defun project-find-file-in (suggested-filename dirs project &optional include-all)
   1085   "Complete a file name in DIRS in PROJECT and visit the result.
   1086 
   1087 SUGGESTED-FILENAME is a relative file name, or part of it, which
   1088 is used as part of \"future history\".
   1089 
   1090 If INCLUDE-ALL is non-nil, or with prefix argument when called
   1091 interactively, include all files from DIRS, except for VCS
   1092 directories listed in `vc-directory-exclusion-list'."
   1093   (let* ((vc-dirs-ignores (mapcar
   1094                            (lambda (dir)
   1095                              (concat dir "/"))
   1096                            vc-directory-exclusion-list))
   1097          (all-files
   1098           (if include-all
   1099               (mapcan
   1100                (lambda (dir) (project--files-in-directory dir vc-dirs-ignores))
   1101                dirs)
   1102             (project-files project dirs)))
   1103          (completion-ignore-case read-file-name-completion-ignore-case)
   1104          (file (funcall project-read-file-name-function
   1105                         "Find file" all-files nil 'file-name-history
   1106                         suggested-filename)))
   1107     (if (string= file "")
   1108         (user-error "You didn't specify the file")
   1109       (find-file file))))
   1110 
   1111 (defun project--completing-read-strict (prompt
   1112                                         collection &optional predicate
   1113                                         hist mb-default)
   1114   (minibuffer-with-setup-hook
   1115       (lambda ()
   1116         (setq-local minibuffer-default-add-function
   1117                     (lambda ()
   1118                       (let ((minibuffer-default mb-default))
   1119                         (minibuffer-default-add-completions)))))
   1120     (completing-read (format "%s: " prompt)
   1121                      collection predicate 'confirm
   1122                      nil
   1123                      hist)))
   1124 
   1125 ;;;###autoload
   1126 (defun project-find-dir ()
   1127   "Start Dired in a directory inside the current project."
   1128   (interactive)
   1129   (let* ((project (project-current t))
   1130          (all-files (project-files project))
   1131          (completion-ignore-case read-file-name-completion-ignore-case)
   1132          ;; FIXME: This misses directories without any files directly
   1133          ;; inside.  Consider DIRS-ONLY as an argument for
   1134          ;; `project-files-filtered', and see
   1135          ;; https://stackoverflow.com/a/50685235/615245 for possible
   1136          ;; implementation.
   1137          (all-dirs (mapcar #'file-name-directory all-files))
   1138          (dir (funcall project-read-file-name-function
   1139                        "Dired"
   1140                        ;; Some completion UIs show duplicates.
   1141                        (delete-dups all-dirs)
   1142                        nil 'file-name-history)))
   1143     (dired dir)))
   1144 
   1145 ;;;###autoload
   1146 (defun project-dired ()
   1147   "Start Dired in the current project's root."
   1148   (interactive)
   1149   (dired (project-root (project-current t))))
   1150 
   1151 ;;;###autoload
   1152 (defun project-vc-dir ()
   1153   "Run VC-Dir in the current project's root."
   1154   (interactive)
   1155   (vc-dir (project-root (project-current t))))
   1156 
   1157 (declare-function comint-check-proc "comint")
   1158 
   1159 ;;;###autoload
   1160 (defun project-shell ()
   1161   "Start an inferior shell in the current project's root directory.
   1162 If a buffer already exists for running a shell in the project's root,
   1163 switch to it.  Otherwise, create a new shell buffer.
   1164 With \\[universal-argument] prefix arg, create a new inferior shell buffer even
   1165 if one already exists."
   1166   (interactive)
   1167   (require 'comint)
   1168   (let* ((default-directory (project-root (project-current t)))
   1169          (default-project-shell-name (project-prefixed-buffer-name "shell"))
   1170          (shell-buffer (get-buffer default-project-shell-name)))
   1171     (if (and shell-buffer (not current-prefix-arg))
   1172         (if (comint-check-proc shell-buffer)
   1173             (pop-to-buffer shell-buffer (bound-and-true-p display-comint-buffer-action))
   1174           (shell shell-buffer))
   1175       (shell (generate-new-buffer-name default-project-shell-name)))))
   1176 
   1177 ;;;###autoload
   1178 (defun project-eshell ()
   1179   "Start Eshell in the current project's root directory.
   1180 If a buffer already exists for running Eshell in the project's root,
   1181 switch to it.  Otherwise, create a new Eshell buffer.
   1182 With \\[universal-argument] prefix arg, create a new Eshell buffer even
   1183 if one already exists."
   1184   (interactive)
   1185   (defvar eshell-buffer-name)
   1186   (let* ((default-directory (project-root (project-current t)))
   1187          (eshell-buffer-name (project-prefixed-buffer-name "eshell"))
   1188          (eshell-buffer (get-buffer eshell-buffer-name)))
   1189     (if (and eshell-buffer (not current-prefix-arg))
   1190         (pop-to-buffer eshell-buffer (bound-and-true-p display-comint-buffer-action))
   1191       (eshell t))))
   1192 
   1193 ;;;###autoload
   1194 (defun project-async-shell-command ()
   1195   "Run `async-shell-command' in the current project's root directory."
   1196   (declare (interactive-only async-shell-command))
   1197   (interactive)
   1198   (let ((default-directory (project-root (project-current t))))
   1199     (call-interactively #'async-shell-command)))
   1200 
   1201 ;;;###autoload
   1202 (defun project-shell-command ()
   1203   "Run `shell-command' in the current project's root directory."
   1204   (declare (interactive-only shell-command))
   1205   (interactive)
   1206   (let ((default-directory (project-root (project-current t))))
   1207     (call-interactively #'shell-command)))
   1208 
   1209 (declare-function fileloop-continue "fileloop" ())
   1210 
   1211 ;;;###autoload
   1212 (defun project-search (regexp)
   1213   "Search for REGEXP in all the files of the project.
   1214 Stops when a match is found.
   1215 To continue searching for the next match, use the
   1216 command \\[fileloop-continue]."
   1217   (interactive "sSearch (regexp): ")
   1218   (fileloop-initialize-search
   1219    regexp (project-files (project-current t)) 'default)
   1220   (fileloop-continue))
   1221 
   1222 ;;;###autoload
   1223 (defun project-query-replace-regexp (from to)
   1224   "Query-replace REGEXP in all the files of the project.
   1225 Stops when a match is found and prompts for whether to replace it.
   1226 At that prompt, the user must type a character saying what to do
   1227 with the match.  Type SPC or `y' to replace the match,
   1228 DEL or `n' to skip and go to the next match.  For more directions,
   1229 type \\[help-command] at that time.
   1230 If you exit the `query-replace', you can later continue the
   1231 `query-replace' loop using the command \\[fileloop-continue]."
   1232   (interactive
   1233    (let ((query-replace-read-from-regexp-default 'find-tag-default-as-regexp))
   1234      (pcase-let ((`(,from ,to)
   1235                   (query-replace-read-args "Query replace (regexp)" t t)))
   1236        (list from to))))
   1237   (fileloop-initialize-replace
   1238    from to
   1239    ;; XXX: Filter out Git submodules, which are not regular files.
   1240    ;; `project-files' can return those, which is arguably suboptimal,
   1241    ;; but removing them eagerly has performance cost.
   1242    (cl-delete-if-not #'file-regular-p (project-files (project-current t)))
   1243    'default)
   1244   (fileloop-continue))
   1245 
   1246 (defvar compilation-read-command)
   1247 (declare-function compilation-read-command "compile")
   1248 
   1249 (defun project-prefixed-buffer-name (mode)
   1250   (concat "*"
   1251           (file-name-nondirectory
   1252            (directory-file-name default-directory))
   1253           "-"
   1254           (downcase mode)
   1255           "*"))
   1256 
   1257 (defcustom project-compilation-buffer-name-function nil
   1258   "Function to compute the name of a project compilation buffer.
   1259 If non-nil, it overrides `compilation-buffer-name-function' for
   1260 `project-compile'."
   1261   :version "28.1"
   1262   :group 'project
   1263   :type '(choice (const :tag "Default" nil)
   1264                  (const :tag "Prefixed with root directory name"
   1265                         project-prefixed-buffer-name)
   1266                  (function :tag "Custom function")))
   1267 
   1268 ;;;###autoload
   1269 (defun project-compile ()
   1270   "Run `compile' in the project root."
   1271   (declare (interactive-only compile))
   1272   (interactive)
   1273   (let ((default-directory (project-root (project-current t)))
   1274         (compilation-buffer-name-function
   1275          (or project-compilation-buffer-name-function
   1276              compilation-buffer-name-function)))
   1277     (call-interactively #'compile)))
   1278 
   1279 (defcustom project-ignore-buffer-conditions nil
   1280   "List of conditions to filter the buffers to be switched to.
   1281 If any of these conditions are satisfied for a buffer in the
   1282 current project, `project-switch-to-buffer',
   1283 `project-display-buffer' and `project-display-buffer-other-frame'
   1284 ignore it.
   1285 See the doc string of `project-kill-buffer-conditions' for the
   1286 general form of conditions."
   1287   :type '(repeat (choice regexp function symbol
   1288                          (cons :tag "Major mode"
   1289                                (const major-mode) symbol)
   1290                          (cons :tag "Derived mode"
   1291                                (const derived-mode) symbol)
   1292                          (cons :tag "Negation"
   1293                                (const not) sexp)
   1294                          (cons :tag "Conjunction"
   1295                                (const and) sexp)
   1296                          (cons :tag "Disjunction"
   1297                                (const or) sexp)))
   1298   :version "29.1"
   1299   :group 'project
   1300   :package-version '(project . "0.8.2"))
   1301 
   1302 (defun project--read-project-buffer ()
   1303   (let* ((pr (project-current t))
   1304          (current-buffer (current-buffer))
   1305          (other-buffer (other-buffer current-buffer))
   1306          (other-name (buffer-name other-buffer))
   1307          (buffers (project-buffers pr))
   1308          (predicate
   1309           (lambda (buffer)
   1310             ;; BUFFER is an entry (BUF-NAME . BUF-OBJ) of Vbuffer_alist.
   1311             (and (memq (cdr buffer) buffers)
   1312                  (not
   1313                   (project--buffer-check
   1314                    (cdr buffer) project-ignore-buffer-conditions))))))
   1315     (read-buffer
   1316      "Switch to buffer: "
   1317      (when (funcall predicate (cons other-name other-buffer))
   1318        other-name)
   1319      nil
   1320      predicate)))
   1321 
   1322 ;;;###autoload
   1323 (defun project-switch-to-buffer (buffer-or-name)
   1324   "Display buffer BUFFER-OR-NAME in the selected window.
   1325 When called interactively, prompts for a buffer belonging to the
   1326 current project.  Two buffers belong to the same project if their
   1327 project instances, as reported by `project-current' in each
   1328 buffer, are identical."
   1329   (interactive (list (project--read-project-buffer)))
   1330   (switch-to-buffer buffer-or-name))
   1331 
   1332 ;;;###autoload
   1333 (defun project-display-buffer (buffer-or-name)
   1334   "Display BUFFER-OR-NAME in some window, without selecting it.
   1335 When called interactively, prompts for a buffer belonging to the
   1336 current project.  Two buffers belong to the same project if their
   1337 project instances, as reported by `project-current' in each
   1338 buffer, are identical.
   1339 
   1340 This function uses `display-buffer' as a subroutine, which see
   1341 for how it is determined where the buffer will be displayed."
   1342   (interactive (list (project--read-project-buffer)))
   1343   (display-buffer buffer-or-name))
   1344 
   1345 ;;;###autoload
   1346 (defun project-display-buffer-other-frame (buffer-or-name)
   1347   "Display BUFFER-OR-NAME preferably in another frame.
   1348 When called interactively, prompts for a buffer belonging to the
   1349 current project.  Two buffers belong to the same project if their
   1350 project instances, as reported by `project-current' in each
   1351 buffer, are identical.
   1352 
   1353 This function uses `display-buffer-other-frame' as a subroutine,
   1354 which see for how it is determined where the buffer will be
   1355 displayed."
   1356   (interactive (list (project--read-project-buffer)))
   1357   (display-buffer-other-frame buffer-or-name))
   1358 
   1359 ;;;###autoload
   1360 (defun project-list-buffers (&optional arg)
   1361   "Display a list of project buffers.
   1362 The list is displayed in a buffer named \"*Buffer List*\".
   1363 
   1364 By default, all project buffers are listed except those whose names
   1365 start with a space (which are for internal use).  With prefix argument
   1366 ARG, show only buffers that are visiting files."
   1367   (interactive "P")
   1368   (let* ((pr (project-current t))
   1369          (buffer-list-function
   1370           (lambda ()
   1371             (seq-filter
   1372              (lambda (buffer)
   1373                (let ((name (buffer-name buffer))
   1374                      (file (buffer-file-name buffer)))
   1375                  (and (or (not (string= (substring name 0 1) " "))
   1376                           file)
   1377                       (not (eq buffer (current-buffer)))
   1378                       (or file (not Buffer-menu-files-only)))))
   1379              (project-buffers pr)))))
   1380     (display-buffer
   1381      (if (version< emacs-version "29.0.50")
   1382          (let ((buf (list-buffers-noselect
   1383                      arg (with-current-buffer
   1384                              (get-buffer-create "*Buffer List*")
   1385                            (let ((Buffer-menu-files-only arg))
   1386                              (funcall buffer-list-function))))))
   1387            (with-current-buffer buf
   1388              (setq-local revert-buffer-function
   1389                          (lambda (&rest _ignored)
   1390                            (list-buffers--refresh
   1391                             (funcall buffer-list-function))
   1392                            (tabulated-list-print t))))
   1393            buf)
   1394        (list-buffers-noselect arg buffer-list-function)))))
   1395 
   1396 (defcustom project-kill-buffer-conditions
   1397   '(buffer-file-name    ; All file-visiting buffers are included.
   1398     ;; Most of temp and logging buffers (aside from hidden ones):
   1399     (and
   1400      (major-mode . fundamental-mode)
   1401      "\\`[^ ]")
   1402     ;; non-text buffer such as xref, occur, vc, log, ...
   1403     (and (derived-mode . special-mode)
   1404          (not (major-mode . help-mode))
   1405          (not (derived-mode . gnus-mode)))
   1406     (derived-mode . compilation-mode)
   1407     (derived-mode . dired-mode)
   1408     (derived-mode . diff-mode)
   1409     (derived-mode . comint-mode)
   1410     (derived-mode . eshell-mode)
   1411     (derived-mode . change-log-mode))
   1412   "List of conditions to kill buffers related to a project.
   1413 This list is used by `project-kill-buffers'.
   1414 Each condition is either:
   1415 - a regular expression, to match a buffer name,
   1416 - a predicate function that takes a buffer object as argument
   1417   and returns non-nil if the buffer should be killed,
   1418 - a cons-cell, where the car describes how to interpret the cdr.
   1419   The car can be one of the following:
   1420   * `major-mode': the buffer is killed if the buffer's major
   1421     mode is eq to the cons-cell's cdr.
   1422   * `derived-mode': the buffer is killed if the buffer's major
   1423     mode is derived from the major mode in the cons-cell's cdr.
   1424   * `not': the cdr is interpreted as a negation of a condition.
   1425   * `and': the cdr is a list of recursive conditions, that all have
   1426     to be met.
   1427   * `or': the cdr is a list of recursive conditions, of which at
   1428     least one has to be met.
   1429 
   1430 If any of these conditions are satisfied for a buffer in the
   1431 current project, it will be killed."
   1432   :type '(repeat (choice regexp function symbol
   1433                          (cons :tag "Major mode"
   1434                                (const major-mode) symbol)
   1435                          (cons :tag "Derived mode"
   1436                                (const derived-mode) symbol)
   1437                          (cons :tag "Negation"
   1438                                (const not) sexp)
   1439                          (cons :tag "Conjunction"
   1440                                (const and) sexp)
   1441                          (cons :tag "Disjunction"
   1442                                (const or) sexp)))
   1443   :version "29.1"
   1444   :group 'project
   1445   :package-version '(project . "0.8.2"))
   1446 
   1447 (defcustom project-kill-buffers-display-buffer-list nil
   1448   "Non-nil to display list of buffers to kill before killing project buffers.
   1449 Used by `project-kill-buffers'."
   1450   :type 'boolean
   1451   :version "29.1"
   1452   :group 'project
   1453   :package-version '(project . "0.8.2")
   1454   :safe #'booleanp)
   1455 
   1456 (defun project--buffer-check (buf conditions)
   1457   "Check if buffer BUF matches any element of the list CONDITIONS.
   1458 See `project-kill-buffer-conditions' or
   1459 `project-ignore-buffer-conditions' for more details on the
   1460 form of CONDITIONS."
   1461   (catch 'match
   1462     (dolist (c conditions)
   1463       (when (cond
   1464              ((stringp c)
   1465               (string-match-p c (buffer-name buf)))
   1466              ((functionp c)
   1467               (funcall c buf))
   1468              ((eq (car-safe c) 'major-mode)
   1469               (eq (buffer-local-value 'major-mode buf)
   1470                   (cdr c)))
   1471              ((eq (car-safe c) 'derived-mode)
   1472               (provided-mode-derived-p
   1473                (buffer-local-value 'major-mode buf)
   1474                (cdr c)))
   1475              ((eq (car-safe c) 'not)
   1476               (not (project--buffer-check buf (cdr c))))
   1477              ((eq (car-safe c) 'or)
   1478               (project--buffer-check buf (cdr c)))
   1479              ((eq (car-safe c) 'and)
   1480               (seq-every-p
   1481                (apply-partially #'project--buffer-check
   1482                                 buf)
   1483                (mapcar #'list (cdr c)))))
   1484         (throw 'match t)))))
   1485 
   1486 (defun project--buffers-to-kill (pr)
   1487   "Return list of buffers in project PR to kill.
   1488 What buffers should or should not be killed is described
   1489 in `project-kill-buffer-conditions'."
   1490   (let (bufs)
   1491     (dolist (buf (project-buffers pr))
   1492       (when (project--buffer-check buf project-kill-buffer-conditions)
   1493         (push buf bufs)))
   1494     bufs))
   1495 
   1496 ;;;###autoload
   1497 (defun project-kill-buffers (&optional no-confirm)
   1498   "Kill the buffers belonging to the current project.
   1499 Two buffers belong to the same project if their project
   1500 instances, as reported by `project-current' in each buffer, are
   1501 identical.  Only the buffers that match a condition in
   1502 `project-kill-buffer-conditions' will be killed.  If NO-CONFIRM
   1503 is non-nil, the command will not ask the user for confirmation.
   1504 NO-CONFIRM is always nil when the command is invoked
   1505 interactively.
   1506 
   1507 Also see the `project-kill-buffers-display-buffer-list' variable."
   1508   (interactive)
   1509   (let* ((pr (project-current t))
   1510          (bufs (project--buffers-to-kill pr))
   1511          (query-user (lambda ()
   1512                        (yes-or-no-p
   1513                         (format "Kill %d buffers in %s? "
   1514                                 (length bufs)
   1515                                 (project-root pr))))))
   1516     (cond (no-confirm
   1517            (mapc #'kill-buffer bufs))
   1518           ((null bufs)
   1519            (message "No buffers to kill"))
   1520           (project-kill-buffers-display-buffer-list
   1521            (when
   1522                (with-current-buffer-window
   1523                    (get-buffer-create "*Buffer List*")
   1524                    `(display-buffer--maybe-at-bottom
   1525                      (dedicated . t)
   1526                      (window-height . (fit-window-to-buffer))
   1527                      (preserve-size . (nil . t))
   1528                      (body-function
   1529                       . ,#'(lambda (_window)
   1530                              (list-buffers-noselect nil bufs))))
   1531                    #'(lambda (window _value)
   1532                        (with-selected-window window
   1533                          (unwind-protect
   1534                              (funcall query-user)
   1535                            (when (window-live-p window)
   1536                              (quit-restore-window window 'kill))))))
   1537              (mapc #'kill-buffer bufs)))
   1538           ((funcall query-user)
   1539            (mapc #'kill-buffer bufs)))))
   1540 
   1541 
   1542 ;;; Project list
   1543 
   1544 (defcustom project-list-file (locate-user-emacs-file "projects")
   1545   "File in which to save the list of known projects."
   1546   :type 'file
   1547   :version "28.1"
   1548   :group 'project)
   1549 
   1550 (defvar project--list 'unset
   1551   "List structure containing root directories of known projects.
   1552 With some possible metadata (to be decided).")
   1553 
   1554 (defun project--read-project-list ()
   1555   "Initialize `project--list' using contents of `project-list-file'."
   1556   (let ((filename project-list-file))
   1557     (setq project--list
   1558           (when (file-exists-p filename)
   1559             (with-temp-buffer
   1560               (insert-file-contents filename)
   1561               (read (current-buffer)))))
   1562     (unless (seq-every-p
   1563              (lambda (elt) (stringp (car-safe elt)))
   1564              project--list)
   1565       (warn "Contents of %s are in wrong format, resetting"
   1566             project-list-file)
   1567       (setq project--list nil))))
   1568 
   1569 (defun project--ensure-read-project-list ()
   1570   "Initialize `project--list' if it isn't already initialized."
   1571   (when (eq project--list 'unset)
   1572     (project--read-project-list)))
   1573 
   1574 (defun project--write-project-list ()
   1575   "Save `project--list' in `project-list-file'."
   1576   (let ((filename project-list-file))
   1577     (with-temp-buffer
   1578       (insert ";;; -*- lisp-data -*-\n")
   1579       (let ((print-length nil)
   1580             (print-level nil))
   1581         (pp project--list (current-buffer)))
   1582       (write-region nil nil filename nil 'silent))))
   1583 
   1584 ;;;###autoload
   1585 (defun project-remember-project (pr &optional no-write)
   1586   "Add project PR to the front of the project list.
   1587 Save the result in `project-list-file' if the list of projects
   1588 has changed, and NO-WRITE is nil."
   1589   (project--ensure-read-project-list)
   1590   (let ((dir (project-root pr)))
   1591     (unless (equal (caar project--list) dir)
   1592       (dolist (ent project--list)
   1593         (when (equal dir (car ent))
   1594           (setq project--list (delq ent project--list))))
   1595       (push (list dir) project--list)
   1596       (unless no-write
   1597         (project--write-project-list)))))
   1598 
   1599 (defun project--remove-from-project-list (project-root report-message)
   1600   "Remove directory PROJECT-ROOT of a missing project from the project list.
   1601 If the directory was in the list before the removal, save the
   1602 result in `project-list-file'.  Announce the project's removal
   1603 from the list using REPORT-MESSAGE, which is a format string
   1604 passed to `message' as its first argument."
   1605   (project--ensure-read-project-list)
   1606   (when-let ((ent (assoc project-root project--list)))
   1607     (setq project--list (delq ent project--list))
   1608     (message report-message project-root)
   1609     (project--write-project-list)))
   1610 
   1611 ;;;###autoload
   1612 (defun project-forget-project (project-root)
   1613   "Remove directory PROJECT-ROOT from the project list.
   1614 PROJECT-ROOT is the root directory of a known project listed in
   1615 the project list."
   1616   (interactive (list (project-prompt-project-dir)))
   1617   (project--remove-from-project-list
   1618    project-root "Project `%s' removed from known projects"))
   1619 
   1620 (defun project-prompt-project-dir ()
   1621   "Prompt the user for a directory that is one of the known project roots.
   1622 The project is chosen among projects known from the project list,
   1623 see `project-list-file'.
   1624 It's also possible to enter an arbitrary directory not in the list."
   1625   (project--ensure-read-project-list)
   1626   (let* ((dir-choice "... (choose a dir)")
   1627          (choices
   1628           ;; XXX: Just using this for the category (for the substring
   1629           ;; completion style).
   1630           (project--file-completion-table
   1631            (append project--list `(,dir-choice))))
   1632          (pr-dir ""))
   1633     (while (equal pr-dir "")
   1634       ;; If the user simply pressed RET, do this again until they don't.
   1635       (setq pr-dir (completing-read "Select project: " choices nil t)))
   1636     (if (equal pr-dir dir-choice)
   1637         (read-directory-name "Select directory: " default-directory nil t)
   1638       pr-dir)))
   1639 
   1640 ;;;###autoload
   1641 (defun project-known-project-roots ()
   1642   "Return the list of root directories of all known projects."
   1643   (project--ensure-read-project-list)
   1644   (mapcar #'car project--list))
   1645 
   1646 ;;;###autoload
   1647 (defun project-execute-extended-command ()
   1648   "Execute an extended command in project root."
   1649   (declare (interactive-only command-execute))
   1650   (interactive)
   1651   (let ((default-directory (project-root (project-current t))))
   1652     (call-interactively #'execute-extended-command)))
   1653 
   1654 (defun project-remember-projects-under (dir &optional recursive)
   1655   "Index all projects below a directory DIR.
   1656 If RECURSIVE is non-nil, recurse into all subdirectories to find
   1657 more projects.  After finishing, a message is printed summarizing
   1658 the progress.  The function returns the number of detected
   1659 projects."
   1660   (interactive "DDirectory: \nP")
   1661   (project--ensure-read-project-list)
   1662   (let ((queue (list dir))
   1663         (count 0)
   1664         (known (make-hash-table
   1665                 :size (* 2 (length project--list))
   1666                 :test #'equal )))
   1667     (dolist (project (mapcar #'car project--list))
   1668       (puthash project t known))
   1669     (while queue
   1670       (when-let ((subdir (pop queue))
   1671                  ((file-directory-p subdir)))
   1672         (when-let ((project (project--find-in-directory subdir))
   1673                    (project-root (project-root project))
   1674                    ((not (gethash project-root known))))
   1675           (project-remember-project project t)
   1676           (puthash project-root t known)
   1677           (message "Found %s..." project-root)
   1678           (setq count (1+ count)))
   1679         (when (and recursive (file-directory-p subdir))
   1680           (setq queue
   1681                 (nconc
   1682                  (directory-files
   1683                   subdir t directory-files-no-dot-files-regexp t)
   1684                  queue)))))
   1685     (unless (eq recursive 'in-progress)
   1686       (if (zerop count)
   1687           (message "No projects were found")
   1688         (project--write-project-list)
   1689         (message "%d project%s were found"
   1690                  count (if (= count 1) "" "s"))))
   1691     count))
   1692 
   1693 (defun project-forget-zombie-projects ()
   1694   "Forget all known projects that don't exist any more."
   1695   (interactive)
   1696   (dolist (proj (project-known-project-roots))
   1697     (unless (file-exists-p proj)
   1698       (project-forget-project proj))))
   1699 
   1700 (defun project-forget-projects-under (dir &optional recursive)
   1701   "Forget all known projects below a directory DIR.
   1702 If RECURSIVE is non-nil, recurse into all subdirectories to
   1703 remove all known projects.  After finishing, a message is printed
   1704 summarizing the progress.  The function returns the number of
   1705 forgotten projects."
   1706   (interactive "DDirectory: \nP")
   1707   (let ((count 0))
   1708     (if recursive
   1709         (dolist (proj (project-known-project-roots))
   1710           (when (file-in-directory-p proj dir)
   1711             (project-forget-project proj)
   1712             (setq count (1+ count))))
   1713       (dolist (proj (project-known-project-roots))
   1714         (when (file-equal-p (file-name-directory proj) dir)
   1715           (project-forget-project proj)
   1716           (setq count (1+ count)))))
   1717     (if (zerop count)
   1718         (message "No projects were forgotten")
   1719       (project--write-project-list)
   1720       (message "%d project%s were forgotten"
   1721                count (if (= count 1) "" "s")))
   1722     count))
   1723 
   1724 
   1725 ;;; Project switching
   1726 
   1727 (defcustom project-switch-commands
   1728   '((project-find-file "Find file")
   1729     (project-find-regexp "Find regexp")
   1730     (project-find-dir "Find directory")
   1731     (project-vc-dir "VC-Dir")
   1732     (project-eshell "Eshell"))
   1733   "Alist mapping commands to descriptions.
   1734 Used by `project-switch-project' to construct a dispatch menu of
   1735 commands available upon \"switching\" to another project.
   1736 
   1737 Each element is of the form (COMMAND LABEL &optional KEY) where
   1738 COMMAND is the command to run when KEY is pressed.  LABEL is used
   1739 to distinguish the menu entries in the dispatch menu.  If KEY is
   1740 absent, COMMAND must be bound in `project-prefix-map', and the
   1741 key is looked up in that map.
   1742 
   1743 The value can also be a symbol, the name of the command to be
   1744 invoked immediately without any dispatch menu."
   1745   :version "28.1"
   1746   :group 'project
   1747   :package-version '(project . "0.6.0")
   1748   :type '(choice
   1749           (repeat :tag "Commands menu"
   1750            (list
   1751             (symbol :tag "Command")
   1752             (string :tag "Label")
   1753             (choice :tag "Key to press"
   1754                     (const :tag "Infer from the keymap" nil)
   1755                     (character :tag "Explicit key"))))
   1756           (symbol :tag "Single command")))
   1757 
   1758 (defcustom project-switch-use-entire-map nil
   1759   "Make `project-switch-project' use entire `project-prefix-map'.
   1760 If nil, `project-switch-project' will only recognize commands
   1761 listed in `project-switch-commands' and signal an error when
   1762 others are invoked.  Otherwise, all keys in `project-prefix-map'
   1763 are legal even if they aren't listed in the dispatch menu."
   1764   :type 'boolean
   1765   :group 'project
   1766   :version "28.1")
   1767 
   1768 (defun project--keymap-prompt ()
   1769   "Return a prompt for the project switching dispatch menu."
   1770   (mapconcat
   1771    (pcase-lambda (`(,cmd ,label ,key))
   1772      (when (characterp cmd) ; Old format, apparently user-customized.
   1773        (let ((tmp cmd))
   1774          ;; TODO: Add a deprecation warning, probably.
   1775          (setq cmd key
   1776                key tmp)))
   1777      (let ((key (if key
   1778                     (vector key)
   1779                   (where-is-internal cmd (list project-prefix-map) t))))
   1780        (format "[%s] %s"
   1781                (propertize (key-description key) 'face 'bold)
   1782                label)))
   1783    project-switch-commands
   1784    "  "))
   1785 
   1786 (defun project--switch-project-command ()
   1787   (let* ((commands-menu
   1788           (mapcar
   1789            (lambda (row)
   1790              (if (characterp (car row))
   1791                  ;; Deprecated format.
   1792                  ;; XXX: Add a warning about it?
   1793                  (reverse row)
   1794                row))
   1795            project-switch-commands))
   1796          (commands-map
   1797           (let ((temp-map (make-sparse-keymap)))
   1798             (set-keymap-parent temp-map project-prefix-map)
   1799             (dolist (row commands-menu temp-map)
   1800               (when-let ((cmd (nth 0 row))
   1801                          (keychar (nth 2 row)))
   1802                 (define-key temp-map (vector keychar) cmd)))))
   1803          command)
   1804     (while (not command)
   1805       (let* ((overriding-local-map commands-map)
   1806              (choice (read-key-sequence (project--keymap-prompt))))
   1807         (when (setq command (lookup-key commands-map choice))
   1808           (unless (or project-switch-use-entire-map
   1809                       (assq command commands-menu))
   1810             ;; TODO: Add some hint to the prompt, like "key not
   1811             ;; recognized" or something.
   1812             (setq command nil)))
   1813         (let ((global-command (lookup-key (current-global-map) choice)))
   1814           (when (memq global-command
   1815                       '(keyboard-quit keyboard-escape-quit))
   1816             (call-interactively global-command)))))
   1817     command))
   1818 
   1819 ;;;###autoload
   1820 (defun project-switch-project (dir)
   1821   "\"Switch\" to another project by running an Emacs command.
   1822 The available commands are presented as a dispatch menu
   1823 made from `project-switch-commands'.
   1824 
   1825 When called in a program, it will use the project corresponding
   1826 to directory DIR."
   1827   (interactive (list (project-prompt-project-dir)))
   1828   (let ((command (if (symbolp project-switch-commands)
   1829                      project-switch-commands
   1830                    (project--switch-project-command))))
   1831     (let ((project-current-directory-override dir))
   1832       (call-interactively command))))
   1833 
   1834 (provide 'project)
   1835 ;;; project.el ends here