branch: externals/javaimp commit 25f5e3f90b8913819571384b7ff27c41ad92690a Author: Filipp Gunbin <fgun...@fastmail.fm> Commit: Filipp Gunbin <fgun...@fastmail.fm>
Improve xref support for classes --- javaimp-gradle.el | 11 +- javaimp-maven.el | 33 +-- javaimp-util.el | 56 ++--- javaimp.el | 619 +++++++++++++++++++++++++++++------------------------- 4 files changed, 396 insertions(+), 323 deletions(-) diff --git a/javaimp-gradle.el b/javaimp-gradle.el index 9ca1b4032f..365506d211 100644 --- a/javaimp-gradle.el +++ b/javaimp-gradle.el @@ -83,11 +83,11 @@ descriptor." (cdr (assq 'parent-id alist))) :file (cdr (assq 'file alist)) :file-orig file-orig - ;; jar/war supported - :final-name (when-let ((final-name (javaimp-cygpath-convert-file-name - (cdr (assq 'final-name alist))))) - (and (member (file-name-extension final-name) '("jar" "war")) - final-name)) + :artifact (when-let ((final-name (javaimp-cygpath-convert-file-name + (cdr (assq 'final-name alist))))) + ;; only jar/war supported + (and (member (file-name-extension final-name) '("jar" "war")) + final-name)) :source-dirs (mapcar #'file-name-as-directory (javaimp-split-native-path (cdr (assq 'source-dirs alist)))) @@ -95,6 +95,7 @@ descriptor." (javaimp-cygpath-convert-file-name (cdr (assq 'build-dir alist)))) :dep-jars (javaimp-split-native-path (cdr (assq 'dep-jars alist))) + :dep-jars-with-source t :load-ts (current-time) :dep-jars-fetcher #'javaimp-gradle--fetch-dep-jars :raw nil)) diff --git a/javaimp-maven.el b/javaimp-maven.el index 801e988f71..3157b4fb6d 100644 --- a/javaimp-maven.el +++ b/javaimp-maven.el @@ -107,7 +107,11 @@ resulting module trees." (xml-parse-region start end))) (defun javaimp-maven--module-from-xml (elt file-orig) - (let ((build-elt (javaimp-xml-child 'build elt))) + (let* ((build-elt (javaimp-xml-child 'build elt)) + (build-dir + (file-name-as-directory + (javaimp-cygpath-convert-file-name + (javaimp-xml-first-child (javaimp-xml-child 'directory build-elt)))))) (make-javaimp-module :id (javaimp-maven--id-from-xml elt) :parent-id (javaimp-maven--id-from-xml (javaimp-xml-child 'parent elt)) @@ -115,14 +119,16 @@ resulting module trees." ;; later, see javaimp-maven--fill-modules-files :file nil :file-orig file-orig - ;; jar/war supported - :final-name (let ((packaging (or (javaimp-xml-first-child - (javaimp-xml-child 'packaging elt)) - "jar"))) - (when (member packaging '("jar" "war")) - (concat (javaimp-xml-first-child - (javaimp-xml-child 'finalName build-elt)) - "." packaging))) + :artifact (let ((packaging (or (javaimp-xml-first-child + (javaimp-xml-child 'packaging elt)) + "jar"))) + ;; only jar/war supported + (when (member packaging '("jar" "war")) + (file-name-concat + build-dir + (concat (javaimp-xml-first-child + (javaimp-xml-child 'finalName build-elt)) + "." packaging)))) :source-dirs (list (file-name-as-directory (javaimp-cygpath-convert-file-name (javaimp-xml-first-child @@ -131,10 +137,11 @@ resulting module trees." (javaimp-cygpath-convert-file-name (javaimp-xml-first-child (javaimp-xml-child 'testSourceDirectory build-elt))))) - :build-dir (file-name-as-directory - (javaimp-cygpath-convert-file-name - (javaimp-xml-first-child (javaimp-xml-child 'directory build-elt)))) - :dep-jars nil ; dep-jars is initialized lazily on demand + :build-dir build-dir + ;; Because dependency list is retrieved by a separate command, we + ;; initialize it lazily on demand + :dep-jars t + :dep-jars-with-source t :load-ts (current-time) :dep-jars-fetcher #'javaimp-maven--fetch-dep-jars :raw elt))) diff --git a/javaimp-util.el b/javaimp-util.el index 2d1595670c..935c033f4a 100644 --- a/javaimp-util.el +++ b/javaimp-util.el @@ -64,19 +64,30 @@ copying.") id parent-id file file-orig - ;; Artifact final name, may be relative to build dir - final-name - source-dirs build-dir - dep-jars + (artifact + nil + :documentation "Artifact (usually jar) file name.") + source-dirs + build-dir + (dep-jars + nil + :documentation "List of dependency jars. t means that the value is not +initialized - use `dep-jars-fetcher' to initialize.") + (dep-jars-with-source + nil + :documentation "Dependency jars which have source directory available in the +same project. It is an alist with each element of the +form (JAR-FILE . MODULE-ID). A value of t means the alist is not +yet initialized.") load-ts - ;; Function to retrieve DEP-JARS for MODULE, called with two - ;; arguments: MODULE and list of parent IDs. Should return a list - ;; of strings - jar file names. - dep-jars-fetcher - ;; Set later on demand - ident-comp-table - ;; Used only during parsing - raw + (dep-jars-fetcher + nil + :documentation "Function to retrieve DEP-JARS for MODULE, +called with two arguments: MODULE and list of parent IDs. Should +return a list of strings - jar file names.") + (raw + nil + :documentation "Internal, used only during parsing") ) (cl-defstruct javaimp-id @@ -155,26 +166,23 @@ UNWRAP is non-nil, then node contents is returned." (defun javaimp-tree-collect-nodes (contents-pred forest) "Return all nodes' contents for which CONTENTS-PRED returns non-nil." - (apply #'seq-concatenate 'list - (mapcar (lambda (tree) - (delq nil - (javaimp-tree--collect-nodes-1 tree contents-pred))) - forest))) + (delq nil + (seq-mapcat (lambda (tree) + (javaimp-tree--collect-nodes-1 tree contents-pred)) + forest))) (defun javaimp-tree--collect-nodes-1 (tree contents-pred) (when tree (cons (and (funcall contents-pred (javaimp-node-contents tree)) (javaimp-node-contents tree)) - (apply #'seq-concatenate 'list - (mapcar (lambda (child) - (delq nil - (javaimp-tree--collect-nodes-1 child contents-pred))) - (javaimp-node-children tree)))))) + (seq-mapcat (lambda (child) + (javaimp-tree--collect-nodes-1 child contents-pred)) + (javaimp-node-children tree))))) (defun javaimp-tree-map-nodes (function pred forest) - "Recursively applies FUNCTION to each node's contents in FOREST -and returns new tree. FUNCTION should return (t . VALUE) if the + "Recursively apply FUNCTION to each node's contents in FOREST and +return new tree. FUNCTION should return (t . VALUE) if the result for this node should be made a list of the form (VALUE . CHILDREN), or (nil . VALUE) for plain VALUE as the result (in this case children are discarded). The result for each node is diff --git a/javaimp.el b/javaimp.el index d89af41e4e..ca762986a6 100644 --- a/javaimp.el +++ b/javaimp.el @@ -100,14 +100,12 @@ ;; ;; Source parsing ;; -;; `javaimp-parse-current-module': defcustom which determines whether -;; we parse the current module for the list of classes. Parsing is -;; implemented in javaimp-parse.el using `syntax-ppss', generally is -;; simple (we do not try to parse the source completely - just the -;; interesting pieces), but can be time-consuming for large projects -;; (to be improved). Currently, on the author's machine, source for -;; java.util.Collections from JDK 11 (~ 5600 lines and > 1000 -;; "scopes") parses in ~1.5 seconds, which is not that bad... +;; Parsing is implemented in javaimp-parse.el using `syntax-ppss', +;; generally is simple (we do not try to parse the source completely - +;; just the interesting pieces), but can be time-consuming for large +;; projects (to be improved). Currently, on the author's machine, +;; source for java.util.Collections from JDK 11 (~ 5600 lines and > +;; 1000 "scopes") parses in ~1.5 seconds, which is not that bad... ;; ;; `javaimp-show-scopes': command to list all parsed "scopes" (blocks ;; of code in braces) in the current buffer, with support for @@ -171,14 +169,6 @@ becomes \"generated-sources/<plugin_name>\" (note the absence of the leading slash)." :type '(repeat (string :tag "Relative directory"))) -(defcustom javaimp-parse-current-module t - "If non-nil, javaimp will try to parse current module's source -files to determine completion alternatives, in addition to those -from module dependencies. This can be time-consuming, that's why -this defcustom exists, to turn it off if it's annoying (perhaps -in per-directory locals)." - :type 'boolean) - (defcustom javaimp-imenu-use-sub-alists nil "If non-nil, make sub-alist for each containing scope (e.g. a class)." @@ -212,15 +202,13 @@ A handler function takes one argument, a FILE.") (defvar javaimp-project-forest nil "Visited projects") -(defvar javaimp-jar-file-classes-cache nil - "Jar file cache, an alist of (FILE . CACHED-FILE), where FILE is -expanded file name and CACHED-FILE is javaimp-cached-file -struct. Suitable for use with `javaimp--collect-from-file-cached'.") - -(defvar javaimp-source-file-idents-cache nil - "Source file cache, an alist of (FILE . CACHED-FILE), where FILE -is expanded file name and CACHED-FILE is javaimp-cached-file -struct. Suitable for use with `javaimp--collect-from-file-cached'.") +;; These variables are all alists of (FILE . CACHED-FILE), where FILE +;; is expanded file name and CACHED-FILE is `javaimp-cached-file' +;; struct. They're suitable for use with +;; `javaimp--collect-from-file-cached'. +(defvar javaimp--jar-idents-cache nil) +(defvar javaimp--module-idents-cache nil) +(defvar javaimp--source-idents-cache nil) (defconst javaimp--jar-error-header "There were errors when reading some of the dependency files, @@ -249,221 +237,56 @@ https://docs.gradle.org/current/userguide/java_library_plugin.html\ (enum . "en"))) -;; Dependencies +;; Subroutines (defsubst javaimp--get-file-ts (file) (file-attribute-modification-time (file-attributes file))) -(defun javaimp--update-module-maybe (node) - (let ((module (javaimp-node-contents node)) - need-update ids) - ;; check if deps are initialized - (unless (javaimp-module-dep-jars module) - (message "Will load dependencies for %s" (javaimp-module-id module)) - (setq need-update t)) - ;; check if this or any parent build file has changed since we - ;; loaded the module - (let ((tmp node)) - (while tmp - (let ((cur (javaimp-node-contents tmp))) - (when (and (not need-update) - (> (max (if (file-exists-p (javaimp-module-file cur)) - (float-time - (javaimp--get-file-ts (javaimp-module-file cur))) - -1) - (if (file-exists-p (javaimp-module-file-orig cur)) - (float-time - (javaimp--get-file-ts (javaimp-module-file-orig cur))) - -1)) - (float-time (javaimp-module-load-ts module)))) - (message "Will reload dependencies for %s because build file changed" - (javaimp-module-id cur)) - (setq need-update t)) - (push (javaimp-module-id cur) ids)) - (setq tmp (javaimp-node-parent tmp)))) - (when need-update - (setf (javaimp-module-dep-jars module) - (funcall (javaimp-module-dep-jars-fetcher module) module ids)) - (setf (javaimp-module-load-ts module) - (current-time))))) - -(defun javaimp--read-jar-classes (file) - "Read FILE which should be a .jar or a .jmod and return classes -contained in it as a list." - (let ((ext (downcase (file-name-extension file)))) - (unless (member ext '("jar" "jmod")) - (error "Unexpected file name: %s" file)) - (let ((javaimp-output-buf-name nil)) - (javaimp-call-java-program - (symbol-value (intern (format "javaimp-%s-program" ext))) - #'javaimp--read-jar-classes-handler - (if (equal ext "jar") "tf" "list") - ;; On cygwin, "jar/jmod" is a native windows program, so file - ;; path needs to be converted appropriately. - (javaimp-cygpath-convert-file-name file 'windows))))) - -(defun javaimp--read-jar-classes-handler () - "Used by `javaimp--read-jar-classes' to handle jar program -output." - (let (result curr) - (while (re-search-forward - (rx (and bol - (? "classes/") ; prefix output by jmod - (group (+ (any alnum "_/$"))) - ".class" - eol)) - nil t) - (setq curr (match-string 1)) - (unless (or (string-suffix-p "module-info" curr) - (string-suffix-p "package-info" curr) - ;; like Provider$1.class - (string-match-p "\\$[[:digit:]]" curr)) - (push - (string-replace "/" "." - (string-replace "$" "." curr)) - result))) - result)) - -(defun javaimp--collect-from-file-cached (file cache-sym fun) - "Return what FUN returns when invoked on FILE, with cache. Use -CACHE-SYM as a cache, it should be an alist with elements of the -form (FILE . CACHED-FILE). If not found in cache, or the cache -is outdated, then values are read using FUN, which should be a -function of one argument, a FILE. If that function throws an -error, the cache for FILE is cleared. FUN may also be nil, in -which case the symbol t is returned for a cache miss, and cache -not updated." - (condition-case err - (let ((cached-file - (alist-get file (symbol-value cache-sym) nil nil #'string=))) - (when (or (not cached-file) - ;; If the file doesn't exist this will be current - ;; time, and thus condition always true - (> (float-time (javaimp--get-file-ts file)) - (float-time (javaimp-cached-file-read-ts cached-file)))) - (setq cached-file (if fun - (make-javaimp-cached-file - :file file - :read-ts (javaimp--get-file-ts file) - :value (funcall fun file)) - t))) - (if (eq cached-file t) - t - (setf (alist-get file (symbol-value cache-sym) nil 'remove #'string=) - cached-file) - (javaimp-cached-file-value cached-file))) - (t - ;; Clear on any error - (setf (alist-get file (symbol-value cache-sym) nil 'remove #'string=) nil) - (signal (car err) (cdr err))))) - - - -;; Some API functions. They do not expose tree structure, return only -;; modules. - -(defun javaimp-find-module (predicate) - "Returns first module in `javaimp-project-forest' for which -PREDICATE returns non-nil." - (javaimp-tree-find-node predicate javaimp-project-forest t)) - -(defun javaimp-collect-modules (predicate) - "Returns all modules in `javaimp-project-forest' for which -PREDICATE returns non-nil." - (javaimp-tree-collect-nodes predicate javaimp-project-forest)) - -(defun javaimp-map-modules (function) - (javaimp-tree-map-nodes function #'always javaimp-project-forest)) - - -;;; Adding imports - -;;;###autoload -(defun javaimp-add-import (classname) - "Import CLASSNAME in the current buffer and call `javaimp-organize-imports'. -Interactively, provide completion alternatives relevant for this -file, additionally filtering them by matching simple class -name (without package) against `symbol-at-point' (with prefix arg -- don't filter). -The set of relevant classes is collected from the following: - -- If `javaimp-java-home' is set then add JDK classes, see -`javaimp--get-jdk-classes'. - -- If current module can be determined, then add all classes from -its dependencies. - -- If `javaimp-parse-current-module' is non-nil, also add classes in -current module or source tree, see -`javaimp--get-current-source-dirs'." - (interactive - (let* ((module (javaimp--detect-module)) - (classes - (nconc - ;; jdk - (when javaimp-java-home - (javaimp--get-jdk-classes javaimp-java-home)) - ;; Module dependencies. We build the list each time - ;; because jars may change. - (when module - (javaimp--collect-from-files - #'javaimp--read-jar-classes (javaimp-module-dep-jars module) - 'javaimp-jar-file-classes-cache)) - ;; Current module or source tree - (when javaimp-parse-current-module - (mapcar #'javaimp--ident-to-fqcn - (seq-mapcat - (lambda (dir) - (javaimp--collect-from-source-dir - #'javaimp--collect-identifiers dir - 'javaimp-source-file-idents-cache)) - (javaimp--get-current-source-dirs module)))))) - (completion-regexp-list - (and (not current-prefix-arg) - (symbol-at-point) - (list (rx (and symbol-start - (literal (symbol-name (symbol-at-point))) - eol)))))) - (list (completing-read "Import: " classes nil t nil nil - (symbol-name (symbol-at-point)))))) - (javaimp-organize-imports (list (cons classname 'normal)))) - -(defun javaimp--detect-module () - (let* ((file (expand-file-name - (or buffer-file-name - (error "Buffer is not visiting a file!")))) - (node (javaimp-tree-find-node - (lambda (m) - (seq-some (lambda (dir) - (string-prefix-p dir file)) - (javaimp-module-source-dirs m))) - javaimp-project-forest))) - (when node - (javaimp--update-module-maybe node) - (javaimp-node-contents node)))) - -(defun javaimp--get-jdk-classes (java-home) - "If 'jmods' subdirectory exists in JAVA-HOME (Java 9+), read all -.jmod files in it. Else, if 'jre/lib' subdirectory exists in -JAVA-HOME (earlier Java versions), read all .jar files in it." - (let ((dir (file-name-concat java-home "jmods"))) - (if (file-directory-p dir) - (javaimp--collect-from-files - #'javaimp--read-jar-classes (directory-files dir t "\\.jmod\\'") - 'javaimp-jar-file-classes-cache) - (setq dir (file-name-concat java-home "jre" "lib")) - (if (file-directory-p dir) - (javaimp--collect-from-files - #'javaimp--read-jar-classes (directory-files dir t "\\.jar\\'") - 'javaimp-jar-file-classes-cache) - (user-error "Could not load JDK classes"))))) +(defun javaimp--collect-from-file (file cache-sym fun) + "Return what FUN returns when invoked on FILE, with cache. FILE +may be just a filename, or a cons cell where car is filename. +Use CACHE-SYM as a cache, it should be an alist with elements of +the form (FILENAME . CACHED-FILE). If not found in cache, or the +cache is outdated, then values are read using FUN, which should +be a function of one argument, a FILE. If that function throws +an error, the cache for FILENAME is cleared. FUN may also be +nil, in which case the symbol t is returned for a cache miss, and +cache not updated." + (let ((filename (if (consp file) (car file) file))) + (condition-case err + (let ((cached-file + (alist-get filename (symbol-value cache-sym) nil nil #'string=))) + (when (or (not cached-file) + ;; If the file doesn't exist this will be current + ;; time, and thus condition always true + (> (float-time (javaimp--get-file-ts filename)) + (float-time (javaimp-cached-file-read-ts cached-file)))) + (setq cached-file (if fun + (make-javaimp-cached-file + :file filename + :read-ts (javaimp--get-file-ts filename) + :value (funcall fun file)) + t))) + (if (eq cached-file t) + t + (setf (alist-get filename (symbol-value cache-sym) nil 'remove #'string=) + cached-file) + (javaimp-cached-file-value cached-file))) + (t + ;; Clear on any error + (setf (alist-get filename (symbol-value cache-sym) nil 'remove #'string=) nil) + (signal (car err) (cdr err)))))) (defun javaimp--collect-from-files (fun files cache-sym) + "Collect values for FILES in a flat list. Each element in FILES +should be a file name, or a cons where car is a file name. FUN +and CACHE-SYM are passed to `javaimp--collect-from-file', which +see." (let (tmp unread res errors) ;; Collect from cache hits (dolist (file files) - (setq tmp (javaimp--collect-from-file-cached file cache-sym nil)) + (setq tmp (javaimp--collect-from-file file cache-sym nil)) (if (eq tmp t) (push file unread) (setq res (nconc res (copy-sequence tmp))))) @@ -473,17 +296,19 @@ JAVA-HOME (earlier Java versions), read all .jar files in it." (format "Reading %d files (%d taken from cache) ..." (length unread) (- (length files) (length unread))) 0 (length unread))) - (i 0)) + (i 0) + filename) (dolist (file unread) - (setq tmp (condition-case err - (javaimp--collect-from-file-cached file cache-sym fun) + (setq filename (if (consp file) (car file) file) + tmp (condition-case err + (javaimp--collect-from-file file cache-sym fun) (t - (push (concat file ": " (error-message-string err)) + (push (concat filename ": " (error-message-string err)) errors) nil))) (setq res (nconc res (copy-sequence tmp))) (setq i (1+ i)) - (progress-reporter-update reporter i file)) + (progress-reporter-update reporter i filename)) (progress-reporter-done reporter))) (when errors (with-output-to-temp-buffer "*Javaimp errors*" @@ -494,32 +319,6 @@ JAVA-HOME (earlier Java versions), read all .jar files in it." (terpri)))) res)) -(defun javaimp--get-current-source-dirs (module) - "Return list of directories where Java sources reside. -If MODULE is non-nil then result is module source dirs and -additional source dirs. Otherwise, try to determine the root of -source tree from 'package' directive in the current buffer. If -there's no such directive, then the last resort is just -`default-directory'." - (if module - (append - (javaimp-module-source-dirs module) - ;; additional source dirs - (mapcar (lambda (dir) - (file-name-as-directory - (file-name-concat (javaimp-module-build-dir module) dir))) - javaimp-additional-source-dirs)) - (list - (if-let ((package (save-excursion - (save-restriction - (widen) - (javaimp-parse-get-package))))) - (string-remove-suffix - (file-name-as-directory - (apply #'file-name-concat (split-string package "\\." t))) - default-directory) - default-directory)))) - (defun javaimp--collect-from-source-dir (fun dir cache-sym) "For each Java source file in DIR, invoke FUN and collect results in a flat list. FUN is given two arguments: a buffer BUF, and @@ -558,16 +357,41 @@ Finally, already parsed buffers are processed in (funcall fun (current-buffer) file))) files cache-sym) ;; Parse unparsed buffers - (let (tmp) - (dolist-with-progress-reporter (buf unparsed-bufs tmp) - (format "Parsing %d buffers..." (length unparsed-bufs)) - (setq tmp (nconc tmp (funcall fun buf nil))))) + (when unparsed-bufs + (let (tmp) + (dolist-with-progress-reporter (buf unparsed-bufs tmp) + (format "Parsing %d buffers..." (length unparsed-bufs)) + (setq tmp (nconc tmp (funcall fun buf nil)))))) ;; Read parsed buffers - usually will be quick - (with-delayed-message - (1 (format "Reading %d buffers..." (length parsed-bufs))) - (seq-mapcat (lambda (buf) - (funcall fun buf nil)) - parsed-bufs)))))) + (when parsed-bufs + (with-delayed-message + (1 (format "Reading %d buffers..." (length parsed-bufs))) + (seq-mapcat (lambda (buf) + (funcall fun buf nil)) + parsed-bufs))))))) + + +(defun javaimp--get-current-source-dir () + "Try to determine current root source directory from 'package' +directive in the current buffer. If there's no such directive, +then just return `default-directory'." + (if-let ((package (save-excursion + (save-restriction + (widen) + (javaimp-parse-get-package))))) + (string-remove-suffix + (file-name-as-directory + (apply #'file-name-concat (split-string package "\\." t))) + default-directory) + default-directory)) + + + +;; Subroutines for identifiers + +(defun javaimp--read-dir-source-idents (dir) + (javaimp--collect-from-source-dir + #'javaimp--collect-identifiers dir 'javaimp--source-idents-cache)) (defun javaimp--collect-identifiers (buf file) "Return all identifiers in buffer BUF, which is temporary if FILE @@ -605,6 +429,241 @@ is non-nil. Suitable for use with ident)) ".")) + +(defun javaimp--read-jar-classes (file) + "Read FILE which should be a .jar or a .jmod and return classes +contained in it as a list." + (let ((ext (downcase (file-name-extension file)))) + (unless (member ext '("jar" "jmod")) + (error "Unexpected file name: %s" file)) + (let ((javaimp-output-buf-name nil)) + (javaimp-call-java-program + (symbol-value (intern (format "javaimp-%s-program" ext))) + #'javaimp--read-jar-classes-handler + (if (equal ext "jar") "tf" "list") + ;; On cygwin, "jar/jmod" is a native windows program, so file + ;; path needs to be converted appropriately. + (javaimp-cygpath-convert-file-name file 'windows))))) + +(defun javaimp--read-jar-classes-handler () + "Used by `javaimp--read-jar-classes' to handle jar program +output." + (let (result curr) + (while (re-search-forward + (rx (and bol + (? "classes/") ; prefix output by jmod + (group (+ (any alnum "_/$"))) + ".class" + eol)) + nil t) + (setq curr (match-string 1)) + (unless (or (string-suffix-p "module-info" curr) + (string-suffix-p "package-info" curr) + ;; like Provider$1.class + (string-match-p "\\$[[:digit:]]" curr)) + (push + (string-replace "/" "." + (string-replace "$" "." curr)) + result))) + result)) + + + +;; Subroutines for working with modules + +(defun javaimp--detect-module () + (let* ((file (expand-file-name + (or buffer-file-name + (error "Buffer is not visiting a file!")))) + (node (javaimp-tree-find-node + (lambda (m) + (seq-some (lambda (dir) + (string-prefix-p dir file)) + (javaimp-module-source-dirs m))) + javaimp-project-forest))) + (when node + (javaimp--update-module-maybe node) + (javaimp-node-contents node)))) + +(defun javaimp--update-module-maybe (node) + (let ((module (javaimp-node-contents node)) + need-update ids) + ;; Check if deps are initialized + (when (eq (javaimp-module-dep-jars module) t) + (message "Will load dependencies for %s" (javaimp-module-id module)) + (setq need-update t)) + ;; Check if this or any parent build file has changed since we + ;; loaded the module + (let ((tmp node)) + (while tmp + (let ((cur (javaimp-node-contents tmp))) + (when (and (not need-update) + (> (max (if (file-exists-p (javaimp-module-file cur)) + (float-time + (javaimp--get-file-ts (javaimp-module-file cur))) + -1) + (if (file-exists-p (javaimp-module-file-orig cur)) + (float-time + (javaimp--get-file-ts (javaimp-module-file-orig cur))) + -1)) + (float-time (javaimp-module-load-ts module)))) + (message "Will reload dependencies for %s because build file changed" + (javaimp-module-id cur)) + (setq need-update t)) + (push (javaimp-module-id cur) ids)) + (setq tmp (javaimp-node-parent tmp)))) + (when need-update + (setf (javaimp-module-dep-jars module) + (funcall (javaimp-module-dep-jars-fetcher module) module ids)) + (setf (javaimp-module-load-ts module) + (current-time))) + (when (or need-update + (eq (javaimp-module-dep-jars-with-source module) t)) + ;; Find out which of dep jars are also available as sources in + ;; the current project + (let* ((all (mapcar (lambda (m) + (cons (javaimp-module-artifact m) (javaimp-module-id m))) + (javaimp-collect-modules + (lambda (m) + (not (string-empty-p (javaimp-module-artifact m))))))) + ;; FIXME we need elements from all as the result, is it + ;; reliable to just put all first? + (matches (seq-intersection + all + (javaimp-module-dep-jars module) + (lambda (el1 el2) + (string= (if (consp el1) (car el1) el1) + (if (consp el2) (car el2) el2)))))) + (setf (javaimp-module-dep-jars-with-source module) matches))))) + +(defun javaimp--collect-module-dep-jars-classes (module) + "Return list of classes from MODULE's jar dependencies. We +build the list each time because jars may change." + (let ((dep-jars-no-source + (seq-difference + (javaimp-module-dep-jars module) + (javaimp-module-dep-jars-with-source module) + (lambda (el1 el2) + (string= (if (consp el1) (car el1) el1) + (if (consp el2) (car el2) el2)))))) + (javaimp--collect-from-files + #'javaimp--read-jar-classes + dep-jars-no-source + 'javaimp--jar-idents-cache))) + +(defun javaimp--collect-module-dep-jars-with-source-idents (module) + "Return list of identifiers from MODULE's dependencies for which +we know where the source is. The list is cached by _artifact +file_, so cache is refreshed only when artifact is rebuilt." + (javaimp--collect-from-files + (lambda (artifact-and-id) + (let* ((mod-id (cdr artifact-and-id)) + (mod (javaimp-find-module + (lambda (m) + (equal (javaimp-module-id m) mod-id))))) + (if mod + (javaimp--read-module-source-idents mod) + (error "Could not find module %s! Please re-visit its \ +top-level project." (javaimp-print-id mod-id))))) + (javaimp-module-dep-jars-with-source module) + 'javaimp--module-idents-cache)) + +(defun javaimp--read-module-source-idents (module) + (let ((source-dirs + (append + (javaimp-module-source-dirs module) + (mapcar (lambda (dir) + (file-name-as-directory + (file-name-concat (javaimp-module-build-dir module) dir))) + javaimp-additional-source-dirs)))) + (seq-mapcat #'javaimp--read-dir-source-idents + source-dirs))) + + + +;; Some API functions. They do not expose tree structure, return only +;; modules. + +(defun javaimp-find-module (predicate) + "Return first module in `javaimp-project-forest' for which +PREDICATE returns non-nil." + (javaimp-tree-find-node predicate javaimp-project-forest t)) + +(defun javaimp-collect-modules (predicate) + "Return all modules in `javaimp-project-forest' for which +PREDICATE returns non-nil." + (javaimp-tree-collect-nodes predicate javaimp-project-forest)) + +(defun javaimp-map-modules (function) + (javaimp-tree-map-nodes function #'always javaimp-project-forest)) + + +;;; Adding imports + +;;;###autoload +(defun javaimp-add-import (classname) + "Import CLASSNAME in the current buffer and call `javaimp-organize-imports'. +Interactively, provide completion alternatives relevant for this +file, additionally filtering them by matching simple class +name (without package) against `symbol-at-point' (with prefix arg +- don't filter). + +The set of relevant classes is collected from the following: + +- If `javaimp-java-home' is set then add JDK classes, see +`javaimp--get-jdk-classes'. + +- If current module can be determined, then add all classes from +its jar dependencies, as well as its source dependencies. + +- Add classes in current module (if any) or source tree (see +`javaimp--get-current-source-dir')." + (interactive + (let* ((module (javaimp--detect-module)) + (classes + (nconc + ;; jdk + (when javaimp-java-home + (javaimp--get-jdk-classes javaimp-java-home)) + (when module + (nconc + (javaimp--collect-module-dep-jars-classes module) + (mapcar #'javaimp--ident-to-fqcn + (javaimp--collect-module-dep-jars-with-source-idents + module)))) + ;; Current module or source tree + (mapcar #'javaimp--ident-to-fqcn + (if module + (javaimp--read-module-source-idents module) + (javaimp--read-dir-source-idents + (javaimp--get-current-source-dir)))))) + (completion-regexp-list + (and (not current-prefix-arg) + (symbol-at-point) + (list (rx (and symbol-start + (literal (symbol-name (symbol-at-point))) + eol)))))) + (list (completing-read "Import: " classes nil t nil nil + (symbol-name (symbol-at-point)))))) + (javaimp-organize-imports (list (cons classname 'normal)))) + +(defun javaimp--get-jdk-classes (java-home) + "If 'jmods' subdirectory exists in JAVA-HOME (Java 9+), read all +.jmod files in it. Else, if 'jre/lib' subdirectory exists in +JAVA-HOME (earlier Java versions), read all .jar files in it." + (let ((dir (file-name-concat java-home "jmods"))) + (if (file-directory-p dir) + (javaimp--collect-from-files + #'javaimp--read-jar-classes (directory-files dir t "\\.jmod\\'") + 'javaimp--jar-idents-cache) + (setq dir (file-name-concat java-home "jre" "lib")) + (if (file-directory-p dir) + (javaimp--collect-from-files + #'javaimp--read-jar-classes (directory-files dir t "\\.jar\\'") + 'javaimp--jar-idents-cache) + (user-error "Could not load JDK classes"))))) + + ;; Organizing imports @@ -789,15 +848,12 @@ in a major mode hook." (defun javaimp-xref--backend () 'javaimp) (defun javaimp-xref--module-completion-table () - (when-let ((mod (javaimp--detect-module))) - (setf (javaimp-module-ident-comp-table mod) - (or (javaimp-module-ident-comp-table mod) - (seq-mapcat - (lambda (dir) - (javaimp--collect-from-source-dir - #'javaimp--collect-identifiers dir - 'javaimp-source-file-idents-cache)) - (javaimp--get-current-source-dirs mod)))))) + (if-let ((module (javaimp--detect-module))) + (nconc + (javaimp--collect-module-dep-jars-with-source-idents module) + (javaimp--read-module-source-idents module)) + (javaimp--read-dir-source-idents + (javaimp--get-current-source-dir)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql 'javaimp))) (javaimp-xref--module-completion-table)) @@ -1165,8 +1221,9 @@ any module's source file." (defun javaimp-flush-cache () "Flush all caches." (interactive) - (setq javaimp-jar-file-classes-cache nil - javaimp-source-file-idents-cache nil)) + (setq javaimp--jar-idents-cache nil + javaimp--module-idents-cache nil + javaimp--source-idents-cache nil)) (provide 'javaimp)