2015-07-10 04:34:41 +03:00
|
|
|
|
;;; project.el --- Operations on the current project -*- lexical-binding: t; -*-
|
|
|
|
|
|
2023-01-01 05:31:12 -05:00
|
|
|
|
;; Copyright (C) 2015-2023 Free Software Foundation, Inc.
|
2023-01-28 03:17:39 +02:00
|
|
|
|
;; Version: 0.9.6
|
2022-02-21 03:09:32 +02:00
|
|
|
|
;; Package-Requires: ((emacs "26.1") (xref "1.4.0"))
|
2020-05-13 11:31:21 +01:00
|
|
|
|
|
|
|
|
|
;; This is a GNU ELPA :core package. Avoid using functionality that
|
|
|
|
|
;; not compatible with the version of Emacs recorded above.
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
|
|
|
|
;; This file is part of GNU Emacs.
|
|
|
|
|
|
|
|
|
|
;; GNU Emacs is free software: you can redistribute it and/or modify
|
|
|
|
|
;; it under the terms of the GNU General Public License as published by
|
|
|
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
;; (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
;; GNU Emacs is distributed in the hope that it will be useful,
|
|
|
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
;; GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
2017-09-13 15:52:52 -07:00
|
|
|
|
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
|
2020-05-25 22:29:06 +03:00
|
|
|
|
;; NOTE: The project API is still experimental and can change in major,
|
|
|
|
|
;; backward-incompatible ways. Everyone is encouraged to try it, and
|
|
|
|
|
;; report to us any problems or use cases we hadn't anticipated, by
|
|
|
|
|
;; sending an email to emacs-devel, or `M-x report-emacs-bug'.
|
|
|
|
|
;;
|
2015-07-10 04:34:41 +03:00
|
|
|
|
;; This file contains generic infrastructure for dealing with
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;; projects, some utility functions, and commands using that
|
|
|
|
|
;; infrastructure.
|
2015-08-05 15:08:00 +03:00
|
|
|
|
;;
|
2015-11-03 02:11:45 +02:00
|
|
|
|
;; The goal is to make it easier for Lisp programs to operate on the
|
2015-08-05 15:08:00 +03:00
|
|
|
|
;; current project, without having to know which package handles
|
|
|
|
|
;; detection of that project type, parsing its config files, etc.
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;;
|
2020-07-20 03:58:08 +03:00
|
|
|
|
;; This file consists of following parts:
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;;
|
2020-07-20 03:58:08 +03:00
|
|
|
|
;; Infrastructure (the public API):
|
|
|
|
|
;;
|
|
|
|
|
;; Function `project-current' that returns the current project
|
|
|
|
|
;; instance based on the value of the hook `project-find-functions',
|
|
|
|
|
;; and several generic functions that act on it.
|
|
|
|
|
;;
|
|
|
|
|
;; `project-root' must be defined for every project.
|
|
|
|
|
;; `project-files' can be overridden for performance purposes.
|
|
|
|
|
;; `project-ignores' and `project-external-roots' describe the project
|
|
|
|
|
;; files and its relations to external directories. `project-files'
|
|
|
|
|
;; should be consistent with `project-ignores'.
|
|
|
|
|
;;
|
2021-08-21 05:26:12 +03:00
|
|
|
|
;; `project-buffers' can be overridden if the project has some unusual
|
|
|
|
|
;; shape (e.g. it contains files residing outside of its root, or some
|
2021-08-21 10:10:28 +03:00
|
|
|
|
;; files inside the root must not be considered a part of it). It
|
2021-08-21 05:26:12 +03:00
|
|
|
|
;; should be consistent with `project-files'.
|
|
|
|
|
;;
|
2020-07-20 03:58:08 +03:00
|
|
|
|
;; This list can change in future versions.
|
|
|
|
|
;;
|
2022-12-01 04:05:49 +02:00
|
|
|
|
;; Transient project:
|
|
|
|
|
;;
|
|
|
|
|
;; An instance of this type can be returned by `project-current' if no
|
|
|
|
|
;; project was detected automatically, and the user had to pick a
|
|
|
|
|
;; directory manually. The fileset it describes is the whole
|
|
|
|
|
;; directory, with the exception of some standard ignored files and
|
|
|
|
|
;; directories. This type has little purpose otherwise, as the only
|
|
|
|
|
;; generic function it provides an override for is `project-root'.
|
|
|
|
|
;;
|
|
|
|
|
;; VC-aware project:
|
2020-07-20 03:58:08 +03:00
|
|
|
|
;;
|
|
|
|
|
;; Originally conceived as an example implementation, now it's a
|
|
|
|
|
;; relatively fast backend that delegates to 'git ls-files' or 'hg
|
|
|
|
|
;; status' to list the project's files. It honors the VC ignore
|
|
|
|
|
;; files, but supports additions to the list using the user option
|
2022-12-01 04:05:49 +02:00
|
|
|
|
;; `project-vc-ignores' (usually through .dir-locals.el). See the
|
|
|
|
|
;; customization group `project-vc' for other options that control its
|
|
|
|
|
;; behavior.
|
|
|
|
|
;;
|
|
|
|
|
;; If the repository is using any other VCS than Git or Hg, the file
|
|
|
|
|
;; listing uses the default mechanism based on `find-program'.
|
|
|
|
|
;;
|
|
|
|
|
;; This project type can also be used for non-VCS controlled
|
|
|
|
|
;; directories, see the variable `project-vc-extra-root-markers'.
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;;
|
|
|
|
|
;; Utils:
|
|
|
|
|
;;
|
|
|
|
|
;; `project-combine-directories' and `project-subtract-directories',
|
2015-12-28 19:05:50 -08:00
|
|
|
|
;; mainly for use in the abovementioned generics' implementations.
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;;
|
2020-10-05 03:02:10 +03:00
|
|
|
|
;; `project-known-project-roots' and `project-remember-project' to
|
|
|
|
|
;; interact with the "known projects" list.
|
|
|
|
|
;;
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;; Commands:
|
|
|
|
|
;;
|
2020-07-20 03:58:08 +03:00
|
|
|
|
;; `project-prefix-map' contains the full list of commands defined in
|
|
|
|
|
;; this package. This map uses the prefix `C-x p' by default.
|
|
|
|
|
;; Type `C-x p f' to find file in the current project.
|
|
|
|
|
;; Type `C-x p C-h' to see all available commands and bindings.
|
|
|
|
|
;;
|
|
|
|
|
;; All commands defined in this package are implemented using the
|
|
|
|
|
;; public API only. As a result, they will work with any project
|
|
|
|
|
;; backend that follows the protocol.
|
|
|
|
|
;;
|
|
|
|
|
;; Any third-party code that wants to use this package should likewise
|
|
|
|
|
;; target the public API. Use any of the built-in commands as the
|
|
|
|
|
;; example.
|
|
|
|
|
;;
|
|
|
|
|
;; How to create a new backend:
|
|
|
|
|
;;
|
|
|
|
|
;; - Consider whether you really should, or whether there are other
|
|
|
|
|
;; ways to reach your goals. If the backend's performance is
|
|
|
|
|
;; significantly lower than that of the built-in one, and it's first
|
|
|
|
|
;; in the list, it will affect all commands that use it. Unless you
|
|
|
|
|
;; are going to be using it only yourself or in special circumstances,
|
|
|
|
|
;; you will probably want it to be fast, and it's unlikely to be a
|
|
|
|
|
;; trivial endeavor. `project-files' is the method to optimize (the
|
|
|
|
|
;; default implementation gets slower the more files the directory
|
|
|
|
|
;; has, and the longer the list of ignores is).
|
|
|
|
|
;;
|
|
|
|
|
;; - Choose the format of the value that represents a project for your
|
|
|
|
|
;; backend (we call it project instance). Don't use any of the
|
2020-07-20 04:07:11 +03:00
|
|
|
|
;; formats from other backends. The format can be arbitrary, as long
|
|
|
|
|
;; as the datatype is something `cl-defmethod' can dispatch on. The
|
|
|
|
|
;; value should be stable (when compared with `equal') across
|
|
|
|
|
;; invocations, meaning calls to that function from buffers belonging
|
|
|
|
|
;; to the same project should return equal values.
|
2020-07-20 03:58:08 +03:00
|
|
|
|
;;
|
|
|
|
|
;; - Write a new function that will determine the current project
|
|
|
|
|
;; based on the directory and add it to `project-find-functions'
|
2021-03-11 22:27:20 +00:00
|
|
|
|
;; (which see) using `add-hook'. It is a good idea to depend on the
|
2020-07-20 03:58:08 +03:00
|
|
|
|
;; directory only, and not on the current major mode, for example.
|
|
|
|
|
;; Because the usual expectation is that all files in the directory
|
|
|
|
|
;; belong to the same project (even if some/most of them are ignored).
|
|
|
|
|
;;
|
|
|
|
|
;; - Define new methods for some or all generic functions for this
|
|
|
|
|
;; backend using `cl-defmethod'. A `project-root' method is
|
|
|
|
|
;; mandatory, `project-files' is recommended, the rest are optional.
|
2015-12-28 06:17:19 +02:00
|
|
|
|
|
|
|
|
|
;;; TODO:
|
|
|
|
|
|
2016-01-07 20:14:40 +03:00
|
|
|
|
;; * Reliably cache the list of files in the project, probably using
|
|
|
|
|
;; filenotify.el (if supported) to invalidate. And avoiding caching
|
|
|
|
|
;; if it's not available (manual cache invalidation is not nice).
|
|
|
|
|
;;
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;; * Build tool related functionality. Start with a `project-build'
|
|
|
|
|
;; command, which should provide completions on tasks to run, and
|
|
|
|
|
;; maybe allow entering some additional arguments. This might
|
|
|
|
|
;; be handled better with a separate API, though. Then we won't
|
|
|
|
|
;; force every project backend to be aware of the build tool(s) the
|
|
|
|
|
;; project is using.
|
|
|
|
|
;;
|
|
|
|
|
;; * Command to (re)build the tag files in all project roots. To that
|
2015-12-29 03:53:32 +02:00
|
|
|
|
;; end, we might need to add a way to provide file whitelist
|
|
|
|
|
;; wildcards for each root to limit etags to certain files (in
|
|
|
|
|
;; addition to the blacklist provided by ignores), and/or allow
|
|
|
|
|
;; specifying additional tag regexps.
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;;
|
|
|
|
|
;; * UI for the user to be able to pick the current project for the
|
|
|
|
|
;; whole Emacs session, independent of the current directory. Or,
|
|
|
|
|
;; in the more advanced case, open a set of projects, and have some
|
|
|
|
|
;; project-related commands to use them all. E.g., have a command
|
2020-05-28 23:01:08 +03:00
|
|
|
|
;; to search for a regexp across all open projects.
|
2015-12-29 03:53:32 +02:00
|
|
|
|
;;
|
|
|
|
|
;; * Support for project-local variables: a UI to edit them, and a
|
|
|
|
|
;; utility function to retrieve a value. Probably useless without
|
|
|
|
|
;; support in various built-in commands. In the API, we might get
|
|
|
|
|
;; away with only adding a `project-configuration-directory' method,
|
|
|
|
|
;; defaulting to the project root the current file/buffer is in.
|
|
|
|
|
;; And prompting otherwise. How to best mix that with backends that
|
|
|
|
|
;; want to set/provide certain variables themselves, is up for
|
|
|
|
|
;; discussion.
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
|
|
|
|
(require 'cl-generic)
|
2022-12-16 00:45:55 +02:00
|
|
|
|
(require 'cl-lib)
|
2020-07-19 10:32:51 +02:00
|
|
|
|
(require 'seq)
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(eval-when-compile (require 'subr-x))
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
2020-06-03 00:27:29 +03:00
|
|
|
|
(defgroup project nil
|
|
|
|
|
"Operations on the current project."
|
2020-06-03 11:45:54 +01:00
|
|
|
|
:version "28.1"
|
2020-06-03 00:27:29 +03:00
|
|
|
|
:group 'tools)
|
|
|
|
|
|
2015-11-10 02:41:06 +02:00
|
|
|
|
(defvar project-find-functions (list #'project-try-vc)
|
2015-07-10 04:34:41 +03:00
|
|
|
|
"Special hook to find the project containing a given directory.
|
|
|
|
|
Each functions on this hook is called in turn with one
|
2020-07-11 15:33:51 +03:00
|
|
|
|
argument, the directory in which to look, and should return
|
|
|
|
|
either nil to mean that it is not applicable, or a project instance.
|
2020-07-12 03:39:16 +03:00
|
|
|
|
The exact form of the project instance is up to each respective
|
|
|
|
|
function; the only practical limitation is to use values that
|
|
|
|
|
`cl-defmethod' can dispatch on, like a cons cell, or a list, or a
|
2020-07-12 20:55:23 +03:00
|
|
|
|
CL struct.")
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
2022-11-24 04:33:01 +02:00
|
|
|
|
(define-obsolete-variable-alias
|
|
|
|
|
'project-current-inhibit-prompt
|
|
|
|
|
'project-current-directory-override
|
|
|
|
|
"29.1")
|
|
|
|
|
|
|
|
|
|
(defvar project-current-directory-override nil
|
|
|
|
|
"Value to use instead of `default-directory' when detecting the project.
|
|
|
|
|
When it is non-nil, `project-current' will always skip prompting too.")
|
2020-05-19 01:00:43 +03:00
|
|
|
|
|
2015-07-10 04:34:41 +03:00
|
|
|
|
;;;###autoload
|
2020-07-11 15:33:51 +03:00
|
|
|
|
(defun project-current (&optional maybe-prompt directory)
|
|
|
|
|
"Return the project instance in DIRECTORY, defaulting to `default-directory'.
|
2020-07-12 20:55:23 +03:00
|
|
|
|
|
|
|
|
|
When no project is found in that directory, the result depends on
|
|
|
|
|
the value of MAYBE-PROMPT: if it is nil or omitted, return nil,
|
2020-07-12 16:57:40 +03:00
|
|
|
|
else ask the user for a directory in which to look for the
|
|
|
|
|
project, and if no project is found there, return a \"transient\"
|
2020-07-12 20:55:23 +03:00
|
|
|
|
project instance.
|
|
|
|
|
|
|
|
|
|
The \"transient\" project instance is a special kind of value
|
|
|
|
|
which denotes a project rooted in that directory and includes all
|
2022-11-30 17:25:55 +02:00
|
|
|
|
the files under the directory except for those that match entries
|
|
|
|
|
in `vc-directory-exclusion-list' or `grep-find-ignored-files'.
|
2020-07-12 16:57:40 +03:00
|
|
|
|
|
|
|
|
|
See the doc string of `project-find-functions' for the general form
|
|
|
|
|
of the project instance object."
|
2022-11-24 04:33:01 +02:00
|
|
|
|
(unless directory (setq directory (or project-current-directory-override
|
|
|
|
|
default-directory)))
|
2020-07-11 15:33:51 +03:00
|
|
|
|
(let ((pr (project--find-in-directory directory)))
|
2015-11-10 02:41:06 +02:00
|
|
|
|
(cond
|
|
|
|
|
(pr)
|
2022-11-24 04:33:01 +02:00
|
|
|
|
((unless project-current-directory-override
|
2020-05-19 01:00:43 +03:00
|
|
|
|
maybe-prompt)
|
2020-07-11 15:33:51 +03:00
|
|
|
|
(setq directory (project-prompt-project-dir)
|
|
|
|
|
pr (project--find-in-directory directory))))
|
2020-05-30 19:57:06 +03:00
|
|
|
|
(when maybe-prompt
|
|
|
|
|
(if pr
|
2020-07-25 03:17:47 +03:00
|
|
|
|
(project-remember-project pr)
|
2021-03-22 00:19:23 +01:00
|
|
|
|
(project--remove-from-project-list
|
|
|
|
|
directory "Project `%s' not found; removed from list")
|
2020-07-11 15:33:51 +03:00
|
|
|
|
(setq pr (cons 'transient directory))))
|
2015-11-10 02:41:06 +02:00
|
|
|
|
pr))
|
|
|
|
|
|
|
|
|
|
(defun project--find-in-directory (dir)
|
2015-07-10 04:34:41 +03:00
|
|
|
|
(run-hook-with-args-until-success 'project-find-functions dir))
|
|
|
|
|
|
2021-03-17 18:49:14 +02:00
|
|
|
|
(defvar project--within-roots-fallback nil)
|
|
|
|
|
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(cl-defgeneric project-root (project)
|
|
|
|
|
"Return root directory of the current project.
|
|
|
|
|
|
|
|
|
|
It usually contains the main build file, dependencies
|
|
|
|
|
configuration file, etc. Though neither is mandatory.
|
|
|
|
|
|
2021-03-18 02:35:10 +02:00
|
|
|
|
The directory name must be absolute.")
|
|
|
|
|
|
|
|
|
|
(cl-defmethod project-root (project
|
|
|
|
|
&context (project--within-roots-fallback
|
|
|
|
|
(eql nil)))
|
|
|
|
|
(car (project-roots project)))
|
2015-11-03 02:11:45 +02:00
|
|
|
|
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(cl-defgeneric project-roots (project)
|
|
|
|
|
"Return the list containing the current project root.
|
2015-07-31 05:37:28 +03:00
|
|
|
|
|
2020-05-23 04:38:27 +03:00
|
|
|
|
The function is obsolete, all projects have one main root anyway,
|
|
|
|
|
and the rest should be possible to express through
|
|
|
|
|
`project-external-roots'."
|
|
|
|
|
;; FIXME: Can we specify project's version here?
|
|
|
|
|
;; FIXME: Could we make this affect cl-defmethod calls too?
|
|
|
|
|
(declare (obsolete project-root "0.3.0"))
|
2021-03-17 18:49:14 +02:00
|
|
|
|
(let ((project--within-roots-fallback t))
|
|
|
|
|
(list (project-root project))))
|
2015-11-03 02:11:45 +02:00
|
|
|
|
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;; FIXME: Add MODE argument, like in `ede-source-paths'?
|
|
|
|
|
(cl-defgeneric project-external-roots (_project)
|
|
|
|
|
"Return the list of external roots for PROJECT.
|
2015-11-03 02:11:45 +02:00
|
|
|
|
|
2015-12-28 06:17:19 +02:00
|
|
|
|
It's the list of directories outside of the project that are
|
|
|
|
|
still related to it. If the project deals with source code then,
|
|
|
|
|
depending on the languages used, this list should include the
|
2020-05-23 04:38:27 +03:00
|
|
|
|
headers search path, load path, class path, and so on."
|
2015-12-28 06:17:19 +02:00
|
|
|
|
nil)
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
2022-11-22 10:55:59 -08:00
|
|
|
|
(cl-defgeneric project-name (project)
|
|
|
|
|
"A human-readable name for the project.
|
|
|
|
|
Nominally unique, but not enforced."
|
2022-12-01 22:12:07 -05:00
|
|
|
|
(file-name-nondirectory (directory-file-name (project-root project))))
|
2022-11-22 10:55:59 -08:00
|
|
|
|
|
2015-08-02 01:01:28 +03:00
|
|
|
|
(cl-defgeneric project-ignores (_project _dir)
|
|
|
|
|
"Return the list of glob patterns to ignore inside DIR.
|
|
|
|
|
Patterns can match both regular files and directories.
|
2015-07-12 17:18:09 +03:00
|
|
|
|
To root an entry, start it with `./'. To match directories only,
|
2020-05-23 04:38:27 +03:00
|
|
|
|
end it with `/'. DIR must be either `project-root' or one of
|
2015-12-28 06:17:19 +02:00
|
|
|
|
`project-external-roots'."
|
2019-02-06 12:25:09 +03:00
|
|
|
|
;; TODO: Document and support regexp ignores as used by Hg.
|
|
|
|
|
;; TODO: Support whitelist entries.
|
2015-07-12 17:18:09 +03:00
|
|
|
|
(require 'grep)
|
|
|
|
|
(defvar grep-find-ignored-files)
|
|
|
|
|
(nconc
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (dir)
|
|
|
|
|
(concat dir "/"))
|
|
|
|
|
vc-directory-exclusion-list)
|
|
|
|
|
grep-find-ignored-files))
|
|
|
|
|
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(defun project--file-completion-table (all-files)
|
|
|
|
|
(lambda (string pred action)
|
|
|
|
|
(cond
|
|
|
|
|
((eq action 'metadata)
|
|
|
|
|
'(metadata . ((category . project-file))))
|
|
|
|
|
(t
|
|
|
|
|
(complete-with-action action all-files string pred)))))
|
2016-01-29 17:43:26 -06:00
|
|
|
|
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(cl-defmethod project-root ((project (head transient)))
|
|
|
|
|
(cdr project))
|
2016-04-07 02:02:13 +03:00
|
|
|
|
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
(cl-defgeneric project-files (project &optional dirs)
|
|
|
|
|
"Return a list of files in directories DIRS in PROJECT.
|
|
|
|
|
DIRS is a list of absolute directories; it should be some
|
2020-05-23 04:38:27 +03:00
|
|
|
|
subset of the project root and external roots.
|
2019-01-14 00:16:19 +03:00
|
|
|
|
|
|
|
|
|
The default implementation uses `find-program'. PROJECT is used
|
|
|
|
|
to find the list of ignores for each directory."
|
2020-06-21 14:31:16 +01:00
|
|
|
|
(mapcan
|
2019-01-14 00:16:19 +03:00
|
|
|
|
(lambda (dir)
|
2019-01-19 03:46:07 +03:00
|
|
|
|
(project--files-in-directory dir
|
|
|
|
|
(project--dir-ignores project dir)))
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(or dirs
|
|
|
|
|
(list (project-root project)))))
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(defun project--files-in-directory (dir ignores &optional files)
|
|
|
|
|
(require 'find-dired)
|
2019-12-29 15:22:11 +03:00
|
|
|
|
(require 'xref)
|
2020-04-29 18:58:42 +03:00
|
|
|
|
(let* ((default-directory dir)
|
2020-04-29 22:46:17 +03:00
|
|
|
|
;; Make sure ~/ etc. in local directory name is
|
|
|
|
|
;; expanded and not left for the shell command
|
|
|
|
|
;; to interpret.
|
2021-04-16 03:52:44 +03:00
|
|
|
|
(localdir (file-name-unquote (file-local-name (expand-file-name dir))))
|
2021-09-06 05:01:07 +03:00
|
|
|
|
(dfn (directory-file-name localdir))
|
|
|
|
|
(command (format "%s -H . %s -type f %s -print0"
|
2020-04-29 18:58:42 +03:00
|
|
|
|
find-program
|
2021-09-06 05:01:07 +03:00
|
|
|
|
(xref--find-ignores-arguments ignores "./")
|
2020-04-29 18:58:42 +03:00
|
|
|
|
(if files
|
|
|
|
|
(concat (shell-quote-argument "(")
|
2022-11-14 02:18:56 +02:00
|
|
|
|
" -name "
|
2020-04-29 18:58:42 +03:00
|
|
|
|
(mapconcat
|
|
|
|
|
#'shell-quote-argument
|
|
|
|
|
(split-string files)
|
2022-11-14 02:18:56 +02:00
|
|
|
|
(concat " -o -name "))
|
2020-04-29 18:58:42 +03:00
|
|
|
|
" "
|
2020-11-04 15:41:53 +01:00
|
|
|
|
(shell-quote-argument ")"))
|
2021-04-16 03:38:23 +03:00
|
|
|
|
"")))
|
2021-10-10 04:14:35 +03:00
|
|
|
|
res)
|
|
|
|
|
(with-temp-buffer
|
|
|
|
|
(let ((status
|
|
|
|
|
(process-file-shell-command command nil t))
|
|
|
|
|
(pt (point-min)))
|
|
|
|
|
(unless (zerop status)
|
2021-10-15 15:02:23 +03:00
|
|
|
|
(goto-char (point-min))
|
|
|
|
|
(if (and
|
|
|
|
|
(not (eql status 127))
|
|
|
|
|
(search-forward "Permission denied\n" nil t))
|
|
|
|
|
(let ((end (1- (point))))
|
|
|
|
|
(re-search-backward "\\`\\|\0")
|
|
|
|
|
(error "File listing failed: %s"
|
|
|
|
|
(buffer-substring (1+ (point)) end)))
|
|
|
|
|
(error "File listing failed: %s" (buffer-string))))
|
2021-10-10 04:14:35 +03:00
|
|
|
|
(goto-char pt)
|
|
|
|
|
(while (search-forward "\0" nil t)
|
|
|
|
|
(push (buffer-substring-no-properties (1+ pt) (1- (point)))
|
|
|
|
|
res)
|
|
|
|
|
(setq pt (point)))))
|
2019-02-07 14:22:47 +03:00
|
|
|
|
(project--remote-file-names
|
2021-10-10 04:14:35 +03:00
|
|
|
|
(mapcar (lambda (s) (concat dfn s))
|
|
|
|
|
(sort res #'string<)))))
|
2019-02-07 14:22:47 +03:00
|
|
|
|
|
|
|
|
|
(defun project--remote-file-names (local-files)
|
2021-04-15 18:41:04 +02:00
|
|
|
|
"Return LOCAL-FILES as if they were on the system of `default-directory'.
|
|
|
|
|
Also quote LOCAL-FILES if `default-directory' is quoted."
|
2019-02-07 14:22:47 +03:00
|
|
|
|
(let ((remote-id (file-remote-p default-directory)))
|
|
|
|
|
(if (not remote-id)
|
2021-04-15 18:41:04 +02:00
|
|
|
|
(if (file-name-quoted-p default-directory)
|
|
|
|
|
(mapcar #'file-name-quote local-files)
|
|
|
|
|
local-files)
|
2019-02-07 14:22:47 +03:00
|
|
|
|
(mapcar (lambda (file)
|
|
|
|
|
(concat remote-id file))
|
|
|
|
|
local-files))))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
|
2021-08-21 05:26:12 +03:00
|
|
|
|
(cl-defgeneric project-buffers (project)
|
2022-11-04 03:16:36 +02:00
|
|
|
|
"Return the list of all live buffers that belong to PROJECT.
|
|
|
|
|
|
2022-11-04 15:56:30 +02:00
|
|
|
|
The default implementation matches each buffer to PROJECT root using
|
|
|
|
|
the buffer's value of `default-directory'."
|
2021-08-21 05:26:12 +03:00
|
|
|
|
(let ((root (expand-file-name (file-name-as-directory (project-root project))))
|
|
|
|
|
bufs)
|
|
|
|
|
(dolist (buf (buffer-list))
|
|
|
|
|
(when (string-prefix-p root (expand-file-name
|
|
|
|
|
(buffer-local-value 'default-directory buf)))
|
|
|
|
|
(push buf bufs)))
|
|
|
|
|
(nreverse bufs)))
|
|
|
|
|
|
2015-08-10 04:04:57 +03:00
|
|
|
|
(defgroup project-vc nil
|
2022-12-01 04:05:49 +02:00
|
|
|
|
"VC-aware project implementation."
|
2016-01-12 20:06:49 -05:00
|
|
|
|
:version "25.1"
|
2020-06-03 00:27:29 +03:00
|
|
|
|
:group 'project)
|
2015-08-10 04:04:57 +03:00
|
|
|
|
|
|
|
|
|
(defcustom project-vc-ignores nil
|
2020-12-02 04:28:35 +02:00
|
|
|
|
"List of patterns to add to `project-ignores'."
|
2015-08-10 04:04:57 +03:00
|
|
|
|
:type '(repeat string)
|
2020-06-03 11:45:54 +01:00
|
|
|
|
:safe #'listp)
|
2015-08-10 04:04:57 +03:00
|
|
|
|
|
2020-05-18 03:36:43 +03:00
|
|
|
|
(defcustom project-vc-merge-submodules t
|
2020-05-18 03:44:26 +03:00
|
|
|
|
"Non-nil to consider submodules part of the parent project.
|
|
|
|
|
|
|
|
|
|
After changing this variable (using Customize or .dir-locals.el)
|
|
|
|
|
you might have to restart Emacs to see the effect."
|
2020-05-18 03:36:43 +03:00
|
|
|
|
:type 'boolean
|
2020-06-03 00:27:29 +03:00
|
|
|
|
:version "28.1"
|
2020-05-18 03:36:43 +03:00
|
|
|
|
:package-version '(project . "0.2.0")
|
2020-06-03 11:45:54 +01:00
|
|
|
|
:safe #'booleanp)
|
2020-05-18 03:36:43 +03:00
|
|
|
|
|
2022-06-02 20:59:53 +02:00
|
|
|
|
(defcustom project-vc-include-untracked t
|
2022-12-01 04:05:49 +02:00
|
|
|
|
"When non-nil, the VC-aware project backend includes untracked files."
|
2022-06-02 20:59:53 +02:00
|
|
|
|
:type 'boolean
|
2022-06-04 03:42:00 +03:00
|
|
|
|
:version "29.1"
|
2022-06-02 20:59:53 +02:00
|
|
|
|
:safe #'booleanp)
|
|
|
|
|
|
2022-11-23 04:16:23 +02:00
|
|
|
|
(defcustom project-vc-name nil
|
2022-12-01 04:05:49 +02:00
|
|
|
|
"When non-nil, the name of the current VC-aware project.
|
2022-11-23 04:16:23 +02:00
|
|
|
|
|
2022-12-01 04:05:49 +02:00
|
|
|
|
The best way to change the value a VC-aware project reports as
|
|
|
|
|
its name, is by setting this in .dir-locals.el."
|
2022-11-29 18:01:19 +02:00
|
|
|
|
:type '(choice (const :tag "Default to the base name" nil)
|
|
|
|
|
(string :tag "Custom name"))
|
2022-11-23 04:16:23 +02:00
|
|
|
|
:version "29.1"
|
2022-12-01 04:05:49 +02:00
|
|
|
|
:package-version '(project . "0.9.0")
|
2022-11-23 04:16:23 +02:00
|
|
|
|
:safe #'stringp)
|
|
|
|
|
|
2022-12-01 04:05:49 +02:00
|
|
|
|
;; Not using regexps because these wouldn't work in Git pathspecs, in
|
|
|
|
|
;; case we decide we need to be able to list nested projects.
|
|
|
|
|
(defcustom project-vc-extra-root-markers nil
|
|
|
|
|
"List of additional markers to signal project roots.
|
|
|
|
|
|
|
|
|
|
A marker is either a base file name or a glob pattern for such.
|
|
|
|
|
|
|
|
|
|
A directory containing such a marker file or a file matching a
|
|
|
|
|
marker pattern will be recognized as the root of a VC-aware
|
|
|
|
|
project.
|
|
|
|
|
|
|
|
|
|
Example values: \".dir-locals.el\", \"package.json\", \"pom.xml\",
|
|
|
|
|
\"requirements.txt\", \"Gemfile\", \"*.gemspec\", \"autogen.sh\".
|
|
|
|
|
|
|
|
|
|
These will be used in addition to regular directory markers such
|
|
|
|
|
as \".git\", \".hg\", and so on, depending on the value of
|
|
|
|
|
`vc-handled-backends'. It is most useful when a project has
|
|
|
|
|
subdirectories inside it that need to be considered as separate
|
|
|
|
|
projects. It can also be used for projects outside of VC
|
|
|
|
|
repositories.
|
|
|
|
|
|
|
|
|
|
In either case, their behavior will still obey the relevant
|
|
|
|
|
variables, such as `project-vc-ignores' or `project-vc-name'."
|
2022-12-01 12:40:28 +01:00
|
|
|
|
:type '(repeat string)
|
2022-12-01 04:05:49 +02:00
|
|
|
|
:version "29.1"
|
|
|
|
|
:package-version '(project . "0.9.0")
|
|
|
|
|
:safe (lambda (val) (and (listp val) (cl-every #'stringp val))))
|
|
|
|
|
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;; FIXME: Using the current approach, major modes are supposed to set
|
|
|
|
|
;; this variable to a buffer-local value. So we don't have access to
|
|
|
|
|
;; the "external roots" of language A from buffers of language B, which
|
|
|
|
|
;; seems desirable in multi-language projects, at least for some
|
|
|
|
|
;; potential uses, like "jump to a file in project or external dirs".
|
|
|
|
|
;;
|
|
|
|
|
;; We could add a second argument to this function: a file extension,
|
|
|
|
|
;; or a language name. Some projects will know the set of languages
|
2022-12-01 04:05:49 +02:00
|
|
|
|
;; used in them; for others, like the VC-aware type, we'll need
|
2015-12-28 06:17:19 +02:00
|
|
|
|
;; auto-detection. I see two options:
|
|
|
|
|
;;
|
|
|
|
|
;; - That could be implemented as a separate second hook, with a
|
|
|
|
|
;; list of functions that return file extensions.
|
|
|
|
|
;;
|
|
|
|
|
;; - This variable will be turned into a hook with "append" semantics,
|
|
|
|
|
;; and each function in it will perform auto-detection when passed
|
|
|
|
|
;; nil instead of an actual file extension. Then this hook will, in
|
|
|
|
|
;; general, be modified globally, and not from major mode functions.
|
|
|
|
|
;;
|
|
|
|
|
;; The second option seems simpler, but the first one has the
|
|
|
|
|
;; advantage that the user could override the list of languages used
|
|
|
|
|
;; in a project via a directory-local variable, thus skipping
|
|
|
|
|
;; languages they're not working on personally (in a big project), or
|
|
|
|
|
;; working around problems in language detection (the detection logic
|
|
|
|
|
;; might be imperfect for the project in question, or it might work
|
|
|
|
|
;; too slowly for the user's taste).
|
|
|
|
|
(defvar project-vc-external-roots-function (lambda () tags-table-list)
|
|
|
|
|
"Function that returns a list of external roots.
|
|
|
|
|
|
|
|
|
|
It should return a list of directory roots that contain source
|
|
|
|
|
files related to the current buffer.
|
|
|
|
|
|
2022-12-01 04:05:49 +02:00
|
|
|
|
The directory names should be absolute. Used in the VC-aware
|
|
|
|
|
project backend implementation of `project-external-roots'.")
|
2015-12-28 06:17:19 +02:00
|
|
|
|
|
2023-02-18 00:50:29 +02:00
|
|
|
|
(defvar project-vc-backend-markers-alist
|
|
|
|
|
`((Git . ".git")
|
|
|
|
|
(Hg . ".hg")
|
|
|
|
|
(Bzr . ".bzr")
|
|
|
|
|
;; See the comment above `vc-svn-admin-directory' for why we're
|
|
|
|
|
;; duplicating the definition.
|
|
|
|
|
(SVN . ,(if (and (memq system-type '(cygwin windows-nt ms-dos))
|
|
|
|
|
(getenv "SVN_ASP_DOT_NET_HACK"))
|
|
|
|
|
"_svn"
|
|
|
|
|
".svn"))
|
|
|
|
|
(DARCS . "_darcs")
|
|
|
|
|
(Fossil . ".fslckout"))
|
|
|
|
|
"Associative list assigning root markers to VC backend symbols.
|
|
|
|
|
|
|
|
|
|
See `project-vc-extra-root-markers' for the marker value format.")
|
|
|
|
|
|
2015-07-10 04:34:41 +03:00
|
|
|
|
(defun project-try-vc (dir)
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(defvar vc-svn-admin-directory)
|
|
|
|
|
(require 'vc-svn)
|
|
|
|
|
;; FIXME: Learn to invalidate when the value of
|
|
|
|
|
;; `project-vc-merge-submodules' or `project-vc-extra-root-markers'
|
|
|
|
|
;; changes.
|
2022-03-03 05:23:26 +02:00
|
|
|
|
(or (vc-file-getprop dir 'project-vc)
|
2023-02-18 00:50:29 +02:00
|
|
|
|
(let* ((backend-markers
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(delete
|
|
|
|
|
nil
|
|
|
|
|
(mapcar
|
2023-02-18 00:50:29 +02:00
|
|
|
|
(lambda (b) (assoc-default b project-vc-backend-markers-alist))
|
2022-12-01 04:05:49 +02:00
|
|
|
|
vc-handled-backends)))
|
|
|
|
|
(marker-re
|
2023-01-20 18:43:56 +02:00
|
|
|
|
(concat
|
|
|
|
|
"\\`"
|
|
|
|
|
(mapconcat
|
|
|
|
|
(lambda (m) (format "\\(%s\\)" (wildcard-to-regexp m)))
|
|
|
|
|
(append backend-markers
|
|
|
|
|
(project--value-in-dir 'project-vc-extra-root-markers dir))
|
|
|
|
|
"\\|")
|
|
|
|
|
"\\'"))
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(locate-dominating-stop-dir-regexp
|
|
|
|
|
(or vc-ignore-dir-regexp locate-dominating-stop-dir-regexp))
|
|
|
|
|
last-matches
|
2022-03-03 05:23:26 +02:00
|
|
|
|
(root
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(locate-dominating-file
|
|
|
|
|
dir
|
|
|
|
|
(lambda (d)
|
|
|
|
|
;; Maybe limit count to 100 when we can drop Emacs < 28.
|
2023-01-28 03:17:39 +02:00
|
|
|
|
(setq last-matches
|
|
|
|
|
(condition-case nil
|
|
|
|
|
(directory-files d nil marker-re t)
|
|
|
|
|
(file-missing nil))))))
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(backend
|
|
|
|
|
(cl-find-if
|
|
|
|
|
(lambda (b)
|
2023-02-18 00:50:29 +02:00
|
|
|
|
(member (assoc-default b project-vc-backend-markers-alist)
|
2022-12-01 04:05:49 +02:00
|
|
|
|
last-matches))
|
|
|
|
|
vc-handled-backends))
|
2022-03-03 05:23:26 +02:00
|
|
|
|
project)
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(when (and
|
|
|
|
|
(eq backend 'Git)
|
2022-12-09 18:15:49 +02:00
|
|
|
|
(project--vc-merge-submodules-p root)
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(project--submodule-p root))
|
|
|
|
|
(let* ((parent (file-name-directory (directory-file-name root))))
|
|
|
|
|
(setq root (vc-call-backend 'Git 'root parent))))
|
2022-03-03 05:23:26 +02:00
|
|
|
|
(when root
|
|
|
|
|
(setq project (list 'vc backend root))
|
|
|
|
|
;; FIXME: Cache for a shorter time.
|
|
|
|
|
(vc-file-setprop dir 'project-vc project)
|
|
|
|
|
project))))
|
2020-05-18 03:36:43 +03:00
|
|
|
|
|
|
|
|
|
(defun project--submodule-p (root)
|
|
|
|
|
;; XXX: We only support Git submodules for now.
|
|
|
|
|
;;
|
|
|
|
|
;; For submodules, at least, we expect the users to prefer them to
|
|
|
|
|
;; be considered part of the parent project. For those who don't,
|
|
|
|
|
;; there is the custom var now.
|
|
|
|
|
;;
|
|
|
|
|
;; Some users may also set up things equivalent to Git submodules
|
2020-05-18 03:44:26 +03:00
|
|
|
|
;; using "git worktree" (for example). However, we expect that most
|
|
|
|
|
;; of them would prefer to treat those as separate projects anyway.
|
2020-05-18 03:36:43 +03:00
|
|
|
|
(let* ((gitfile (expand-file-name ".git" root)))
|
|
|
|
|
(cond
|
|
|
|
|
((file-directory-p gitfile)
|
|
|
|
|
nil)
|
|
|
|
|
((with-temp-buffer
|
|
|
|
|
(insert-file-contents gitfile)
|
|
|
|
|
(goto-char (point-min))
|
|
|
|
|
;; Kind of a hack to distinguish a submodule from
|
|
|
|
|
;; other cases of .git files pointing elsewhere.
|
|
|
|
|
(looking-at "gitdir: [./]+/\\.git/modules/"))
|
|
|
|
|
t)
|
|
|
|
|
(t nil))))
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(cl-defmethod project-root ((project (head vc)))
|
2022-03-03 05:23:26 +02:00
|
|
|
|
(nth 2 project))
|
2015-07-10 04:34:41 +03:00
|
|
|
|
|
2015-12-28 06:17:19 +02:00
|
|
|
|
(cl-defmethod project-external-roots ((project (head vc)))
|
2015-11-08 14:46:22 +02:00
|
|
|
|
(project-subtract-directories
|
|
|
|
|
(project-combine-directories
|
2015-12-28 06:17:19 +02:00
|
|
|
|
(mapcar
|
|
|
|
|
#'file-name-as-directory
|
|
|
|
|
(funcall project-vc-external-roots-function)))
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(list (project-root project))))
|
2015-08-10 04:04:57 +03:00
|
|
|
|
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(cl-defmethod project-files ((project (head vc)) &optional dirs)
|
2020-06-21 14:31:16 +01:00
|
|
|
|
(mapcan
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(lambda (dir)
|
2022-12-09 18:15:49 +02:00
|
|
|
|
(let ((ignores (project--value-in-dir 'project-vc-ignores (nth 2 project)))
|
2022-12-02 04:03:03 +02:00
|
|
|
|
(backend (cadr project)))
|
|
|
|
|
(when backend
|
|
|
|
|
(require (intern (concat "vc-" (downcase (symbol-name backend))))))
|
2022-03-03 05:23:26 +02:00
|
|
|
|
(if (and (file-equal-p dir (nth 2 project))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(cond
|
|
|
|
|
((eq backend 'Hg))
|
|
|
|
|
((and (eq backend 'Git)
|
|
|
|
|
(or
|
2020-12-02 01:41:40 +02:00
|
|
|
|
(not ignores)
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(version<= "1.9" (vc-git--program-version)))))))
|
2020-12-02 01:41:40 +02:00
|
|
|
|
(project--vc-list-files dir backend ignores)
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(project--files-in-directory
|
|
|
|
|
dir
|
|
|
|
|
(project--dir-ignores project dir)))))
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(or dirs
|
|
|
|
|
(list (project-root project)))))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
|
2019-10-04 11:29:49 +03:00
|
|
|
|
(declare-function vc-git--program-version "vc-git")
|
|
|
|
|
(declare-function vc-git--run-command-string "vc-git")
|
|
|
|
|
(declare-function vc-hg-command "vc-hg")
|
|
|
|
|
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(defun project--vc-list-files (dir backend extra-ignores)
|
2022-01-05 04:08:10 +02:00
|
|
|
|
(defvar vc-git-use-literal-pathspecs)
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(pcase backend
|
|
|
|
|
(`Git
|
2022-12-09 23:21:10 +02:00
|
|
|
|
(let* ((default-directory (expand-file-name (file-name-as-directory dir)))
|
|
|
|
|
(args '("-z"))
|
|
|
|
|
(vc-git-use-literal-pathspecs nil)
|
|
|
|
|
(include-untracked (project--value-in-dir
|
|
|
|
|
'project-vc-include-untracked
|
|
|
|
|
dir))
|
|
|
|
|
files)
|
2022-06-02 20:59:53 +02:00
|
|
|
|
(setq args (append args
|
|
|
|
|
'("-c" "--exclude-standard")
|
2022-12-09 23:21:10 +02:00
|
|
|
|
(and include-untracked '("-o"))))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(when extra-ignores
|
|
|
|
|
(setq args (append args
|
|
|
|
|
(cons "--"
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (i)
|
2020-12-02 04:28:35 +02:00
|
|
|
|
(format
|
|
|
|
|
":(exclude,glob,top)%s"
|
|
|
|
|
(if (string-match "\\*\\*" i)
|
|
|
|
|
;; Looks like pathspec glob
|
|
|
|
|
;; format already.
|
|
|
|
|
i
|
|
|
|
|
(if (string-match "\\./" i)
|
|
|
|
|
;; ./abc -> abc
|
|
|
|
|
(setq i (substring i 2))
|
|
|
|
|
;; abc -> **/abc
|
|
|
|
|
(setq i (concat "**/" i))
|
|
|
|
|
;; FIXME: '**/abc' should also
|
|
|
|
|
;; match a directory with that
|
|
|
|
|
;; name, but doesn't (git 2.25.1).
|
|
|
|
|
;; Maybe we should replace
|
|
|
|
|
;; such entries with two.
|
|
|
|
|
(if (string-match "/\\'" i)
|
|
|
|
|
;; abc/ -> abc/**
|
|
|
|
|
(setq i (concat i "**"))))
|
|
|
|
|
i)))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
extra-ignores)))))
|
2019-12-27 18:18:41 +03:00
|
|
|
|
(setq files
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (file) (concat default-directory file))
|
|
|
|
|
(split-string
|
|
|
|
|
(apply #'vc-git--run-command-string nil "ls-files" args)
|
|
|
|
|
"\0" t)))
|
2022-12-09 18:15:49 +02:00
|
|
|
|
(when (project--vc-merge-submodules-p default-directory)
|
2020-06-18 01:30:32 +03:00
|
|
|
|
;; Unfortunately, 'ls-files --recurse-submodules' conflicts with '-o'.
|
|
|
|
|
(let* ((submodules (project--git-submodules))
|
|
|
|
|
(sub-files
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (module)
|
|
|
|
|
(when (file-directory-p module)
|
|
|
|
|
(project--vc-list-files
|
|
|
|
|
(concat default-directory module)
|
|
|
|
|
backend
|
|
|
|
|
extra-ignores)))
|
|
|
|
|
submodules)))
|
|
|
|
|
(setq files
|
|
|
|
|
(apply #'nconc files sub-files))))
|
2020-05-20 01:54:33 +03:00
|
|
|
|
;; 'git ls-files' returns duplicate entries for merge conflicts.
|
|
|
|
|
;; XXX: Better solutions welcome, but this seems cheap enough.
|
|
|
|
|
(delete-consecutive-dups files)))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(`Hg
|
2022-12-09 23:21:10 +02:00
|
|
|
|
(let* ((default-directory (expand-file-name (file-name-as-directory dir)))
|
|
|
|
|
(include-untracked (project--value-in-dir
|
|
|
|
|
'project-vc-include-untracked
|
|
|
|
|
dir))
|
|
|
|
|
(args (list (concat "-mcard" (and include-untracked "u"))
|
|
|
|
|
"--no-status"
|
|
|
|
|
"-0")))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(when extra-ignores
|
|
|
|
|
(setq args (nconc args
|
|
|
|
|
(mapcan
|
|
|
|
|
(lambda (i)
|
|
|
|
|
(list "--exclude" i))
|
2019-10-04 11:29:49 +03:00
|
|
|
|
extra-ignores))))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(with-temp-buffer
|
2019-10-04 15:50:16 +03:00
|
|
|
|
(apply #'vc-hg-command t 0 "." "status" args)
|
|
|
|
|
(mapcar
|
2019-10-05 12:32:11 +03:00
|
|
|
|
(lambda (s) (concat default-directory s))
|
2019-10-04 15:50:16 +03:00
|
|
|
|
(split-string (buffer-string) "\0" t)))))))
|
2019-10-04 02:03:04 +03:00
|
|
|
|
|
2022-12-09 18:15:49 +02:00
|
|
|
|
(defun project--vc-merge-submodules-p (dir)
|
|
|
|
|
(project--value-in-dir
|
|
|
|
|
'project-vc-merge-submodules
|
|
|
|
|
dir))
|
|
|
|
|
|
2019-12-27 18:18:41 +03:00
|
|
|
|
(defun project--git-submodules ()
|
|
|
|
|
;; 'git submodule foreach' is much slower.
|
|
|
|
|
(condition-case nil
|
|
|
|
|
(with-temp-buffer
|
|
|
|
|
(insert-file-contents ".gitmodules")
|
|
|
|
|
(let (res)
|
|
|
|
|
(goto-char (point-min))
|
2022-06-14 04:00:22 +03:00
|
|
|
|
(while (re-search-forward "^[ \t]*path *= *\\(.+\\)" nil t)
|
2019-12-27 18:18:41 +03:00
|
|
|
|
(push (match-string 1) res))
|
|
|
|
|
(nreverse res)))
|
|
|
|
|
(file-missing nil)))
|
|
|
|
|
|
2015-08-02 01:01:28 +03:00
|
|
|
|
(cl-defmethod project-ignores ((project (head vc)) dir)
|
2022-03-03 05:23:26 +02:00
|
|
|
|
(let* ((root (nth 2 project))
|
2020-06-26 09:37:52 +01:00
|
|
|
|
backend)
|
2015-08-10 04:04:57 +03:00
|
|
|
|
(append
|
2022-12-01 04:05:49 +02:00
|
|
|
|
(when (and backend
|
|
|
|
|
(file-equal-p dir root))
|
2022-03-03 05:23:26 +02:00
|
|
|
|
(setq backend (cadr project))
|
2020-12-02 04:28:35 +02:00
|
|
|
|
(delq
|
|
|
|
|
nil
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (entry)
|
|
|
|
|
(cond
|
|
|
|
|
((eq ?! (aref entry 0))
|
|
|
|
|
;; No support for whitelisting (yet).
|
|
|
|
|
nil)
|
|
|
|
|
((string-match "\\(/\\)[^/]" entry)
|
|
|
|
|
;; FIXME: This seems to be Git-specific.
|
|
|
|
|
;; And / in the entry (start or even the middle) means
|
|
|
|
|
;; the pattern is "rooted". Or actually it is then
|
|
|
|
|
;; relative to its respective .gitignore (of which there
|
|
|
|
|
;; could be several), but we only support .gitignore at
|
|
|
|
|
;; the root.
|
|
|
|
|
(if (= (match-beginning 0) 0)
|
|
|
|
|
(replace-match "./" t t entry 1)
|
|
|
|
|
(concat "./" entry)))
|
|
|
|
|
(t entry)))
|
2021-08-31 22:24:02 +02:00
|
|
|
|
(condition-case nil
|
|
|
|
|
(vc-call-backend backend 'ignore-completion-table root)
|
|
|
|
|
(vc-not-supported () nil)))))
|
2022-12-09 18:15:49 +02:00
|
|
|
|
(project--value-in-dir 'project-vc-ignores root)
|
2019-01-19 03:46:07 +03:00
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (dir)
|
|
|
|
|
(concat dir "/"))
|
|
|
|
|
vc-directory-exclusion-list))))
|
2015-07-12 17:18:09 +03:00
|
|
|
|
|
2015-11-03 02:11:45 +02:00
|
|
|
|
(defun project-combine-directories (&rest lists-of-dirs)
|
|
|
|
|
"Return a sorted and culled list of directory names.
|
|
|
|
|
Appends the elements of LISTS-OF-DIRS together, removes
|
|
|
|
|
non-existing directories, as well as directories a parent of
|
|
|
|
|
whose is already in the list."
|
2015-07-10 04:34:41 +03:00
|
|
|
|
(let* ((dirs (sort
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (dir)
|
|
|
|
|
(file-name-as-directory (expand-file-name dir)))
|
2015-11-03 02:11:45 +02:00
|
|
|
|
(apply #'append lists-of-dirs))
|
2015-07-10 04:34:41 +03:00
|
|
|
|
#'string<))
|
|
|
|
|
(ref dirs))
|
|
|
|
|
;; Delete subdirectories from the list.
|
|
|
|
|
(while (cdr ref)
|
|
|
|
|
(if (string-prefix-p (car ref) (cadr ref))
|
|
|
|
|
(setcdr ref (cddr ref))
|
|
|
|
|
(setq ref (cdr ref))))
|
|
|
|
|
(cl-delete-if-not #'file-exists-p dirs)))
|
|
|
|
|
|
2015-11-03 02:11:45 +02:00
|
|
|
|
(defun project-subtract-directories (files dirs)
|
|
|
|
|
"Return a list of elements from FILES that are outside of DIRS.
|
|
|
|
|
DIRS must contain directory names."
|
2015-11-10 02:41:06 +02:00
|
|
|
|
;; Sidestep the issue of expanded/abbreviated file names here.
|
|
|
|
|
(cl-set-difference files dirs :test #'file-in-directory-p))
|
2015-11-03 02:11:45 +02:00
|
|
|
|
|
2022-12-09 18:15:49 +02:00
|
|
|
|
(defun project--value-in-dir (var dir)
|
|
|
|
|
(with-temp-buffer
|
|
|
|
|
(setq default-directory dir)
|
|
|
|
|
(let ((enable-local-variables :all))
|
|
|
|
|
(hack-dir-local-variables-non-file-buffer))
|
|
|
|
|
(symbol-value var)))
|
|
|
|
|
|
2021-08-21 05:26:12 +03:00
|
|
|
|
(cl-defmethod project-buffers ((project (head vc)))
|
|
|
|
|
(let* ((root (expand-file-name (file-name-as-directory (project-root project))))
|
2022-12-09 18:15:49 +02:00
|
|
|
|
(modules (unless (or (project--vc-merge-submodules-p root)
|
2021-08-21 05:26:12 +03:00
|
|
|
|
(project--submodule-p root))
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (m) (format "%s%s/" root m))
|
|
|
|
|
(project--git-submodules))))
|
|
|
|
|
dd
|
|
|
|
|
bufs)
|
|
|
|
|
(dolist (buf (buffer-list))
|
|
|
|
|
(setq dd (expand-file-name (buffer-local-value 'default-directory buf)))
|
|
|
|
|
(when (and (string-prefix-p root dd)
|
|
|
|
|
(not (cl-find-if (lambda (module) (string-prefix-p module dd))
|
|
|
|
|
modules)))
|
|
|
|
|
(push buf bufs)))
|
|
|
|
|
(nreverse bufs)))
|
|
|
|
|
|
2022-11-23 04:16:23 +02:00
|
|
|
|
(cl-defmethod project-name ((_project (head vc)))
|
|
|
|
|
(or project-vc-name
|
|
|
|
|
(cl-call-next-method)))
|
|
|
|
|
|
2020-06-18 02:10:33 +03:00
|
|
|
|
|
|
|
|
|
;;; Project commands
|
|
|
|
|
|
2020-06-18 02:05:31 +03:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defvar project-prefix-map
|
|
|
|
|
(let ((map (make-sparse-keymap)))
|
2020-08-28 19:23:01 +02:00
|
|
|
|
(define-key map "!" 'project-shell-command)
|
|
|
|
|
(define-key map "&" 'project-async-shell-command)
|
2020-06-18 02:05:31 +03:00
|
|
|
|
(define-key map "f" 'project-find-file)
|
2020-07-20 03:58:08 +03:00
|
|
|
|
(define-key map "F" 'project-or-external-find-file)
|
2020-06-18 02:10:33 +03:00
|
|
|
|
(define-key map "b" 'project-switch-to-buffer)
|
2020-06-18 02:05:31 +03:00
|
|
|
|
(define-key map "s" 'project-shell)
|
2021-09-21 03:34:00 +03:00
|
|
|
|
(define-key map "d" 'project-find-dir)
|
|
|
|
|
(define-key map "D" 'project-dired)
|
2020-06-18 02:05:31 +03:00
|
|
|
|
(define-key map "v" 'project-vc-dir)
|
|
|
|
|
(define-key map "c" 'project-compile)
|
|
|
|
|
(define-key map "e" 'project-eshell)
|
2020-06-18 18:42:28 +03:00
|
|
|
|
(define-key map "k" 'project-kill-buffers)
|
2020-06-18 02:05:31 +03:00
|
|
|
|
(define-key map "p" 'project-switch-project)
|
|
|
|
|
(define-key map "g" 'project-find-regexp)
|
2020-07-20 03:58:08 +03:00
|
|
|
|
(define-key map "G" 'project-or-external-find-regexp)
|
2020-06-18 02:05:31 +03:00
|
|
|
|
(define-key map "r" 'project-query-replace-regexp)
|
2020-12-20 00:16:32 +02:00
|
|
|
|
(define-key map "x" 'project-execute-extended-command)
|
2022-11-15 20:54:39 +02:00
|
|
|
|
(define-key map "\C-b" 'project-list-buffers)
|
2020-06-18 02:05:31 +03:00
|
|
|
|
map)
|
|
|
|
|
"Keymap for project commands.")
|
|
|
|
|
|
|
|
|
|
;;;###autoload (define-key ctl-x-map "p" project-prefix-map)
|
|
|
|
|
|
2020-07-23 18:55:42 -07:00
|
|
|
|
;; We can't have these place-specific maps inherit from
|
|
|
|
|
;; project-prefix-map because project--other-place-command needs to
|
|
|
|
|
;; know which map the key binding came from, as if it came from one of
|
|
|
|
|
;; these maps, we don't want to set display-buffer-overriding-action
|
|
|
|
|
|
|
|
|
|
(defvar project-other-window-map
|
|
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
|
|
(define-key map "\C-o" #'project-display-buffer)
|
|
|
|
|
map)
|
|
|
|
|
"Keymap for project commands that display buffers in other windows.")
|
|
|
|
|
|
|
|
|
|
(defvar project-other-frame-map
|
|
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
|
|
(define-key map "\C-o" #'project-display-buffer-other-frame)
|
|
|
|
|
map)
|
|
|
|
|
"Keymap for project commands that display buffers in other frames.")
|
|
|
|
|
|
|
|
|
|
(defun project--other-place-command (action &optional map)
|
|
|
|
|
(let* ((key (read-key-sequence-vector nil t))
|
|
|
|
|
(place-cmd (lookup-key map key))
|
|
|
|
|
(generic-cmd (lookup-key project-prefix-map key))
|
2020-07-27 02:51:39 +03:00
|
|
|
|
(switch-to-buffer-obey-display-actions t)
|
2020-07-23 18:55:42 -07:00
|
|
|
|
(display-buffer-overriding-action (unless place-cmd action)))
|
|
|
|
|
(if-let ((cmd (or place-cmd generic-cmd)))
|
|
|
|
|
(call-interactively cmd)
|
|
|
|
|
(user-error "%s is undefined" (key-description key)))))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-other-window-command ()
|
|
|
|
|
"Run project command, displaying resultant buffer in another window.
|
|
|
|
|
|
|
|
|
|
The following commands are available:
|
|
|
|
|
|
|
|
|
|
\\{project-prefix-map}
|
|
|
|
|
\\{project-other-window-map}"
|
|
|
|
|
(interactive)
|
|
|
|
|
(project--other-place-command '((display-buffer-pop-up-window)
|
|
|
|
|
(inhibit-same-window . t))
|
|
|
|
|
project-other-window-map))
|
|
|
|
|
|
|
|
|
|
;;;###autoload (define-key ctl-x-4-map "p" #'project-other-window-command)
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-other-frame-command ()
|
|
|
|
|
"Run project command, displaying resultant buffer in another frame.
|
|
|
|
|
|
|
|
|
|
The following commands are available:
|
|
|
|
|
|
|
|
|
|
\\{project-prefix-map}
|
|
|
|
|
\\{project-other-frame-map}"
|
|
|
|
|
(interactive)
|
|
|
|
|
(project--other-place-command '((display-buffer-pop-up-frame))
|
|
|
|
|
project-other-frame-map))
|
|
|
|
|
|
|
|
|
|
;;;###autoload (define-key ctl-x-5-map "p" #'project-other-frame-command)
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-other-tab-command ()
|
|
|
|
|
"Run project command, displaying resultant buffer in a new tab.
|
|
|
|
|
|
|
|
|
|
The following commands are available:
|
|
|
|
|
|
|
|
|
|
\\{project-prefix-map}"
|
|
|
|
|
(interactive)
|
|
|
|
|
(project--other-place-command '((display-buffer-in-new-tab))))
|
|
|
|
|
|
2020-09-03 13:34:08 +01:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(when (bound-and-true-p tab-prefix-map)
|
|
|
|
|
(define-key tab-prefix-map "p" #'project-other-tab-command))
|
2020-07-23 18:55:42 -07:00
|
|
|
|
|
2015-11-06 05:08:51 +02:00
|
|
|
|
(declare-function grep-read-files "grep")
|
2016-01-07 20:14:40 +03:00
|
|
|
|
(declare-function xref--find-ignores-arguments "xref")
|
2015-11-06 05:08:51 +02:00
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-find-regexp (regexp)
|
2015-12-28 06:17:19 +02:00
|
|
|
|
"Find all matches for REGEXP in the current project's roots.
|
2015-11-06 05:08:51 +02:00
|
|
|
|
With \\[universal-argument] prefix, you can specify the directory
|
2017-11-17 15:39:02 +02:00
|
|
|
|
to search in, and the file name pattern to search for. The
|
|
|
|
|
pattern may use abbreviations defined in `grep-files-aliases',
|
|
|
|
|
e.g. entering `ch' is equivalent to `*.[ch]'. As whitespace
|
|
|
|
|
triggers completion when entering a pattern, including it
|
|
|
|
|
requires quoting, e.g. `\\[quoted-insert]<space>'."
|
2015-11-06 05:08:51 +02:00
|
|
|
|
(interactive (list (project--read-regexp)))
|
2019-12-29 15:22:11 +03:00
|
|
|
|
(require 'xref)
|
2020-03-21 13:26:19 +02:00
|
|
|
|
(require 'grep)
|
2021-04-02 01:24:57 +03:00
|
|
|
|
(let* ((caller-dir default-directory)
|
|
|
|
|
(pr (project-current t))
|
2021-02-06 22:59:00 +02:00
|
|
|
|
(default-directory (project-root pr))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(files
|
|
|
|
|
(if (not current-prefix-arg)
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(project-files pr)
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(let ((dir (read-directory-name "Base directory: "
|
2021-04-02 01:24:57 +03:00
|
|
|
|
caller-dir nil t)))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(project--files-in-directory dir
|
2019-05-03 01:52:05 +03:00
|
|
|
|
nil
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(grep-read-files regexp))))))
|
2022-02-21 03:09:32 +02:00
|
|
|
|
(xref-show-xrefs
|
2019-05-24 04:50:44 +03:00
|
|
|
|
(apply-partially #'project--find-regexp-in-files regexp files)
|
|
|
|
|
nil)))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
|
|
|
|
|
(defun project--dir-ignores (project dir)
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(let ((root (project-root project)))
|
|
|
|
|
(if (not (file-in-directory-p dir root))
|
2019-01-19 03:46:07 +03:00
|
|
|
|
(project-ignores nil nil) ;The defaults.
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(let ((ignores (project-ignores project root)))
|
|
|
|
|
(if (file-equal-p root dir)
|
|
|
|
|
ignores
|
|
|
|
|
;; FIXME: Update the "rooted" ignores to relate to DIR instead.
|
|
|
|
|
(cl-delete-if (lambda (str) (string-prefix-p "./" str))
|
|
|
|
|
ignores))))))
|
2015-11-06 05:08:51 +02:00
|
|
|
|
|
|
|
|
|
;;;###autoload
|
2015-12-28 06:17:19 +02:00
|
|
|
|
(defun project-or-external-find-regexp (regexp)
|
|
|
|
|
"Find all matches for REGEXP in the project roots or external roots.
|
2015-11-06 05:08:51 +02:00
|
|
|
|
With \\[universal-argument] prefix, you can specify the file name
|
|
|
|
|
pattern to search for."
|
|
|
|
|
(interactive (list (project--read-regexp)))
|
2019-12-29 15:22:11 +03:00
|
|
|
|
(require 'xref)
|
2015-11-10 02:41:06 +02:00
|
|
|
|
(let* ((pr (project-current t))
|
2021-02-06 22:59:00 +02:00
|
|
|
|
(default-directory (project-root pr))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(files
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(project-files pr (cons
|
|
|
|
|
(project-root pr)
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(project-external-roots pr)))))
|
2022-02-21 03:09:32 +02:00
|
|
|
|
(xref-show-xrefs
|
2019-05-24 04:50:44 +03:00
|
|
|
|
(apply-partially #'project--find-regexp-in-files regexp files)
|
|
|
|
|
nil)))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
|
|
|
|
|
(defun project--find-regexp-in-files (regexp files)
|
2020-03-30 23:16:03 +03:00
|
|
|
|
(unless files
|
|
|
|
|
(user-error "Empty file list"))
|
2021-04-22 03:29:09 +03:00
|
|
|
|
(let ((xrefs (xref-matches-in-files regexp files)))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(unless xrefs
|
|
|
|
|
(user-error "No matches for: %s" regexp))
|
2019-05-24 04:50:44 +03:00
|
|
|
|
xrefs))
|
2019-01-18 06:38:12 +03:00
|
|
|
|
|
2021-03-29 22:34:51 +03:00
|
|
|
|
(defvar project-regexp-history-variable 'grep-regexp-history)
|
|
|
|
|
|
2015-11-06 05:08:51 +02:00
|
|
|
|
(defun project--read-regexp ()
|
2021-03-09 04:06:01 +02:00
|
|
|
|
(let ((sym (thing-at-point 'symbol t)))
|
2021-03-29 22:34:51 +03:00
|
|
|
|
(read-regexp "Find regexp" (and sym (regexp-quote sym))
|
|
|
|
|
project-regexp-history-variable)))
|
2015-11-06 05:08:51 +02:00
|
|
|
|
|
2016-01-08 14:32:27 +03:00
|
|
|
|
;;;###autoload
|
2021-10-14 03:43:42 +03:00
|
|
|
|
(defun project-find-file (&optional include-all)
|
2020-05-23 04:38:27 +03:00
|
|
|
|
"Visit a file (with completion) in the current project.
|
2020-11-30 03:33:14 +02:00
|
|
|
|
|
2021-09-14 00:56:20 +03:00
|
|
|
|
The filename at point (determined by `thing-at-point'), if any,
|
2021-10-14 03:43:42 +03:00
|
|
|
|
is available as part of \"future history\".
|
|
|
|
|
|
|
|
|
|
If INCLUDE-ALL is non-nil, or with prefix argument when called
|
|
|
|
|
interactively, include all files under the project root, except
|
|
|
|
|
for VCS directories listed in `vc-directory-exclusion-list'."
|
|
|
|
|
(interactive "P")
|
2016-01-07 20:14:40 +03:00
|
|
|
|
(let* ((pr (project-current t))
|
2022-05-30 19:30:54 +03:00
|
|
|
|
(root (project-root pr))
|
|
|
|
|
(dirs (list root)))
|
|
|
|
|
(project-find-file-in
|
|
|
|
|
(or (thing-at-point 'filename)
|
|
|
|
|
(and buffer-file-name (file-relative-name buffer-file-name root)))
|
|
|
|
|
dirs pr include-all)))
|
2016-01-07 20:14:40 +03:00
|
|
|
|
|
2016-01-08 14:32:27 +03:00
|
|
|
|
;;;###autoload
|
2021-10-14 03:43:42 +03:00
|
|
|
|
(defun project-or-external-find-file (&optional include-all)
|
2020-05-23 04:38:27 +03:00
|
|
|
|
"Visit a file (with completion) in the current project or external roots.
|
2020-11-30 03:33:14 +02:00
|
|
|
|
|
2021-09-14 00:56:20 +03:00
|
|
|
|
The filename at point (determined by `thing-at-point'), if any,
|
2021-10-14 03:43:42 +03:00
|
|
|
|
is available as part of \"future history\".
|
|
|
|
|
|
|
|
|
|
If INCLUDE-ALL is non-nil, or with prefix argument when called
|
|
|
|
|
interactively, include all files under the project root, except
|
|
|
|
|
for VCS directories listed in `vc-directory-exclusion-list'."
|
|
|
|
|
(interactive "P")
|
2016-01-07 20:14:40 +03:00
|
|
|
|
(let* ((pr (project-current t))
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(dirs (cons
|
|
|
|
|
(project-root pr)
|
2016-01-07 20:14:40 +03:00
|
|
|
|
(project-external-roots pr))))
|
2021-10-14 03:43:42 +03:00
|
|
|
|
(project-find-file-in (thing-at-point 'filename) dirs pr include-all)))
|
2016-01-07 20:14:40 +03:00
|
|
|
|
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(defcustom project-read-file-name-function #'project--read-file-cpd-relative
|
|
|
|
|
"Function to call to read a file name from a list.
|
|
|
|
|
For the arguments list, see `project--read-file-cpd-relative'."
|
2019-05-14 23:40:31 +03:00
|
|
|
|
:type '(choice (const :tag "Read with completion from relative names"
|
|
|
|
|
project--read-file-cpd-relative)
|
|
|
|
|
(const :tag "Read with completion from absolute names"
|
|
|
|
|
project--read-file-absolute)
|
|
|
|
|
(function :tag "Custom function" nil))
|
2020-06-03 00:27:29 +03:00
|
|
|
|
:group 'project
|
2019-05-14 23:40:31 +03:00
|
|
|
|
:version "27.1")
|
2019-05-14 05:09:19 +03:00
|
|
|
|
|
|
|
|
|
(defun project--read-file-cpd-relative (prompt
|
|
|
|
|
all-files &optional predicate
|
2021-09-14 00:56:20 +03:00
|
|
|
|
hist mb-default)
|
2019-05-16 23:26:27 +01:00
|
|
|
|
"Read a file name, prompting with PROMPT.
|
|
|
|
|
ALL-FILES is a list of possible file name completions.
|
2021-09-14 00:56:20 +03:00
|
|
|
|
|
|
|
|
|
PREDICATE and HIST have the same meaning as in `completing-read'.
|
|
|
|
|
|
|
|
|
|
MB-DEFAULT is used as part of \"future history\", to be inserted
|
|
|
|
|
by the user at will."
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(let* ((common-parent-directory
|
|
|
|
|
(let ((common-prefix (try-completion "" all-files)))
|
|
|
|
|
(if (> (length common-prefix) 0)
|
|
|
|
|
(file-name-directory common-prefix))))
|
|
|
|
|
(cpd-length (length common-parent-directory))
|
|
|
|
|
(prompt (if (zerop cpd-length)
|
|
|
|
|
prompt
|
|
|
|
|
(concat prompt (format " in %s" common-parent-directory))))
|
2021-09-22 21:07:49 +03:00
|
|
|
|
(included-cpd (when (member common-parent-directory all-files)
|
|
|
|
|
(setq all-files
|
|
|
|
|
(delete common-parent-directory all-files))
|
|
|
|
|
t))
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(substrings (mapcar (lambda (s) (substring s cpd-length)) all-files))
|
2021-09-22 21:07:49 +03:00
|
|
|
|
(_ (when included-cpd
|
|
|
|
|
(setq substrings (cons "./" substrings))))
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(new-collection (project--file-completion-table substrings))
|
2022-12-16 00:45:55 +02:00
|
|
|
|
(abbr-cpd (abbreviate-file-name common-parent-directory))
|
2022-12-19 21:46:40 +02:00
|
|
|
|
(abbr-cpd-length (length abbr-cpd))
|
2022-12-16 00:45:55 +02:00
|
|
|
|
(relname (cl-letf ((history-add-new-input nil)
|
|
|
|
|
((symbol-value hist)
|
|
|
|
|
(mapcan
|
|
|
|
|
(lambda (s)
|
|
|
|
|
(and (string-prefix-p abbr-cpd s)
|
2022-12-19 21:46:40 +02:00
|
|
|
|
(not (eq abbr-cpd-length (length s)))
|
|
|
|
|
(list (substring s abbr-cpd-length))))
|
2022-12-16 00:45:55 +02:00
|
|
|
|
(symbol-value hist))))
|
2022-10-11 20:21:34 +02:00
|
|
|
|
(project--completing-read-strict prompt
|
|
|
|
|
new-collection
|
|
|
|
|
predicate
|
|
|
|
|
hist mb-default)))
|
|
|
|
|
(absname (expand-file-name relname common-parent-directory)))
|
|
|
|
|
(when (and hist history-add-new-input)
|
2022-11-28 23:50:41 +01:00
|
|
|
|
(add-to-history hist (abbreviate-file-name absname)))
|
2022-10-11 20:21:34 +02:00
|
|
|
|
absname))
|
2019-05-14 05:09:19 +03:00
|
|
|
|
|
|
|
|
|
(defun project--read-file-absolute (prompt
|
|
|
|
|
all-files &optional predicate
|
2021-09-14 00:56:20 +03:00
|
|
|
|
hist mb-default)
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(project--completing-read-strict prompt
|
|
|
|
|
(project--file-completion-table all-files)
|
|
|
|
|
predicate
|
2021-09-14 00:56:20 +03:00
|
|
|
|
hist mb-default))
|
|
|
|
|
|
2021-10-14 03:43:42 +03:00
|
|
|
|
(defun project-find-file-in (suggested-filename dirs project &optional include-all)
|
2021-09-14 00:56:20 +03:00
|
|
|
|
"Complete a file name in DIRS in PROJECT and visit the result.
|
2019-05-14 05:09:19 +03:00
|
|
|
|
|
2021-09-14 00:56:20 +03:00
|
|
|
|
SUGGESTED-FILENAME is a relative file name, or part of it, which
|
2021-10-14 03:43:42 +03:00
|
|
|
|
is used as part of \"future history\".
|
|
|
|
|
|
|
|
|
|
If INCLUDE-ALL is non-nil, or with prefix argument when called
|
|
|
|
|
interactively, include all files from DIRS, except for VCS
|
|
|
|
|
directories listed in `vc-directory-exclusion-list'."
|
|
|
|
|
(let* ((vc-dirs-ignores (mapcar
|
|
|
|
|
(lambda (dir)
|
|
|
|
|
(concat dir "/"))
|
|
|
|
|
vc-directory-exclusion-list))
|
|
|
|
|
(all-files
|
|
|
|
|
(if include-all
|
|
|
|
|
(mapcan
|
|
|
|
|
(lambda (dir) (project--files-in-directory dir vc-dirs-ignores))
|
|
|
|
|
dirs)
|
|
|
|
|
(project-files project dirs)))
|
2020-06-17 00:50:31 +03:00
|
|
|
|
(completion-ignore-case read-file-name-completion-ignore-case)
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(file (funcall project-read-file-name-function
|
2022-05-30 19:30:54 +03:00
|
|
|
|
"Find file" all-files nil 'file-name-history
|
2021-09-14 00:56:20 +03:00
|
|
|
|
suggested-filename)))
|
2016-01-30 07:21:31 +03:00
|
|
|
|
(if (string= file "")
|
|
|
|
|
(user-error "You didn't specify the file")
|
|
|
|
|
(find-file file))))
|
|
|
|
|
|
|
|
|
|
(defun project--completing-read-strict (prompt
|
|
|
|
|
collection &optional predicate
|
2021-09-14 00:56:20 +03:00
|
|
|
|
hist mb-default)
|
2021-08-06 03:30:10 +03:00
|
|
|
|
(minibuffer-with-setup-hook
|
|
|
|
|
(lambda ()
|
|
|
|
|
(setq-local minibuffer-default-add-function
|
|
|
|
|
(lambda ()
|
2021-09-14 00:56:20 +03:00
|
|
|
|
(let ((minibuffer-default mb-default))
|
2021-08-06 03:30:10 +03:00
|
|
|
|
(minibuffer-default-add-completions)))))
|
2021-08-06 11:34:02 +03:00
|
|
|
|
(completing-read (format "%s: " prompt)
|
2021-08-06 03:30:10 +03:00
|
|
|
|
collection predicate 'confirm
|
|
|
|
|
nil
|
|
|
|
|
hist)))
|
2016-01-07 20:14:40 +03:00
|
|
|
|
|
2021-09-21 03:34:00 +03:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-find-dir ()
|
|
|
|
|
"Start Dired in a directory inside the current project."
|
|
|
|
|
(interactive)
|
|
|
|
|
(let* ((project (project-current t))
|
|
|
|
|
(all-files (project-files project))
|
|
|
|
|
(completion-ignore-case read-file-name-completion-ignore-case)
|
|
|
|
|
;; FIXME: This misses directories without any files directly
|
|
|
|
|
;; inside. Consider DIRS-ONLY as an argument for
|
|
|
|
|
;; `project-files-filtered', and see
|
|
|
|
|
;; https://stackoverflow.com/a/50685235/615245 for possible
|
|
|
|
|
;; implementation.
|
|
|
|
|
(all-dirs (mapcar #'file-name-directory all-files))
|
|
|
|
|
(dir (funcall project-read-file-name-function
|
|
|
|
|
"Dired"
|
|
|
|
|
;; Some completion UIs show duplicates.
|
|
|
|
|
(delete-dups all-dirs)
|
2022-05-30 19:30:54 +03:00
|
|
|
|
nil 'file-name-history)))
|
2021-09-21 03:34:00 +03:00
|
|
|
|
(dired dir)))
|
|
|
|
|
|
2020-05-12 04:17:23 +03:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-dired ()
|
2020-06-19 10:52:00 +03:00
|
|
|
|
"Start Dired in the current project's root."
|
2020-05-12 04:17:23 +03:00
|
|
|
|
(interactive)
|
2020-05-25 21:10:03 +02:00
|
|
|
|
(dired (project-root (project-current t))))
|
2020-05-12 04:17:23 +03:00
|
|
|
|
|
2020-06-02 02:01:25 +03:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-vc-dir ()
|
2020-06-19 10:52:00 +03:00
|
|
|
|
"Run VC-Dir in the current project's root."
|
2020-06-02 02:01:25 +03:00
|
|
|
|
(interactive)
|
|
|
|
|
(vc-dir (project-root (project-current t))))
|
|
|
|
|
|
2022-03-05 04:32:54 +02:00
|
|
|
|
(declare-function comint-check-proc "comint")
|
|
|
|
|
|
2020-06-02 02:01:25 +03:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-shell ()
|
2020-06-20 04:02:18 +03:00
|
|
|
|
"Start an inferior shell in the current project's root directory.
|
2020-06-21 19:20:37 +03:00
|
|
|
|
If a buffer already exists for running a shell in the project's root,
|
|
|
|
|
switch to it. Otherwise, create a new shell buffer.
|
|
|
|
|
With \\[universal-argument] prefix arg, create a new inferior shell buffer even
|
2020-06-22 01:47:55 +03:00
|
|
|
|
if one already exists."
|
2020-06-02 02:01:25 +03:00
|
|
|
|
(interactive)
|
2022-03-05 04:32:54 +02:00
|
|
|
|
(require 'comint)
|
2020-06-20 04:02:18 +03:00
|
|
|
|
(let* ((default-directory (project-root (project-current t)))
|
2021-04-25 00:54:01 +03:00
|
|
|
|
(default-project-shell-name (project-prefixed-buffer-name "shell"))
|
2020-06-20 04:02:18 +03:00
|
|
|
|
(shell-buffer (get-buffer default-project-shell-name)))
|
|
|
|
|
(if (and shell-buffer (not current-prefix-arg))
|
2022-03-05 04:32:54 +02:00
|
|
|
|
(if (comint-check-proc shell-buffer)
|
|
|
|
|
(pop-to-buffer shell-buffer (bound-and-true-p display-comint-buffer-action))
|
|
|
|
|
(shell shell-buffer))
|
2020-06-20 04:02:18 +03:00
|
|
|
|
(shell (generate-new-buffer-name default-project-shell-name)))))
|
2020-06-02 02:01:25 +03:00
|
|
|
|
|
2020-05-12 04:17:23 +03:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-eshell ()
|
2020-06-20 11:54:22 +02:00
|
|
|
|
"Start Eshell in the current project's root directory.
|
2020-06-21 19:20:37 +03:00
|
|
|
|
If a buffer already exists for running Eshell in the project's root,
|
|
|
|
|
switch to it. Otherwise, create a new Eshell buffer.
|
|
|
|
|
With \\[universal-argument] prefix arg, create a new Eshell buffer even
|
2020-06-22 01:47:55 +03:00
|
|
|
|
if one already exists."
|
2020-05-12 04:17:23 +03:00
|
|
|
|
(interactive)
|
2020-06-22 01:49:20 +03:00
|
|
|
|
(defvar eshell-buffer-name)
|
2020-06-20 11:54:22 +02:00
|
|
|
|
(let* ((default-directory (project-root (project-current t)))
|
2021-04-25 00:54:01 +03:00
|
|
|
|
(eshell-buffer-name (project-prefixed-buffer-name "eshell"))
|
2020-06-20 11:54:22 +02:00
|
|
|
|
(eshell-buffer (get-buffer eshell-buffer-name)))
|
|
|
|
|
(if (and eshell-buffer (not current-prefix-arg))
|
2022-01-11 19:19:37 +02:00
|
|
|
|
(pop-to-buffer eshell-buffer (bound-and-true-p display-comint-buffer-action))
|
2020-06-20 11:54:22 +02:00
|
|
|
|
(eshell t))))
|
2020-05-12 04:17:23 +03:00
|
|
|
|
|
2020-08-28 19:23:01 +02:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-async-shell-command ()
|
|
|
|
|
"Run `async-shell-command' in the current project's root directory."
|
2021-01-19 21:50:11 +02:00
|
|
|
|
(declare (interactive-only async-shell-command))
|
2021-01-20 21:19:23 +02:00
|
|
|
|
(interactive)
|
2020-08-28 19:23:01 +02:00
|
|
|
|
(let ((default-directory (project-root (project-current t))))
|
|
|
|
|
(call-interactively #'async-shell-command)))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-shell-command ()
|
|
|
|
|
"Run `shell-command' in the current project's root directory."
|
2021-01-19 21:50:11 +02:00
|
|
|
|
(declare (interactive-only shell-command))
|
2021-01-20 21:19:23 +02:00
|
|
|
|
(interactive)
|
2020-08-28 19:23:01 +02:00
|
|
|
|
(let ((default-directory (project-root (project-current t))))
|
|
|
|
|
(call-interactively #'shell-command)))
|
|
|
|
|
|
2019-02-07 12:20:09 +03:00
|
|
|
|
(declare-function fileloop-continue "fileloop" ())
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-search (regexp)
|
|
|
|
|
"Search for REGEXP in all the files of the project.
|
|
|
|
|
Stops when a match is found.
|
2020-03-21 13:26:19 +02:00
|
|
|
|
To continue searching for the next match, use the
|
|
|
|
|
command \\[fileloop-continue]."
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
(interactive "sSearch (regexp): ")
|
2019-02-07 12:20:09 +03:00
|
|
|
|
(fileloop-initialize-search
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
regexp (project-files (project-current t)) 'default)
|
2019-02-07 12:20:09 +03:00
|
|
|
|
(fileloop-continue))
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
|
|
|
|
|
;;;###autoload
|
2019-02-14 04:08:44 +03:00
|
|
|
|
(defun project-query-replace-regexp (from to)
|
2020-03-21 13:26:19 +02:00
|
|
|
|
"Query-replace REGEXP in all the files of the project.
|
|
|
|
|
Stops when a match is found and prompts for whether to replace it.
|
2022-04-21 14:54:45 +03:00
|
|
|
|
At that prompt, the user must type a character saying what to do
|
|
|
|
|
with the match. Type SPC or `y' to replace the match,
|
|
|
|
|
DEL or `n' to skip and go to the next match. For more directions,
|
|
|
|
|
type \\[help-command] at that time.
|
2021-09-22 20:26:40 +02:00
|
|
|
|
If you exit the `query-replace', you can later continue the
|
|
|
|
|
`query-replace' loop using the command \\[fileloop-continue]."
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
(interactive
|
2022-02-01 22:07:20 +02:00
|
|
|
|
(let ((query-replace-read-from-regexp-default 'find-tag-default-as-regexp))
|
|
|
|
|
(pcase-let ((`(,from ,to)
|
|
|
|
|
(query-replace-read-args "Query replace (regexp)" t t)))
|
|
|
|
|
(list from to))))
|
2019-02-07 12:20:09 +03:00
|
|
|
|
(fileloop-initialize-replace
|
2022-06-04 03:13:42 +03:00
|
|
|
|
from to
|
|
|
|
|
;; XXX: Filter out Git submodules, which are not regular files.
|
|
|
|
|
;; `project-files' can return those, which is arguably suboptimal,
|
|
|
|
|
;; but removing them eagerly has performance cost.
|
|
|
|
|
(cl-delete-if-not #'file-regular-p (project-files (project-current t)))
|
|
|
|
|
'default)
|
2019-02-07 12:20:09 +03:00
|
|
|
|
(fileloop-continue))
|
* lisp/multifile.el: New file, extracted from etags.el
The main motivation for this change was the introduction of
project-query-replace. dired's multi-file query&replace was implemented
on top of etags.el even though it did not use TAGS in any way, so I moved
this generic multifile code into its own package, with a nicer interface,
and then used that in project.el.
* lisp/progmodes/project.el (project-files): New generic function.
(project-search, project-query-replace): New commands.
* lisp/dired-aux.el (dired-do-search, dired-do-query-replace-regexp):
Use multifile.el instead of etags.el.
* lisp/progmodes/etags.el: Remove redundant :groups.
(next-file-list): Remove var.
(tags-loop-revert-buffers): Make it an obsolete alias.
(next-file): Don't autoload (it can't do anything useful before some
other etags.el function setup the multifile operation).
(tags--all-files): New function, extracted from next-file.
(tags-next-file): Rename from next-file.
Rewrite using tags--all-files and multifile-next-file.
(next-file): Keep it as an obsolete alias.
(tags-loop-operate, tags-loop-scan): Mark as obsolete.
(tags--compat-files, tags--compat-initialize): New function.
(tags-loop-continue): Rewrite using multifile-continue. Mark as obsolete.
(tags--last-search-operate-function): New var.
(tags-search, tags-query-replace): Rewrite using multifile.el.
* lisp/emacs-lisp/generator.el (iter-end-of-sequence): Use 'define-error'.
(iter-make): New macro.
(iter-empty): New iterator.
* lisp/menu-bar.el (menu-bar-search-menu, menu-bar-replace-menu):
tags-loop-continue -> multifile-continue.
2018-09-22 11:46:35 -04:00
|
|
|
|
|
2020-06-02 02:01:25 +03:00
|
|
|
|
(defvar compilation-read-command)
|
|
|
|
|
(declare-function compilation-read-command "compile")
|
|
|
|
|
|
2021-04-23 03:14:44 +03:00
|
|
|
|
(defun project-prefixed-buffer-name (mode)
|
|
|
|
|
(concat "*"
|
|
|
|
|
(file-name-nondirectory
|
|
|
|
|
(directory-file-name default-directory))
|
|
|
|
|
"-"
|
|
|
|
|
(downcase mode)
|
|
|
|
|
"*"))
|
|
|
|
|
|
|
|
|
|
(defcustom project-compilation-buffer-name-function nil
|
|
|
|
|
"Function to compute the name of a project compilation buffer.
|
|
|
|
|
If non-nil, it overrides `compilation-buffer-name-function' for
|
|
|
|
|
`project-compile'."
|
2021-04-25 00:54:01 +03:00
|
|
|
|
:version "28.1"
|
2021-04-23 03:14:44 +03:00
|
|
|
|
:group 'project
|
|
|
|
|
:type '(choice (const :tag "Default" nil)
|
|
|
|
|
(const :tag "Prefixed with root directory name"
|
|
|
|
|
project-prefixed-buffer-name)
|
|
|
|
|
(function :tag "Custom function")))
|
|
|
|
|
|
2020-05-19 19:30:14 +02:00
|
|
|
|
;;;###autoload
|
2021-01-10 10:43:41 +01:00
|
|
|
|
(defun project-compile ()
|
|
|
|
|
"Run `compile' in the project root."
|
2021-01-19 21:50:11 +02:00
|
|
|
|
(declare (interactive-only compile))
|
2021-01-20 21:19:23 +02:00
|
|
|
|
(interactive)
|
2021-04-23 03:14:44 +03:00
|
|
|
|
(let ((default-directory (project-root (project-current t)))
|
|
|
|
|
(compilation-buffer-name-function
|
|
|
|
|
(or project-compilation-buffer-name-function
|
|
|
|
|
compilation-buffer-name-function)))
|
2021-01-10 10:43:41 +01:00
|
|
|
|
(call-interactively #'compile)))
|
2020-05-19 19:30:14 +02:00
|
|
|
|
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(defcustom project-ignore-buffer-conditions nil
|
|
|
|
|
"List of conditions to filter the buffers to be switched to.
|
|
|
|
|
If any of these conditions are satisfied for a buffer in the
|
|
|
|
|
current project, `project-switch-to-buffer',
|
|
|
|
|
`project-display-buffer' and `project-display-buffer-other-frame'
|
|
|
|
|
ignore it.
|
|
|
|
|
See the doc string of `project-kill-buffer-conditions' for the
|
|
|
|
|
general form of conditions."
|
|
|
|
|
:type '(repeat (choice regexp function symbol
|
|
|
|
|
(cons :tag "Major mode"
|
|
|
|
|
(const major-mode) symbol)
|
|
|
|
|
(cons :tag "Derived mode"
|
|
|
|
|
(const derived-mode) symbol)
|
|
|
|
|
(cons :tag "Negation"
|
|
|
|
|
(const not) sexp)
|
|
|
|
|
(cons :tag "Conjunction"
|
|
|
|
|
(const and) sexp)
|
|
|
|
|
(cons :tag "Disjunction"
|
|
|
|
|
(const or) sexp)))
|
|
|
|
|
:version "29.1"
|
|
|
|
|
:group 'project
|
|
|
|
|
:package-version '(project . "0.8.2"))
|
|
|
|
|
|
2020-07-24 13:36:39 -07:00
|
|
|
|
(defun project--read-project-buffer ()
|
2020-07-05 03:35:00 +03:00
|
|
|
|
(let* ((pr (project-current t))
|
2020-06-19 02:21:55 +03:00
|
|
|
|
(current-buffer (current-buffer))
|
|
|
|
|
(other-buffer (other-buffer current-buffer))
|
|
|
|
|
(other-name (buffer-name other-buffer))
|
2021-08-21 05:26:12 +03:00
|
|
|
|
(buffers (project-buffers pr))
|
2020-06-19 02:21:55 +03:00
|
|
|
|
(predicate
|
|
|
|
|
(lambda (buffer)
|
|
|
|
|
;; BUFFER is an entry (BUF-NAME . BUF-OBJ) of Vbuffer_alist.
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(and (memq (cdr buffer) buffers)
|
|
|
|
|
(not
|
|
|
|
|
(project--buffer-check
|
|
|
|
|
(cdr buffer) project-ignore-buffer-conditions))))))
|
2020-07-24 13:36:39 -07:00
|
|
|
|
(read-buffer
|
|
|
|
|
"Switch to buffer: "
|
|
|
|
|
(when (funcall predicate (cons other-name other-buffer))
|
|
|
|
|
other-name)
|
|
|
|
|
nil
|
|
|
|
|
predicate)))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-switch-to-buffer (buffer-or-name)
|
|
|
|
|
"Display buffer BUFFER-OR-NAME in the selected window.
|
|
|
|
|
When called interactively, prompts for a buffer belonging to the
|
|
|
|
|
current project. Two buffers belong to the same project if their
|
|
|
|
|
project instances, as reported by `project-current' in each
|
|
|
|
|
buffer, are identical."
|
|
|
|
|
(interactive (list (project--read-project-buffer)))
|
2020-07-24 13:54:49 -07:00
|
|
|
|
(switch-to-buffer buffer-or-name))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-display-buffer (buffer-or-name)
|
|
|
|
|
"Display BUFFER-OR-NAME in some window, without selecting it.
|
|
|
|
|
When called interactively, prompts for a buffer belonging to the
|
|
|
|
|
current project. Two buffers belong to the same project if their
|
|
|
|
|
project instances, as reported by `project-current' in each
|
|
|
|
|
buffer, are identical.
|
|
|
|
|
|
|
|
|
|
This function uses `display-buffer' as a subroutine, which see
|
|
|
|
|
for how it is determined where the buffer will be displayed."
|
|
|
|
|
(interactive (list (project--read-project-buffer)))
|
|
|
|
|
(display-buffer buffer-or-name))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-display-buffer-other-frame (buffer-or-name)
|
|
|
|
|
"Display BUFFER-OR-NAME preferably in another frame.
|
|
|
|
|
When called interactively, prompts for a buffer belonging to the
|
|
|
|
|
current project. Two buffers belong to the same project if their
|
|
|
|
|
project instances, as reported by `project-current' in each
|
|
|
|
|
buffer, are identical.
|
|
|
|
|
|
|
|
|
|
This function uses `display-buffer-other-frame' as a subroutine,
|
|
|
|
|
which see for how it is determined where the buffer will be
|
|
|
|
|
displayed."
|
|
|
|
|
(interactive (list (project--read-project-buffer)))
|
2020-07-27 03:05:16 +03:00
|
|
|
|
(display-buffer-other-frame buffer-or-name))
|
2020-06-18 02:09:41 +03:00
|
|
|
|
|
2022-11-15 20:54:39 +02:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-list-buffers (&optional arg)
|
|
|
|
|
"Display a list of project buffers.
|
|
|
|
|
The list is displayed in a buffer named \"*Buffer List*\".
|
|
|
|
|
|
|
|
|
|
By default, all project buffers are listed except those whose names
|
|
|
|
|
start with a space (which are for internal use). With prefix argument
|
|
|
|
|
ARG, show only buffers that are visiting files."
|
|
|
|
|
(interactive "P")
|
2022-12-13 19:47:47 +02:00
|
|
|
|
(let* ((pr (project-current t))
|
|
|
|
|
(buffer-list-function
|
|
|
|
|
(lambda ()
|
|
|
|
|
(seq-filter
|
|
|
|
|
(lambda (buffer)
|
|
|
|
|
(let ((name (buffer-name buffer))
|
|
|
|
|
(file (buffer-file-name buffer)))
|
|
|
|
|
(and (or (not (string= (substring name 0 1) " "))
|
|
|
|
|
file)
|
|
|
|
|
(not (eq buffer (current-buffer)))
|
|
|
|
|
(or file (not Buffer-menu-files-only)))))
|
|
|
|
|
(project-buffers pr)))))
|
2022-11-15 20:54:39 +02:00
|
|
|
|
(display-buffer
|
|
|
|
|
(if (version< emacs-version "29.0.50")
|
2022-12-13 19:47:47 +02:00
|
|
|
|
(let ((buf (list-buffers-noselect
|
|
|
|
|
arg (with-current-buffer
|
|
|
|
|
(get-buffer-create "*Buffer List*")
|
|
|
|
|
(let ((Buffer-menu-files-only arg))
|
|
|
|
|
(funcall buffer-list-function))))))
|
2022-11-15 20:54:39 +02:00
|
|
|
|
(with-current-buffer buf
|
|
|
|
|
(setq-local revert-buffer-function
|
|
|
|
|
(lambda (&rest _ignored)
|
2022-12-13 19:47:47 +02:00
|
|
|
|
(list-buffers--refresh
|
|
|
|
|
(funcall buffer-list-function))
|
2022-11-15 20:54:39 +02:00
|
|
|
|
(tabulated-list-print t))))
|
|
|
|
|
buf)
|
2022-12-13 19:47:47 +02:00
|
|
|
|
(list-buffers-noselect arg buffer-list-function)))))
|
2022-11-15 20:54:39 +02:00
|
|
|
|
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(defcustom project-kill-buffer-conditions
|
2022-06-17 15:22:29 +03:00
|
|
|
|
'(buffer-file-name ; All file-visiting buffers are included.
|
2022-11-04 02:58:58 +02:00
|
|
|
|
;; Most of temp and logging buffers (aside from hidden ones):
|
|
|
|
|
(and
|
|
|
|
|
(major-mode . fundamental-mode)
|
2022-11-05 19:45:57 +02:00
|
|
|
|
"\\`[^ ]")
|
2020-07-29 01:21:56 +03:00
|
|
|
|
;; non-text buffer such as xref, occur, vc, log, ...
|
2022-06-17 15:22:29 +03:00
|
|
|
|
(and (derived-mode . special-mode)
|
2022-11-04 03:03:29 +02:00
|
|
|
|
(not (major-mode . help-mode))
|
|
|
|
|
(not (derived-mode . gnus-mode)))
|
2022-06-17 15:22:29 +03:00
|
|
|
|
(derived-mode . compilation-mode)
|
|
|
|
|
(derived-mode . dired-mode)
|
|
|
|
|
(derived-mode . diff-mode)
|
|
|
|
|
(derived-mode . comint-mode)
|
|
|
|
|
(derived-mode . eshell-mode)
|
|
|
|
|
(derived-mode . change-log-mode))
|
2020-07-29 01:21:56 +03:00
|
|
|
|
"List of conditions to kill buffers related to a project.
|
|
|
|
|
This list is used by `project-kill-buffers'.
|
|
|
|
|
Each condition is either:
|
|
|
|
|
- a regular expression, to match a buffer name,
|
|
|
|
|
- a predicate function that takes a buffer object as argument
|
|
|
|
|
and returns non-nil if the buffer should be killed,
|
|
|
|
|
- a cons-cell, where the car describes how to interpret the cdr.
|
|
|
|
|
The car can be one of the following:
|
|
|
|
|
* `major-mode': the buffer is killed if the buffer's major
|
2022-06-17 15:22:29 +03:00
|
|
|
|
mode is eq to the cons-cell's cdr.
|
2022-04-14 10:24:27 +02:00
|
|
|
|
* `derived-mode': the buffer is killed if the buffer's major
|
2022-06-17 15:22:29 +03:00
|
|
|
|
mode is derived from the major mode in the cons-cell's cdr.
|
2020-07-29 01:21:56 +03:00
|
|
|
|
* `not': the cdr is interpreted as a negation of a condition.
|
|
|
|
|
* `and': the cdr is a list of recursive conditions, that all have
|
|
|
|
|
to be met.
|
|
|
|
|
* `or': the cdr is a list of recursive conditions, of which at
|
|
|
|
|
least one has to be met.
|
|
|
|
|
|
2020-09-21 13:29:10 +02:00
|
|
|
|
If any of these conditions are satisfied for a buffer in the
|
2020-07-29 01:21:56 +03:00
|
|
|
|
current project, it will be killed."
|
|
|
|
|
:type '(repeat (choice regexp function symbol
|
|
|
|
|
(cons :tag "Major mode"
|
|
|
|
|
(const major-mode) symbol)
|
|
|
|
|
(cons :tag "Derived mode"
|
|
|
|
|
(const derived-mode) symbol)
|
|
|
|
|
(cons :tag "Negation"
|
|
|
|
|
(const not) sexp)
|
|
|
|
|
(cons :tag "Conjunction"
|
|
|
|
|
(const and) sexp)
|
|
|
|
|
(cons :tag "Disjunction"
|
|
|
|
|
(const or) sexp)))
|
2021-12-15 02:57:20 +03:00
|
|
|
|
:version "29.1"
|
2020-07-29 01:21:56 +03:00
|
|
|
|
:group 'project
|
2021-12-15 02:57:20 +03:00
|
|
|
|
:package-version '(project . "0.8.2"))
|
2020-06-18 04:00:38 +03:00
|
|
|
|
|
2021-11-29 16:42:02 +01:00
|
|
|
|
(defcustom project-kill-buffers-display-buffer-list nil
|
|
|
|
|
"Non-nil to display list of buffers to kill before killing project buffers.
|
|
|
|
|
Used by `project-kill-buffers'."
|
|
|
|
|
:type 'boolean
|
|
|
|
|
:version "29.1"
|
|
|
|
|
:group 'project
|
2021-12-15 02:58:45 +03:00
|
|
|
|
:package-version '(project . "0.8.2")
|
2021-11-29 16:42:02 +01:00
|
|
|
|
:safe #'booleanp)
|
|
|
|
|
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(defun project--buffer-check (buf conditions)
|
2020-07-29 01:21:56 +03:00
|
|
|
|
"Check if buffer BUF matches any element of the list CONDITIONS.
|
2022-02-23 09:25:32 +01:00
|
|
|
|
See `project-kill-buffer-conditions' or
|
|
|
|
|
`project-ignore-buffer-conditions' for more details on the
|
|
|
|
|
form of CONDITIONS."
|
|
|
|
|
(catch 'match
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(dolist (c conditions)
|
|
|
|
|
(when (cond
|
|
|
|
|
((stringp c)
|
|
|
|
|
(string-match-p c (buffer-name buf)))
|
2022-06-17 15:22:29 +03:00
|
|
|
|
((functionp c)
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(funcall c buf))
|
2022-04-14 10:24:27 +02:00
|
|
|
|
((eq (car-safe c) 'major-mode)
|
2022-06-17 15:22:29 +03:00
|
|
|
|
(eq (buffer-local-value 'major-mode buf)
|
|
|
|
|
(cdr c)))
|
|
|
|
|
((eq (car-safe c) 'derived-mode)
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(provided-mode-derived-p
|
|
|
|
|
(buffer-local-value 'major-mode buf)
|
|
|
|
|
(cdr c)))
|
|
|
|
|
((eq (car-safe c) 'not)
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(not (project--buffer-check buf (cdr c))))
|
2020-07-29 01:21:56 +03:00
|
|
|
|
((eq (car-safe c) 'or)
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(project--buffer-check buf (cdr c)))
|
2020-07-29 01:21:56 +03:00
|
|
|
|
((eq (car-safe c) 'and)
|
|
|
|
|
(seq-every-p
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(apply-partially #'project--buffer-check
|
2020-07-29 01:21:56 +03:00
|
|
|
|
buf)
|
|
|
|
|
(mapcar #'list (cdr c)))))
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(throw 'match t)))))
|
2020-07-29 01:21:56 +03:00
|
|
|
|
|
|
|
|
|
(defun project--buffers-to-kill (pr)
|
|
|
|
|
"Return list of buffers in project PR to kill.
|
|
|
|
|
What buffers should or should not be killed is described
|
|
|
|
|
in `project-kill-buffer-conditions'."
|
|
|
|
|
(let (bufs)
|
2021-08-21 05:26:12 +03:00
|
|
|
|
(dolist (buf (project-buffers pr))
|
2022-02-23 09:25:32 +01:00
|
|
|
|
(when (project--buffer-check buf project-kill-buffer-conditions)
|
2020-06-18 04:00:38 +03:00
|
|
|
|
(push buf bufs)))
|
2020-07-29 01:21:56 +03:00
|
|
|
|
bufs))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-kill-buffers (&optional no-confirm)
|
|
|
|
|
"Kill the buffers belonging to the current project.
|
|
|
|
|
Two buffers belong to the same project if their project
|
|
|
|
|
instances, as reported by `project-current' in each buffer, are
|
|
|
|
|
identical. Only the buffers that match a condition in
|
|
|
|
|
`project-kill-buffer-conditions' will be killed. If NO-CONFIRM
|
|
|
|
|
is non-nil, the command will not ask the user for confirmation.
|
|
|
|
|
NO-CONFIRM is always nil when the command is invoked
|
2022-04-16 17:30:32 +02:00
|
|
|
|
interactively.
|
|
|
|
|
|
|
|
|
|
Also see the `project-kill-buffers-display-buffer-list' variable."
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(interactive)
|
|
|
|
|
(let* ((pr (project-current t))
|
2021-11-29 16:42:02 +01:00
|
|
|
|
(bufs (project--buffers-to-kill pr))
|
|
|
|
|
(query-user (lambda ()
|
|
|
|
|
(yes-or-no-p
|
|
|
|
|
(format "Kill %d buffers in %s? "
|
|
|
|
|
(length bufs)
|
|
|
|
|
(project-root pr))))))
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(cond (no-confirm
|
|
|
|
|
(mapc #'kill-buffer bufs))
|
|
|
|
|
((null bufs)
|
|
|
|
|
(message "No buffers to kill"))
|
2021-11-29 16:42:02 +01:00
|
|
|
|
(project-kill-buffers-display-buffer-list
|
|
|
|
|
(when
|
|
|
|
|
(with-current-buffer-window
|
|
|
|
|
(get-buffer-create "*Buffer List*")
|
|
|
|
|
`(display-buffer--maybe-at-bottom
|
|
|
|
|
(dedicated . t)
|
|
|
|
|
(window-height . (fit-window-to-buffer))
|
|
|
|
|
(preserve-size . (nil . t))
|
|
|
|
|
(body-function
|
|
|
|
|
. ,#'(lambda (_window)
|
|
|
|
|
(list-buffers-noselect nil bufs))))
|
|
|
|
|
#'(lambda (window _value)
|
|
|
|
|
(with-selected-window window
|
|
|
|
|
(unwind-protect
|
|
|
|
|
(funcall query-user)
|
|
|
|
|
(when (window-live-p window)
|
|
|
|
|
(quit-restore-window window 'kill))))))
|
|
|
|
|
(mapc #'kill-buffer bufs)))
|
|
|
|
|
((funcall query-user)
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(mapc #'kill-buffer bufs)))))
|
2020-06-18 04:00:38 +03:00
|
|
|
|
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
|
|
|
|
;;; Project list
|
|
|
|
|
|
2020-06-04 19:58:36 +02:00
|
|
|
|
(defcustom project-list-file (locate-user-emacs-file "projects")
|
2020-06-19 10:52:00 +03:00
|
|
|
|
"File in which to save the list of known projects."
|
2020-06-03 11:45:54 +01:00
|
|
|
|
:type 'file
|
|
|
|
|
:version "28.1"
|
2020-06-03 00:27:29 +03:00
|
|
|
|
:group 'project)
|
|
|
|
|
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(defvar project--list 'unset
|
2020-06-26 03:41:42 +03:00
|
|
|
|
"List structure containing root directories of known projects.
|
|
|
|
|
With some possible metadata (to be decided).")
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
|
|
|
|
(defun project--read-project-list ()
|
2020-06-19 10:52:00 +03:00
|
|
|
|
"Initialize `project--list' using contents of `project-list-file'."
|
2020-06-03 00:27:29 +03:00
|
|
|
|
(let ((filename project-list-file))
|
2020-05-29 16:58:09 +02:00
|
|
|
|
(setq project--list
|
|
|
|
|
(when (file-exists-p filename)
|
|
|
|
|
(with-temp-buffer
|
|
|
|
|
(insert-file-contents filename)
|
2020-06-26 03:41:42 +03:00
|
|
|
|
(read (current-buffer)))))
|
|
|
|
|
(unless (seq-every-p
|
2020-06-26 09:37:52 +01:00
|
|
|
|
(lambda (elt) (stringp (car-safe elt)))
|
2020-06-26 03:41:42 +03:00
|
|
|
|
project--list)
|
|
|
|
|
(warn "Contents of %s are in wrong format, resetting"
|
|
|
|
|
project-list-file)
|
|
|
|
|
(setq project--list nil))))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
|
|
|
|
(defun project--ensure-read-project-list ()
|
2020-06-19 10:52:00 +03:00
|
|
|
|
"Initialize `project--list' if it isn't already initialized."
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(when (eq project--list 'unset)
|
|
|
|
|
(project--read-project-list)))
|
|
|
|
|
|
|
|
|
|
(defun project--write-project-list ()
|
2020-06-19 10:52:00 +03:00
|
|
|
|
"Save `project--list' in `project-list-file'."
|
2020-06-03 00:27:29 +03:00
|
|
|
|
(let ((filename project-list-file))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(with-temp-buffer
|
2020-06-05 19:32:30 +02:00
|
|
|
|
(insert ";;; -*- lisp-data -*-\n")
|
2020-11-08 21:45:02 +02:00
|
|
|
|
(let ((print-length nil)
|
|
|
|
|
(print-level nil))
|
|
|
|
|
(pp project--list (current-buffer)))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(write-region nil nil filename nil 'silent))))
|
|
|
|
|
|
2020-07-25 03:17:47 +03:00
|
|
|
|
;;;###autoload
|
2021-08-31 14:12:13 +02:00
|
|
|
|
(defun project-remember-project (pr &optional no-write)
|
2020-06-02 19:20:14 +02:00
|
|
|
|
"Add project PR to the front of the project list.
|
2021-08-31 14:12:13 +02:00
|
|
|
|
Save the result in `project-list-file' if the list of projects
|
|
|
|
|
has changed, and NO-WRITE is nil."
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(project--ensure-read-project-list)
|
2020-06-04 19:29:10 +02:00
|
|
|
|
(let ((dir (project-root pr)))
|
2020-06-05 19:32:30 +02:00
|
|
|
|
(unless (equal (caar project--list) dir)
|
2020-08-05 22:57:01 +02:00
|
|
|
|
(dolist (ent project--list)
|
|
|
|
|
(when (equal dir (car ent))
|
|
|
|
|
(setq project--list (delq ent project--list))))
|
2020-06-05 19:32:30 +02:00
|
|
|
|
(push (list dir) project--list)
|
2021-08-31 14:12:13 +02:00
|
|
|
|
(unless no-write
|
|
|
|
|
(project--write-project-list)))))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
2021-03-22 00:19:23 +01:00
|
|
|
|
(defun project--remove-from-project-list (project-root report-message)
|
|
|
|
|
"Remove directory PROJECT-ROOT of a missing project from the project list.
|
2020-05-12 03:22:30 +03:00
|
|
|
|
If the directory was in the list before the removal, save the
|
2020-06-19 10:52:00 +03:00
|
|
|
|
result in `project-list-file'. Announce the project's removal
|
2021-03-22 00:19:23 +01:00
|
|
|
|
from the list using REPORT-MESSAGE, which is a format string
|
|
|
|
|
passed to `message' as its first argument."
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(project--ensure-read-project-list)
|
2021-03-22 00:19:23 +01:00
|
|
|
|
(when-let ((ent (assoc project-root project--list)))
|
2020-08-05 22:57:01 +02:00
|
|
|
|
(setq project--list (delq ent project--list))
|
2021-03-22 00:19:23 +01:00
|
|
|
|
(message report-message project-root)
|
2020-05-12 03:22:30 +03:00
|
|
|
|
(project--write-project-list)))
|
|
|
|
|
|
2021-03-22 00:19:23 +01:00
|
|
|
|
;;;###autoload
|
2021-09-21 16:07:39 +03:00
|
|
|
|
(defun project-forget-project (project-root)
|
2021-03-22 00:19:23 +01:00
|
|
|
|
"Remove directory PROJECT-ROOT from the project list.
|
|
|
|
|
PROJECT-ROOT is the root directory of a known project listed in
|
|
|
|
|
the project list."
|
|
|
|
|
(interactive (list (project-prompt-project-dir)))
|
|
|
|
|
(project--remove-from-project-list
|
2021-03-25 01:20:26 +02:00
|
|
|
|
project-root "Project `%s' removed from known projects"))
|
2021-03-22 00:19:23 +01:00
|
|
|
|
|
2020-05-12 03:22:30 +03:00
|
|
|
|
(defun project-prompt-project-dir ()
|
2020-06-19 10:52:00 +03:00
|
|
|
|
"Prompt the user for a directory that is one of the known project roots.
|
|
|
|
|
The project is chosen among projects known from the project list,
|
|
|
|
|
see `project-list-file'.
|
|
|
|
|
It's also possible to enter an arbitrary directory not in the list."
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(project--ensure-read-project-list)
|
|
|
|
|
(let* ((dir-choice "... (choose a dir)")
|
2020-05-12 03:32:05 +03:00
|
|
|
|
(choices
|
|
|
|
|
;; XXX: Just using this for the category (for the substring
|
|
|
|
|
;; completion style).
|
|
|
|
|
(project--file-completion-table
|
|
|
|
|
(append project--list `(,dir-choice))))
|
2021-09-14 00:53:05 +03:00
|
|
|
|
(pr-dir ""))
|
|
|
|
|
(while (equal pr-dir "")
|
|
|
|
|
;; If the user simply pressed RET, do this again until they don't.
|
|
|
|
|
(setq pr-dir (completing-read "Select project: " choices nil t)))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
(if (equal pr-dir dir-choice)
|
2020-05-29 21:37:12 +03:00
|
|
|
|
(read-directory-name "Select directory: " default-directory nil t)
|
2020-05-12 03:22:30 +03:00
|
|
|
|
pr-dir)))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
2020-06-24 02:59:31 +03:00
|
|
|
|
;;;###autoload
|
2020-06-26 03:33:13 +03:00
|
|
|
|
(defun project-known-project-roots ()
|
|
|
|
|
"Return the list of root directories of all known projects."
|
2020-06-24 02:59:31 +03:00
|
|
|
|
(project--ensure-read-project-list)
|
2020-06-26 03:33:13 +03:00
|
|
|
|
(mapcar #'car project--list))
|
2020-06-24 02:59:31 +03:00
|
|
|
|
|
2020-12-20 00:16:32 +02:00
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-execute-extended-command ()
|
|
|
|
|
"Execute an extended command in project root."
|
|
|
|
|
(declare (interactive-only command-execute))
|
|
|
|
|
(interactive)
|
|
|
|
|
(let ((default-directory (project-root (project-current t))))
|
|
|
|
|
(call-interactively #'execute-extended-command)))
|
|
|
|
|
|
2021-08-31 14:12:13 +02:00
|
|
|
|
(defun project-remember-projects-under (dir &optional recursive)
|
|
|
|
|
"Index all projects below a directory DIR.
|
|
|
|
|
If RECURSIVE is non-nil, recurse into all subdirectories to find
|
|
|
|
|
more projects. After finishing, a message is printed summarizing
|
|
|
|
|
the progress. The function returns the number of detected
|
|
|
|
|
projects."
|
|
|
|
|
(interactive "DDirectory: \nP")
|
|
|
|
|
(project--ensure-read-project-list)
|
2022-09-10 14:56:08 +02:00
|
|
|
|
(let ((queue (list dir))
|
|
|
|
|
(count 0)
|
2021-08-31 14:12:13 +02:00
|
|
|
|
(known (make-hash-table
|
|
|
|
|
:size (* 2 (length project--list))
|
|
|
|
|
:test #'equal )))
|
|
|
|
|
(dolist (project (mapcar #'car project--list))
|
|
|
|
|
(puthash project t known))
|
|
|
|
|
(while queue
|
|
|
|
|
(when-let ((subdir (pop queue))
|
2022-09-10 14:56:08 +02:00
|
|
|
|
((file-directory-p subdir)))
|
|
|
|
|
(when-let ((project (project--find-in-directory subdir))
|
|
|
|
|
(project-root (project-root project))
|
|
|
|
|
((not (gethash project-root known))))
|
|
|
|
|
(project-remember-project project t)
|
|
|
|
|
(puthash project-root t known)
|
|
|
|
|
(message "Found %s..." project-root)
|
2021-08-31 14:12:13 +02:00
|
|
|
|
(setq count (1+ count)))
|
2022-09-10 14:56:08 +02:00
|
|
|
|
(when (and recursive (file-directory-p subdir))
|
|
|
|
|
(setq queue
|
|
|
|
|
(nconc
|
|
|
|
|
(directory-files
|
|
|
|
|
subdir t directory-files-no-dot-files-regexp t)
|
|
|
|
|
queue)))))
|
2021-08-31 14:12:13 +02:00
|
|
|
|
(unless (eq recursive 'in-progress)
|
|
|
|
|
(if (zerop count)
|
|
|
|
|
(message "No projects were found")
|
|
|
|
|
(project--write-project-list)
|
|
|
|
|
(message "%d project%s were found"
|
|
|
|
|
count (if (= count 1) "" "s"))))
|
|
|
|
|
count))
|
|
|
|
|
|
|
|
|
|
(defun project-forget-zombie-projects ()
|
|
|
|
|
"Forget all known projects that don't exist any more."
|
|
|
|
|
(interactive)
|
|
|
|
|
(dolist (proj (project-known-project-roots))
|
|
|
|
|
(unless (file-exists-p proj)
|
|
|
|
|
(project-forget-project proj))))
|
|
|
|
|
|
|
|
|
|
(defun project-forget-projects-under (dir &optional recursive)
|
|
|
|
|
"Forget all known projects below a directory DIR.
|
|
|
|
|
If RECURSIVE is non-nil, recurse into all subdirectories to
|
|
|
|
|
remove all known projects. After finishing, a message is printed
|
|
|
|
|
summarizing the progress. The function returns the number of
|
|
|
|
|
forgotten projects."
|
|
|
|
|
(interactive "DDirectory: \nP")
|
|
|
|
|
(let ((count 0))
|
|
|
|
|
(if recursive
|
|
|
|
|
(dolist (proj (project-known-project-roots))
|
|
|
|
|
(when (file-in-directory-p proj dir)
|
|
|
|
|
(project-forget-project proj)
|
|
|
|
|
(setq count (1+ count))))
|
|
|
|
|
(dolist (proj (project-known-project-roots))
|
|
|
|
|
(when (file-equal-p (file-name-directory proj) dir)
|
|
|
|
|
(project-forget-project proj)
|
|
|
|
|
(setq count (1+ count)))))
|
|
|
|
|
(if (zerop count)
|
|
|
|
|
(message "No projects were forgotten")
|
|
|
|
|
(project--write-project-list)
|
|
|
|
|
(message "%d project%s were forgotten"
|
|
|
|
|
count (if (= count 1) "" "s")))
|
|
|
|
|
count))
|
|
|
|
|
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
|
|
|
|
;;; Project switching
|
|
|
|
|
|
2020-12-13 22:50:46 +02:00
|
|
|
|
(defcustom project-switch-commands
|
|
|
|
|
'((project-find-file "Find file")
|
|
|
|
|
(project-find-regexp "Find regexp")
|
2021-09-21 03:34:00 +03:00
|
|
|
|
(project-find-dir "Find directory")
|
2020-12-13 22:50:46 +02:00
|
|
|
|
(project-vc-dir "VC-Dir")
|
|
|
|
|
(project-eshell "Eshell"))
|
|
|
|
|
"Alist mapping commands to descriptions.
|
2020-05-16 09:53:43 +02:00
|
|
|
|
Used by `project-switch-project' to construct a dispatch menu of
|
|
|
|
|
commands available upon \"switching\" to another project.
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
2020-12-13 22:50:46 +02:00
|
|
|
|
Each element is of the form (COMMAND LABEL &optional KEY) where
|
|
|
|
|
COMMAND is the command to run when KEY is pressed. LABEL is used
|
|
|
|
|
to distinguish the menu entries in the dispatch menu. If KEY is
|
|
|
|
|
absent, COMMAND must be bound in `project-prefix-map', and the
|
2021-09-07 04:44:38 +03:00
|
|
|
|
key is looked up in that map.
|
|
|
|
|
|
|
|
|
|
The value can also be a symbol, the name of the command to be
|
|
|
|
|
invoked immediately without any dispatch menu."
|
2020-12-13 22:50:46 +02:00
|
|
|
|
:version "28.1"
|
2021-04-23 03:24:06 +03:00
|
|
|
|
:group 'project
|
2020-12-13 22:50:46 +02:00
|
|
|
|
:package-version '(project . "0.6.0")
|
2021-09-07 04:44:38 +03:00
|
|
|
|
:type '(choice
|
|
|
|
|
(repeat :tag "Commands menu"
|
|
|
|
|
(list
|
|
|
|
|
(symbol :tag "Command")
|
|
|
|
|
(string :tag "Label")
|
|
|
|
|
(choice :tag "Key to press"
|
|
|
|
|
(const :tag "Infer from the keymap" nil)
|
|
|
|
|
(character :tag "Explicit key"))))
|
|
|
|
|
(symbol :tag "Single command")))
|
2020-12-13 22:50:46 +02:00
|
|
|
|
|
|
|
|
|
(defcustom project-switch-use-entire-map nil
|
|
|
|
|
"Make `project-switch-project' use entire `project-prefix-map'.
|
|
|
|
|
If nil, `project-switch-project' will only recognize commands
|
|
|
|
|
listed in `project-switch-commands' and signal an error when
|
|
|
|
|
others are invoked. Otherwise, all keys in `project-prefix-map'
|
|
|
|
|
are legal even if they aren't listed in the dispatch menu."
|
2020-12-14 12:31:54 +01:00
|
|
|
|
:type 'boolean
|
2021-04-23 03:24:06 +03:00
|
|
|
|
:group 'project
|
2020-12-13 22:50:46 +02:00
|
|
|
|
:version "28.1")
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
|
|
|
|
(defun project--keymap-prompt ()
|
2020-09-21 13:29:10 +02:00
|
|
|
|
"Return a prompt for the project switching dispatch menu."
|
2020-05-17 08:49:08 +02:00
|
|
|
|
(mapconcat
|
2020-12-13 22:50:46 +02:00
|
|
|
|
(pcase-lambda (`(,cmd ,label ,key))
|
|
|
|
|
(when (characterp cmd) ; Old format, apparently user-customized.
|
|
|
|
|
(let ((tmp cmd))
|
|
|
|
|
;; TODO: Add a deprecation warning, probably.
|
|
|
|
|
(setq cmd key
|
|
|
|
|
key tmp)))
|
|
|
|
|
(let ((key (if key
|
|
|
|
|
(vector key)
|
2021-04-05 18:15:16 +02:00
|
|
|
|
(where-is-internal cmd (list project-prefix-map) t))))
|
2020-12-13 22:50:46 +02:00
|
|
|
|
(format "[%s] %s"
|
|
|
|
|
(propertize (key-description key) 'face 'bold)
|
|
|
|
|
label)))
|
2020-05-17 13:54:32 +02:00
|
|
|
|
project-switch-commands
|
2020-05-17 08:49:08 +02:00
|
|
|
|
" "))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
2021-09-07 04:44:38 +03:00
|
|
|
|
(defun project--switch-project-command ()
|
2021-04-06 16:51:28 +02:00
|
|
|
|
(let* ((commands-menu
|
|
|
|
|
(mapcar
|
|
|
|
|
(lambda (row)
|
|
|
|
|
(if (characterp (car row))
|
|
|
|
|
;; Deprecated format.
|
|
|
|
|
;; XXX: Add a warning about it?
|
|
|
|
|
(reverse row)
|
|
|
|
|
row))
|
|
|
|
|
project-switch-commands))
|
|
|
|
|
(commands-map
|
|
|
|
|
(let ((temp-map (make-sparse-keymap)))
|
|
|
|
|
(set-keymap-parent temp-map project-prefix-map)
|
|
|
|
|
(dolist (row commands-menu temp-map)
|
|
|
|
|
(when-let ((cmd (nth 0 row))
|
|
|
|
|
(keychar (nth 2 row)))
|
|
|
|
|
(define-key temp-map (vector keychar) cmd)))))
|
|
|
|
|
command)
|
2020-12-13 22:50:46 +02:00
|
|
|
|
(while (not command)
|
2021-08-01 23:43:37 +03:00
|
|
|
|
(let* ((overriding-local-map commands-map)
|
|
|
|
|
(choice (read-key-sequence (project--keymap-prompt))))
|
2021-04-06 16:51:28 +02:00
|
|
|
|
(when (setq command (lookup-key commands-map choice))
|
2020-12-13 22:50:46 +02:00
|
|
|
|
(unless (or project-switch-use-entire-map
|
|
|
|
|
(assq command commands-menu))
|
|
|
|
|
;; TODO: Add some hint to the prompt, like "key not
|
|
|
|
|
;; recognized" or something.
|
2021-04-07 03:24:17 +03:00
|
|
|
|
(setq command nil)))
|
|
|
|
|
(let ((global-command (lookup-key (current-global-map) choice)))
|
|
|
|
|
(when (memq global-command
|
|
|
|
|
'(keyboard-quit keyboard-escape-quit))
|
|
|
|
|
(call-interactively global-command)))))
|
2021-09-07 04:44:38 +03:00
|
|
|
|
command))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun project-switch-project (dir)
|
|
|
|
|
"\"Switch\" to another project by running an Emacs command.
|
|
|
|
|
The available commands are presented as a dispatch menu
|
|
|
|
|
made from `project-switch-commands'.
|
|
|
|
|
|
|
|
|
|
When called in a program, it will use the project corresponding
|
|
|
|
|
to directory DIR."
|
|
|
|
|
(interactive (list (project-prompt-project-dir)))
|
|
|
|
|
(let ((command (if (symbolp project-switch-commands)
|
|
|
|
|
project-switch-commands
|
|
|
|
|
(project--switch-project-command))))
|
2022-11-24 04:33:01 +02:00
|
|
|
|
(let ((project-current-directory-override dir))
|
|
|
|
|
(call-interactively command))))
|
2020-05-09 17:27:06 +02:00
|
|
|
|
|
2015-07-10 04:34:41 +03:00
|
|
|
|
(provide 'project)
|
|
|
|
|
;;; project.el ends here
|