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

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

[elpa] master f213965: packages/javaimp: full rewrite


From: Filipp Gunbin
Subject: [elpa] master f213965: packages/javaimp: full rewrite
Date: Mon, 11 Apr 2016 19:24:33 +0000

branch: master
commit f213965b93b982d406404529503ed3346c983dac
Author: Filipp Gunbin <address@hidden>
Commit: Filipp Gunbin <address@hidden>

    packages/javaimp: full rewrite
    
    * Use defstruct's from cl-lib.
    * Parse pom.xml structure into tree for correct updates.
    * Clean up docs.
    * Simplify code.
---
 packages/javaimp/javaimp.el | 1200 +++++++++++++++++++++----------------------
 1 files changed, 597 insertions(+), 603 deletions(-)

diff --git a/packages/javaimp/javaimp.el b/packages/javaimp/javaimp.el
index df402ce..a32a9a0 100644
--- a/packages/javaimp/javaimp.el
+++ b/packages/javaimp/javaimp.el
@@ -1,79 +1,56 @@
 ;;; javaimp.el --- Add and reorder Java import statements in Maven projects  
-*- lexical-binding: t; -*-
 
-;; Copyright (C) 2014, 2015  Free Software Foundation, Inc.
+;; Copyright (C) 2014, 2015, 2016  Free Software Foundation, Inc.
 
 ;; Author: Filipp Gunbin <address@hidden>
 ;; Maintainer: Filipp Gunbin <address@hidden>
-;; Version: 0.6
+;; Version: 0.7
 ;; Keywords: java, maven, programming
 
 ;;; Commentary:
 
 ;; Allows to manage Java import statements in Maven projects.
 ;;
-;;   Quick start: customize `javaimp-import-group-alist', `javaimp-jdk-home'
-;; and call `javaimp-maven-visit-root', then in a Java buffer visiting a
-;; file under that module or one of its submodules call
-;; `javaimp-organize-imports' or `javaimp-add-import'.  `javaimp-add-import'
-;; will provide you a helpful completion, and the default value (the one
-;; you'll get if you hit `M-n' in the minibuffer) is the symbol under point,
-;; so usually it's enough to hit `M-n', then add some starting letters of a
-;; package and hit `TAB'.  The module does not add all needed imports
-;; automatically!  It only helps you to quickly add imports when stepping
-;; through compilation errors.
+;;   Quick start:
 ;;
+;; - customize `javaimp-import-group-alist'
+;;
+;; - call `javaimp-maven-visit-project', giving it the top-level project
+;; directory where pom.xml resides
+;;
+;; Then in a Java buffer visiting a file under that project or one of its
+;; submodules call `javaimp-organize-imports' or `javaimp-add-import'.
+;;
+;; This module does not add all needed imports automatically!  It only helps
+;; you to quickly add imports when stepping through compilation errors.
+;;
+;;
+;;   Some details:
+;; 
 ;; If Maven failed, you can see its output in the buffer named by
 ;; `javaimp-debug-buf-name' (default is "*javaimp-debug*").
 ;;
 ;; Contents of jar files and Maven project structures (pom.xml) are cached,
 ;; so usually only first command should take a considerable amount of time
-;; to complete.  When it is detected that a particular jar or pom.xml file's
-;; timestamp changed, it is re-read and cache is updated.
+;; to complete.  If a modules's pom.xml or any of its parents' pom.xml was
+;; changed (i.e. any of them was modified after information was loaded),
+;; `mvn dependency:build-classpath' is re-run on the current module.  If a
+;; jar file was changed, its contents are re-read.
 ;;
-;; Details on variables.
-;; 
-;;   `javaimp-import-group-alist' defines the order of import statement
-;; groups.  By default java.* and javax.* imports are assigned an order of
-;; 10, which is low, so it puts those imports at the beginning.  Your
-;; project's imports typically should come after, so the sample config below
-;; sets 80 for them.
-;; 
-;;  `javaimp-jdk-home' is a path for JDK.  It is used to scan JDK jars.
-;; Usually you will need to set this.
+;; If you make some changes which change project hierarchy, you should
+;; re-visit the parent again with `javaimp-maven-visit-project'.
 ;;
-;;  `javaimp-additional-source-dirs' is a list specifying directories where
-;; additional (e.g. generated) source files reside.  Each directory is a
-;; relative path from ${project.build.directory} project property value.
+;; Currently inner classes are filtered out from completion alternatives.
+;; You can always import top-level class and use qualified name.
 ;; 
-;;  `javaimp-mvn-program' defines path of the `mvn' program.  Use if it's
-;; not on `exec-path'.
-;;
-;;  `javaimp-cygpath-program' defines path of the `cygpath' program (applies
-;; to Cygwin only, of course).  Use if it's not on `exec-path'.
-;;
-;;  `javaimp-jar-program' defines path of the `jar' program.  Use if it's
-;; not on `exec-path'.
-;;  
-;; Details on commands.
 ;;
-;;   `javaimp-maven-visit-root' is the first command you should issue to
-;; use this module.  It reads the pom structure recursively and records
-;; which files belong to which module.  Maven help:effective-pom command is
-;; used to do that.
-;;
-;;   `javaimp-organize-imports' groups import statement and writes those
-;; group according to the value of `javaimp-import-group-alist'.  Imports
-;; which are not matched by any regexp in that variable are assigned a
-;; default order defined by `javaimp-import-default-order' (50 by default).
-;;
-;; Sample setup (put this into your .emacs):
+;;   Example of initialization:
 ;; 
 ;; (require 'javaimp)
 ;; 
-;; (add-to-list 'javaimp-import-group-alist 
'("\\`\\(ru\\.yota\\.\\|tv\\.okko\\.\\)" . 80))
+;; (add-to-list 'javaimp-import-group-alist
+;;   '("\\`\\(my\\.company\\.\\|my\\.company2\\.\\)" . 80))
 ;; 
-;; (setq javaimp-jdk-home (getenv "JAVA_HOME"))
-;; (setq javaimp-include-current-project-classes t)
 ;; (setq javaimp-additional-source-dirs '("generated-sources/thrift"))
 ;; 
 ;; (add-hook 'java-mode-hook
@@ -83,23 +60,28 @@
 ;; 
 ;; 
 ;; TODO:
-;; 
-;; Support adding static imports by giving a prefix argument to
-;; `javaimp-add-import'.
 ;;  
-;; Use functions `cygwin-convert-file-name-from-windows' and
+;; - use functions `cygwin-convert-file-name-from-windows' and
 ;; `cygwin-convert-file-name-to-windows' when they are available instead of
-;; calling `cygpath'.  See
-;; https://cygwin.com/ml/cygwin/2013-03/msg00228.html.
-
+;; calling `cygpath'.  See https://cygwin.com/ml/cygwin/2013-03/msg00228.html.
+;;
+;; - save/restore state
+;;
+;; - `javaimp-add-import': without prefix arg narrow alternatives by local 
name;
+;; with prefix arg include all classes in alternatives
+;; 
 
 ;;; Code:
 
+(require 'cl-lib)
+(require 'seq)
+(require 'xml)
+
 
-;;; User options
+;; User options
 
 (defgroup javaimp ()
-  "Add and reorder Java import statements in Maven projects.")
+  "Add and reorder Java import statements in Maven projects")
 
 (defcustom javaimp-import-group-alist '(("\\`javax?\\." . 10))
   "Specifies how to group classes and how to order resulting
@@ -107,7 +89,7 @@ groups in the imports list.
 
 Each element should be of the form `(CLASSNAME-REGEXP . ORDER)'
 where `CLASSNAME-REGEXP' is a regexp matching the fully qualified
-class name.
+class name.  Lowest-order groups are placed earlier.
 
 The order of classes which were not matched is defined by
 `javaimp-import-default-order'.")
@@ -116,8 +98,9 @@ The order of classes which were not matched is defined by
   "Defines the order of classes which were not matched by
 `javaimp-import-group-alist'")
 
-(defcustom javaimp-jdk-home nil
-  "Path to the JDK")
+(defcustom javaimp-jdk-home (getenv "JAVA_HOME")
+  "Path to the JDK.  It is used to find JDK jars to scan.  By
+default, it is set from the JAVA_HOME environment variable.")
 
 (defcustom javaimp-additional-source-dirs nil
   "List of directories where additional (e.g. generated)
@@ -137,607 +120,618 @@ Custom values set in plugin configuration in pom.xml 
are not
 supported yet.")
 
 (defcustom javaimp-mvn-program "mvn"
-  "Path to the `mvn' program")
+  "Path to the `mvn' program.  Customize it if the program is not
+on `exec-path'.")
 
-(defcustom javaimp-cygpath-program "cygpath"
-  "Path to the `cygpath' program")
+(defcustom javaimp-cygpath-program
+  (if (eq system-type 'cygwin) "cygpath")
+  "Path to the `cygpath' program (Cygwin only).  Customize it if
+the program is not on `exec-path'.")
 
 (defcustom javaimp-jar-program "jar"
-  "Path to the `jar' program")
-
-(defcustom javaimp-include-current-project-classes t
-  "If non-nil, current project's classes are included into completion
-alternatives.
+  "Path to the `jar' program used to read contents of jar files.
+Customize it if the program is not on `exec-path'.")
 
-Only top-level classes are included.")
+(defcustom javaimp-include-current-module-classes t
+  "If non-nil, current module's classes are included into
+completion alternatives.  `javaimp-add-import' will find all java
+files in the current project and add their fully-qualified names
+to the completion alternatives list.")
 
 
-;;; Variables and constants
+;; Variables and constants
 
-(defvar javaimp-maven-root-modules nil
-  "Loaded root Maven modules")
+(defvar javaimp-project-forest nil
+  "Visited projects.")
 
-(defvar javaimp-jar-classes-cache nil
-  "Jar classes cache")
+(defvar javaimp-cached-jars nil
+  "Alist of cached jars.  Each element is of the form (FILE
+  . CACHED-JAR).")
 
 (defconst javaimp-debug-buf-name "*javaimp-debug*")
 
-
-;;; Dealing with XML
-
-(defun javaimp-xml-child-list (xml-tree child-name)
-  "Returns list of children of XML-TREE filtered by CHILD-NAME"
-  (let (result)
-    (dolist (child (cddr xml-tree) result)
-      (when (and (listp child)
-                (eq (car child) child-name))
-       (push child result)))))
-
-(defun javaimp-xml-child (name el)
-  "Returns a child of EL named by symbol NAME"
-  (assq name (cddr el)))
-
-(defun javaimp-xml-first-child (el)
-  "Returns a first child of EL"
-  (car (cddr el)))
-
-
-;; A module is represented as a list of the form `(ARTIFACT POM-FILE
-;; SOURCE-DIR TEST-SOURCE-DIR BUILD-DIR POM-FILE-MOD-TS PARENT PARENT-TS)'.
-
-(defsubst javaimp-make-mod (artifact pom-file source-dir
-                                    test-source-dir build-dir
-                                    pom-file-mod-ts jars-list
-                                    parent parent-ts)
-  (list artifact pom-file source-dir test-source-dir build-dir
-       pom-file-mod-ts jars-list parent parent-ts))
-
-(defsubst javaimp-get-mod-artifact (module)
-  (nth 0 module))
-
-(defsubst javaimp-get-mod-pom-file (module)
-  (nth 1 module))
-
-(defsubst javaimp-get-mod-source-dir (module)
-  (nth 2 module))
-
-(defsubst javaimp-get-mod-test-source-dir (module)
-  (nth 3 module))
-
-(defsubst javaimp-get-mod-build-dir (module)
-  (nth 4 module))
-
-(defsubst javaimp-get-mod-pom-mod-ts (module)
-  (nth 5 module))
-(defsubst javaimp-set-mod-pom-mod-ts (module value)
-  (setcar (nthcdr 5 module) value))
-
-(defsubst javaimp-get-mod-pom-deps (module)
-  (nth 6 module))
-(defsubst javaimp-set-mod-pom-deps (module value)
-  (setcar (nthcdr 6 module) value))
-
-(defsubst javaimp-get-mod-parent (module)
-  (nth 7 module))
-(defsubst javaimp-set-mod-parent (module value)
-  (setcar (nthcdr 7 module) value))
-
-(defsubst javaimp-get-mod-parent-ts (module)
-  (nth 8 module))
-(defsubst javaimp-set-mod-parent-ts (module value)
-  (setcar (nthcdr 8 module) value))
-
-
-;; An artifact is represented as a list: (GROUP-ID ARTIFACT-ID VERSION).
-
-;; FIXME: use cl-defstruct!
+;; Structs
 
-(defun javaimp-make-artifact (group-id artifact-id version)
-  (list group-id artifact-id version))
+(cl-defstruct javaimp-node
+  parent children contents)
 
-(defun javaimp-artifact-group-id (artifact)
-  (car artifact))
+(cl-defstruct javaimp-module
+  id parent-id
+  file
+  final-name
+  packaging
+  source-dir test-source-dir build-dir
+  modules
+  dep-jars
+  load-ts)
 
-(defun javaimp-artifact-artifact-id (artifact)
-  (cadr artifact))
-
-(defun javaimp-artifact-version (artifact)
-  (nth 2 artifact))
-
-(defun javaimp-artifact-to-string (artifact)
-  (format "%s:%s:%s"
-         (javaimp-artifact-artifact-id artifact)
-         (javaimp-artifact-group-id artifact)
-         (javaimp-artifact-version artifact))) ;FIXME: `artifact' is not a 
function!
-
-(defun javaimp-parse-artifact (artifact)
-  (apply #'javaimp-make-artifact (split-string artifact ":")))
+(cl-defstruct javaimp-id
+  group artifact version)
 
+(cl-defstruct javaimp-cached-jar
+  file read-ts classes)
 
 
-;; A jar is represented as follows: `(JAR-PATH JAR-MOD-TS . CLASSES-LIST).
+;; Utilities
 
-(defsubst javaimp-make-jar (jar-path jar-mod-ts classes-list)
-  (cons jar-path (cons jar-mod-ts classes-list)))
-
-(defsubst javaimp-get-jar-path (jar)
-  (car jar))
+(defun javaimp--xml-children (xml-tree child-name)
+  "Returns list of children of XML-TREE filtered by CHILD-NAME"
+  (seq-filter (lambda (child)
+               (and (consp child)
+                    (eq (car child) child-name)))
+             (cddr xml-tree)))
 
-(defsubst javaimp-get-jar-mod-ts (jar)
-  (cadr jar))
+(defun javaimp--xml-child (name el)
+  "Returns a child of EL named by symbol NAME"
+  (assq name (cddr el)))
 
-(defsubst javaimp-set-jar-mod-ts (jar value)
-  (setcar (cdr jar) value))
+(defun javaimp--xml-first-child (el)
+  "Returns a first child of EL"
+  (car (cddr el)))
 
-(defsubst javaimp-get-jar-classes-list (jar)
-  (cddr jar))
+(defun javaimp--get-file-ts (file)
+  (nth 5 (file-attributes file)))
 
-(defsubst javaimp-set-jar-classes-list (jar value)
-  (setcdr (cdr jar) value))
+(defun javaimp--get-jdk-jars ()
+  (if javaimp-jdk-home
+      (let ((jre-lib-dir
+            (concat (file-name-as-directory javaimp-jdk-home)
+                    (file-name-as-directory "jre")
+                    (file-name-as-directory "lib"))))
+       (directory-files jre-lib-dir t "\\.jar\\'"))))
+
+(defun javaimp-cygpath-convert-maybe (path &optional mode is-really-path)
+  "On Cygwin, converts PATH using cygpath according to MODE and
+IS-REALLY-PATH.  If MODE is `unix' (the default), adds -u switch.
+If MODE is `windows', adds -m switch.  If `is-really-path' is
+non-nil, adds `-p' switch.  On other systems, PATH is returned
+unchanged."
+  (if (eq system-type 'cygwin)
+      (progn
+       (unless mode (setq mode 'unix))
+       (let (args)
+         (push (cond ((eq mode 'unix) "-u")
+                     ((eq mode 'windows) "-m")
+                     (t (error "Invalid mode: %s" mode)))
+               args)
+         (and is-really-path (push "-p" args))
+         (push path args)
+         (car (apply #'process-lines javaimp-cygpath-program args))))
+    path))
 
 
-;;; Loading maven projects tree
+;; Project loading
 
 ;;;###autoload
-(defun javaimp-maven-visit-root (path)
-  "Loads all modules starting from root module identified by
-PATH.  PATH should point to a directory."
-  (interactive "DVisit maven root project: ")
-  (let ((root-pom (expand-file-name
-                  (concat (file-name-as-directory path) "pom.xml")))
-       modules existing-module)
-    (unless (file-readable-p root-pom)
-      (error "Cannot read root pom: %s" root-pom))
-    (setq modules (javaimp-maven-load-module-tree root-pom))
-    ;; if a root module with such path is already loaded, replace its
-    ;; modules
-    (setq existing-module (assoc root-pom javaimp-maven-root-modules))
-    (if existing-module
-       (setcdr existing-module modules)
-      (push (cons root-pom modules) javaimp-maven-root-modules))
-    (message "Loaded modules for %s" path)))
-
-(defun javaimp-get-projects (xml-tree)
-  (cond ((assq 'projects xml-tree)
-        (javaimp-xml-child-list (assq 'projects xml-tree) 'project))
-       ((assq 'project xml-tree)
-        (list (assq 'project xml-tree)))
-       (t
-        (error "Cannot find projects in mvn output"))))
+(defun javaimp-maven-visit-project (path)
+  "Loads a project and its submodules.  PATH should point to a
+directory containing pom.xml.
+
+Calls `mvn help:effective-pom' on the pom.xml in the PATH, reads
+project structure from the output and records which files belong
+to which modules and other module information.
+
+After being processed by this command, the module tree becomes
+known to javaimp and `javaimp-add-import' maybe called inside any
+module file."
+  (interactive "DVisit maven project: ")
+  (let ((file (expand-file-name
+              (concat (file-name-as-directory path) "pom.xml"))))
+    (unless (file-readable-p file)
+      (error "Cannot read file: %s" file))
+    ;; delete previous loaded tree, if any
+    (setq javaimp-project-forest
+         (seq-remove (lambda (tree)
+                       (equal (javaimp-module-file (javaimp-node-contents 
tree))
+                              file))
+                     javaimp-project-forest))
+    (let ((tree (javaimp--maven-xml-load-tree file)))
+      (if tree
+         (push tree javaimp-project-forest)))
+    (message "Loaded tree for %s" file)))
 
-(defun javaimp-maven-load-module-tree (pom)
-  "Returns an alist of all Maven modules in a hierarchy starting
-with POM"
+
+;; Maven XML routines 
+
+(defun javaimp--maven-xml-load-tree (file)
+  "Invokes `mvn help:effective-pom' on FILE and using its output
+creates a tree of Maven projects starting from FILE.  Children
+which link to the parent via the <parent> element are inheriting
+children and are also included.  Subordinate modules with no
+inheritance are not included."
+  (let ((xml-tree (javaimp--maven-xml-read-effective-pom file)))
+    (cond ((assq 'project xml-tree)
+          (let ((project-elt (assq 'project xml-tree))
+                (submodules (javaimp--xml-children
+                             (javaimp--xml-child 'modules project-elt) 
'module)))
+            (and submodules
+                 ;; no real children
+                 (message "Independent submodules: %s"
+                          (mapconcat #'javaimp--xml-first-child submodules ", 
")))
+            (let ((module (javaimp--maven-xml-parse-module project-elt)))
+              (javaimp--maven-build-tree
+               (javaimp-module-id module) nil (list module) file))))
+         ((assq 'projects xml-tree)
+          ;; we have are inheriting children - they and their children, if
+          ;; any, are listed in a linear list
+          (let* ((project-elts (javaimp--xml-children
+                                (assq 'projects xml-tree) 'project))
+                 (all-modules (mapcar #'javaimp--maven-xml-parse-module 
project-elts)))
+            (message "Total modules: %d" (length all-modules))
+            (javaimp--maven-build-tree
+             (javaimp-module-id (car all-modules)) nil all-modules file)))
+         (t
+          ;; neither <project> nor <projects> - error
+          (error "Invalid `help:effective-pom' output")))))
+
+(defun javaimp--maven-xml-read-effective-pom (pom)
+  "Calls `mvn help:effective:pom and returns XML parse tree"
   (message "Loading root pom %s..." pom)
-  (javaimp-call-mvn
+  (javaimp--maven-call
    pom "help:effective-pom"
    (lambda ()
-     (let (xml-start-pos xml-end-pos)
-       ;; find where we should start parsing XML
-       (goto-char (point-min))
-       (re-search-forward "<\\?xml\\|<projects?")
-       (setq xml-start-pos (match-beginning 0))
-       ;; determine the start tag
-       (goto-char (point-min))
-       (re-search-forward "<\\(projects?\\)")
-       ;; find closing tag which is also the end of the region to parse
-       (search-forward (concat "</" (match-string 1) ">"))
-       (setq xml-end-pos (match-end 0))
-       ;; parse
-       (let ((artifact-pomfile-alist
-             (javaimp-build-artifact-pomfile-alist (list pom)))
-            (children (javaimp-get-projects
-                       (xml-parse-region xml-start-pos xml-end-pos))))
-        (javaimp-maven-build-children children artifact-pomfile-alist))))))
-
-(defun javaimp-make-artifact-from-xml (node)
-  (javaimp-make-artifact
-   (javaimp-xml-first-child (javaimp-xml-child 'groupId node))
-   (javaimp-xml-first-child (javaimp-xml-child 'artifactId node))
-   (javaimp-xml-first-child (javaimp-xml-child 'version node))))
-
-(defun javaimp-get-pom-file-path-lax (artifact artifact-pomfile-alist)
-  (assoc-default
-   artifact artifact-pomfile-alist
-   (lambda (tested target)
-     (or (equal target tested)
-        (equal (javaimp-artifact-artifact-id target)
-               (javaimp-artifact-artifact-id tested))))))
-
-(defun javaimp-maven-build-children (projects artifact-pomfile-alist)
-  (let (result)
-    (dolist (proj projects result)
-      (let* ((artifact (javaimp-make-artifact-from-xml proj))
-            (pom-file-path (javaimp-get-pom-file-path-lax
-                            artifact artifact-pomfile-alist))
-            (build (javaimp-xml-child 'build proj))
-            (source-dir (javaimp-xml-first-child
-                         (javaimp-xml-child 'sourceDirectory build))) 
-            (test-source-dir (javaimp-xml-first-child
-                              (javaimp-xml-child 'testSourceDirectory
-                                                 build)))
-            (build-dir (javaimp-xml-first-child
-                        (javaimp-xml-child 'directory build)))
-            (parent (javaimp-make-artifact-from-xml
-                     (javaimp-xml-child 'parent proj))))
-       (push (javaimp-make-mod 
-              artifact
-              pom-file-path
-              (file-name-as-directory
-               (if (eq system-type 'cygwin) 
-                   (car (process-lines javaimp-cygpath-program "-u"
-                                       source-dir))
-                 source-dir))
-              (file-name-as-directory
-               (if (eq system-type 'cygwin) 
-                   (car (process-lines javaimp-cygpath-program "-u" 
-                                       test-source-dir))
-                 test-source-dir))
-              (file-name-as-directory
-               (if (eq system-type 'cygwin) 
-                   (car (process-lines javaimp-cygpath-program "-u" 
-                                       build-dir))
-                 build-dir))
-              nil nil parent nil)
-             result)))))
-
-(defun javaimp-build-artifact-pomfile-alist (pom-file-list)
-  "Recursively builds an alist where each element is of the
-form (\"ARTIFACT\" . \"POM-FILE-PATH\"). This is needed because
-there is no pom file path in the output of `mvn
-help:effective-pom'.  Each pom file path in POM-FILE-LIST should
-be in platform's default format."
-  (when pom-file-list
-    (let ((pom-file (car pom-file-list))
-         xml-tree project)
-      (message "Saving artifact id -> pom file mapping for %s" pom-file)
-      (with-temp-buffer
-       (insert-file-contents pom-file)
-       (setq xml-tree (xml-parse-region (point-min) (point-max))))
-      (setq project (if (assq 'top xml-tree)
-                       (assq 'project (cddr (assq 'top xml-tree)))
-                     (assq 'project xml-tree)))
-      (cons
-       ;; this pom
-       (cons (javaimp-make-artifact-from-xml project) pom-file)
-       (append
-       ;; submodules
-       (javaimp-build-artifact-pomfile-alist
-        (mapcar (lambda (submodule)
-                  (expand-file-name
-                   (concat
-                    ;; this pom's path
-                    (file-name-directory pom-file)
-                    ;; relative submodule directory
-                    (file-name-as-directory
-                     (let ((submodule-path (car (cddr submodule))))
-                       (if (eq system-type 'cygwin)
-                           (car (process-lines javaimp-cygpath-program "-u" 
-                                               submodule-path))
-                         submodule-path)))
-                    ;; well-known file name
-                    "pom.xml")))
-                (javaimp-xml-child-list (assq 'modules (cddr project)) 
'module)))
-       ;; rest items
-       (javaimp-build-artifact-pomfile-alist (cdr pom-file-list)))))))
-
-(defun javaimp-call-mvn (pom-file target handler)
+     (let ((xml-start-pos
+           (save-excursion
+             (progn
+               (goto-char (point-min))
+               (re-search-forward "<\\?xml\\|<projects?")
+               (match-beginning 0))))
+          (xml-end-pos
+           (save-excursion
+             (progn
+               (goto-char (point-min))
+               (re-search-forward "<\\(projects?\\)")
+               ;; corresponding closing tag is the end of parse region
+               (search-forward (concat "</" (match-string 1) ">"))
+               (match-end 0)))))
+       (xml-parse-region xml-start-pos xml-end-pos)))))
+
+(defun javaimp--maven-xml-parse-module (project-elt)
+  (let ((build-elt (javaimp--xml-child 'build project-elt)))
+    (make-javaimp-module
+     :id (javaimp--maven-xml-extract-id project-elt)
+     :parent-id (javaimp--maven-xml-extract-id (javaimp--xml-child 'parent 
project-elt))
+     ;; we set `file' slot later because raw <project> element does not contain
+     ;; pom file path, so we need to construct it during tree construction
+     :file nil
+     :final-name (javaimp--xml-first-child
+                 (javaimp--xml-child 'finalName build-elt))
+     :packaging (javaimp--xml-first-child
+                (javaimp--xml-child 'packaging project-elt))
+     :source-dir (file-name-as-directory
+                 (javaimp-cygpath-convert-maybe
+                  (javaimp--xml-first-child
+                   (javaimp--xml-child 'sourceDirectory build-elt))))
+     :test-source-dir (file-name-as-directory
+                      (javaimp-cygpath-convert-maybe
+                       (javaimp--xml-first-child
+                        (javaimp--xml-child 'testSourceDirectory build-elt))))
+     :build-dir (file-name-as-directory
+                (javaimp-cygpath-convert-maybe
+                 (javaimp--xml-first-child (javaimp--xml-child 'directory 
build-elt))))
+     :modules (mapcar (lambda (module-elt)
+                       (javaimp--xml-first-child module-elt))
+                     (javaimp--xml-children (javaimp--xml-child 'modules 
project-elt) 'module))
+     :dep-jars nil                   ; dep-jars is initialized lazily on demand
+     :load-ts (current-time))))
+
+(defun javaimp--maven-xml-extract-id (elt)
+  (make-javaimp-id
+   :group (javaimp--xml-first-child (javaimp--xml-child 'groupId elt))
+   :artifact (javaimp--xml-first-child (javaimp--xml-child 'artifactId elt))
+   :version (javaimp--xml-first-child (javaimp--xml-child 'version elt))))
+
+(defun javaimp--maven-xml-file-matches (file id parent-id)
+  (let* ((xml-tree (with-temp-buffer
+                    (insert-file-contents file)
+                    (xml-parse-region (point-min) (point-max))))
+        (project-elt (assq 'project xml-tree))
+        (tested-id (javaimp--maven-xml-extract-id project-elt))
+        (tested-parent-id (javaimp--maven-xml-extract-id (assq 'parent 
project-elt))))
+    ;; seems that the only mandatory component in tested ids is artifact, while
+    ;; group and version may be inherited and thus not presented in pom.xml
+    (let ((test (if (or (null (javaimp-id-group tested-id))
+                       (null (javaimp-id-version tested-id))
+                       (null (javaimp-id-group tested-parent-id))
+                       (null (javaimp-id-version tested-parent-id)))
+                   (progn
+                     (message "File %s contains incomplete id, using lax 
match" file)
+                     (lambda (first second)
+                       (equal (javaimp-id-artifact first) (javaimp-id-artifact 
second))))
+                 #'equal)))
+      (and (funcall test tested-id id)
+          (funcall test tested-parent-id parent-id)))))
+
+
+;; Maven routines
+
+(defun javaimp--maven-call (pom-file target handler)
   "Runs Maven target TARGET on POM-FILE, then calls HANDLER in
 the temporary buffer and returns its result"
   (message "Calling \"mvn %s\" on pom: %s" target pom-file)
   (with-temp-buffer
-    (let* ((pom-file (if (eq system-type 'cygwin) 
-                        (car (process-lines javaimp-cygpath-program 
-                                            "-m" pom-file))
-                      pom-file))
+    (let* ((pom-file (javaimp-cygpath-convert-maybe pom-file))
           (status
-           ;; FIXME on GNU/Linux Maven strangely outputs ^M chars. Check
-           ;; also jar output with the same var binding below.
-           (let ((coding-system-for-read (when (eq system-type 'cygwin) 
'utf-8-dos)))
+           ;; TODO check 
 in Maven output on Gnu/Linux
+           (let ((coding-system-for-read
+                  (if (eq system-type 'cygwin) 'utf-8-dos)))
              (process-file javaimp-mvn-program nil t nil "-f" pom-file 
target)))
-          (output-buf (current-buffer)))
+          (buf (current-buffer)))
       (with-current-buffer (get-buffer-create javaimp-debug-buf-name)
        (erase-buffer)
-       (insert-buffer-substring output-buf))
-      (unless (and (numberp status) (= status 0))
-       (error "Maven target \"%s\" failed with status \"%s\""
-              target status))
+       (insert-buffer-substring buf))
+      (or (and (numberp status) (= status 0))
+         (error "Maven target \"%s\" failed with status \"%s\"" target status))
+      (goto-char (point-min))
       (funcall handler))))
 
+(defun javaimp--maven-build-tree (id parent-node all-modules file)
+  (message "Building tree for project: %s" id)
+  (let ((this (or (seq-find (lambda (m) (equal (javaimp-module-id m) id))
+                           all-modules)
+                 (error "Cannot find module %s!" id)))
+       ;; although each real parent has <modules> section, more reliable
+       ;; way to build hirarchy is to analyze <parent> node in each child
+       (children (seq-filter (lambda (m) (equal (javaimp-module-parent-id m) 
id))
+                             all-modules)))
+    (if (and (null children)
+            (string= (javaimp-module-packaging this) "pom"))
+       (progn (message "Skipping empty aggregate module %s" (javaimp-module-id 
this))
+              nil)
+      ;; here we can finally set the `file' slot as the path is known at
+      ;; this time
+      (setf (javaimp-module-file this) file)
+      ;; make node
+      (let ((this-node (make-javaimp-node
+                       :parent parent-node
+                       :children nil
+                       :contents this)))
+       (setf (javaimp-node-children this-node)
+             (mapcar (lambda (child)
+                       (let ((child-file
+                              (javaimp--maven-get-submodule-file
+                               child file (javaimp-module-modules this))))
+                         (javaimp--maven-build-tree
+                          (javaimp-module-id child) this-node all-modules 
child-file)))
+                     children))
+       this-node))))
+
+(defun javaimp--maven-get-submodule-file (submodule parent-file 
rel-paths-from-parent)
+  ;; seems that the only reliable way to match a module parsed from
+  ;; <project> element with module relative path taken from <modules> is to
+  ;; visit pom and check that id and parent-id matches
+  (let* ((parent-dir (file-name-directory parent-file))
+        (files (mapcar (lambda (rel-path)
+                         (concat parent-dir
+                                 (file-name-as-directory rel-path)
+                                 "pom.xml"))
+                       rel-paths-from-parent)))
+    (or (seq-find
+        (lambda (file)
+          (javaimp--maven-xml-file-matches
+           file (javaimp-module-id submodule) (javaimp-module-parent-id 
submodule)))
+        files)
+       (error "Cannot find file for module: %s" (javaimp-module-id 
submodule)))))
+
 
-;;; Reading and caching dependencies
+;;; Loading dep-jars
+
+(defun javaimp--maven-update-module-maybe (node)
+  (let (need-update)
+    ;; are deps not initialized?
+    (let ((module (javaimp-node-contents node)))
+      (if (null (javaimp-module-dep-jars module))
+         (setq need-update t)))
+    ;; were any pom.xml files updated after last load?
+    (let ((tmp node))
+      (while (and tmp
+                 (not need-update))
+       (let ((module (javaimp-node-contents tmp)))
+         (if (> (float-time (javaimp--get-file-ts (javaimp-module-file 
module)))
+                (float-time (javaimp-module-load-ts module)))
+             (setq need-update t)))
+       (setq tmp (javaimp-node-parent tmp))))
+    (when need-update
+      ;; update current module
+      (let ((module (javaimp-node-contents node)))
+       ;; reload & update dep-jars
+       (setf (javaimp-module-dep-jars module)
+             (javaimp--maven-fetch-dep-jars module))
+       ;; update load-ts
+       (setf (javaimp-module-load-ts module) (current-time))))))
+
+(defun javaimp--maven-fetch-dep-jars (module)
+  (let ((raw-line
+        (javaimp--maven-call
+         (javaimp-module-file module) "dependency:build-classpath"
+         (lambda ()
+           (goto-char (point-min))
+           (search-forward "Dependencies classpath:")
+           (forward-line 1)
+           (thing-at-point 'line))))
+       (separator-regex (concat "[" path-separator "\n" "]+")))
+    (split-string (javaimp-cygpath-convert-maybe raw-line 'unix t) 
separator-regex t)))
 
-(defun javaimp-maven-fetch-module-deps (module)
-  "Returns list of dependency jars for MODULE"
-  (javaimp-call-mvn
-   (javaimp-get-mod-pom-file module) "dependency:build-classpath"
-   (lambda ()
-     (let (deps-line)
-       (goto-char (point-min))
-       (search-forward "Dependencies classpath:")
-       (forward-line 1)
-       (setq deps-line (thing-at-point 'line))
-       (when (eq system-type 'cygwin)
-        (setq deps-line (car (process-lines javaimp-cygpath-program 
-                                            "-up" 
-                                            deps-line))))
-       (split-string deps-line (concat "[" path-separator "\n" "]+") t)))))
-
-(defun javaimp-get-file-ts (file)
-  (nth 5 (file-attributes file)))
 
-(defun javaimp-any-file-ts-updated (files)
-  (if (null files)
-      nil
-    (let ((curr-ts (javaimp-get-file-ts (car (car files))))
-         (last-ts (cdr (car files))))
-      (or (null last-ts)               ; reading for the first time?
-         (not (equal (float-time curr-ts) (float-time last-ts)))
-         (javaimp-any-file-ts-updated (cdr files))))))
-
-(defun javaimp-get-dep-jars-cached (module parent)
-  "Returns a list of dependency jar file paths for a MODULE.
-Both MODULE and PARENT poms are checked for updates because
-PARENT pom may have some versions which are inherited by the
-MODULE."
-  (when (javaimp-any-file-ts-updated
-        (remq nil (list (cons (javaimp-get-mod-pom-file module)
-                              (javaimp-get-mod-pom-mod-ts module))
-                        (when parent
-                          (cons
-                           (javaimp-get-mod-pom-file parent)
-                           ;; here we check the saved parent ts because it
-                           ;; matters what version we had when we were
-                           ;; reloading this pom the last time
-                           (javaimp-get-mod-parent-ts module))))))
-    ;; (re-)fetch dependencies
-    (javaimp-set-mod-pom-deps
-     module (javaimp-maven-fetch-module-deps module))
-    ;; update timestamps
-    (javaimp-set-mod-pom-mod-ts
-     module (javaimp-get-file-ts (javaimp-get-mod-pom-file module)))
-    (when parent
-      (javaimp-set-mod-parent-ts
-       module (javaimp-get-file-ts (javaimp-get-mod-pom-file parent)))))
-  (javaimp-get-mod-pom-deps module))
-
-(defun javaimp-get-jdk-jars ()
-  "Returns list of jars from the jre/lib subdirectory of the JDK
-directory"
-  (when javaimp-jdk-home
-    (directory-files (concat (file-name-as-directory javaimp-jdk-home)
-                            (file-name-as-directory "jre/lib"))
-                    t "\\.jar$")))
-
-(defun javaimp-get-jar-classes-cached (jar)
-  (let ((current-jar-mod-ts
-        (nth 5 (file-attributes (javaimp-get-jar-path jar)))))
-    (unless (equal (float-time (javaimp-get-jar-mod-ts jar))
-                  (float-time current-jar-mod-ts))
-      (javaimp-set-jar-classes-list jar (javaimp-fetch-jar-classes jar))
-      (javaimp-set-jar-mod-ts jar current-jar-mod-ts))
-    (javaimp-get-jar-classes-list jar)))
-
-(defun javaimp-fetch-jar-classes (jar)
-  (let ((jar-file (javaimp-get-jar-path jar))
-       result)
-    (message "Reading classes in jar: %s" jar-file)
-    (with-temp-buffer
-      (let ((jar-file (if (eq system-type 'cygwin) 
-                         (car (process-lines javaimp-cygpath-program 
-                                             "-m" jar-file))
-                       jar-file))
-           (coding-system-for-read (when (eq system-type 'cygwin) 'utf-8-dos)))
-       (process-file javaimp-jar-program nil t nil "-tf" jar-file))
+
+;; Working with jar classes
+
+(defun javaimp--get-jar-classes (file)
+  (let ((cached (cdr (assoc file javaimp-cached-jars))))
+    (cond ((null cached)
+          ;; create, load & put into cache
+          (setq cached
+                (make-javaimp-cached-jar
+                 :file file
+                 :read-ts (javaimp--get-file-ts file)
+                 :classes (javaimp--fetch-jar-classes file)))
+          (push (cons file cached) javaimp-cached-jars))
+         ((> (float-time (javaimp--get-file-ts (javaimp-cached-jar-file 
cached)))
+             (float-time (javaimp-cached-jar-read-ts cached)))
+          ;; reload
+          (setf (javaimp-cached-jar-classes cached) 
(javaimp--fetch-jar-classes file))
+          ;; update read-ts
+          (setf (javaimp-cached-jar-read-ts cached) (current-time))))
+    ;; return from cached
+    (javaimp-cached-jar-classes cached)))
+
+(defun javaimp--fetch-jar-classes (file)
+  (message "Reading classes in file: %s" file)
+  (with-temp-buffer
+    (let ((coding-system-for-read (and (eq system-type 'cygwin) 'utf-8-dos)))
+      ;; On Cygwin, "jar" is a Windows program, so file path needs to be
+      ;; converted appropriately.
+      (process-file javaimp-jar-program nil t nil
+                   ;; `jar' accepts commands/options as a single string
+                   "tf" (javaimp-cygpath-convert-maybe file 'windows))
       (goto-char (point-min))
-      (while (re-search-forward "^\\(.+\\)\\.class$" nil t)
-       (push (replace-regexp-in-string "[/$]" "." (match-string 1))
-             result))
-      result)))
-
-(defun javaimp-collect-jar-classes (jar-paths)
-  (let (result jar)
-    (dolist (jar-path jar-paths result)
-      (setq jar (assoc jar-path javaimp-jar-classes-cache))
-      (unless jar
-       (setq jar (javaimp-make-jar jar-path nil nil))
-       (push jar javaimp-jar-classes-cache))
-      (setq result (append (javaimp-get-jar-classes-cached jar) result)))))
-
-(defun javaimp-get-module-from-root (roots predicate)
-  (if (null roots)
-      nil
-    (let ((result (javaimp-get-module (cdr (car roots)) predicate)))
-      (or result
-         (javaimp-get-module-from-root (cdr roots) predicate)))))
-
-(defun javaimp-get-module (modules predicate)
-  (cond ((null modules)
-        nil)
-       ((funcall predicate (car modules))
-        (car modules))
-       (t
-        (javaimp-get-module (cdr modules) predicate))))
+      (while (search-forward "/" nil t)
+       (replace-match "."))
+      (goto-char (point-min))
+      (let (result)
+       (while (re-search-forward "\\(^[[:alnum:]._]+\\)\\.class$" nil t)
+         (push (match-string 1) result))
+       result))))
+
+
+;; Some API functions
 
-(defun javaimp-get-module-by-file (file)
-  (javaimp-get-module-from-root
-   javaimp-maven-root-modules
-   (lambda (mod)
-     (or (string-prefix-p (javaimp-get-mod-source-dir mod) file)
-        (string-prefix-p (javaimp-get-mod-test-source-dir mod) file)))))
+(defun javaimp-get-all-modules ()
+  (javaimp-select-nodes (lambda (module) t)))
 
-(defun javaimp-get-module-by-artifact (artifact)
-  (javaimp-get-module-from-root
-   javaimp-maven-root-modules
-   (lambda (mod)
-     (equal (javaimp-get-mod-artifact mod) artifact))))
+(defun javaimp-find-node (predicate)
+  (javaimp--find-in-forest javaimp-project-forest predicate))
+
+(defun javaimp-select-nodes (predicate)
+  (javaimp--select-from-forest javaimp-project-forest predicate))
+
+
+;; Tree search routines
+
+(defun javaimp--find-in-forest (forest predicate)
+  (catch 'found
+    (dolist (tree forest)
+      (javaimp--find-node tree predicate))))
+
+(defun javaimp--find-node (tree predicate)
+  (if tree
+      (progn (if (funcall predicate (javaimp-node-contents tree))
+                (throw 'found tree))
+            (dolist (child (javaimp-node-children tree))
+              (javaimp--find-node child predicate)))))
+
+(defun javaimp--select-from-forest (forest predicate)
+  (apply #'seq-concatenate 'list
+        (mapcar (lambda (tree)
+                  (javaimp--select-nodes tree predicate))
+                forest)))
+
+(defun javaimp--select-nodes (tree predicate)
+  (if tree
+      (append (if (funcall predicate (javaimp-node-contents tree))
+                 (list tree))
+             (apply #'seq-concatenate 'list
+                    (mapcar (lambda (child)
+                              (javaimp--select-nodes child predicate))
+                            (javaimp-node-children tree))))))
 
 
-;;; Adding and organizing imports
+;;; Adding imports
 
 ;;;###autoload
 (defun javaimp-add-import (classname)
   "Imports CLASSNAME in the current file.  Interactively,
-performs class name completion based on the current module's
-dependencies, JDK jars and top-level classes in the current
-module."
+asks for a class to import, adds import statement and calls
+`javaimp-organize-imports'.  Import statements are not
+duplicated.  Completion alternatives are constructed based on
+this module's dependencies' classes, JDK classes and top-level
+classes in the current module."
   (interactive
-   (let* ((file (expand-file-name
-                (or buffer-file-name
-                    (error "Buffer is not visiting a file!"))))
-         (module (or (javaimp-get-module-by-file file)
-                     (error "Cannot determine module for file: %s" file)))
-         (parent (javaimp-get-module-by-artifact
-                  (javaimp-get-mod-parent module))))
-     (list (completing-read
-           "Import: "
-           (append
-            (javaimp-collect-jar-classes
-             (append (javaimp-get-dep-jars-cached module parent)
-                     (javaimp-get-jdk-jars)))
-            (and javaimp-include-current-project-classes
-                 (javaimp-get-module-classes module)))
-           nil t nil nil (symbol-name (symbol-at-point))))))
-  (javaimp-organize-imports classname))
-
-(defun javaimp-get-module-classes (module)
-  "Scans current project and returns a list of top-level classes in both the
-source directory and test source directory"
-  (let ((src-dir (javaimp-get-mod-source-dir module))
-       (test-src-dir (javaimp-get-mod-test-source-dir module))
-       (build-dir (javaimp-get-mod-build-dir module)))
-    (append
-     (and javaimp-additional-source-dirs
-         (seq-mapcat
+   (progn
+     (barf-if-buffer-read-only)
+     (let* ((file (expand-file-name
+                  (or buffer-file-name
+                      (error "Buffer is not visiting a file!"))))
+           (node (or (javaimp-find-node
+                      (lambda (m)
+                        (or (string-prefix-p (javaimp-module-source-dir m) 
file)
+                            (string-prefix-p (javaimp-module-test-source-dir 
m) file))))
+                     (error "Cannot find module by file: %s" file))))
+       (javaimp--maven-update-module-maybe node)
+       (let ((module (javaimp-node-contents node)))
+        (list (completing-read
+               "Import: "
+               (append
+                ;; we're not caching full list of classes coming from module
+                ;; dependencies because jars may change and we need to reload
+                ;; them
+                (let ((jars (append (javaimp-module-dep-jars module)
+                                    (javaimp--get-jdk-jars))))
+                  (apply #'seq-concatenate 'list
+                         (mapcar #'javaimp--get-jar-classes jars)))
+                (and javaimp-include-current-module-classes
+                     (javaimp--get-module-classes module)))
+               nil t nil nil (symbol-name (symbol-at-point))))))))
+  (javaimp-organize-imports (cons classname 'ordinary)))
+
+(defun javaimp--get-module-classes (module)
+  "Returns list of top-level classes in current module"
+  (append
+   (let ((build-dir (javaimp-module-build-dir module)))
+     (and (seq-mapcat
           (lambda (rel-dir)
-            (let ((dir (file-name-as-directory (concat build-dir rel-dir))))
+            (let ((dir (concat build-dir (file-name-as-directory rel-dir))))
               (and (file-accessible-directory-p dir)
-                   (javaimp-get-directory-classes dir nil))))
-          javaimp-additional-source-dirs))
-     (and (file-accessible-directory-p test-src-dir)
-         (javaimp-get-directory-classes test-src-dir nil))
-     (and (file-accessible-directory-p src-dir)
-         (javaimp-get-directory-classes src-dir nil)))))
-
-(defun javaimp-get-directory-classes (dir prefix)
-  "Returns the list of classes found in the directory DIR.  PREFIX is the
-initial package prefix."
-  (let (result)
-    ;; traverse subdirectories
-    (dolist (file (directory-files-and-attributes dir nil nil t))
-      (if (and (eq (cadr file) t)
-              (not (or (string= (car file) ".")
-                       (string= (car file) ".."))))
-         (setq result
-               (append (javaimp-get-directory-classes 
-                        (concat dir (file-name-as-directory (car file)))
-                        (concat prefix (car file) "."))
-                       result))))
-    ;; add .java files in the current directory
-    (dolist (file (directory-files-and-attributes dir nil "\\.java\\'" t))
-      (unless (cadr file)
-       (push (concat prefix (file-name-sans-extension (car file))) result)))
-    result))
-
-(defun javaimp-add-to-import-groups (new-class groups)
-  "Subroutine of `javaimp-organize-imports'"
-  (let* ((order (or (assoc-default new-class javaimp-import-group-alist
-                                  'string-match)
-                   javaimp-import-default-order))
-        (group (assoc order groups)))
-    (if group
-       (progn
-         ;; add only if this class is not already there
-         (unless (member new-class (cdr group))
-           (setcdr group (cons new-class (cdr group))))
-         groups)
-      (cons (cons order (list new-class)) groups))))
-
-(defun javaimp-insert-import-groups (groups static-p)
-  "Inserts all imports in GROUPS.  Non-nil STATIC-P means that
-  all imports are static."
-  (when groups
-    (dolist (group (sort groups (lambda (g1 g2)
-                                 (< (car g1) (car g2)))))
-      (dolist (class (sort (cdr group) 'string<))
-       (insert (concat "import " (when static-p "static ") class ";\n")))
-      (insert ?\n))
-    ;; remove newline after the last group
-    (delete-char -1)))
+                   (javaimp--get-directory-classes dir nil))))
+          javaimp-additional-source-dirs)))
+   (let ((dir (javaimp-module-source-dir module)))
+     (and (file-accessible-directory-p dir)
+         (javaimp--get-directory-classes dir nil)))
+   (let ((dir (javaimp-module-test-source-dir module)))
+     (and (file-accessible-directory-p dir)
+         (javaimp--get-directory-classes dir nil)))))
+
+(defun javaimp--get-directory-classes (dir prefix)
+  (append
+   ;; .java files in current directory
+   (mapcar (lambda (file)
+            (concat prefix (file-name-sans-extension (car file))))
+          (seq-filter (lambda (file) (null (cadr file))) ;only files
+                      (directory-files-and-attributes dir nil "\\.java\\'" t)))
+   ;; descend into subdirectories
+   (apply #'seq-concatenate 'list
+         (mapcar (lambda (subdir)
+                   (let ((name (car subdir)))
+                     (javaimp--get-directory-classes 
+                      (concat dir (file-name-as-directory name)) (concat 
prefix name "."))))
+                 (seq-filter (lambda (file) (and (cadr file) ;only directories
+                                                 (null (member (car file) 
'("." "..")))))
+                             (directory-files-and-attributes dir nil nil 
t))))))
 
-;;;###autoload
-(defun javaimp-organize-imports (&rest new-classes)
-  "Groups and orders import statements in the current buffer.  Groups are
-formed and ordered according to `javaimp-import-group-alist'.  Classes within a
-single group are ordered in a lexicographic order. Optional NEW-CLASSES
-argument is a list of additional classes to import."
-  (interactive)
-  (barf-if-buffer-read-only)
-  (save-excursion
-    (let (import-groups static-import-groups old-imports-start)
-      ;; existing imports
-      (goto-char (point-min))
-      (while (re-search-forward
-             "^\\s-*import\\s-+\\(static\\s-+\\)?\\([._[:word:]]+\\)"
-             nil t)
-       (if (null (match-string 1))
-           (setq import-groups
-                 (javaimp-add-to-import-groups (match-string 2)
-                                               import-groups))
-         (setq static-import-groups
-               (javaimp-add-to-import-groups (match-string 2)
-                                             static-import-groups)))
-       (beginning-of-line)
-       (unless old-imports-start (setq old-imports-start (point)))
-       (delete-region (point) (line-beginning-position 2))
-       ;; delete whatever was between import statements
-       (when (/= (point) old-imports-start)
-         (delete-region old-imports-start (point))))
-      ;; new imports
-      (dolist (class new-classes)
-       (setq import-groups (javaimp-add-to-import-groups class import-groups)))
-      ;; insert all
-      (if (or import-groups static-import-groups)
-         (progn
-           ;; prepare the position
-           (cond (old-imports-start
-                  ;; when there were any imports, do not touch blank lines
-                  ;; before imports
-                  (goto-char old-imports-start))
-                 ((re-search-forward "^\\s-*package\\s-" nil t)
-                  ;; when there is a package statement, insert one or two
-                  ;; blank lines after it
-                  (when (= (forward-line) 1) (insert ?\n)) ;; last line?
-                  (insert ?\n))
-                 (t
-                  ;; otherwise, start at the bob, insert one empty line
-                  ;; after point
-                  (goto-char (point-min))
-                  (insert ?\n)
-                  (backward-char)))
-           (javaimp-insert-import-groups import-groups nil)
-           (and import-groups static-import-groups (insert ?\n))
-           (javaimp-insert-import-groups static-import-groups t))
-       (message "Nothing to organize")))))
+
+;; Organizing imports
 
 ;;;###autoload
-(defun javaimp-invalidate-jar-classes-cache ()
-  "Resets jar classes cache (debugging only)"
-  (interactive)
-  (setq javaimp-jar-classes-cache nil))
+(defun javaimp-organize-imports (&rest new-imports)
+  "Groups import statements according to the value of
+`javaimp-import-group-alist' (which see) and prints resulting
+groups leaving one blank line in between.
 
-;;;###autoload
-(defun javaimp-forget-all-visited-modules ()
-  "Resets `javaimp-maven-root-modules' (debugging only)"
-  (interactive)
-  (setq javaimp-maven-root-modules nil))
+Classes within a single group are ordered in a lexicographic
+order.
 
-;;;###autoload
-(defun javaimp-reset ()
-  "Resets all data (debugging only)"
+Imports not matched by any regexp in `javaimp-import-group-alist'
+are assigned a default order defined by
+`javaimp-import-default-order'.
+
+NEW-IMPORTS is a list of additional imports; each element should
+be of the form (CLASS . TYPE), where CLASS is a string and TYPE
+is `'ordinary' or `'static'.  Interactively, NEW-IMPORTS is nil."
   (interactive)
-  (javaimp-forget-all-visited-modules)
-  (javaimp-invalidate-jar-classes-cache))
+  (barf-if-buffer-read-only)
+  (save-excursion
+    (goto-char (point-min))
+    (let* ((old-data (javaimp--parse-imports))
+          (first (car old-data))
+          (last (cadr old-data))
+          (all-imports (append new-imports (cddr old-data))))
+      ;; delete old imports, if any
+      (if first
+         (progn
+           (goto-char last)
+           (forward-line)
+           (delete-region first (point))))
+      (javaimp--prepare-for-insertion first)
+      (setq all-imports
+           (delete-duplicates all-imports
+                              :test (lambda (first second)
+                                      (equal (car first) (car second)))))
+      ;; assign order
+      (let ((with-order
+            (mapcar
+             (lambda (import)
+               (let ((order (or (assoc-default (car import)
+                                               javaimp-import-group-alist
+                                               'string-match)
+                                javaimp-import-default-order)))
+                 (cons import order)))
+             all-imports)))
+       (setq with-order
+             (sort with-order
+                   (lambda (first second)
+                     ;; sort by order, name
+                     (if (= (cdr first) (cdr second))
+                         (string< (caar first) (caar second))
+                       (< (cdr first) (cdr second))))))
+       (javaimp--insert-imports with-order)))))
+
+(defun javaimp--parse-imports ()
+  (let (first last list)
+    (while (re-search-forward 
"^\\s-*import\\s-+\\(static\\s-+\\)?\\([._[:word:]]+\\)" nil t)
+      (push (cons (match-string 2) (if (match-string 1) 'static 'ordinary)) 
list)
+      (setq last (line-beginning-position))
+      (or first (setq first last)))
+    (cons first (cons last list))))
+
+(defun javaimp--prepare-for-insertion (start)
+  (cond (start
+        ;; if there were any imports, we start inserting at the same place
+        (goto-char start))
+       ((re-search-forward "^\\s-*package\\s-" nil t)
+        ;; if there's a package directive, move to the next line, creating it
+        ;; if needed
+        (end-of-line)
+        (if (eobp)
+            (insert ?\n)
+          (forward-line))
+        ;; then insert one blank line and we're done
+        (insert ?\n))
+       (t
+        ;; otherwise, just go to bob
+        (goto-char (point-min)))))
+
+(defun javaimp--insert-imports (imports)
+  (let ((static (seq-filter (lambda (elt)
+                             (eq (cdar elt) 'static))
+                           imports))
+       (ordinary (seq-filter (lambda (elt)
+                               (eq (cdar elt) 'ordinary))
+                             imports)))
+    (javaimp--insert-import-group "import static %s;" static)
+    (and static ordinary (insert ?\n))
+    (javaimp--insert-import-group "import %s;" ordinary)))
+
+(defun javaimp--insert-import-group (pattern imports)
+  (let (last-order)
+    (dolist (import imports)
+      ;; if adjacent imports have different order value, insert a newline
+      ;; between them
+      (let ((order (cdr import)))
+       (and last-order
+            (/= order last-order)
+            (insert ?\n))
+       (insert (format pattern (caar import)) ?\n)
+       (setq last-order order)))))
 
 (provide 'javaimp)
 



reply via email to

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