;;; project.el --- Operations on the current project -*- lexical-binding: t; -*-
-;; Copyright (C) 2015 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2016 Free Software Foundation, Inc.
;; This file is part of GNU Emacs.
;;; Commentary:
;; This file contains generic infrastructure for dealing with
-;; projects, and a number of public functions: finding the current
-;; root, related project directories, and library directories. This
-;; list is to be extended in future versions.
+;; projects, some utility functions, and commands using that
+;; infrastructure.
;;
;; The goal is to make it easier for Lisp programs to operate on the
;; current project, without having to know which package handles
;; detection of that project type, parsing its config files, etc.
+;;
+;; 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'.
+;;
+;; Infrastructure:
+;;
+;; Function `project-current', to determine the current project
+;; instance, and 3 (at the moment) generic functions that act on it.
+;; This list is to be extended in future versions.
+;;
+;; Utils:
+;;
+;; `project-combine-directories' and `project-subtract-directories',
+;; mainly for use in the abovementioned generics' implementations.
+;;
+;; Commands:
+;;
+;; `project-find-regexp' and `project-or-external-find-regexp' use the
+;; current API, and thus will work in any project that has an adapter.
+
+;;; TODO:
+
+;; * 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).
+;;
+;; * Allow the backend to override the file-listing logic? Maybe also
+;; to delegate file name completion to an external tool.
+;;
+;; * 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
+;; 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.
+;;
+;; * 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
+;; to search for a regexp across all open projects. Provide a
+;; history of projects that were opened in the past (storing it as a
+;; list of directories should suffice).
+;;
+;; * 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.
;;; Code:
argument (the directory) and should return either nil to mean
that it is not applicable, or a project instance.")
-;; 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 "library 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 library".
-;;
-;; We can 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.
-(defvar project-library-roots-function 'etags-library-roots
- "Function that returns a list of library roots.
-
-It should return a list of directories that contain source files
-related to the current buffer. Depending on the language, it
-should include the headers search path, load path, class path,
-and so on.
-
-The directory names should be absolute. Used in the default
-implementation of `project-library-roots'.")
-
;;;###autoload
(defun project-current (&optional maybe-prompt dir)
"Return the project instance in DIR or `default-directory'.
When no project found in DIR, and MAYBE-PROMPT is non-nil, ask
-the user for a different directory to look in."
+the user for a different directory to look in. If that directory
+is not a part of a detectable project either, return a
+`transient' project instance rooted in it."
(unless dir (setq dir default-directory))
(let ((pr (project--find-in-directory dir)))
(cond
(setq dir (read-directory-name "Choose the project directory: " dir nil t)
pr (project--find-in-directory dir))
(unless pr
- (user-error "No project found in `%s'" dir))))
+ (message "Using '%s' as a transient project root" dir)
+ (setq pr (cons 'transient dir)))))
pr))
(defun project--find-in-directory (dir)
(run-hook-with-args-until-success 'project-find-functions dir))
-;; FIXME: Add MODE argument, like in `ede-source-paths'?
-(cl-defgeneric project-library-roots (project)
- "Return the list of library roots for PROJECT.
-
-It's the list of directories outside of the project that contain
-related source files.
+(cl-defgeneric project-roots (project)
+ "Return the list of directory roots of the current project.
-Project-specific version of `project-library-roots-function',
-which see. Unless it knows better, a specialized implementation
-should use the value returned by that function."
- (project-subtract-directories
- (project-combine-directories
- (funcall project-library-roots-function))
- (project-roots project)))
+Most often it's just one directory which contains the project
+build file and everything else in the project. But in more
+advanced configurations, a project can span multiple directories.
-(cl-defgeneric project-roots (project)
- "Return the list of directory roots belonging to the current project.
+The directory names should be absolute.")
-Most often it's just one directory, which contains the project
-file and everything else in the project. But in more advanced
-configurations, a project can span multiple directories.
+;; FIXME: Add MODE argument, like in `ede-source-paths'?
+(cl-defgeneric project-external-roots (_project)
+ "Return the list of external roots for PROJECT.
-The rule of thumb for whether to include a directory here, and not
-in `project-library-roots', is whether its contents are meant to
-be edited together with the rest of the project.
+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
+headers search path, load path, class path, and so on.
-The directory names should be absolute.")
+The rule of thumb for whether to include a directory here, and
+not in `project-roots', is whether its contents are meant to be
+edited together with the rest of the project."
+ nil)
(cl-defgeneric project-ignores (_project _dir)
"Return the list of glob patterns to ignore inside DIR.
Patterns can match both regular files and directories.
To root an entry, start it with `./'. To match directories only,
end it with `/'. DIR must be one of `project-roots' or
-`project-library-roots'."
+`project-external-roots'."
(require 'grep)
(defvar grep-find-ignored-files)
(nconc
vc-directory-exclusion-list)
grep-find-ignored-files))
+(cl-defgeneric project-file-completion-table (project dirs)
+ "Return a completion table for files in directories DIRS in PROJECT.
+DIRS is a list of absolute directories; it should be some
+subset of the project roots and external roots.
+
+The default implementation uses `grep-find-program'. PROJECT is used
+to find the list of ignores for each directory."
+ ;; FIXME: Uniquely abbreviate the roots?
+ (require 'xref)
+ (let ((all-files
+ (cl-mapcan
+ (lambda (dir)
+ (let ((command
+ (format "%s %s %s -type f -print0"
+ grep-find-program
+ dir
+ (xref--find-ignores-arguments
+ (project-ignores project dir)
+ (expand-file-name dir)))))
+ (split-string (shell-command-to-string command) "\0" t)))
+ dirs)))
+ (lambda (string pred action)
+ (cond
+ ((eq action 'metadata)
+ '(metadata . ((category . project-file))))
+ (t
+ (complete-with-action action all-files string pred))))))
+
+(cl-defmethod project-roots ((project (head transient)))
+ (list (cdr project)))
+
(defgroup project-vc nil
"Project implementation using the VC package."
+ :version "25.1"
:group 'tools)
-(defcustom project-vc-library-roots nil
- "List ot directories to include in `project-library-roots'.
-The file names can be absolute, or relative to the project root."
- :type '(repeat file)
- :safe 'listp)
-
(defcustom project-vc-ignores nil
- "List ot patterns to include in `project-ignores'."
+ "List of patterns to include in `project-ignores'."
:type '(repeat string)
:safe 'listp)
+;; 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'.")
+
(defun project-try-vc (dir)
(let* ((backend (ignore-errors (vc-responsible-backend dir)))
(root (and backend (ignore-errors
(cl-defmethod project-roots ((project (head vc)))
(list (cdr project)))
-(cl-defmethod project-library-roots ((project (head vc)))
+(cl-defmethod project-external-roots ((project (head vc)))
(project-subtract-directories
(project-combine-directories
- (append
- (let ((root (cdr project)))
- (mapcar
- (lambda (dir) (file-name-as-directory (expand-file-name dir root)))
- (project--value-in-dir 'project-vc-library-roots root)))
- (funcall project-library-roots-function)))
+ (mapcar
+ #'file-name-as-directory
+ (funcall project-vc-external-roots-function)))
(project-roots project)))
(cl-defmethod project-ignores ((project (head vc)) dir)
(defun project--value-in-dir (var dir)
(with-temp-buffer
(setq default-directory dir)
- (hack-dir-local-variables-non-file-buffer)
+ (let ((enable-local-variables :all))
+ (hack-dir-local-variables-non-file-buffer))
(symbol-value var)))
(declare-function grep-read-files "grep")
-(declare-function xref-collect-matches "xref")
(declare-function xref--show-xrefs "xref")
+(declare-function xref-backend-identifier-at-point "xref")
+(declare-function xref--find-ignores-arguments "xref")
;;;###autoload
(defun project-find-regexp (regexp)
- "Find all matches for REGEXP in the current project.
+ "Find all matches for REGEXP in the current project's roots.
With \\[universal-argument] prefix, you can specify the directory
to search in, and the file name pattern to search for."
(interactive (list (project--read-regexp)))
(project--find-regexp-in dirs regexp pr)))
;;;###autoload
-(defun project-or-libraries-find-regexp (regexp)
- "Find all matches for REGEXP in the current project or libraries.
+(defun project-or-external-find-regexp (regexp)
+ "Find all matches for REGEXP in the project roots or external roots.
With \\[universal-argument] prefix, you can specify the file name
pattern to search for."
(interactive (list (project--read-regexp)))
(let* ((pr (project-current t))
(dirs (append
(project-roots pr)
- (project-library-roots pr))))
+ (project-external-roots pr))))
(project--find-regexp-in dirs regexp pr)))
(defun project--read-regexp ()
- (defvar xref-identifier-at-point-function)
- (require 'xref)
- (read-regexp "Find regexp"
- (funcall xref-identifier-at-point-function)))
+ (let ((id (xref-backend-identifier-at-point (xref-find-backend))))
+ (read-regexp "Find regexp" (and id (regexp-quote id)))))
(defun project--find-regexp-in (dirs regexp project)
(require 'grep)
(user-error "No matches for: %s" regexp))
(xref--show-xrefs xrefs nil)))
+;;;###autoload
+(defun project-find-file ()
+ "Visit a file (with completion) in the current project's roots.
+The completion default is the filename at point, if one is
+recognized."
+ (interactive)
+ (let* ((pr (project-current t))
+ (dirs (project-roots pr)))
+ (project-find-file-in (thing-at-point 'filename) dirs pr)))
+
+;;;###autoload
+(defun project-or-external-find-file ()
+ "Visit a file (with completion) in the current project's roots or external roots.
+The completion default is the filename at point, if one is
+recognized."
+ (interactive)
+ (let* ((pr (project-current t))
+ (dirs (append
+ (project-roots pr)
+ (project-external-roots pr))))
+ (project-find-file-in (thing-at-point 'filename) dirs pr)))
+
+(defun project-find-file-in (filename dirs project)
+ "Complete FILENAME in DIRS in PROJECT and visit the result."
+ (let* ((table (project-file-completion-table project dirs))
+ (file (project--completing-read-strict
+ "Find file" table nil nil
+ filename)))
+ (if (string= file "")
+ (user-error "You didn't specify the file")
+ (find-file file))))
+
+(defun project--completing-read-strict (prompt
+ collection &optional predicate
+ hist default inherit-input-method)
+ ;; Tried both expanding the default before showing the prompt, and
+ ;; removing it when it has no matches. Neither seems natural
+ ;; enough. Removal is confusing; early expansion makes the prompt
+ ;; too long.
+ (let* ((new-prompt (if default
+ (format "%s (default %s): " prompt default)
+ (format "%s: " prompt)))
+ (res (completing-read new-prompt
+ collection predicate t
+ nil hist default inherit-input-method)))
+ (if (and (equal res default)
+ (not (test-completion res collection predicate)))
+ (completing-read (format "%s: " prompt)
+ collection predicate t res hist nil
+ inherit-input-method)
+ res)))
+
(provide 'project)
;;; project.el ends here