diff options
Diffstat (limited to 'lisp/progmodes/project.el')
-rw-r--r-- | lisp/progmodes/project.el | 197 |
1 files changed, 141 insertions, 56 deletions
diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 186840ae29b..0da6084a1e3 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -23,7 +23,7 @@ ;; projects, and a number of public functions: finding the current ;; root, related project directories, search path, etc. ;; -;; The goal is to make it easy for Lisp programs to operate on the +;; 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. @@ -31,63 +31,93 @@ (require 'cl-generic) -(defvar project-find-functions (list #'project-try-vc - #'project-ask-user) +(defvar project-find-functions (list #'project-try-vc) "Special hook to find the project containing a given directory. Each functions on this hook is called in turn with one argument (the directory) and should return either nil to mean that it is not applicable, or a project instance.") -(declare-function etags-search-path "etags" ()) - -(defvar project-search-path-function #'etags-search-path - "Function that returns a list of source root directories. +;; FIXME: Using the current approach, 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. -The directories in which we can recursively look for the -declarations or other references to the symbols used in the -current buffer. Depending on the language, it should include the -headers search path, load path, class path, or so on. +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. This variable is -normally set by the major mode. Used in the default -implementation of `project-search-path'.") +The directory names should be absolute. Used in the default +implementation of `project-library-roots'.") ;;;###autoload -(defun project-current (&optional dir) - "Return the project instance in DIR or `default-directory'." +(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." (unless dir (setq dir default-directory)) + (let ((pr (project--find-in-directory dir))) + (cond + (pr) + (maybe-prompt + (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)))) + 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-search-path (project) - "Return the list of source root directories. -Any directory roots where source (or header, etc) files used by -the current project may be found, inside or outside of the -current project tree(s). The directory names should be absolute. - -Unless it really knows better, a specialized implementation -should take into account the value returned by -`project-search-path-function' and call -`project-prune-directories' on the result." - (project-prune-directories - (append - ;; We don't know the project layout, like where the sources are, - ;; so we simply include the roots. - (project-roots project) - (funcall project-search-path-function)))) +(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. + +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))) (cl-defgeneric project-roots (project) - "Return the list of directory roots related to the current project. -It should include the current project root, as well as the roots -of any other currently open projects, if they're meant to be -edited together. The directory names should be absolute.") + "Return the list of directory roots belonging to the current project. + +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. + +The rule of tumb 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. + +The directory names should be absolute.") (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 either one of `project-roots', or -an element of `project-search-path'." +end it with `/'. DIR must be one of `project-roots' or +`project-library-roots'." (require 'grep) (defvar grep-find-ignored-files) (nconc @@ -101,8 +131,8 @@ an element of `project-search-path'." "Project implementation using the VC package." :group 'tools) -(defcustom project-vc-search-path nil - "List ot directories to include in `project-search-path'. +(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) @@ -121,13 +151,16 @@ The file names can be absolute, or relative to the project root." (cl-defmethod project-roots ((project (head vc))) (list (cdr project))) -(cl-defmethod project-search-path ((project (head vc))) - (append - (let ((root (cdr project))) - (mapcar - (lambda (dir) (expand-file-name dir root)) - (project--value-in-dir 'project-vc-search-path root))) - (cl-call-next-method))) +(cl-defmethod project-library-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))) + (project-roots project))) (cl-defmethod project-ignores ((project (head vc)) dir) (let* ((root (cdr project)) @@ -144,19 +177,16 @@ The file names can be absolute, or relative to the project root." (project--value-in-dir 'project-vc-ignores root) (cl-call-next-method)))) -(defun project-ask-user (dir) - (cons 'user (read-directory-name "Project root: " dir nil t))) - -(cl-defmethod project-roots ((project (head user))) - (list (cdr project))) - -(defun project-prune-directories (dirs) - "Returns a copy of DIRS sorted, without subdirectories or non-existing ones." +(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." (let* ((dirs (sort (mapcar (lambda (dir) (file-name-as-directory (expand-file-name dir))) - dirs) + (apply #'append lists-of-dirs)) #'string<)) (ref dirs)) ;; Delete subdirectories from the list. @@ -166,11 +196,66 @@ The file names can be absolute, or relative to the project root." (setq ref (cdr ref)))) (cl-delete-if-not #'file-exists-p dirs))) +(defun project-subtract-directories (files dirs) + "Return a list of elements from FILES that are outside of DIRS. +DIRS must contain directory names." + ;; Sidestep the issue of expanded/abbreviated file names here. + (cl-set-difference files dirs :test #'file-in-directory-p)) + (defun project--value-in-dir (var dir) (with-temp-buffer (setq default-directory dir) (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") + +;;;###autoload +(defun project-find-regexp (regexp) + "Find all matches for REGEXP in the current project. +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))) + (let* ((pr (project-current t)) + (dirs (if current-prefix-arg + (list (read-directory-name "Base directory: " + nil default-directory t)) + (project-roots pr)))) + (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. +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--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))) + +(defun project--find-regexp-in (dirs regexp project) + (require 'grep) + (let* ((files (if current-prefix-arg + (grep-read-files regexp) + "*")) + (xrefs (cl-mapcan + (lambda (dir) + (xref-collect-matches regexp files dir + (project-ignores project dir))) + dirs))) + (unless xrefs + (user-error "No matches for: %s" regexp)) + (xref--show-xrefs xrefs nil))) + (provide 'project) ;;; project.el ends here |