emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/javaimp 25f5e3f90b: Improve xref support for classes


From: Filipp Gunbin
Subject: [elpa] externals/javaimp 25f5e3f90b: Improve xref support for classes
Date: Mon, 9 May 2022 12:28:21 -0400 (EDT)

branch: externals/javaimp
commit 25f5e3f90b8913819571384b7ff27c41ad92690a
Author: Filipp Gunbin <fgunbin@fastmail.fm>
Commit: Filipp Gunbin <fgunbin@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)
 



reply via email to

[Prev in Thread] Current Thread [Next in Thread]