X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/f213965b93b982d406404529503ed3346c983dac..f10533854f4c7bb54247a11981191bf37b70cb36:/packages/javaimp/javaimp.el diff --git a/packages/javaimp/javaimp.el b/packages/javaimp/javaimp.el index a32a9a0a1..d1c68657d 100644 --- a/packages/javaimp/javaimp.el +++ b/packages/javaimp/javaimp.el @@ -4,7 +4,7 @@ ;; Author: Filipp Gunbin ;; Maintainer: Filipp Gunbin -;; Version: 0.7 +;; Version: 0.6 ;; Keywords: java, maven, programming ;;; Commentary: @@ -14,7 +14,6 @@ ;; Quick start: ;; ;; - customize `javaimp-import-group-alist' -;; ;; - call `javaimp-maven-visit-project', giving it the top-level project ;; directory where pom.xml resides ;; @@ -24,52 +23,50 @@ ;; 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: @@ -81,14 +78,15 @@ ;; 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 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 +Each element should be of the form (CLASSNAME-REGEXP . ORDER) +where CLASSNAME-REGEXP is a regexp matching the fully qualified class name. Lowest-order groups are placed earlier. The order of classes which were not matched is defined by @@ -98,9 +96,10 @@ 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 (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) @@ -142,7 +141,7 @@ to the completion alternatives list.") ;; 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 @@ -193,12 +192,13 @@ to the completion alternatives list.") (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 @@ -234,7 +234,7 @@ 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: ") + (interactive "DVisit maven project in directory: ") (let ((file (expand-file-name (concat (file-name-as-directory path) "pom.xml")))) (unless (file-readable-p file) @@ -245,79 +245,61 @@ module 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))) -;; 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 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\\|")) + (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 elements" + (let ((project (assq 'project xml-tree)) + (projects (assq 'projects xml-tree))) + (cond (project + (list project)) + (projects + (javaimp--xml-children projects 'project)) (t - ;; neither nor - 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\\|")) - (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 nor 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 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)) + ;; 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 @@ -331,7 +313,7 @@ inheritance are not included." (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)))) @@ -385,41 +367,42 @@ the temporary buffer and returns its result" (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 section, more reliable - ;; way to build hirarchy is to analyze 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 - ;; element with module relative path taken from is to - ;; visit pom and check that id and parent-id matches + ;; Seems that the only reliable way to match a module parsed from + ;; element with module relative path taken from 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 @@ -437,41 +420,42 @@ the temporary buffer and returns its result" ;;; 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)) ;; Working with jar classes @@ -499,7 +483,7 @@ the temporary buffer and returns its result" (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 @@ -514,53 +498,61 @@ the temporary buffer and returns its result" result)))) -;; 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)) - -;; 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)))))) + +;; 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))) + ;;; 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 @@ -572,7 +564,7 @@ classes in the current module." (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)))) @@ -598,15 +590,18 @@ classes in the current 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 (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))))) @@ -622,10 +617,11 @@ classes in the current module." (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)))))) @@ -635,18 +631,22 @@ classes in the current module." (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'. 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." +is `ordinary' or `static'. Interactively, NEW-IMPORTS is nil." (interactive) (barf-if-buffer-read-only) (save-excursion @@ -655,35 +655,39 @@ is `'ordinary' or `'static'. Interactively, NEW-IMPORTS is nil." (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) @@ -698,8 +702,8 @@ is `'ordinary' or `'static'. Interactively, NEW-IMPORTS is nil." ;; 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)