;;; 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 <fgunbin@fastmail.fm>
;; Maintainer: Filipp Gunbin <fgunbin@fastmail.fm>
;; 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.
-;;
-;; 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.
+;; so usually only the first command should take a considerable amount of
+;; time to complete. If a module's pom.xml or any of its parents' pom.xml
+;; (within visited tree) 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.
;;
-;; `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.
-;;
-;; `javaimp-mvn-program' defines path of the `mvn' program. Use if it's
-;; not on `exec-path'.
+;; Currently inner classes are filtered out from completion alternatives.
+;; You can always import top-level class and use qualified name.
;;
-;; `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.
+;; Example of initialization:
;;
-;; `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.
+;; (require 'javaimp)
;;
-;; `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).
+;; (add-to-list 'javaimp-import-group-alist
+;; '("\\`\\(my\\.company\\.\\|my\\.company2\\.\\)" . 80))
;;
-;; Sample setup (put this into your .emacs):
-;;
-;; (require 'javaimp)
-;;
-;; (add-to-list 'javaimp-import-group-alist '("\\`\\(ru\\.yota\\.\\|tv\\.okko\\.\\)" . 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
;; (lambda ()
;; (local-set-key "\C-ci" 'javaimp-add-import)
;; (local-set-key "\C-co" 'javaimp-organize-imports)))
-;;
-;;
+;;
+;;
;; 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, on restore check if a root exists and delete it if
+;; not
+;;
+;; - `javaimp-add-import': without prefix arg narrow alternatives by local name;
+;; with prefix arg include all classes in alternatives
+;;
+;; - :type for defcustom
;;; Code:
+(require 'cl-lib)
+(require 'seq)
+(require 'xml)
+
\f
-;;; User options
+;; User options
(defgroup javaimp ()
- "Add and reorder Java import statements in Maven projects.")
+ "Add and reorder Java import statements in Maven projects"
+ :group 'c)
(defcustom javaimp-import-group-alist '(("\\`javax?\\." . 10))
"Specifies how to group classes and how to order resulting
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'.")
"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-java-home (getenv "JAVA_HOME")
+ "Path to the JDK. Directory jre/lib underneath this path is
+searched for JDK libraries. By default, it is initialized from
+the JAVA_HOME environment variable.")
(defcustom javaimp-additional-source-dirs nil
"List of directories where additional (e.g. generated)
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")
+ "Path to the `jar' program used to read contents of jar files.
+Customize it if the program is not on `exec-path'.")
-(defcustom javaimp-include-current-project-classes t
- "If non-nil, current project's classes are included into completion
-alternatives.
-
-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.")
\f
-;;; 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*")
-\f
-;;; 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)))
-
-\f
-;; 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))
-
-\f
-;; 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)
\f
-;; 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 ()
+ (and javaimp-java-home
+ (file-accessible-directory-p javaimp-java-home)
+ (let ((lib-dir
+ (concat (file-name-as-directory javaimp-java-home)
+ (file-name-as-directory "jre")
+ (file-name-as-directory "lib"))))
+ (directory-files 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))
\f
-;;; 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 in directory: ")
+ (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"
+\f
+;; 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)))))
+
+\f
+;; 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 \r 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)))))
+
\f
-;;; 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))
+\f
+;; 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))))
+
+\f
+;; Tree search routines
+
+(defun javaimp--find-node (predicate)
+ (javaimp--find-node-in-forest javaimp-project-forest predicate))
+
+(defun javaimp--select-nodes (predicate)
+ (javaimp--select-nodes-from-forest javaimp-project-forest predicate))
+
+(defun javaimp--find-node-in-forest (forest predicate)
+ (catch 'found
+ (dolist (tree forest)
+ (javaimp--find-node-in-tree tree predicate))))
+
+(defun javaimp--find-node-in-tree (tree predicate)
+ (if tree
+ (progn (if (funcall predicate (javaimp-node-contents tree))
+ (throw 'found tree))
+ (dolist (child (javaimp-node-children tree))
+ (javaimp--find-node-in-tree child predicate)))))
+
+(defun javaimp--select-nodes-from-forest (forest predicate)
+ (apply #'seq-concatenate 'list
+ (mapcar (lambda (tree)
+ (javaimp--select-nodes-from-tree tree predicate))
+ forest)))
+
+(defun javaimp--select-nodes-from-tree (tree predicate)
+ (if tree
+ (append (if (funcall predicate (javaimp-node-contents tree))
+ (list tree))
+ (apply #'seq-concatenate 'list
+ (mapcar (lambda (child)
+ (javaimp--select-nodes-from-tree child predicate))
+ (javaimp-node-children tree))))))
+
+\f
+;; Some API functions
+
+;; do not expose tree structure, return only modules
-(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-find-module (predicate)
+ (let ((node (javaimp--find-node predicate)))
+ (and node
+ (javaimp-node-contents node))))
-(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-select-modules (predicate)
+ (mapcar #'javaimp-node-contents
+ (javaimp--select-nodes predicate)))
\f
-;;; 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."
+ "Imports classname in the current file. Interactively,
+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)))
+ ;; additional source dirs
+ (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)))
+ ;; source dir
+ (let ((dir (javaimp-module-source-dir module)))
+ (and (file-accessible-directory-p dir)
+ (javaimp--get-directory-classes dir nil)))
+ ;; test source dir
+ (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 (eq (cadr file) t) ;only directories
+ (null (member (car file) '("." "..")))))
+ (directory-files-and-attributes dir nil nil t))))))
+
+\f
+;; Organizing imports
;;;###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."
+(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 between groups.
+
+If the file already contains some import statements, this command
+rewrites them, starting with the same place. Else, if the the
+file contains package directive, this command inserts one blank
+line below and then imports. Otherwise, imports are inserted at
+the beginning of buffer.
+
+Classes within a single group are ordered in a lexicographic
+order. 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)
(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)
+ (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))))
+ (if all-imports
(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")))))
-
-;;;###autoload
-(defun javaimp-invalidate-jar-classes-cache ()
- "Resets jar classes cache (debugging only)"
- (interactive)
- (setq javaimp-jar-classes-cache nil))
-
-;;;###autoload
-(defun javaimp-forget-all-visited-modules ()
- "Resets `javaimp-maven-root-modules' (debugging only)"
- (interactive)
- (setq javaimp-maven-root-modules nil))
-
-;;;###autoload
-(defun javaimp-reset ()
- "Resets all data (debugging only)"
- (interactive)
- (javaimp-forget-all-visited-modules)
- (javaimp-invalidate-jar-classes-cache))
+ ;; 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
+ (cl-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)))
+ (message "Nothing to organize!")))))
+
+(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, insert one blank line below and
+ ;; leave point after it
+ (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)