project.el (73568B)
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.4 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 (defun project-try-vc (dir) 498 (defvar vc-svn-admin-directory) 499 (require 'vc-svn) 500 ;; FIXME: Learn to invalidate when the value of 501 ;; `project-vc-merge-submodules' or `project-vc-extra-root-markers' 502 ;; changes. 503 (or (vc-file-getprop dir 'project-vc) 504 (let* ((backend-markers-alist `((Git . ".git") 505 (Hg . ".hg") 506 (Bzr . ".bzr") 507 (SVN . ,vc-svn-admin-directory) 508 (DARCS . "_darcs") 509 (Fossil . ".fslckout"))) 510 (backend-markers 511 (delete 512 nil 513 (mapcar 514 (lambda (b) (assoc-default b backend-markers-alist)) 515 vc-handled-backends))) 516 (marker-re 517 (mapconcat 518 (lambda (m) (format "\\(%s\\)" (wildcard-to-regexp m))) 519 (append backend-markers 520 (project--value-in-dir 'project-vc-extra-root-markers dir)) 521 "\\|")) 522 (locate-dominating-stop-dir-regexp 523 (or vc-ignore-dir-regexp locate-dominating-stop-dir-regexp)) 524 last-matches 525 (root 526 (locate-dominating-file 527 dir 528 (lambda (d) 529 ;; Maybe limit count to 100 when we can drop Emacs < 28. 530 (setq last-matches (directory-files d nil marker-re t))))) 531 (backend 532 (cl-find-if 533 (lambda (b) 534 (member (assoc-default b backend-markers-alist) 535 last-matches)) 536 vc-handled-backends)) 537 project) 538 (when (and 539 (eq backend 'Git) 540 (project--vc-merge-submodules-p root) 541 (project--submodule-p root)) 542 (let* ((parent (file-name-directory (directory-file-name root)))) 543 (setq root (vc-call-backend 'Git 'root parent)))) 544 (when root 545 (setq project (list 'vc backend root)) 546 ;; FIXME: Cache for a shorter time. 547 (vc-file-setprop dir 'project-vc project) 548 project)))) 549 550 (defun project--submodule-p (root) 551 ;; XXX: We only support Git submodules for now. 552 ;; 553 ;; For submodules, at least, we expect the users to prefer them to 554 ;; be considered part of the parent project. For those who don't, 555 ;; there is the custom var now. 556 ;; 557 ;; Some users may also set up things equivalent to Git submodules 558 ;; using "git worktree" (for example). However, we expect that most 559 ;; of them would prefer to treat those as separate projects anyway. 560 (let* ((gitfile (expand-file-name ".git" root))) 561 (cond 562 ((file-directory-p gitfile) 563 nil) 564 ((with-temp-buffer 565 (insert-file-contents gitfile) 566 (goto-char (point-min)) 567 ;; Kind of a hack to distinguish a submodule from 568 ;; other cases of .git files pointing elsewhere. 569 (looking-at "gitdir: [./]+/\\.git/modules/")) 570 t) 571 (t nil)))) 572 573 (cl-defmethod project-root ((project (head vc))) 574 (nth 2 project)) 575 576 (cl-defmethod project-external-roots ((project (head vc))) 577 (project-subtract-directories 578 (project-combine-directories 579 (mapcar 580 #'file-name-as-directory 581 (funcall project-vc-external-roots-function))) 582 (list (project-root project)))) 583 584 (cl-defmethod project-files ((project (head vc)) &optional dirs) 585 (mapcan 586 (lambda (dir) 587 (let ((ignores (project--value-in-dir 'project-vc-ignores (nth 2 project))) 588 (backend (cadr project))) 589 (when backend 590 (require (intern (concat "vc-" (downcase (symbol-name backend)))))) 591 (if (and (file-equal-p dir (nth 2 project)) 592 (cond 593 ((eq backend 'Hg)) 594 ((and (eq backend 'Git) 595 (or 596 (not ignores) 597 (version<= "1.9" (vc-git--program-version))))))) 598 (project--vc-list-files dir backend ignores) 599 (project--files-in-directory 600 dir 601 (project--dir-ignores project dir))))) 602 (or dirs 603 (list (project-root project))))) 604 605 (declare-function vc-git--program-version "vc-git") 606 (declare-function vc-git--run-command-string "vc-git") 607 (declare-function vc-hg-command "vc-hg") 608 609 (defun project--vc-list-files (dir backend extra-ignores) 610 (defvar vc-git-use-literal-pathspecs) 611 (pcase backend 612 (`Git 613 (let* ((default-directory (expand-file-name (file-name-as-directory dir))) 614 (args '("-z")) 615 (vc-git-use-literal-pathspecs nil) 616 (include-untracked (project--value-in-dir 617 'project-vc-include-untracked 618 dir)) 619 files) 620 (setq args (append args 621 '("-c" "--exclude-standard") 622 (and include-untracked '("-o")))) 623 (when extra-ignores 624 (setq args (append args 625 (cons "--" 626 (mapcar 627 (lambda (i) 628 (format 629 ":(exclude,glob,top)%s" 630 (if (string-match "\\*\\*" i) 631 ;; Looks like pathspec glob 632 ;; format already. 633 i 634 (if (string-match "\\./" i) 635 ;; ./abc -> abc 636 (setq i (substring i 2)) 637 ;; abc -> **/abc 638 (setq i (concat "**/" i)) 639 ;; FIXME: '**/abc' should also 640 ;; match a directory with that 641 ;; name, but doesn't (git 2.25.1). 642 ;; Maybe we should replace 643 ;; such entries with two. 644 (if (string-match "/\\'" i) 645 ;; abc/ -> abc/** 646 (setq i (concat i "**")))) 647 i))) 648 extra-ignores))))) 649 (setq files 650 (mapcar 651 (lambda (file) (concat default-directory file)) 652 (split-string 653 (apply #'vc-git--run-command-string nil "ls-files" args) 654 "\0" t))) 655 (when (project--vc-merge-submodules-p default-directory) 656 ;; Unfortunately, 'ls-files --recurse-submodules' conflicts with '-o'. 657 (let* ((submodules (project--git-submodules)) 658 (sub-files 659 (mapcar 660 (lambda (module) 661 (when (file-directory-p module) 662 (project--vc-list-files 663 (concat default-directory module) 664 backend 665 extra-ignores))) 666 submodules))) 667 (setq files 668 (apply #'nconc files sub-files)))) 669 ;; 'git ls-files' returns duplicate entries for merge conflicts. 670 ;; XXX: Better solutions welcome, but this seems cheap enough. 671 (delete-consecutive-dups files))) 672 (`Hg 673 (let* ((default-directory (expand-file-name (file-name-as-directory dir))) 674 (include-untracked (project--value-in-dir 675 'project-vc-include-untracked 676 dir)) 677 (args (list (concat "-mcard" (and include-untracked "u")) 678 "--no-status" 679 "-0"))) 680 (when extra-ignores 681 (setq args (nconc args 682 (mapcan 683 (lambda (i) 684 (list "--exclude" i)) 685 extra-ignores)))) 686 (with-temp-buffer 687 (apply #'vc-hg-command t 0 "." "status" args) 688 (mapcar 689 (lambda (s) (concat default-directory s)) 690 (split-string (buffer-string) "\0" t))))))) 691 692 (defun project--vc-merge-submodules-p (dir) 693 (project--value-in-dir 694 'project-vc-merge-submodules 695 dir)) 696 697 (defun project--git-submodules () 698 ;; 'git submodule foreach' is much slower. 699 (condition-case nil 700 (with-temp-buffer 701 (insert-file-contents ".gitmodules") 702 (let (res) 703 (goto-char (point-min)) 704 (while (re-search-forward "^[ \t]*path *= *\\(.+\\)" nil t) 705 (push (match-string 1) res)) 706 (nreverse res))) 707 (file-missing nil))) 708 709 (cl-defmethod project-ignores ((project (head vc)) dir) 710 (let* ((root (nth 2 project)) 711 backend) 712 (append 713 (when (and backend 714 (file-equal-p dir root)) 715 (setq backend (cadr project)) 716 (delq 717 nil 718 (mapcar 719 (lambda (entry) 720 (cond 721 ((eq ?! (aref entry 0)) 722 ;; No support for whitelisting (yet). 723 nil) 724 ((string-match "\\(/\\)[^/]" entry) 725 ;; FIXME: This seems to be Git-specific. 726 ;; And / in the entry (start or even the middle) means 727 ;; the pattern is "rooted". Or actually it is then 728 ;; relative to its respective .gitignore (of which there 729 ;; could be several), but we only support .gitignore at 730 ;; the root. 731 (if (= (match-beginning 0) 0) 732 (replace-match "./" t t entry 1) 733 (concat "./" entry))) 734 (t entry))) 735 (condition-case nil 736 (vc-call-backend backend 'ignore-completion-table root) 737 (vc-not-supported () nil))))) 738 (project--value-in-dir 'project-vc-ignores root) 739 (mapcar 740 (lambda (dir) 741 (concat dir "/")) 742 vc-directory-exclusion-list)))) 743 744 (defun project-combine-directories (&rest lists-of-dirs) 745 "Return a sorted and culled list of directory names. 746 Appends the elements of LISTS-OF-DIRS together, removes 747 non-existing directories, as well as directories a parent of 748 whose is already in the list." 749 (let* ((dirs (sort 750 (mapcar 751 (lambda (dir) 752 (file-name-as-directory (expand-file-name dir))) 753 (apply #'append lists-of-dirs)) 754 #'string<)) 755 (ref dirs)) 756 ;; Delete subdirectories from the list. 757 (while (cdr ref) 758 (if (string-prefix-p (car ref) (cadr ref)) 759 (setcdr ref (cddr ref)) 760 (setq ref (cdr ref)))) 761 (cl-delete-if-not #'file-exists-p dirs))) 762 763 (defun project-subtract-directories (files dirs) 764 "Return a list of elements from FILES that are outside of DIRS. 765 DIRS must contain directory names." 766 ;; Sidestep the issue of expanded/abbreviated file names here. 767 (cl-set-difference files dirs :test #'file-in-directory-p)) 768 769 (defun project--value-in-dir (var dir) 770 (with-temp-buffer 771 (setq default-directory dir) 772 (let ((enable-local-variables :all)) 773 (hack-dir-local-variables-non-file-buffer)) 774 (symbol-value var))) 775 776 (cl-defmethod project-buffers ((project (head vc))) 777 (let* ((root (expand-file-name (file-name-as-directory (project-root project)))) 778 (modules (unless (or (project--vc-merge-submodules-p root) 779 (project--submodule-p root)) 780 (mapcar 781 (lambda (m) (format "%s%s/" root m)) 782 (project--git-submodules)))) 783 dd 784 bufs) 785 (dolist (buf (buffer-list)) 786 (setq dd (expand-file-name (buffer-local-value 'default-directory buf))) 787 (when (and (string-prefix-p root dd) 788 (not (cl-find-if (lambda (module) (string-prefix-p module dd)) 789 modules))) 790 (push buf bufs))) 791 (nreverse bufs))) 792 793 (cl-defmethod project-name ((_project (head vc))) 794 (or project-vc-name 795 (cl-call-next-method))) 796 797 798 ;;; Project commands 799 800 ;;;###autoload 801 (defvar project-prefix-map 802 (let ((map (make-sparse-keymap))) 803 (define-key map "!" 'project-shell-command) 804 (define-key map "&" 'project-async-shell-command) 805 (define-key map "f" 'project-find-file) 806 (define-key map "F" 'project-or-external-find-file) 807 (define-key map "b" 'project-switch-to-buffer) 808 (define-key map "s" 'project-shell) 809 (define-key map "d" 'project-find-dir) 810 (define-key map "D" 'project-dired) 811 (define-key map "v" 'project-vc-dir) 812 (define-key map "c" 'project-compile) 813 (define-key map "e" 'project-eshell) 814 (define-key map "k" 'project-kill-buffers) 815 (define-key map "p" 'project-switch-project) 816 (define-key map "g" 'project-find-regexp) 817 (define-key map "G" 'project-or-external-find-regexp) 818 (define-key map "r" 'project-query-replace-regexp) 819 (define-key map "x" 'project-execute-extended-command) 820 (define-key map "\C-b" 'project-list-buffers) 821 map) 822 "Keymap for project commands.") 823 824 ;;;###autoload (define-key ctl-x-map "p" project-prefix-map) 825 826 ;; We can't have these place-specific maps inherit from 827 ;; project-prefix-map because project--other-place-command needs to 828 ;; know which map the key binding came from, as if it came from one of 829 ;; these maps, we don't want to set display-buffer-overriding-action 830 831 (defvar project-other-window-map 832 (let ((map (make-sparse-keymap))) 833 (define-key map "\C-o" #'project-display-buffer) 834 map) 835 "Keymap for project commands that display buffers in other windows.") 836 837 (defvar project-other-frame-map 838 (let ((map (make-sparse-keymap))) 839 (define-key map "\C-o" #'project-display-buffer-other-frame) 840 map) 841 "Keymap for project commands that display buffers in other frames.") 842 843 (defun project--other-place-command (action &optional map) 844 (let* ((key (read-key-sequence-vector nil t)) 845 (place-cmd (lookup-key map key)) 846 (generic-cmd (lookup-key project-prefix-map key)) 847 (switch-to-buffer-obey-display-actions t) 848 (display-buffer-overriding-action (unless place-cmd action))) 849 (if-let ((cmd (or place-cmd generic-cmd))) 850 (call-interactively cmd) 851 (user-error "%s is undefined" (key-description key))))) 852 853 ;;;###autoload 854 (defun project-other-window-command () 855 "Run project command, displaying resultant buffer in another window. 856 857 The following commands are available: 858 859 \\{project-prefix-map} 860 \\{project-other-window-map}" 861 (interactive) 862 (project--other-place-command '((display-buffer-pop-up-window) 863 (inhibit-same-window . t)) 864 project-other-window-map)) 865 866 ;;;###autoload (define-key ctl-x-4-map "p" #'project-other-window-command) 867 868 ;;;###autoload 869 (defun project-other-frame-command () 870 "Run project command, displaying resultant buffer in another frame. 871 872 The following commands are available: 873 874 \\{project-prefix-map} 875 \\{project-other-frame-map}" 876 (interactive) 877 (project--other-place-command '((display-buffer-pop-up-frame)) 878 project-other-frame-map)) 879 880 ;;;###autoload (define-key ctl-x-5-map "p" #'project-other-frame-command) 881 882 ;;;###autoload 883 (defun project-other-tab-command () 884 "Run project command, displaying resultant buffer in a new tab. 885 886 The following commands are available: 887 888 \\{project-prefix-map}" 889 (interactive) 890 (project--other-place-command '((display-buffer-in-new-tab)))) 891 892 ;;;###autoload 893 (when (bound-and-true-p tab-prefix-map) 894 (define-key tab-prefix-map "p" #'project-other-tab-command)) 895 896 (declare-function grep-read-files "grep") 897 (declare-function xref--find-ignores-arguments "xref") 898 899 ;;;###autoload 900 (defun project-find-regexp (regexp) 901 "Find all matches for REGEXP in the current project's roots. 902 With \\[universal-argument] prefix, you can specify the directory 903 to search in, and the file name pattern to search for. The 904 pattern may use abbreviations defined in `grep-files-aliases', 905 e.g. entering `ch' is equivalent to `*.[ch]'. As whitespace 906 triggers completion when entering a pattern, including it 907 requires quoting, e.g. `\\[quoted-insert]<space>'." 908 (interactive (list (project--read-regexp))) 909 (require 'xref) 910 (require 'grep) 911 (let* ((caller-dir default-directory) 912 (pr (project-current t)) 913 (default-directory (project-root pr)) 914 (files 915 (if (not current-prefix-arg) 916 (project-files pr) 917 (let ((dir (read-directory-name "Base directory: " 918 caller-dir nil t))) 919 (project--files-in-directory dir 920 nil 921 (grep-read-files regexp)))))) 922 (xref-show-xrefs 923 (apply-partially #'project--find-regexp-in-files regexp files) 924 nil))) 925 926 (defun project--dir-ignores (project dir) 927 (let ((root (project-root project))) 928 (if (not (file-in-directory-p dir root)) 929 (project-ignores nil nil) ;The defaults. 930 (let ((ignores (project-ignores project root))) 931 (if (file-equal-p root dir) 932 ignores 933 ;; FIXME: Update the "rooted" ignores to relate to DIR instead. 934 (cl-delete-if (lambda (str) (string-prefix-p "./" str)) 935 ignores)))))) 936 937 ;;;###autoload 938 (defun project-or-external-find-regexp (regexp) 939 "Find all matches for REGEXP in the project roots or external roots. 940 With \\[universal-argument] prefix, you can specify the file name 941 pattern to search for." 942 (interactive (list (project--read-regexp))) 943 (require 'xref) 944 (let* ((pr (project-current t)) 945 (default-directory (project-root pr)) 946 (files 947 (project-files pr (cons 948 (project-root pr) 949 (project-external-roots pr))))) 950 (xref-show-xrefs 951 (apply-partially #'project--find-regexp-in-files regexp files) 952 nil))) 953 954 (defun project--find-regexp-in-files (regexp files) 955 (unless files 956 (user-error "Empty file list")) 957 (let ((xrefs (xref-matches-in-files regexp files))) 958 (unless xrefs 959 (user-error "No matches for: %s" regexp)) 960 xrefs)) 961 962 (defvar project-regexp-history-variable 'grep-regexp-history) 963 964 (defun project--read-regexp () 965 (let ((sym (thing-at-point 'symbol t))) 966 (read-regexp "Find regexp" (and sym (regexp-quote sym)) 967 project-regexp-history-variable))) 968 969 ;;;###autoload 970 (defun project-find-file (&optional include-all) 971 "Visit a file (with completion) in the current project. 972 973 The filename at point (determined by `thing-at-point'), if any, 974 is available as part of \"future history\". 975 976 If INCLUDE-ALL is non-nil, or with prefix argument when called 977 interactively, include all files under the project root, except 978 for VCS directories listed in `vc-directory-exclusion-list'." 979 (interactive "P") 980 (let* ((pr (project-current t)) 981 (root (project-root pr)) 982 (dirs (list root))) 983 (project-find-file-in 984 (or (thing-at-point 'filename) 985 (and buffer-file-name (file-relative-name buffer-file-name root))) 986 dirs pr include-all))) 987 988 ;;;###autoload 989 (defun project-or-external-find-file (&optional include-all) 990 "Visit a file (with completion) in the current project or external roots. 991 992 The filename at point (determined by `thing-at-point'), if any, 993 is available as part of \"future history\". 994 995 If INCLUDE-ALL is non-nil, or with prefix argument when called 996 interactively, include all files under the project root, except 997 for VCS directories listed in `vc-directory-exclusion-list'." 998 (interactive "P") 999 (let* ((pr (project-current t)) 1000 (dirs (cons 1001 (project-root pr) 1002 (project-external-roots pr)))) 1003 (project-find-file-in (thing-at-point 'filename) dirs pr include-all))) 1004 1005 (defcustom project-read-file-name-function #'project--read-file-cpd-relative 1006 "Function to call to read a file name from a list. 1007 For the arguments list, see `project--read-file-cpd-relative'." 1008 :type '(choice (const :tag "Read with completion from relative names" 1009 project--read-file-cpd-relative) 1010 (const :tag "Read with completion from absolute names" 1011 project--read-file-absolute) 1012 (function :tag "Custom function" nil)) 1013 :group 'project 1014 :version "27.1") 1015 1016 (defun project--read-file-cpd-relative (prompt 1017 all-files &optional predicate 1018 hist mb-default) 1019 "Read a file name, prompting with PROMPT. 1020 ALL-FILES is a list of possible file name completions. 1021 1022 PREDICATE and HIST have the same meaning as in `completing-read'. 1023 1024 MB-DEFAULT is used as part of \"future history\", to be inserted 1025 by the user at will." 1026 (let* ((common-parent-directory 1027 (let ((common-prefix (try-completion "" all-files))) 1028 (if (> (length common-prefix) 0) 1029 (file-name-directory common-prefix)))) 1030 (cpd-length (length common-parent-directory)) 1031 (prompt (if (zerop cpd-length) 1032 prompt 1033 (concat prompt (format " in %s" common-parent-directory)))) 1034 (included-cpd (when (member common-parent-directory all-files) 1035 (setq all-files 1036 (delete common-parent-directory all-files)) 1037 t)) 1038 (substrings (mapcar (lambda (s) (substring s cpd-length)) all-files)) 1039 (_ (when included-cpd 1040 (setq substrings (cons "./" substrings)))) 1041 (new-collection (project--file-completion-table substrings)) 1042 (abbr-cpd (abbreviate-file-name common-parent-directory)) 1043 (abbr-cpd-length (length abbr-cpd)) 1044 (relname (cl-letf ((history-add-new-input nil) 1045 ((symbol-value hist) 1046 (mapcan 1047 (lambda (s) 1048 (and (string-prefix-p abbr-cpd s) 1049 (not (eq abbr-cpd-length (length s))) 1050 (list (substring s abbr-cpd-length)))) 1051 (symbol-value hist)))) 1052 (project--completing-read-strict prompt 1053 new-collection 1054 predicate 1055 hist mb-default))) 1056 (absname (expand-file-name relname common-parent-directory))) 1057 (when (and hist history-add-new-input) 1058 (add-to-history hist (abbreviate-file-name absname))) 1059 absname)) 1060 1061 (defun project--read-file-absolute (prompt 1062 all-files &optional predicate 1063 hist mb-default) 1064 (project--completing-read-strict prompt 1065 (project--file-completion-table all-files) 1066 predicate 1067 hist mb-default)) 1068 1069 (defun project-find-file-in (suggested-filename dirs project &optional include-all) 1070 "Complete a file name in DIRS in PROJECT and visit the result. 1071 1072 SUGGESTED-FILENAME is a relative file name, or part of it, which 1073 is used as part of \"future history\". 1074 1075 If INCLUDE-ALL is non-nil, or with prefix argument when called 1076 interactively, include all files from DIRS, except for VCS 1077 directories listed in `vc-directory-exclusion-list'." 1078 (let* ((vc-dirs-ignores (mapcar 1079 (lambda (dir) 1080 (concat dir "/")) 1081 vc-directory-exclusion-list)) 1082 (all-files 1083 (if include-all 1084 (mapcan 1085 (lambda (dir) (project--files-in-directory dir vc-dirs-ignores)) 1086 dirs) 1087 (project-files project dirs))) 1088 (completion-ignore-case read-file-name-completion-ignore-case) 1089 (file (funcall project-read-file-name-function 1090 "Find file" all-files nil 'file-name-history 1091 suggested-filename))) 1092 (if (string= file "") 1093 (user-error "You didn't specify the file") 1094 (find-file file)))) 1095 1096 (defun project--completing-read-strict (prompt 1097 collection &optional predicate 1098 hist mb-default) 1099 (minibuffer-with-setup-hook 1100 (lambda () 1101 (setq-local minibuffer-default-add-function 1102 (lambda () 1103 (let ((minibuffer-default mb-default)) 1104 (minibuffer-default-add-completions))))) 1105 (completing-read (format "%s: " prompt) 1106 collection predicate 'confirm 1107 nil 1108 hist))) 1109 1110 ;;;###autoload 1111 (defun project-find-dir () 1112 "Start Dired in a directory inside the current project." 1113 (interactive) 1114 (let* ((project (project-current t)) 1115 (all-files (project-files project)) 1116 (completion-ignore-case read-file-name-completion-ignore-case) 1117 ;; FIXME: This misses directories without any files directly 1118 ;; inside. Consider DIRS-ONLY as an argument for 1119 ;; `project-files-filtered', and see 1120 ;; https://stackoverflow.com/a/50685235/615245 for possible 1121 ;; implementation. 1122 (all-dirs (mapcar #'file-name-directory all-files)) 1123 (dir (funcall project-read-file-name-function 1124 "Dired" 1125 ;; Some completion UIs show duplicates. 1126 (delete-dups all-dirs) 1127 nil 'file-name-history))) 1128 (dired dir))) 1129 1130 ;;;###autoload 1131 (defun project-dired () 1132 "Start Dired in the current project's root." 1133 (interactive) 1134 (dired (project-root (project-current t)))) 1135 1136 ;;;###autoload 1137 (defun project-vc-dir () 1138 "Run VC-Dir in the current project's root." 1139 (interactive) 1140 (vc-dir (project-root (project-current t)))) 1141 1142 (declare-function comint-check-proc "comint") 1143 1144 ;;;###autoload 1145 (defun project-shell () 1146 "Start an inferior shell in the current project's root directory. 1147 If a buffer already exists for running a shell in the project's root, 1148 switch to it. Otherwise, create a new shell buffer. 1149 With \\[universal-argument] prefix arg, create a new inferior shell buffer even 1150 if one already exists." 1151 (interactive) 1152 (require 'comint) 1153 (let* ((default-directory (project-root (project-current t))) 1154 (default-project-shell-name (project-prefixed-buffer-name "shell")) 1155 (shell-buffer (get-buffer default-project-shell-name))) 1156 (if (and shell-buffer (not current-prefix-arg)) 1157 (if (comint-check-proc shell-buffer) 1158 (pop-to-buffer shell-buffer (bound-and-true-p display-comint-buffer-action)) 1159 (shell shell-buffer)) 1160 (shell (generate-new-buffer-name default-project-shell-name))))) 1161 1162 ;;;###autoload 1163 (defun project-eshell () 1164 "Start Eshell in the current project's root directory. 1165 If a buffer already exists for running Eshell in the project's root, 1166 switch to it. Otherwise, create a new Eshell buffer. 1167 With \\[universal-argument] prefix arg, create a new Eshell buffer even 1168 if one already exists." 1169 (interactive) 1170 (defvar eshell-buffer-name) 1171 (let* ((default-directory (project-root (project-current t))) 1172 (eshell-buffer-name (project-prefixed-buffer-name "eshell")) 1173 (eshell-buffer (get-buffer eshell-buffer-name))) 1174 (if (and eshell-buffer (not current-prefix-arg)) 1175 (pop-to-buffer eshell-buffer (bound-and-true-p display-comint-buffer-action)) 1176 (eshell t)))) 1177 1178 ;;;###autoload 1179 (defun project-async-shell-command () 1180 "Run `async-shell-command' in the current project's root directory." 1181 (declare (interactive-only async-shell-command)) 1182 (interactive) 1183 (let ((default-directory (project-root (project-current t)))) 1184 (call-interactively #'async-shell-command))) 1185 1186 ;;;###autoload 1187 (defun project-shell-command () 1188 "Run `shell-command' in the current project's root directory." 1189 (declare (interactive-only shell-command)) 1190 (interactive) 1191 (let ((default-directory (project-root (project-current t)))) 1192 (call-interactively #'shell-command))) 1193 1194 (declare-function fileloop-continue "fileloop" ()) 1195 1196 ;;;###autoload 1197 (defun project-search (regexp) 1198 "Search for REGEXP in all the files of the project. 1199 Stops when a match is found. 1200 To continue searching for the next match, use the 1201 command \\[fileloop-continue]." 1202 (interactive "sSearch (regexp): ") 1203 (fileloop-initialize-search 1204 regexp (project-files (project-current t)) 'default) 1205 (fileloop-continue)) 1206 1207 ;;;###autoload 1208 (defun project-query-replace-regexp (from to) 1209 "Query-replace REGEXP in all the files of the project. 1210 Stops when a match is found and prompts for whether to replace it. 1211 At that prompt, the user must type a character saying what to do 1212 with the match. Type SPC or `y' to replace the match, 1213 DEL or `n' to skip and go to the next match. For more directions, 1214 type \\[help-command] at that time. 1215 If you exit the `query-replace', you can later continue the 1216 `query-replace' loop using the command \\[fileloop-continue]." 1217 (interactive 1218 (let ((query-replace-read-from-regexp-default 'find-tag-default-as-regexp)) 1219 (pcase-let ((`(,from ,to) 1220 (query-replace-read-args "Query replace (regexp)" t t))) 1221 (list from to)))) 1222 (fileloop-initialize-replace 1223 from to 1224 ;; XXX: Filter out Git submodules, which are not regular files. 1225 ;; `project-files' can return those, which is arguably suboptimal, 1226 ;; but removing them eagerly has performance cost. 1227 (cl-delete-if-not #'file-regular-p (project-files (project-current t))) 1228 'default) 1229 (fileloop-continue)) 1230 1231 (defvar compilation-read-command) 1232 (declare-function compilation-read-command "compile") 1233 1234 (defun project-prefixed-buffer-name (mode) 1235 (concat "*" 1236 (file-name-nondirectory 1237 (directory-file-name default-directory)) 1238 "-" 1239 (downcase mode) 1240 "*")) 1241 1242 (defcustom project-compilation-buffer-name-function nil 1243 "Function to compute the name of a project compilation buffer. 1244 If non-nil, it overrides `compilation-buffer-name-function' for 1245 `project-compile'." 1246 :version "28.1" 1247 :group 'project 1248 :type '(choice (const :tag "Default" nil) 1249 (const :tag "Prefixed with root directory name" 1250 project-prefixed-buffer-name) 1251 (function :tag "Custom function"))) 1252 1253 ;;;###autoload 1254 (defun project-compile () 1255 "Run `compile' in the project root." 1256 (declare (interactive-only compile)) 1257 (interactive) 1258 (let ((default-directory (project-root (project-current t))) 1259 (compilation-buffer-name-function 1260 (or project-compilation-buffer-name-function 1261 compilation-buffer-name-function))) 1262 (call-interactively #'compile))) 1263 1264 (defcustom project-ignore-buffer-conditions nil 1265 "List of conditions to filter the buffers to be switched to. 1266 If any of these conditions are satisfied for a buffer in the 1267 current project, `project-switch-to-buffer', 1268 `project-display-buffer' and `project-display-buffer-other-frame' 1269 ignore it. 1270 See the doc string of `project-kill-buffer-conditions' for the 1271 general form of conditions." 1272 :type '(repeat (choice regexp function symbol 1273 (cons :tag "Major mode" 1274 (const major-mode) symbol) 1275 (cons :tag "Derived mode" 1276 (const derived-mode) symbol) 1277 (cons :tag "Negation" 1278 (const not) sexp) 1279 (cons :tag "Conjunction" 1280 (const and) sexp) 1281 (cons :tag "Disjunction" 1282 (const or) sexp))) 1283 :version "29.1" 1284 :group 'project 1285 :package-version '(project . "0.8.2")) 1286 1287 (defun project--read-project-buffer () 1288 (let* ((pr (project-current t)) 1289 (current-buffer (current-buffer)) 1290 (other-buffer (other-buffer current-buffer)) 1291 (other-name (buffer-name other-buffer)) 1292 (buffers (project-buffers pr)) 1293 (predicate 1294 (lambda (buffer) 1295 ;; BUFFER is an entry (BUF-NAME . BUF-OBJ) of Vbuffer_alist. 1296 (and (memq (cdr buffer) buffers) 1297 (not 1298 (project--buffer-check 1299 (cdr buffer) project-ignore-buffer-conditions)))))) 1300 (read-buffer 1301 "Switch to buffer: " 1302 (when (funcall predicate (cons other-name other-buffer)) 1303 other-name) 1304 nil 1305 predicate))) 1306 1307 ;;;###autoload 1308 (defun project-switch-to-buffer (buffer-or-name) 1309 "Display buffer BUFFER-OR-NAME in the selected window. 1310 When called interactively, prompts for a buffer belonging to the 1311 current project. Two buffers belong to the same project if their 1312 project instances, as reported by `project-current' in each 1313 buffer, are identical." 1314 (interactive (list (project--read-project-buffer))) 1315 (switch-to-buffer buffer-or-name)) 1316 1317 ;;;###autoload 1318 (defun project-display-buffer (buffer-or-name) 1319 "Display BUFFER-OR-NAME in some window, without selecting it. 1320 When called interactively, prompts for a buffer belonging to the 1321 current project. Two buffers belong to the same project if their 1322 project instances, as reported by `project-current' in each 1323 buffer, are identical. 1324 1325 This function uses `display-buffer' as a subroutine, which see 1326 for how it is determined where the buffer will be displayed." 1327 (interactive (list (project--read-project-buffer))) 1328 (display-buffer buffer-or-name)) 1329 1330 ;;;###autoload 1331 (defun project-display-buffer-other-frame (buffer-or-name) 1332 "Display BUFFER-OR-NAME preferably in another frame. 1333 When called interactively, prompts for a buffer belonging to the 1334 current project. Two buffers belong to the same project if their 1335 project instances, as reported by `project-current' in each 1336 buffer, are identical. 1337 1338 This function uses `display-buffer-other-frame' as a subroutine, 1339 which see for how it is determined where the buffer will be 1340 displayed." 1341 (interactive (list (project--read-project-buffer))) 1342 (display-buffer-other-frame buffer-or-name)) 1343 1344 ;;;###autoload 1345 (defun project-list-buffers (&optional arg) 1346 "Display a list of project buffers. 1347 The list is displayed in a buffer named \"*Buffer List*\". 1348 1349 By default, all project buffers are listed except those whose names 1350 start with a space (which are for internal use). With prefix argument 1351 ARG, show only buffers that are visiting files." 1352 (interactive "P") 1353 (let* ((pr (project-current t)) 1354 (buffer-list-function 1355 (lambda () 1356 (seq-filter 1357 (lambda (buffer) 1358 (let ((name (buffer-name buffer)) 1359 (file (buffer-file-name buffer))) 1360 (and (or (not (string= (substring name 0 1) " ")) 1361 file) 1362 (not (eq buffer (current-buffer))) 1363 (or file (not Buffer-menu-files-only))))) 1364 (project-buffers pr))))) 1365 (display-buffer 1366 (if (version< emacs-version "29.0.50") 1367 (let ((buf (list-buffers-noselect 1368 arg (with-current-buffer 1369 (get-buffer-create "*Buffer List*") 1370 (let ((Buffer-menu-files-only arg)) 1371 (funcall buffer-list-function)))))) 1372 (with-current-buffer buf 1373 (setq-local revert-buffer-function 1374 (lambda (&rest _ignored) 1375 (list-buffers--refresh 1376 (funcall buffer-list-function)) 1377 (tabulated-list-print t)))) 1378 buf) 1379 (list-buffers-noselect arg buffer-list-function))))) 1380 1381 (defcustom project-kill-buffer-conditions 1382 '(buffer-file-name ; All file-visiting buffers are included. 1383 ;; Most of temp and logging buffers (aside from hidden ones): 1384 (and 1385 (major-mode . fundamental-mode) 1386 "\\`[^ ]") 1387 ;; non-text buffer such as xref, occur, vc, log, ... 1388 (and (derived-mode . special-mode) 1389 (not (major-mode . help-mode)) 1390 (not (derived-mode . gnus-mode))) 1391 (derived-mode . compilation-mode) 1392 (derived-mode . dired-mode) 1393 (derived-mode . diff-mode) 1394 (derived-mode . comint-mode) 1395 (derived-mode . eshell-mode) 1396 (derived-mode . change-log-mode)) 1397 "List of conditions to kill buffers related to a project. 1398 This list is used by `project-kill-buffers'. 1399 Each condition is either: 1400 - a regular expression, to match a buffer name, 1401 - a predicate function that takes a buffer object as argument 1402 and returns non-nil if the buffer should be killed, 1403 - a cons-cell, where the car describes how to interpret the cdr. 1404 The car can be one of the following: 1405 * `major-mode': the buffer is killed if the buffer's major 1406 mode is eq to the cons-cell's cdr. 1407 * `derived-mode': the buffer is killed if the buffer's major 1408 mode is derived from the major mode in the cons-cell's cdr. 1409 * `not': the cdr is interpreted as a negation of a condition. 1410 * `and': the cdr is a list of recursive conditions, that all have 1411 to be met. 1412 * `or': the cdr is a list of recursive conditions, of which at 1413 least one has to be met. 1414 1415 If any of these conditions are satisfied for a buffer in the 1416 current project, it will be killed." 1417 :type '(repeat (choice regexp function symbol 1418 (cons :tag "Major mode" 1419 (const major-mode) symbol) 1420 (cons :tag "Derived mode" 1421 (const derived-mode) symbol) 1422 (cons :tag "Negation" 1423 (const not) sexp) 1424 (cons :tag "Conjunction" 1425 (const and) sexp) 1426 (cons :tag "Disjunction" 1427 (const or) sexp))) 1428 :version "29.1" 1429 :group 'project 1430 :package-version '(project . "0.8.2")) 1431 1432 (defcustom project-kill-buffers-display-buffer-list nil 1433 "Non-nil to display list of buffers to kill before killing project buffers. 1434 Used by `project-kill-buffers'." 1435 :type 'boolean 1436 :version "29.1" 1437 :group 'project 1438 :package-version '(project . "0.8.2") 1439 :safe #'booleanp) 1440 1441 (defun project--buffer-check (buf conditions) 1442 "Check if buffer BUF matches any element of the list CONDITIONS. 1443 See `project-kill-buffer-conditions' or 1444 `project-ignore-buffer-conditions' for more details on the 1445 form of CONDITIONS." 1446 (catch 'match 1447 (dolist (c conditions) 1448 (when (cond 1449 ((stringp c) 1450 (string-match-p c (buffer-name buf))) 1451 ((functionp c) 1452 (funcall c buf)) 1453 ((eq (car-safe c) 'major-mode) 1454 (eq (buffer-local-value 'major-mode buf) 1455 (cdr c))) 1456 ((eq (car-safe c) 'derived-mode) 1457 (provided-mode-derived-p 1458 (buffer-local-value 'major-mode buf) 1459 (cdr c))) 1460 ((eq (car-safe c) 'not) 1461 (not (project--buffer-check buf (cdr c)))) 1462 ((eq (car-safe c) 'or) 1463 (project--buffer-check buf (cdr c))) 1464 ((eq (car-safe c) 'and) 1465 (seq-every-p 1466 (apply-partially #'project--buffer-check 1467 buf) 1468 (mapcar #'list (cdr c))))) 1469 (throw 'match t))))) 1470 1471 (defun project--buffers-to-kill (pr) 1472 "Return list of buffers in project PR to kill. 1473 What buffers should or should not be killed is described 1474 in `project-kill-buffer-conditions'." 1475 (let (bufs) 1476 (dolist (buf (project-buffers pr)) 1477 (when (project--buffer-check buf project-kill-buffer-conditions) 1478 (push buf bufs))) 1479 bufs)) 1480 1481 ;;;###autoload 1482 (defun project-kill-buffers (&optional no-confirm) 1483 "Kill the buffers belonging to the current project. 1484 Two buffers belong to the same project if their project 1485 instances, as reported by `project-current' in each buffer, are 1486 identical. Only the buffers that match a condition in 1487 `project-kill-buffer-conditions' will be killed. If NO-CONFIRM 1488 is non-nil, the command will not ask the user for confirmation. 1489 NO-CONFIRM is always nil when the command is invoked 1490 interactively. 1491 1492 Also see the `project-kill-buffers-display-buffer-list' variable." 1493 (interactive) 1494 (let* ((pr (project-current t)) 1495 (bufs (project--buffers-to-kill pr)) 1496 (query-user (lambda () 1497 (yes-or-no-p 1498 (format "Kill %d buffers in %s? " 1499 (length bufs) 1500 (project-root pr)))))) 1501 (cond (no-confirm 1502 (mapc #'kill-buffer bufs)) 1503 ((null bufs) 1504 (message "No buffers to kill")) 1505 (project-kill-buffers-display-buffer-list 1506 (when 1507 (with-current-buffer-window 1508 (get-buffer-create "*Buffer List*") 1509 `(display-buffer--maybe-at-bottom 1510 (dedicated . t) 1511 (window-height . (fit-window-to-buffer)) 1512 (preserve-size . (nil . t)) 1513 (body-function 1514 . ,#'(lambda (_window) 1515 (list-buffers-noselect nil bufs)))) 1516 #'(lambda (window _value) 1517 (with-selected-window window 1518 (unwind-protect 1519 (funcall query-user) 1520 (when (window-live-p window) 1521 (quit-restore-window window 'kill)))))) 1522 (mapc #'kill-buffer bufs))) 1523 ((funcall query-user) 1524 (mapc #'kill-buffer bufs))))) 1525 1526 1527 ;;; Project list 1528 1529 (defcustom project-list-file (locate-user-emacs-file "projects") 1530 "File in which to save the list of known projects." 1531 :type 'file 1532 :version "28.1" 1533 :group 'project) 1534 1535 (defvar project--list 'unset 1536 "List structure containing root directories of known projects. 1537 With some possible metadata (to be decided).") 1538 1539 (defun project--read-project-list () 1540 "Initialize `project--list' using contents of `project-list-file'." 1541 (let ((filename project-list-file)) 1542 (setq project--list 1543 (when (file-exists-p filename) 1544 (with-temp-buffer 1545 (insert-file-contents filename) 1546 (read (current-buffer))))) 1547 (unless (seq-every-p 1548 (lambda (elt) (stringp (car-safe elt))) 1549 project--list) 1550 (warn "Contents of %s are in wrong format, resetting" 1551 project-list-file) 1552 (setq project--list nil)))) 1553 1554 (defun project--ensure-read-project-list () 1555 "Initialize `project--list' if it isn't already initialized." 1556 (when (eq project--list 'unset) 1557 (project--read-project-list))) 1558 1559 (defun project--write-project-list () 1560 "Save `project--list' in `project-list-file'." 1561 (let ((filename project-list-file)) 1562 (with-temp-buffer 1563 (insert ";;; -*- lisp-data -*-\n") 1564 (let ((print-length nil) 1565 (print-level nil)) 1566 (pp project--list (current-buffer))) 1567 (write-region nil nil filename nil 'silent)))) 1568 1569 ;;;###autoload 1570 (defun project-remember-project (pr &optional no-write) 1571 "Add project PR to the front of the project list. 1572 Save the result in `project-list-file' if the list of projects 1573 has changed, and NO-WRITE is nil." 1574 (project--ensure-read-project-list) 1575 (let ((dir (project-root pr))) 1576 (unless (equal (caar project--list) dir) 1577 (dolist (ent project--list) 1578 (when (equal dir (car ent)) 1579 (setq project--list (delq ent project--list)))) 1580 (push (list dir) project--list) 1581 (unless no-write 1582 (project--write-project-list))))) 1583 1584 (defun project--remove-from-project-list (project-root report-message) 1585 "Remove directory PROJECT-ROOT of a missing project from the project list. 1586 If the directory was in the list before the removal, save the 1587 result in `project-list-file'. Announce the project's removal 1588 from the list using REPORT-MESSAGE, which is a format string 1589 passed to `message' as its first argument." 1590 (project--ensure-read-project-list) 1591 (when-let ((ent (assoc project-root project--list))) 1592 (setq project--list (delq ent project--list)) 1593 (message report-message project-root) 1594 (project--write-project-list))) 1595 1596 ;;;###autoload 1597 (defun project-forget-project (project-root) 1598 "Remove directory PROJECT-ROOT from the project list. 1599 PROJECT-ROOT is the root directory of a known project listed in 1600 the project list." 1601 (interactive (list (project-prompt-project-dir))) 1602 (project--remove-from-project-list 1603 project-root "Project `%s' removed from known projects")) 1604 1605 (defun project-prompt-project-dir () 1606 "Prompt the user for a directory that is one of the known project roots. 1607 The project is chosen among projects known from the project list, 1608 see `project-list-file'. 1609 It's also possible to enter an arbitrary directory not in the list." 1610 (project--ensure-read-project-list) 1611 (let* ((dir-choice "... (choose a dir)") 1612 (choices 1613 ;; XXX: Just using this for the category (for the substring 1614 ;; completion style). 1615 (project--file-completion-table 1616 (append project--list `(,dir-choice)))) 1617 (pr-dir "")) 1618 (while (equal pr-dir "") 1619 ;; If the user simply pressed RET, do this again until they don't. 1620 (setq pr-dir (completing-read "Select project: " choices nil t))) 1621 (if (equal pr-dir dir-choice) 1622 (read-directory-name "Select directory: " default-directory nil t) 1623 pr-dir))) 1624 1625 ;;;###autoload 1626 (defun project-known-project-roots () 1627 "Return the list of root directories of all known projects." 1628 (project--ensure-read-project-list) 1629 (mapcar #'car project--list)) 1630 1631 ;;;###autoload 1632 (defun project-execute-extended-command () 1633 "Execute an extended command in project root." 1634 (declare (interactive-only command-execute)) 1635 (interactive) 1636 (let ((default-directory (project-root (project-current t)))) 1637 (call-interactively #'execute-extended-command))) 1638 1639 (defun project-remember-projects-under (dir &optional recursive) 1640 "Index all projects below a directory DIR. 1641 If RECURSIVE is non-nil, recurse into all subdirectories to find 1642 more projects. After finishing, a message is printed summarizing 1643 the progress. The function returns the number of detected 1644 projects." 1645 (interactive "DDirectory: \nP") 1646 (project--ensure-read-project-list) 1647 (let ((queue (list dir)) 1648 (count 0) 1649 (known (make-hash-table 1650 :size (* 2 (length project--list)) 1651 :test #'equal ))) 1652 (dolist (project (mapcar #'car project--list)) 1653 (puthash project t known)) 1654 (while queue 1655 (when-let ((subdir (pop queue)) 1656 ((file-directory-p subdir))) 1657 (when-let ((project (project--find-in-directory subdir)) 1658 (project-root (project-root project)) 1659 ((not (gethash project-root known)))) 1660 (project-remember-project project t) 1661 (puthash project-root t known) 1662 (message "Found %s..." project-root) 1663 (setq count (1+ count))) 1664 (when (and recursive (file-directory-p subdir)) 1665 (setq queue 1666 (nconc 1667 (directory-files 1668 subdir t directory-files-no-dot-files-regexp t) 1669 queue))))) 1670 (unless (eq recursive 'in-progress) 1671 (if (zerop count) 1672 (message "No projects were found") 1673 (project--write-project-list) 1674 (message "%d project%s were found" 1675 count (if (= count 1) "" "s")))) 1676 count)) 1677 1678 (defun project-forget-zombie-projects () 1679 "Forget all known projects that don't exist any more." 1680 (interactive) 1681 (dolist (proj (project-known-project-roots)) 1682 (unless (file-exists-p proj) 1683 (project-forget-project proj)))) 1684 1685 (defun project-forget-projects-under (dir &optional recursive) 1686 "Forget all known projects below a directory DIR. 1687 If RECURSIVE is non-nil, recurse into all subdirectories to 1688 remove all known projects. After finishing, a message is printed 1689 summarizing the progress. The function returns the number of 1690 forgotten projects." 1691 (interactive "DDirectory: \nP") 1692 (let ((count 0)) 1693 (if recursive 1694 (dolist (proj (project-known-project-roots)) 1695 (when (file-in-directory-p proj dir) 1696 (project-forget-project proj) 1697 (setq count (1+ count)))) 1698 (dolist (proj (project-known-project-roots)) 1699 (when (file-equal-p (file-name-directory proj) dir) 1700 (project-forget-project proj) 1701 (setq count (1+ count))))) 1702 (if (zerop count) 1703 (message "No projects were forgotten") 1704 (project--write-project-list) 1705 (message "%d project%s were forgotten" 1706 count (if (= count 1) "" "s"))) 1707 count)) 1708 1709 1710 ;;; Project switching 1711 1712 (defcustom project-switch-commands 1713 '((project-find-file "Find file") 1714 (project-find-regexp "Find regexp") 1715 (project-find-dir "Find directory") 1716 (project-vc-dir "VC-Dir") 1717 (project-eshell "Eshell")) 1718 "Alist mapping commands to descriptions. 1719 Used by `project-switch-project' to construct a dispatch menu of 1720 commands available upon \"switching\" to another project. 1721 1722 Each element is of the form (COMMAND LABEL &optional KEY) where 1723 COMMAND is the command to run when KEY is pressed. LABEL is used 1724 to distinguish the menu entries in the dispatch menu. If KEY is 1725 absent, COMMAND must be bound in `project-prefix-map', and the 1726 key is looked up in that map. 1727 1728 The value can also be a symbol, the name of the command to be 1729 invoked immediately without any dispatch menu." 1730 :version "28.1" 1731 :group 'project 1732 :package-version '(project . "0.6.0") 1733 :type '(choice 1734 (repeat :tag "Commands menu" 1735 (list 1736 (symbol :tag "Command") 1737 (string :tag "Label") 1738 (choice :tag "Key to press" 1739 (const :tag "Infer from the keymap" nil) 1740 (character :tag "Explicit key")))) 1741 (symbol :tag "Single command"))) 1742 1743 (defcustom project-switch-use-entire-map nil 1744 "Make `project-switch-project' use entire `project-prefix-map'. 1745 If nil, `project-switch-project' will only recognize commands 1746 listed in `project-switch-commands' and signal an error when 1747 others are invoked. Otherwise, all keys in `project-prefix-map' 1748 are legal even if they aren't listed in the dispatch menu." 1749 :type 'boolean 1750 :group 'project 1751 :version "28.1") 1752 1753 (defun project--keymap-prompt () 1754 "Return a prompt for the project switching dispatch menu." 1755 (mapconcat 1756 (pcase-lambda (`(,cmd ,label ,key)) 1757 (when (characterp cmd) ; Old format, apparently user-customized. 1758 (let ((tmp cmd)) 1759 ;; TODO: Add a deprecation warning, probably. 1760 (setq cmd key 1761 key tmp))) 1762 (let ((key (if key 1763 (vector key) 1764 (where-is-internal cmd (list project-prefix-map) t)))) 1765 (format "[%s] %s" 1766 (propertize (key-description key) 'face 'bold) 1767 label))) 1768 project-switch-commands 1769 " ")) 1770 1771 (defun project--switch-project-command () 1772 (let* ((commands-menu 1773 (mapcar 1774 (lambda (row) 1775 (if (characterp (car row)) 1776 ;; Deprecated format. 1777 ;; XXX: Add a warning about it? 1778 (reverse row) 1779 row)) 1780 project-switch-commands)) 1781 (commands-map 1782 (let ((temp-map (make-sparse-keymap))) 1783 (set-keymap-parent temp-map project-prefix-map) 1784 (dolist (row commands-menu temp-map) 1785 (when-let ((cmd (nth 0 row)) 1786 (keychar (nth 2 row))) 1787 (define-key temp-map (vector keychar) cmd))))) 1788 command) 1789 (while (not command) 1790 (let* ((overriding-local-map commands-map) 1791 (choice (read-key-sequence (project--keymap-prompt)))) 1792 (when (setq command (lookup-key commands-map choice)) 1793 (unless (or project-switch-use-entire-map 1794 (assq command commands-menu)) 1795 ;; TODO: Add some hint to the prompt, like "key not 1796 ;; recognized" or something. 1797 (setq command nil))) 1798 (let ((global-command (lookup-key (current-global-map) choice))) 1799 (when (memq global-command 1800 '(keyboard-quit keyboard-escape-quit)) 1801 (call-interactively global-command))))) 1802 command)) 1803 1804 ;;;###autoload 1805 (defun project-switch-project (dir) 1806 "\"Switch\" to another project by running an Emacs command. 1807 The available commands are presented as a dispatch menu 1808 made from `project-switch-commands'. 1809 1810 When called in a program, it will use the project corresponding 1811 to directory DIR." 1812 (interactive (list (project-prompt-project-dir))) 1813 (let ((command (if (symbolp project-switch-commands) 1814 project-switch-commands 1815 (project--switch-project-command)))) 1816 (let ((project-current-directory-override dir)) 1817 (call-interactively command)))) 1818 1819 (provide 'project) 1820 ;;; project.el ends here