2015-07-10 04:34:41 +03:00
|
|
|
|
;;; project.el --- Operations on the current project -*- lexical-binding: t; -*-
|
|
|
|
|
|
2021-01-01 01:13:56 -08:00
|
|
|
|
;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
|
2021-09-13 17:43:43 +01:00
|
|
|
|
;; Version: 0.7.1
|
2021-01-28 04:18:12 +01:00
|
|
|
|
;; Package-Requires: ((emacs "26.1") (xref "1.0.2"))
|
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.
|
|
|
|
|
;;
|
|
|
|
|
;; VC project:
|
|
|
|
|
;;
|
|
|
|
|
;; 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
|
|
|
|
|
;; `project-vc-ignores' (usually through .dir-locals.el).
|
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)
|
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
|
|
|
|
|
2020-05-19 01:00:43 +03:00
|
|
|
|
(defvar project-current-inhibit-prompt nil
|
|
|
|
|
"Non-nil to skip prompting the user in `project-current'.")
|
|
|
|
|
|
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
|
2020-07-12 21:48:39 +03:00
|
|
|
|
the files under the directory except for those that should be
|
|
|
|
|
ignored (per `project-ignores').
|
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."
|
2020-07-11 15:33:51 +03:00
|
|
|
|
(unless directory (setq directory default-directory))
|
|
|
|
|
(let ((pr (project--find-in-directory directory)))
|
2015-11-10 02:41:06 +02:00
|
|
|
|
(cond
|
|
|
|
|
(pr)
|
2020-05-19 01:00:43 +03:00
|
|
|
|
((unless project-current-inhibit-prompt
|
|
|
|
|
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
|
|
|
|
|
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)
|
2019-01-18 06:38:12 +03:00
|
|
|
|
(defvar find-name-arg)
|
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 "(")
|
|
|
|
|
" " find-name-arg " "
|
|
|
|
|
(mapconcat
|
|
|
|
|
#'shell-quote-argument
|
|
|
|
|
(split-string files)
|
|
|
|
|
(concat " -o " find-name-arg " "))
|
|
|
|
|
" "
|
2020-11-04 15:41:53 +01:00
|
|
|
|
(shell-quote-argument ")"))
|
2021-04-16 03:38:23 +03:00
|
|
|
|
"")))
|
|
|
|
|
(output (with-output-to-string
|
|
|
|
|
(with-current-buffer standard-output
|
|
|
|
|
(let ((status
|
|
|
|
|
(process-file-shell-command command nil t)))
|
|
|
|
|
(unless (zerop status)
|
|
|
|
|
(error "File listing failed: %s" (buffer-string))))))))
|
2019-02-07 14:22:47 +03:00
|
|
|
|
(project--remote-file-names
|
2021-09-06 05:01:07 +03:00
|
|
|
|
(mapcar (lambda (s) (concat dfn (substring s 1)))
|
|
|
|
|
(sort (split-string output "\0" t)
|
|
|
|
|
#'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)
|
|
|
|
|
"Return the list of all live buffers that belong to PROJECT."
|
|
|
|
|
(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
|
2020-05-18 03:36:43 +03:00
|
|
|
|
"Project implementation based on the VC package."
|
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
|
|
|
|
|
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
|
|
|
|
|
;; used in them; for others, like VC-based projects, we'll need
|
|
|
|
|
;; 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.
|
|
|
|
|
|
|
|
|
|
The directory names should be absolute. Used in the VC project
|
|
|
|
|
backend implementation of `project-external-roots'.")
|
|
|
|
|
|
2015-07-10 04:34:41 +03:00
|
|
|
|
(defun project-try-vc (dir)
|
2020-07-06 00:50:32 +03:00
|
|
|
|
(let* ((backend
|
|
|
|
|
;; FIXME: This is slow. Cache it.
|
|
|
|
|
(ignore-errors (vc-responsible-backend dir)))
|
2019-12-27 18:18:41 +03:00
|
|
|
|
(root
|
|
|
|
|
(pcase backend
|
|
|
|
|
('Git
|
|
|
|
|
;; Don't stop at submodule boundary.
|
2020-07-06 00:50:32 +03:00
|
|
|
|
;; FIXME: Cache for a shorter time.
|
2019-12-27 18:18:41 +03:00
|
|
|
|
(or (vc-file-getprop dir 'project-git-root)
|
2020-05-18 03:36:43 +03:00
|
|
|
|
(let ((root (vc-call-backend backend 'root dir)))
|
2020-05-15 08:10:22 +03:00
|
|
|
|
(vc-file-setprop
|
|
|
|
|
dir 'project-git-root
|
2020-05-18 03:36:43 +03:00
|
|
|
|
(if (and
|
2020-05-18 03:44:26 +03:00
|
|
|
|
;; FIXME: Invalidate the cache when the value
|
|
|
|
|
;; of this variable changes.
|
2020-06-18 01:30:32 +03:00
|
|
|
|
(project--vc-merge-submodules-p root)
|
2020-05-18 03:36:43 +03:00
|
|
|
|
(project--submodule-p root))
|
|
|
|
|
(let* ((parent (file-name-directory
|
|
|
|
|
(directory-file-name root))))
|
|
|
|
|
(vc-call-backend backend 'root parent))
|
|
|
|
|
root)))))
|
2019-12-27 18:18:41 +03:00
|
|
|
|
('nil nil)
|
|
|
|
|
(_ (ignore-errors (vc-call-backend backend 'root dir))))))
|
2015-07-10 04:34:41 +03:00
|
|
|
|
(and root (cons 'vc root))))
|
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)))
|
|
|
|
|
(cdr 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)
|
2020-12-02 01:41:40 +02:00
|
|
|
|
(let ((ignores (project--value-in-dir 'project-vc-ignores dir))
|
|
|
|
|
backend)
|
2019-10-04 02:03:04 +03:00
|
|
|
|
(if (and (file-equal-p dir (cdr project))
|
|
|
|
|
(setq backend (vc-responsible-backend dir))
|
|
|
|
|
(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)
|
|
|
|
|
(pcase backend
|
|
|
|
|
(`Git
|
2019-10-05 12:32:11 +03:00
|
|
|
|
(let ((default-directory (expand-file-name (file-name-as-directory dir)))
|
2019-12-27 18:18:41 +03:00
|
|
|
|
(args '("-z"))
|
|
|
|
|
files)
|
2019-10-04 02:03:04 +03:00
|
|
|
|
;; Include unregistered.
|
|
|
|
|
(setq args (append args '("-c" "-o" "--exclude-standard")))
|
|
|
|
|
(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)))
|
2020-06-18 01:30:32 +03:00
|
|
|
|
(when (project--vc-merge-submodules-p default-directory)
|
|
|
|
|
;; 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
|
2019-10-05 12:32:11 +03:00
|
|
|
|
(let ((default-directory (expand-file-name (file-name-as-directory dir)))
|
2019-10-04 15:50:16 +03:00
|
|
|
|
args)
|
2019-10-04 02:03:04 +03:00
|
|
|
|
;; Include unregistered.
|
2019-10-04 15:50:16 +03:00
|
|
|
|
(setq args (nconc args '("-mcardu" "--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
|
|
|
|
|
2020-06-18 01:30:32 +03: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))
|
|
|
|
|
(while (re-search-forward "path *= *\\(.+\\)" nil t)
|
|
|
|
|
(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)
|
2015-08-10 04:04:57 +03:00
|
|
|
|
(let* ((root (cdr project))
|
2020-06-26 09:37:52 +01:00
|
|
|
|
backend)
|
2015-08-10 04:04:57 +03:00
|
|
|
|
(append
|
2015-08-02 01:01:28 +03:00
|
|
|
|
(when (file-equal-p dir root)
|
|
|
|
|
(setq backend (vc-responsible-backend root))
|
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)))))
|
2015-08-10 04:04:57 +03: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
|
|
|
|
|
2020-07-27 03:04:09 +03: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))))
|
|
|
|
|
(modules (unless (or (project--vc-merge-submodules-p root)
|
|
|
|
|
(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)))
|
|
|
|
|
|
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)
|
|
|
|
|
(define-key map "d" 'project-dired)
|
|
|
|
|
(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)
|
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")
|
|
|
|
|
(declare-function xref--show-xrefs "xref")
|
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))))))
|
2019-05-24 04:50:44 +03:00
|
|
|
|
(xref--show-xrefs
|
|
|
|
|
(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)))))
|
2019-05-24 04:50:44 +03:00
|
|
|
|
(xref--show-xrefs
|
|
|
|
|
(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
|
2019-05-20 15:24:47 -07:00
|
|
|
|
(defun project-find-file ()
|
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,
|
|
|
|
|
is available as part of \"future history\"."
|
2016-01-07 20:14:40 +03:00
|
|
|
|
(interactive)
|
|
|
|
|
(let* ((pr (project-current t))
|
2020-05-23 04:38:27 +03:00
|
|
|
|
(dirs (list (project-root pr))))
|
2019-05-20 15:24:47 -07:00
|
|
|
|
(project-find-file-in (thing-at-point 'filename) dirs pr)))
|
2016-01-07 20:14:40 +03:00
|
|
|
|
|
2016-01-08 14:32:27 +03:00
|
|
|
|
;;;###autoload
|
2016-01-07 20:14:40 +03:00
|
|
|
|
(defun project-or-external-find-file ()
|
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,
|
|
|
|
|
is available as part of \"future history\"."
|
2016-01-07 20:14:40 +03:00
|
|
|
|
(interactive)
|
|
|
|
|
(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))))
|
2016-01-29 17:43:26 -06:00
|
|
|
|
(project-find-file-in (thing-at-point 'filename) dirs pr)))
|
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))))
|
|
|
|
|
(substrings (mapcar (lambda (s) (substring s cpd-length)) all-files))
|
|
|
|
|
(new-collection (project--file-completion-table substrings))
|
|
|
|
|
(res (project--completing-read-strict prompt
|
|
|
|
|
new-collection
|
|
|
|
|
predicate
|
2021-09-14 00:56:20 +03:00
|
|
|
|
hist mb-default)))
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(concat common-parent-directory res)))
|
|
|
|
|
|
|
|
|
|
(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))
|
|
|
|
|
|
|
|
|
|
(defun project-find-file-in (suggested-filename dirs project)
|
|
|
|
|
"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
|
|
|
|
|
is used as part of \"future history\"."
|
2019-05-14 05:09:19 +03:00
|
|
|
|
(let* ((all-files (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
|
2020-06-26 09:37:52 +01:00
|
|
|
|
"Find file" all-files nil nil
|
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
|
|
|
|
|
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))))
|
|
|
|
|
|
|
|
|
|
;;;###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)
|
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))
|
2021-03-21 08:02:28 +01:00
|
|
|
|
(pop-to-buffer-same-window 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))
|
2021-03-08 12:38:41 +01:00
|
|
|
|
(pop-to-buffer-same-window eshell-buffer)
|
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.
|
|
|
|
|
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
|
|
|
|
|
(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
|
* 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
|
|
|
|
from to (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
|
|
|
|
|
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.
|
2021-08-21 05:26:12 +03:00
|
|
|
|
(memq (cdr buffer) buffers))))
|
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
|
|
|
|
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(defcustom project-kill-buffer-conditions
|
|
|
|
|
'(buffer-file-name ; All file-visiting buffers are included.
|
|
|
|
|
;; Most of the temp buffers in the background:
|
|
|
|
|
(major-mode . fundamental-mode)
|
|
|
|
|
;; non-text buffer such as xref, occur, vc, log, ...
|
|
|
|
|
(and (derived-mode . special-mode)
|
|
|
|
|
(not (major-mode . help-mode)))
|
|
|
|
|
(derived-mode . compilation-mode)
|
|
|
|
|
(derived-mode . dired-mode)
|
|
|
|
|
(derived-mode . diff-mode))
|
|
|
|
|
"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
|
|
|
|
|
mode is eq to the cons-cell's cdr
|
2020-09-18 12:15:46 +03:00
|
|
|
|
* `derived-mode': the buffer is killed if the buffer's major
|
2020-07-29 01:21:56 +03:00
|
|
|
|
mode is derived from the major mode denoted by the cons-cell's
|
|
|
|
|
cdr
|
|
|
|
|
* `not': the cdr is interpreted as a negation of a condition.
|
|
|
|
|
* `and': the cdr is a list of recursive conditions, that all have
|
|
|
|
|
to be met.
|
|
|
|
|
* `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)))
|
2020-06-26 03:27:39 +03:00
|
|
|
|
:version "28.1"
|
2020-07-29 01:21:56 +03:00
|
|
|
|
:group 'project
|
|
|
|
|
:package-version '(project . "0.6.0"))
|
2020-06-18 04:00:38 +03:00
|
|
|
|
|
|
|
|
|
(defun project--buffer-list (pr)
|
|
|
|
|
"Return the list of all buffers in project PR."
|
2021-05-06 20:43:02 +03:00
|
|
|
|
(let ((conn (file-remote-p (project-root pr)))
|
2021-05-03 11:35:41 +02:00
|
|
|
|
bufs)
|
2020-06-18 04:00:38 +03:00
|
|
|
|
(dolist (buf (buffer-list))
|
2021-05-06 20:43:02 +03:00
|
|
|
|
;; For now we go with the assumption that a project must reside
|
|
|
|
|
;; entirely on one host. We might relax that in the future.
|
|
|
|
|
(when (and (equal conn
|
|
|
|
|
(file-remote-p (buffer-local-value 'default-directory buf)))
|
2021-05-03 11:35:41 +02:00
|
|
|
|
(equal pr
|
|
|
|
|
(with-current-buffer buf
|
|
|
|
|
(project-current))))
|
2020-07-05 03:35:00 +03:00
|
|
|
|
(push buf bufs)))
|
2020-06-18 04:00:38 +03:00
|
|
|
|
(nreverse bufs)))
|
|
|
|
|
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(defun project--kill-buffer-check (buf conditions)
|
|
|
|
|
"Check if buffer BUF matches any element of the list CONDITIONS.
|
|
|
|
|
See `project-kill-buffer-conditions' for more details on the form
|
|
|
|
|
of CONDITIONS."
|
|
|
|
|
(catch 'kill
|
|
|
|
|
(dolist (c conditions)
|
|
|
|
|
(when (cond
|
|
|
|
|
((stringp c)
|
|
|
|
|
(string-match-p c (buffer-name buf)))
|
|
|
|
|
((symbolp c)
|
|
|
|
|
(funcall c buf))
|
|
|
|
|
((eq (car-safe c) 'major-mode)
|
|
|
|
|
(eq (buffer-local-value 'major-mode buf)
|
|
|
|
|
(cdr c)))
|
|
|
|
|
((eq (car-safe c) 'derived-mode)
|
|
|
|
|
(provided-mode-derived-p
|
|
|
|
|
(buffer-local-value 'major-mode buf)
|
|
|
|
|
(cdr c)))
|
|
|
|
|
((eq (car-safe c) 'not)
|
|
|
|
|
(not (project--kill-buffer-check buf (cdr c))))
|
|
|
|
|
((eq (car-safe c) 'or)
|
|
|
|
|
(project--kill-buffer-check buf (cdr c)))
|
|
|
|
|
((eq (car-safe c) 'and)
|
|
|
|
|
(seq-every-p
|
|
|
|
|
(apply-partially #'project--kill-buffer-check
|
|
|
|
|
buf)
|
|
|
|
|
(mapcar #'list (cdr c)))))
|
|
|
|
|
(throw 'kill t)))))
|
|
|
|
|
|
|
|
|
|
(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))
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(when (project--kill-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
|
2020-09-18 20:30:00 +02:00
|
|
|
|
interactively."
|
2020-07-29 01:21:56 +03:00
|
|
|
|
(interactive)
|
|
|
|
|
(let* ((pr (project-current t))
|
|
|
|
|
(bufs (project--buffers-to-kill pr)))
|
|
|
|
|
(cond (no-confirm
|
|
|
|
|
(mapc #'kill-buffer bufs))
|
|
|
|
|
((null bufs)
|
|
|
|
|
(message "No buffers to kill"))
|
|
|
|
|
((yes-or-no-p (format "Kill %d buffers in %s? "
|
|
|
|
|
(length bufs)
|
|
|
|
|
(project-root pr)))
|
|
|
|
|
(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
|
|
|
|
|
(defun project-remember-project (pr)
|
2020-06-02 19:20:14 +02:00
|
|
|
|
"Add project PR to the front of the project list.
|
2020-06-19 10:52:00 +03:00
|
|
|
|
Save the result in `project-list-file' if the list of projects has changed."
|
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)
|
2020-06-02 19:20:14 +02:00
|
|
|
|
(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
|
|
|
|
|
(defun project-remove-known-project (project-root)
|
|
|
|
|
"Remove directory PROJECT-ROOT from the project list.
|
|
|
|
|
PROJECT-ROOT is the root directory of a known project listed in
|
|
|
|
|
the project list."
|
|
|
|
|
(interactive (list (project-prompt-project-dir)))
|
|
|
|
|
(project--remove-from-project-list
|
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)))
|
|
|
|
|
|
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")
|
|
|
|
|
(project-dired "Dired")
|
|
|
|
|
(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))))
|
2020-06-04 19:56:32 +02:00
|
|
|
|
(let ((default-directory dir)
|
|
|
|
|
(project-current-inhibit-prompt t))
|
2020-12-13 22:50:46 +02:00
|
|
|
|
(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
|