branch: javaimp_devel commit 0d6cb9e7b7d4082408db66df8072e5aac3f4b528 Author: Filipp Gunbin <fgun...@fastmail.fm> Commit: Filipp Gunbin <fgun...@fastmail.fm>
in progress --- packages/javaimp/javaimp.el | 452 +++++++++++++++++++++++-------------------- 1 files changed, 241 insertions(+), 211 deletions(-) diff --git a/packages/javaimp/javaimp.el b/packages/javaimp/javaimp.el index 3bf11e3..233d7ae 100644 --- a/packages/javaimp/javaimp.el +++ b/packages/javaimp/javaimp.el @@ -4,7 +4,7 @@ ;; Author: Filipp Gunbin <fgun...@fastmail.fm> ;; Maintainer: Filipp Gunbin <fgun...@fastmail.fm> -;; Version: 0.6 +;; Version: 0.7 ;; Keywords: java, maven, programming ;;; Commentary: @@ -46,8 +46,8 @@ ;; If you make some changes which change project hierarchy, you should ;; re-visit the parent again with `javaimp-maven-visit-project'. ;; -;; May work unreliable with inner classes, but you can always import -;; top-level class and use qualified name. +;; Currently inner classes are filtered out from completion alternatives. +;; You can always import top-level class and use qualified name. ;; ;; User options: ;; @@ -140,6 +140,7 @@ (require 'cl-macs) (require 'seq) +(require 'xml) ;; User options @@ -205,8 +206,9 @@ to the completion alternatives list.") (defvar javaimp-project-forest nil "Visited projects") -(defvar javaimp-jar-cache nil - "Cache for jar contents") +(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*") @@ -216,10 +218,9 @@ to the completion alternatives list.") parent children contents) (cl-defstruct javaimp-module - id parent-id file file-ts final-name packaging - source-dir test-source-dir build-dir - modules - dep-jars) + id parent-id file final-name packaging + source-dir test-source-dir build-dir modules + dep-jars load-ts) (defun javaimp-print-id (id) (format "%s:%s:%s" @@ -231,12 +232,13 @@ to the completion alternatives list.") (:print-function #'javaimp-print-id)) group artifact version) -(cl-defstruct javaimp-jar - file file-ts classes) +(cl-defstruct javaimp-cached-jar + file read-ts classes) +;; Utilities -(defun javaimp-xml-child-list (xml-tree child-name) +(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) @@ -244,18 +246,42 @@ to the completion alternatives list.") (eq (car child) child-name)) (push child result))))) -(defun javaimp-xml-child (name el) +(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) +(defun javaimp--xml-first-child (el) "Returns a first child of EL" (car (cddr el))) +(defun javaimp--get-file-ts (file) + (nth 5 (file-attributes file))) -(defun javaimp-cygpath-convert-maybe (path) - (if (eq system-type 'cygwin) - (car (process-lines javaimp-cygpath-program "-u" path)) +(defun javaimp--get-jdk-jars () + (if javaimp-jdk-home + (let ((jre-lib-dir + (concat (file-name-as-directory javaimp-jdk-home) + (file-name-as-directory "jre") + (file-name-as-directory "lib")))) + (directory-files jre-lib-dir t "\\.jar\\'")))) + +(defun javaimp-cygpath-convert-maybe (path &optional mode is-really-path) + "On Cygwin, converts PATH using cygpath according to MODE and +IS-REALLY-PATH. If MODE is `unix' (the default), adds -u switch. +If MODE is `windows', adds -m switch. If `is-really-path' is +non-nil, adds `-p' switch. On other systems, PATH is returned +unchanged." + (if (eq system-type 'cygwin) + (progn + (unless mode (setq mode 'unix)) + (let (args) + (push (cond ((eq mode 'unix) "-u") + ((eq mode 'windows) "-m") + (t (error "Invalid mode: %s" mode))) + args) + (and is-really-path (push "-p" args)) + (push path args) + (car (apply #'process-lines javaimp-cygpath-program args)))) path)) @@ -280,6 +306,9 @@ directory containing pom.xml." ;; TODO file should start to sink down from there; at each step append directory ;; from <module> to it + +;; 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 @@ -290,18 +319,18 @@ inheritance are not included." (cond ((assq 'project xml-tree) ;; no real children (let ((project-elt (assq 'project xml-tree)) - (submodules (javaimp-xml-child-list - (javaimp-xml-child 'modules project-elt) 'module))) + (submodules (javaimp--xml-child-list + (javaimp--xml-child 'modules project-elt) 'module))) (and submodules (message "Independent submodules: %s" - (mapconcat #'javaimp-xml-first-child submodules ", "))) + (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-child-list + (let* ((project-elts (javaimp--xml-child-list (assq 'projects xml-tree) 'project)) (all-modules (mapcar #'javaimp--maven-xml-parse-module project-elts))) (javaimp--maven-build-tree @@ -332,27 +361,6 @@ inheritance are not included." (match-end 0))))) (xml-parse-region xml-start-pos xml-end-pos))))) -(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 (javaimp-cygpath-convert-maybe pom-file)) - (status - ;; TODO check in Maven output on Gnu/Linux - (let ((coding-system-for-read - (if (eq system-type 'cygwin) 'utf-8-dos))) - (process-file javaimp-mvn-program nil t nil "-f" pom-file target))) - (b (current-buffer))) - (with-current-buffer (get-buffer-create javaimp-debug-buf-name) - (erase-buffer) - (insert-buffer-substring b)) - (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-xml-parse-module (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 @@ -360,39 +368,65 @@ the temporary buffer and returns its result" ;; TODO javaimp-maven-process-projects ;; ;; TODO set everything immediately + ;; + ;; dep-jars are initialized lazily on request ) +;; TODO remove (defun javaimp-maven-process-projects (projects-elts) (mapcar (lambda (project-elt) - (let ((build-elt (javaimp-xml-child 'build project-elt))) + (let ((build-elt (javaimp--xml-child 'build project-elt))) (javaimp-make-mod - (javaimp--maven-extract-id project-elt) + (javaimp--maven-xml-extract-id project-elt) nil ;pom-file will be set later (file-name-as-directory (javaimp-cygpath-convert-maybe - (javaimp-xml-first-child (javaimp-xml-child 'sourceDirectory build-elt)))) + (javaimp--xml-first-child (javaimp--xml-child 'sourceDirectory build-elt)))) (file-name-as-directory (javaimp-cygpath-convert-maybe - (javaimp-xml-first-child (javaimp-xml-child 'testSourceDirectory build-elt)))) + (javaimp--xml-first-child (javaimp--xml-child 'testSourceDirectory build-elt)))) (file-name-as-directory (javaimp-cygpath-convert-maybe - (javaimp-xml-first-child (javaimp-xml-child 'directory build-elt)))) + (javaimp--xml-first-child (javaimp--xml-child 'directory build-elt)))) nil ;pom-file-mod-ts will be set later nil ;jar-list will be set later - (javaimp--maven-extract-id - (javaimp-xml-child 'parent project-elt)) + (javaimp--maven-xml-extract-id + (javaimp--xml-child 'parent project-elt)) nil ;parent-ts will be set later - (javaimp-xml-first-child (javaimp-xml-child 'finalName build-elt)) + (javaimp--xml-first-child (javaimp--xml-child 'finalName build-elt)) ))) projects-elts)) -(defun javaimp--maven-extract-id (elt) +(defun javaimp--maven-xml-extract-id (elt) (make-javaimp-id - (javaimp-xml-first-child (javaimp-xml-child 'groupId elt)) - (javaimp-xml-first-child (javaimp-xml-child 'artifactId elt)) - (javaimp-xml-first-child (javaimp-xml-child 'version elt)))) + (javaimp--xml-first-child (javaimp--xml-child 'groupId elt)) + (javaimp--xml-first-child (javaimp--xml-child 'artifactId elt)) + (javaimp--xml-first-child (javaimp--xml-child 'version elt)))) + + +;; 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 (javaimp-cygpath-convert-maybe pom-file)) + (status + ;; TODO check in Maven output on Gnu/Linux + (let ((coding-system-for-read + (if (eq system-type 'cygwin) 'utf-8-dos))) + (process-file javaimp-mvn-program nil t nil "-f" pom-file target))) + (b (current-buffer))) + (with-current-buffer (get-buffer-create javaimp-debug-buf-name) + (erase-buffer) + (insert-buffer-substring b)) + (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) (let ((this (or (javaimp--find-by-id id all-modules) @@ -432,12 +466,13 @@ the temporary buffer and returns its result" parent-rel-paths))) (or (seq-find (lambda (file) + ;; TODO move xml subroutine out (let* ((xml-tree (with-temp-buffer (insert-file-contents file) (xml-parse-region (point-min) (point-max)))) (project-elt (assq 'project xml-tree)) - (id (javaimp--maven-extract-id project-elt)) - (parent-id (javaimp--maven-extract-id (assq 'parent project-elt)))) + (id (javaimp--maven-xml-extract-id project-elt)) + (parent-id (javaimp--maven-xml-extract-id (assq 'parent project-elt)))) ;; TODO we need lax matching because some id ;; components may be missing (and (equal (javaimp-module-id module) id) @@ -445,137 +480,118 @@ the temporary buffer and returns its result" files) (error "Cannot find file for module: %s" (javaimp-module-id module)))) -(defun javaimp--find-by-id (id modules) - ;; TODO seq-find - ) + +;;; 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))) + + +;; Working with jar classes + +(defun javaimp--get-jar-classes (file) + (let ((cached (car (assoc file javaimp-cached-jars)))) + (cond ((null cached) + ;; create, load & put into cache + (setq cached + (make-javaimp-cached-jar + file (javaimp--get-file-ts file) (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) (javaimp--fetch-jar-classes file)) + ;; update read-ts + (setf (javaimp-cached-jar-read-ts) (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 + "-t" "-f" (javaimp-cygpath-convert-maybe file 'windows)) + (goto-char (point-min)) + (keep-lines "^[^$]+\\.class$") + (goto-char (point-min)) + (replace-string "/" ".") + (goto-char (point-min)) + (let (result) + (while (not eobp) + (push (thing-at-point 'line) result) + (forward-line)) + result)))) + +;; Searching and navigating through projects -(defun javaimp--find-by-parent-id (parent-id modules) - ;; TODO seq-find +(defun javaimp--find-by-id (id) + ;; TODO call javaimp-find-modules; check that 1 result (or return first and write warning) ) - -;;; Working with module dependency JARs +(defun javaimp--find-by-parent-id (parent-id) + ;; TODO call javaimp-find-modules; check that 1 result + ) -(defun javaimp-maven-fetch-module-deps (module) - "Returns list of dependency jars for MODULE" - (javaimp--maven-call - (javaimp-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--find-by-source-file (file) + ;; todo check that 1 result + ) -(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-deps-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-mod-pom-file module) - (javaimp-mod-pom-mod-ts module)) - (when parent - (cons - (javaimp-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-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-mod-pom-file module))) - (when parent - (javaimp-set-mod-parent-ts - module (javaimp-get-file-ts (javaimp-mod-pom-file parent))))) - (javaimp-mod-pom-deps module)) - -(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\\'")))) - -;; Working with jar and its cache - -(defun javaimp-fetch-jar-classes (jar) - (let ((jar-file (javaimp-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)) - (goto-char (point-min)) - (while (re-search-forward "^\\(.+\\)\\.class$" nil t) - (push (replace-regexp-in-string "[/$]" "." (match-string 1)) - result)) - result))) - -(defun javaimp-get-jar-classes-cached (jar) - (let ((current-jar-mod-ts - (nth 5 (file-attributes (javaimp-jar-path jar))))) - (unless (equal (float-time (javaimp-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-jar-classes-list jar))) - -(defun javaimp-collect-classes (jar-paths) - (let (result jar) - (dolist (jar-path jar-paths result) - (setq jar (assoc jar-path javaimp-jar-cache)) - (unless jar - (setq jar (javaimp-make-jar jar-path nil nil)) - (push jar javaimp-jar-cache)) - (setq result (append (javaimp-get-jar-classes-cached jar) result))))) +(defun javaimp-find-modules (predicate) + (javaimp--find-modules-forest javaimp-project-forest predicate)) -;; Searching and navigating through projects +(defun javaimp--find-modules-forest (forest predicate) + (apply #'seq-concatenate 'list + (mapcar (lambda (tree) + (javaimp--find-modules-tree tree predicate)) + forest))) -(defun javaimp-find-module (predicate) - (javaimp--find-module javaimp-project-forest predicate)) +(defun javaimp--find-modules-tree (tree predicate) + ;; TODO + ) -(defun javaimp--find-module (projects predicate) - (if projects - (let ((project (car projects))) - (or (javaimp--get-module (cdr project) predicate) - (javaimp--find-module (cdr projects) predicate))))) -(defun javaimp--get-module (modules predicate) - (if modules - (let ((module (car modules))) - (if (funcall predicate module) - module - (javaimp--get-module (cdr modules) predicate))))) ;;; Some functions for use in other programs +;; TODO remove (defun javaimp-get-source-directories () (append (mapcar (lambda (root) @@ -583,16 +599,18 @@ MODULE." javaimp-project-forest))) (defun javaimp-get-all-modules () - "Returns flat list of all child modules." + "Returns list of all modules" (apply #'seq-concatenate 'list (mapcar #'cdr javaimp-project-forest))) -;;; Adding and organizing imports +;;; Adding imports ;; TODO without prefix arg narrow alternatives by local name; with prefix ;; arg - include all classes in alternatives +;; TODO search only modules with packaging != pom + ;;;###autoload (defun javaimp-add-import (classname) "Imports CLASSNAME in the current file. Interactively, @@ -608,20 +626,25 @@ module." (or (string-prefix-p (javaimp-mod-source-dir m) file) (string-prefix-p (javaimp-mod-test-source-dir m) file)))) (error "Cannot find module by file: %s" file)))) + (javaimp--maven-update-module-maybe module) (list (completing-read "Import: " (append - (let ((jars (append (javaimp-get-deps-cached module) - (javaimp-get-jdk-jars)))) - (javaimp-collect-classes jars)) + ;; 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-classes-from-source module))) + (javaimp--classes-from-source module))) nil t nil nil (symbol-name (symbol-at-point)))))) (javaimp-organize-imports classname)) -(defun javaimp-classes-from-source (module) - "Scans current project and returns a list of top-level classes in both the -source directory and test source directory" +(defun javaimp--classes-from-source (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-mod-source-dir module)) (test-src-dir (javaimp-mod-test-source-dir module)) (build-dir (javaimp-mod-build-dir module))) @@ -658,31 +681,8 @@ initial package prefix." (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))) + +;; Organizing imports ;;;###autoload (defun javaimp-organize-imports (&rest new-classes) @@ -739,18 +739,48 @@ argument is a list of additional classes to import." (javaimp-insert-import-groups static-import-groups t)) (message "Nothing to organize"))))) +(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))) + + + +;; Interactive functions for debugging + (defun javaimp-invalidate-jar-classes-cache () - "Resets jar classes cache (debugging only)" + "Resets jar classes cache" (interactive) (setq javaimp-jar-cache nil)) (defun javaimp-forget-all-visited-modules () - "Resets `javaimp-project-forest' (debugging only)" + "Resets `javaimp-project-forest'" (interactive) (setq javaimp-project-forest nil)) (defun javaimp-reset () - "Resets all data (debugging only)" + "Resets all data" (interactive) (javaimp-forget-all-visited-modules) (javaimp-invalidate-jar-classes-cache))