;; Quick start:
;;
;; - customize `javaimp-import-group-alist'
-;;
;; - call `javaimp-maven-visit-project', giving it the top-level project
;; directory where pom.xml resides
;;
;; 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. 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.
-;;
-;; If you make some changes which change project hierarchy, you should
-;; re-visit the parent again with `javaimp-maven-visit-project'.
+;; 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.
;;
;; Currently inner classes are filtered out from completion alternatives.
;; You can always import top-level class and use qualified name.
-;;
+;;
;;
;; Example of initialization:
-;;
+;;
;; (require 'javaimp)
-;;
+;;
;; (add-to-list 'javaimp-import-group-alist
;; '("\\`\\(my\\.company\\.\\|my\\.company2\\.\\)" . 80))
-;;
+;;
;; (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:
-;;
+;;
;; - 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
+;; - 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:
;; 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
"Defines the order of classes which were not matched by
`javaimp-import-group-alist'")
-(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-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)
;; Variables and constants
(defvar javaimp-project-forest nil
- "Visited projects.")
+ "Visited projects")
(defvar javaimp-cached-jars nil
"Alist of cached jars. Each element is of the form (FILE
(nth 5 (file-attributes file)))
(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\\'"))))
+ (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
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: ")
+ (interactive "DVisit maven project in directory: ")
(let ((file (expand-file-name
(concat (file-name-as-directory path) "pom.xml"))))
(unless (file-readable-p file)
(equal (javaimp-module-file (javaimp-node-contents tree))
file))
javaimp-project-forest))
- (let ((tree (javaimp--maven-xml-load-tree file)))
+ (message "Loading file %s..." file)
+ (let* ((xml-tree
+ (javaimp--maven-call file "help:effective-pom"
+ #'javaimp--maven-xml-effective-pom-handler))
+ (projects (javaimp--maven-xml-extract-projects xml-tree))
+ (modules (mapcar #'javaimp--maven-xml-parse-project projects))
+ ;; first module is always root
+ (tree (javaimp--maven-build-tree (car modules) nil modules file)))
(if tree
(push tree javaimp-project-forest)))
(message "Loaded tree for %s" file)))
\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)))
+;; Maven XML routines
+
+(defun javaimp--maven-xml-effective-pom-handler ()
+ (let ((start
+ (save-excursion
+ (progn
+ (goto-char (point-min))
+ (re-search-forward "<\\?xml\\|<projects?")
+ (match-beginning 0))))
+ (end
+ (save-excursion
+ (progn
+ (goto-char (point-min))
+ (re-search-forward "<\\(projects?\\)")
+ ;; corresponding close tag is the end of parse region
+ (search-forward (concat "</" (match-string 1) ">"))
+ (match-end 0)))))
+ (xml-parse-region start end)))
+
+(defun javaimp--maven-xml-extract-projects (xml-tree)
+ "Analyzes result of `mvn help:effective-pom' and returns list
+of <project> elements"
+ (let ((project (assq 'project xml-tree))
+ (projects (assq 'projects xml-tree)))
+ (cond (project
+ (list project))
+ (projects
+ (javaimp--xml-children projects 'project))
(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--maven-call
- pom "help:effective-pom"
- (lambda ()
- (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)))
+ (error "Neither <project> nor <projects> was found in pom")))))
+
+(defun javaimp--maven-xml-parse-project (project)
+ (let ((build-elt (javaimp--xml-child 'build project)))
(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
+ :id (javaimp--maven-xml-extract-id project)
+ :parent-id (javaimp--maven-xml-extract-id (javaimp--xml-child 'parent project))
+ ;; <project> element does not contain pom file path (we set :file slot later)
: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))
+ (javaimp--xml-child 'packaging project))
:source-dir (file-name-as-directory
(javaimp-cygpath-convert-maybe
(javaimp--xml-first-child
(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))
+ (javaimp--xml-children (javaimp--xml-child 'modules project) 'module))
:dep-jars nil ; dep-jars is initialized lazily on demand
:load-ts (current-time))))
(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)))
+(defun javaimp--maven-build-tree (this parent-node all file)
+ (message "Building tree for module: %s" (javaimp-module-id this))
+ (let ((children
+ ;; reliable way to find children is to look for modules with "this" as
+ ;; the parent
+ (seq-filter (lambda (m) (equal (javaimp-module-parent-id m)
+ (javaimp-module-id this)))
+ all)))
(if (and (null children)
- (string= (javaimp-module-packaging this) "pom"))
- (progn (message "Skipping empty aggregate module %s" (javaimp-module-id this))
+ (equal (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
+ ;; filepath was not set before, but now we know it
(setf (javaimp-module-file this) file)
- ;; make node
- (let ((this-node (make-javaimp-node
+ ;; node
+ (let* ((this-node (make-javaimp-node
:parent parent-node
:children nil
- :contents this)))
- (setf (javaimp-node-children this-node)
+ :contents this))
+ ;; recursively build child nodes
+ (child-nodes
(mapcar (lambda (child)
(let ((child-file
+ ;; !! this is hack
(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))
+ child this-node all child-file)))
+ children)))
+ (setf (javaimp-node-children this-node) child-nodes)
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
+ ;; 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
;;; 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 ((module (javaimp-node-contents node))
+ need-update)
+ ;; check if deps are initialized
+ (or (javaimp-module-dep-jars module)
+ (progn (message "Loading dependencies: %s" (javaimp-module-id module))
+ (setq need-update t)))
+ ;; check if any pom up to the top one has changed
(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)))
+ (let ((checked (javaimp-node-contents tmp)))
+ (if (> (float-time (javaimp--get-file-ts (javaimp-module-file checked)))
(float-time (javaimp-module-load-ts module)))
- (setq need-update t)))
+ (progn
+ (message "Reloading %s (pom changed)" (javaimp-module-id checked))
+ (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))))))
+ (let* ((new-dep-jars (javaimp--maven-fetch-dep-jars module))
+ (new-load-ts (current-time)))
+ (setf (javaimp-module-dep-jars module) new-dep-jars)
+ (setf (javaimp-module-load-ts module) new-load-ts)))))
(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)))
-
+ (let* ((path (javaimp--maven-call (javaimp-module-file module)
+ "dependency:build-classpath"
+ #'javaimp--maven-build-classpath-handler))
+ (converted-path (javaimp-cygpath-convert-maybe path 'unix t))
+ (path-separator-regex (concat "[" path-separator "\n" "]+")))
+ (split-string converted-path path-separator-regex t)))
+
+(defun javaimp--maven-build-classpath-handler ()
+ (goto-char (point-min))
+ (search-forward "Dependencies classpath:")
+ (forward-line 1)
+ (thing-at-point 'line))
\f
;; Working with jar classes
(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
+ ;; 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
result))))
\f
-;; Some API functions
-
-(defun javaimp-get-all-modules ()
- (javaimp-select-nodes (lambda (module) t)))
-
-(defun javaimp-find-node (predicate)
- (javaimp--find-in-forest javaimp-project-forest predicate))
+;; Tree search routines
-(defun javaimp-select-nodes (predicate)
- (javaimp--select-from-forest javaimp-project-forest predicate))
+(defun javaimp--find-node (predicate)
+ (javaimp--find-node-in-forest javaimp-project-forest predicate))
-\f
-;; Tree search routines
+(defun javaimp--select-nodes (predicate)
+ (javaimp--select-nodes-from-forest javaimp-project-forest predicate))
-(defun javaimp--find-in-forest (forest predicate)
+(defun javaimp--find-node-in-forest (forest predicate)
(catch 'found
(dolist (tree forest)
- (javaimp--find-node tree predicate))))
+ (javaimp--find-node-in-tree tree predicate))))
-(defun javaimp--find-node (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 child predicate)))))
+ (javaimp--find-node-in-tree child predicate)))))
-(defun javaimp--select-from-forest (forest predicate)
+(defun javaimp--select-nodes-from-forest (forest predicate)
(apply #'seq-concatenate 'list
(mapcar (lambda (tree)
- (javaimp--select-nodes tree predicate))
+ (javaimp--select-nodes-from-tree tree predicate))
forest)))
-(defun javaimp--select-nodes (tree predicate)
+(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 child predicate))
+ (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-find-module (predicate)
+ (let ((node (javaimp--find-node predicate)))
+ (and node
+ (javaimp-node-contents node))))
+
+(defun javaimp-select-modules (predicate)
+ (mapcar #'javaimp-node-contents
+ (javaimp--select-nodes predicate)))
+
\f
;;; Adding imports
;;;###autoload
(defun javaimp-add-import (classname)
- "Imports CLASSNAME in the current file. Interactively,
+ "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
(let* ((file (expand-file-name
(or buffer-file-name
(error "Buffer is not visiting a file!"))))
- (node (or (javaimp-find-node
+ (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))))
"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 (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)))
+ ;; 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)))))
(apply #'seq-concatenate 'list
(mapcar (lambda (subdir)
(let ((name (car subdir)))
- (javaimp--get-directory-classes
+ (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) '("." "..")))))
+ (seq-filter (lambda (file)
+ (and (eq (cadr file) t) ;only directories
+ (null (member (car file) '("." "..")))))
(directory-files-and-attributes dir nil nil t))))))
\f
(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.
+groups leaving one blank line between groups.
-Classes within a single group are ordered in a lexicographic
-order.
+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.
-Imports not matched by any regexp in `javaimp-import-group-alist'
+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'.
(first (car old-data))
(last (cadr old-data))
(all-imports (append new-imports (cddr old-data))))
- ;; delete old imports, if any
- (if first
+ (if all-imports
(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)))))
+ ;; 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)
;; 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
+ ;; if there's a package directive, insert one blank line below and
+ ;; leave point after it
(end-of-line)
(if (eobp)
(insert ?\n)