]> code.delx.au - gnu-emacs-elpa/blob - packages/javaimp/javaimp.el
javaimp: doc fixes
[gnu-emacs-elpa] / packages / javaimp / javaimp.el
1 ;;; javaimp.el --- Add and reorder Java import statements in Maven projects -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2014 Free Software Foundation, Inc.
4
5 ;; Author: Filipp Gunbin <fgunbin@fastmail.fm>
6 ;; Maintainer: Filipp Gunbin <fgunbin@fastmail.fm>
7 ;; Version: 0.5
8 ;; Keywords: java, maven, programming
9
10 ;;; Commentary:
11
12 ;; Allows to manage Java import statements in Maven projects.
13 ;;
14 ;; Quick start: customize `javaimp-import-group-alist', `javaimp-jdk-home'
15 ;; and call `javaimp-maven-visit-root', then in a Java buffer visiting a
16 ;; file under that module or one of its submodules call
17 ;; `javaimp-organize-imports' or `javaimp-add-import'. `javaimp-add-import'
18 ;; will provide you a helpful completion, and the default value (the one
19 ;; you'll get if you hit `M-n' in the minibuffer) is the symbol under point,
20 ;; so usually it's enough to hit `M-n', then add some starting letters of a
21 ;; package and hit `TAB'. The module does not add all needed imports
22 ;; automatically! It only helps you to quickly add imports when stepping
23 ;; through compilation errors.
24 ;;
25 ;; If Maven failed, you can see its output in the buffer named by
26 ;; `javaimp-debug-buf-name' (default is "*javaimp-debug*").
27 ;;
28 ;; Contents of jar files and Maven project structures (pom.xml) are cached,
29 ;; so usually only first command should take a considerable amount of time
30 ;; to complete. When it is detected that a particular jar or pom.xml file's
31 ;; timestamp changed, it is re-read and cache is updated.
32 ;;
33 ;; Details on variables.
34 ;;
35 ;; `javaimp-import-group-alist' defines the order of import statement
36 ;; groups. By default java.* and javax.* imports are assigned an order of
37 ;; 10, which is low, so it puts those imports at the beginning. Your
38 ;; project's imports typically should come after, so the sample config below
39 ;; sets 80 for them.
40 ;;
41 ;; `javaimp-jdk-home' is a path for JDK. It is used to scan JDK jars.
42 ;; Usually you will need to set this.
43 ;;
44 ;; `javaimp-mvn-program' defines path of the `mvn' program. Use if it's
45 ;; not on `exec-path'.
46 ;;
47 ;; `javaimp-cygpath-program' defines path of the `cygpath' program (applies
48 ;; to Cygwin only, of course). Use if it's not on `exec-path'.
49 ;;
50 ;; `javaimp-jar-program' defines path of the `jar' program. Use if it's
51 ;; not on `exec-path'.
52 ;;
53 ;; Details on commands.
54 ;;
55 ;; `javaimp-maven-visit-root' is the first command you should issue to
56 ;; use this module. It reads the pom structure recursively and records
57 ;; which files belong to which module. Maven help:effective-pom command is
58 ;; used to do that.
59 ;;
60 ;; `javaimp-organize-imports' groups import statement and writes those
61 ;; group according to the value of `javaimp-import-group-alist'. Imports
62 ;; which are not matched by any regexp in that variable are assigned a
63 ;; default order defined by `javaimp-import-default-order' (50 by default).
64 ;;
65 ;; Sample setup (put this into your .emacs):
66 ;;
67 ;; (require 'javaimp)
68 ;; (add-to-list
69 ;; 'javaimp-import-group-alist '("\\`\\(ru\\.yota\\.\\|tv\\.okko\\.\\)" . 80))
70 ;; (setq javaimp-jdk-home "/opt/java")
71 ;; (add-hook 'java-mode-hook
72 ;; (lambda ()
73 ;; (local-set-key "\C-ci" 'javaimp-add-import)
74 ;; (local-set-key "\C-co" 'javaimp-organize-imports)))
75 ;;
76 ;;
77 ;; TODO:
78 ;;
79 ;; Support adding static imports by giving a prefix argument to
80 ;; `javaimp-add-import'.
81 ;;
82 ;; Use functions `cygwin-convert-file-name-from-windows' and
83 ;; `cygwin-convert-file-name-to-windows' when they are available instead of
84 ;; calling `cygpath'. See
85 ;; https://cygwin.com/ml/cygwin/2013-03/msg00228.html.
86
87 ;;; Code:
88
89 \f
90 ;;; User options
91
92 (defcustom javaimp-import-group-alist '(("\\`javax?\\." . 10))
93 "Specifies how to group classes and how to order resulting groups in the
94 imports list. Each element should be of the form `(CLASSNAME-REGEXP
95 . ORDER)' where `CLASSNAME-REGEXP' is a regexp matching the fully qualified
96 class name. The order of classes which were not matched is defined by
97 `javaimp-import-default-order'.")
98
99 (defcustom javaimp-import-default-order 50
100 "Defines the order of classes which were not matched by
101 `javaimp-import-group-alist'")
102
103 (defcustom javaimp-jdk-home nil
104 "Path to the JDK")
105
106 (defcustom javaimp-mvn-program "mvn"
107 "Path to the `mvn' program")
108
109 (defcustom javaimp-cygpath-program "cygpath"
110 "Path to the `cygpath' program")
111
112 (defcustom javaimp-jar-program "jar"
113 "Path to the `jar' program")
114
115 (defcustom javaimp-include-current-project-classes t
116 "If non-nil, current project's classes are included into
117 completion alternatives. Only top-level classes are included.")
118
119 \f
120 ;;; Variables and constants
121
122 (defvar javaimp-maven-root-modules nil
123 "Loaded root Maven modules")
124
125 (defvar javaimp-jar-classes-cache nil
126 "Jar classes cache")
127
128 (defconst javaimp-debug-buf-name "*javaimp-debug*")
129
130 \f
131 ;;; Dealing with XML
132
133 (defun javaimp-xml-child-list (xml-tree child-name)
134 "Returns list of children of XML-TREE filtered by CHILD-NAME."
135 (let (result)
136 (dolist (child (cddr xml-tree) result)
137 (when (and (listp child)
138 (eq (car child) child-name))
139 (push child result)))))
140
141 \f
142 ;; A module is represented as a list of the form `(ARTIFACT-ID POM-FILE
143 ;; SOURCE-DIR TEST-SOURCE-DIR POM-FILE-MOD-TS JARS-LIST)'.
144
145 (defsubst javaimp-make-mod (artifact-id pom-file source-dir test-source-dir
146 pom-file-mod-ts jars-list)
147 (list artifact-id pom-file source-dir test-source-dir
148 pom-file-mod-ts jars-list))
149
150 (defsubst javaimp-get-mod-artifact-id (module)
151 (nth 0 module))
152
153 (defsubst javaimp-get-mod-pom-file (module)
154 (nth 1 module))
155
156 (defsubst javaimp-get-mod-source-dir (module)
157 (nth 2 module))
158
159 (defsubst javaimp-get-mod-test-source-dir (module)
160 (nth 3 module))
161
162 (defsubst javaimp-get-mod-pom-mod-ts (module)
163 (nth 4 module))
164 (defsubst javaimp-set-mod-pom-mod-ts (module value)
165 (setcar (nthcdr 4 module) value))
166
167 (defsubst javaimp-get-mod-pom-deps (module)
168 (nth 5 module))
169 (defsubst javaimp-set-mod-pom-deps (module value)
170 (setcar (nthcdr 5 module) value))
171
172 \f
173 ;; A jar is represented as follows: `(JAR-PATH JAR-MOD-TS . CLASSES-LIST).
174
175 (defsubst javaimp-make-jar (jar-path jar-mod-ts classes-list)
176 (cons jar-path (cons jar-mod-ts classes-list)))
177
178 (defsubst javaimp-get-jar-path (jar)
179 (car jar))
180
181 (defsubst javaimp-get-jar-mod-ts (jar)
182 (cadr jar))
183
184 (defsubst javaimp-set-jar-mod-ts (jar value)
185 (setcar (cdr jar) value))
186
187 (defsubst javaimp-get-jar-classes-list (jar)
188 (cddr jar))
189
190 (defsubst javaimp-set-jar-classes-list (jar value)
191 (setcdr (cdr jar) value))
192
193 \f
194 ;;; Loading maven projects tree
195
196 ;;;###autoload
197 (defun javaimp-maven-visit-root (path)
198 "Loads all modules starting from root module identified by
199 PATH. PATH should point to a directory."
200 (interactive "DVisit maven root project: ")
201 (let ((root-pom (expand-file-name
202 (concat (file-name-as-directory path) "pom.xml")))
203 modules existing-module)
204 (unless (file-readable-p root-pom)
205 (error "Cannot read root pom: %s" root-pom))
206 (setq modules (javaimp-maven-load-module-tree root-pom))
207 ;; if a root module with such path is already loaded, replace its
208 ;; modules
209 (setq existing-module (assoc root-pom javaimp-maven-root-modules))
210 (if existing-module
211 (setcdr existing-module modules)
212 (push (cons root-pom modules) javaimp-maven-root-modules))
213 (message "Loaded modules for %s" path)))
214
215 (defun javaimp-maven-load-module-tree (pom)
216 "Returns an alist of all Maven modules in a hierarchy starting
217 with POM"
218 (message "Loading root pom %s..." pom)
219 (javaimp-call-mvn
220 pom "help:effective-pom"
221 (lambda ()
222 (let (xml-start-pos xml-end-pos project-extractor)
223 (goto-char (point-min))
224 (re-search-forward "<\\?xml\\|<project")
225 (setq xml-start-pos (match-beginning 0))
226 ;; build module tree
227 (setq project-extractor
228 (cond ((search-forward "</projects>" nil t)
229 ;; returns all <project> nodes below <projects>
230 (lambda (xml-tree)
231 (javaimp-xml-child-list (assq 'projects xml-tree) 'project)))
232 ((search-forward "</project>" nil t)
233 ;; returns a list with a single <project> as the sole element
234 (lambda (xml-tree)
235 (list (assq 'project xml-tree))))
236 (t (error "Cannot find projects in mvn output"))))
237 (setq xml-end-pos (match-end 0))
238 (javaimp-maven-build-module-tree
239 (funcall project-extractor (xml-parse-region xml-start-pos xml-end-pos))
240 (javaimp-build-artifact-pomfile-alist (list pom)))))))
241
242 (defun javaimp-maven-build-module-tree (projects artifact-pomfile-alist)
243 (let (result)
244 (dolist (proj projects result)
245 (let* ((artifact-id (car (cddr (assq 'artifactId (cddr proj)))))
246 (pom-file-path (cdr (assoc artifact-id artifact-pomfile-alist)))
247 (source-dir (car (cddr (assq 'sourceDirectory
248 (cddr (assq 'build (cddr proj)))))))
249 (test-source-dir (car (cddr (assq 'testSourceDirectory
250 (cddr (assq 'build (cddr proj))))))))
251 (push (javaimp-make-mod
252 artifact-id pom-file-path
253 (file-name-as-directory
254 (if (eq system-type 'cygwin)
255 (car (process-lines javaimp-cygpath-program "-u"
256 source-dir))
257 source-dir))
258 (file-name-as-directory
259 (if (eq system-type 'cygwin)
260 (car (process-lines javaimp-cygpath-program "-u"
261 test-source-dir))
262 test-source-dir))
263 nil nil)
264 result)))))
265
266 (defun javaimp-build-artifact-pomfile-alist (pom-file-list)
267 "Recursively builds an alist where each element is of the
268 form (\"ARTIFACT-ID\" . \"POM-FILE-PATH\"). This is needed
269 because there is no pom file path in the output of `mvn
270 help:effective-pom'. Each pom file path in POM-FILE-LIST should
271 be in platform's default format."
272 (when pom-file-list
273 (let ((pom-file (car pom-file-list))
274 xml-tree project)
275 (message "Saving artifact id -> pom file mapping for %s" pom-file)
276 (with-temp-buffer
277 (insert-file-contents pom-file)
278 (setq xml-tree (xml-parse-region (point-min) (point-max))))
279 (setq project (if (assq 'top xml-tree)
280 (assq 'project (cddr (assq 'top xml-tree)))
281 (assq 'project xml-tree)))
282 (cons
283 ;; this pom
284 (cons (car (cddr (assq 'artifactId (cddr project)))) pom-file)
285 (append
286 ;; submodules
287 (javaimp-build-artifact-pomfile-alist
288 (mapcar (lambda (submodule)
289 (expand-file-name
290 (concat
291 ;; this pom's path
292 (file-name-directory pom-file)
293 ;; relative submodule directory
294 (file-name-as-directory
295 (let ((submodule-path (car (cddr submodule))))
296 (if (eq system-type 'cygwin)
297 (car (process-lines javaimp-cygpath-program "-u"
298 submodule-path))
299 submodule-path)))
300 ;; well-known file name
301 "pom.xml")))
302 (javaimp-xml-child-list (assq 'modules (cddr project)) 'module)))
303 ;; rest items
304 (javaimp-build-artifact-pomfile-alist (cdr pom-file-list)))))))
305
306 (defun javaimp-call-mvn (pom-file target handler)
307 "Runs Maven target TARGET on POM-FILE, then calls HANDLER in
308 the temporary buffer and returns its result"
309 (message "Calling \"mvn %s\" on pom: %s" target pom-file)
310 (with-temp-buffer
311 (let* ((pom-file (if (eq system-type 'cygwin)
312 (car (process-lines javaimp-cygpath-program
313 "-m" pom-file))
314 pom-file))
315 (status
316 ;; FIXME on GNU/Linux Maven strangely outputs ^M chars. Check
317 ;; also jar output with the same var binding below.
318 (let ((coding-system-for-read (when (eq system-type 'cygwin) 'utf-8-dos)))
319 (process-file javaimp-mvn-program nil t nil "-f" pom-file target)))
320 (output-buf (current-buffer)))
321 (with-current-buffer (get-buffer-create javaimp-debug-buf-name)
322 (erase-buffer)
323 (insert-buffer-substring output-buf))
324 (unless (and (numberp status) (= status 0))
325 (error "Maven target \"%s\" failed with status \"%s\""
326 target status))
327 (funcall handler))))
328
329 \f
330 ;;; Reading and caching dependencies
331
332 (defun javaimp-maven-fetch-module-deps (module)
333 "Returns list of dependency jars for MODULE"
334 (javaimp-call-mvn
335 (javaimp-get-mod-pom-file module) "dependency:build-classpath"
336 (lambda ()
337 (let (deps-line)
338 (goto-char (point-min))
339 (search-forward "Dependencies classpath:")
340 (forward-line 1)
341 (setq deps-line (thing-at-point 'line))
342 (when (eq system-type 'cygwin)
343 (setq deps-line (car (process-lines javaimp-cygpath-program
344 "-up"
345 deps-line))))
346 (split-string deps-line (concat "[" path-separator "\n" "]+") t)))))
347
348 (defun javaimp-get-dep-jars-cached (module)
349 "Returns a list of dependency jar file paths for a MODULE"
350 (let ((current-pom-file-mod-ts
351 (nth 5 (file-attributes (javaimp-get-mod-pom-file module)))))
352 (unless (and (javaimp-get-mod-pom-mod-ts module)
353 (equal (float-time (javaimp-get-mod-pom-mod-ts module))
354 (float-time current-pom-file-mod-ts)))
355 ;; cache entry does not exist or is invalid - refresh it
356 (javaimp-set-mod-pom-deps module (javaimp-maven-fetch-module-deps module))
357 (javaimp-set-mod-pom-mod-ts module current-pom-file-mod-ts))
358 (javaimp-get-mod-pom-deps module)))
359
360 (defun javaimp-get-jdk-jars ()
361 "Returns list of jars from the jre/lib subdirectory of the JDK
362 directory"
363 (when javaimp-jdk-home
364 (directory-files (concat (file-name-as-directory javaimp-jdk-home)
365 (file-name-as-directory "jre/lib"))
366 t "\\.jar$")))
367
368
369 (defun javaimp-get-jar-classes-cached (jar)
370 (let ((current-jar-mod-ts
371 (nth 5 (file-attributes (javaimp-get-jar-path jar)))))
372 (unless (equal (float-time (javaimp-get-jar-mod-ts jar))
373 (float-time current-jar-mod-ts))
374 (javaimp-set-jar-classes-list jar (javaimp-fetch-jar-classes jar))
375 (javaimp-set-jar-mod-ts jar current-jar-mod-ts))
376 (javaimp-get-jar-classes-list jar)))
377
378 (defun javaimp-fetch-jar-classes (jar)
379 (let ((jar-file (javaimp-get-jar-path jar))
380 result)
381 (message "Reading classes in jar: %s" jar-file)
382 (with-temp-buffer
383 (let ((jar-file (if (eq system-type 'cygwin)
384 (car (process-lines javaimp-cygpath-program
385 "-m" jar-file))
386 jar-file))
387 (coding-system-for-read (when (eq system-type 'cygwin) 'utf-8-dos)))
388 (process-file javaimp-jar-program nil t nil "-tf" jar-file))
389 (goto-char (point-min))
390 (while (re-search-forward "^\\(.+\\)\\.class$" nil t)
391 (push (replace-regexp-in-string "[/$]" "." (match-string 1))
392 result))
393 result)))
394
395 (defun javaimp-collect-jar-classes (jar-paths)
396 (let (result jar)
397 (dolist (jar-path jar-paths result)
398 (setq jar (assoc jar-path javaimp-jar-classes-cache))
399 (unless jar
400 (setq jar (javaimp-make-jar jar-path nil nil))
401 (push jar javaimp-jar-classes-cache))
402 (setq result (append (javaimp-get-jar-classes-cached jar) result)))))
403
404 (defun javaimp-determine-module (file)
405 "Returns a module in which the source file FILE resides"
406 (let ((root-modules javaimp-maven-root-modules)
407 result)
408 (while (and root-modules (not result))
409 (setq result (javaimp-determine-module-from-root file (car root-modules)))
410 (setq root-modules (cdr root-modules)))
411 result))
412
413 (defun javaimp-determine-module-from-root (file root-module)
414 "Searches a hierarchy of modules starting at ROOT-MODULE for
415 source file FILE"
416 (let ((modules (cdr root-module))
417 result)
418 (while (and modules (not result))
419 (if (or (string-prefix-p (javaimp-get-mod-source-dir (car modules)) file)
420 (string-prefix-p (javaimp-get-mod-test-source-dir (car modules)) file))
421 (setq result (car modules)))
422 (setq modules (cdr modules)))
423 result))
424
425 \f
426 ;;; Adding and organizing imports
427
428 ;;;###autoload
429 (defun javaimp-add-import (classname)
430 "Imports CLASSNAME in the current file. Interactively,
431 performs class name completion based on the current module's
432 dependencies, JDK jars and top-level classes in the current
433 module."
434 (interactive
435 (let* ((file (expand-file-name
436 (or buffer-file-name
437 (error "Buffer is not visiting a file!"))))
438 (module (or (javaimp-determine-module file)
439 (error "Cannot determine module for file: %s" file))))
440 (list (completing-read
441 "Import: "
442 (append
443 (javaimp-collect-jar-classes
444 (append (javaimp-get-dep-jars-cached module)
445 (javaimp-get-jdk-jars)))
446 (and javaimp-include-current-project-classes
447 (javaimp-get-module-classes module)))
448 nil t nil nil (word-at-point)))))
449 (javaimp-organize-imports classname))
450
451 (defun javaimp-get-module-classes (module)
452 "Scans current project and returns a list of top-level classes in both the
453 source directory and test source directory"
454 (let ((src-dir (javaimp-get-mod-source-dir module))
455 (test-src-dir (javaimp-get-mod-test-source-dir module)))
456 (append (and (file-accessible-directory-p src-dir)
457 (javaimp-get-directory-classes src-dir nil))
458 (and (file-accessible-directory-p test-src-dir)
459 (javaimp-get-directory-classes test-src-dir nil)))))
460
461 (defun javaimp-get-directory-classes (dir prefix)
462 "Returns the list of classes found in the directory DIR. PREFIX is the
463 initial package prefix."
464 (let (result)
465 ;; traverse subdirectories
466 (dolist (file (directory-files-and-attributes dir nil nil t))
467 (if (and (eq (cadr file) t)
468 (not (or (string= (car file) ".")
469 (string= (car file) ".."))))
470 (setq result
471 (append (javaimp-get-directory-classes
472 (concat dir (file-name-as-directory (car file)))
473 (concat prefix (car file) "."))
474 result))))
475 ;; add .java files in the current directory
476 (dolist (file (directory-files-and-attributes dir nil "\\.java\\'" t))
477 (unless (cadr file)
478 (push (concat prefix (file-name-sans-extension (car file))) result)))
479 result))
480
481 (defun javaimp-add-to-import-groups (new-class groups)
482 "Subroutine of `javaimp-organize-imports'"
483 (let* ((order (or (assoc-default new-class javaimp-import-group-alist
484 'string-match)
485 javaimp-import-default-order))
486 (group (assoc order groups)))
487 (if group
488 (progn
489 ;; add only if this class is not already there
490 (unless (member new-class (cdr group))
491 (setcdr group (cons new-class (cdr group))))
492 groups)
493 (cons (cons order (list new-class)) groups))))
494
495 (defun javaimp-insert-import-groups (groups static-p)
496 "Inserts all imports in GROUPS. Non-nil STATIC-P means that
497 all imports are static."
498 (when groups
499 (dolist (group (sort groups (lambda (g1 g2)
500 (< (car g1) (car g2)))))
501 (dolist (class (sort (cdr group) 'string<))
502 (insert (concat "import " (when static-p "static ") class ";\n")))
503 (insert ?\n))
504 ;; remove newline after the last group
505 (delete-char -1)))
506
507 ;;;###autoload
508 (defun javaimp-organize-imports (&rest new-classes)
509 "Groups and orders import statements in the current buffer. Groups are
510 formed and ordered according to `javaimp-import-group-alist'. Classes within a
511 single group are ordered in a lexicographic order. Optional NEW-CLASSES
512 argument is a list of additional classes to import."
513 (interactive)
514 (barf-if-buffer-read-only)
515 (save-excursion
516 (let ((kill-whole-line t)
517 import-groups static-import-groups old-imports-start)
518 ;; existing imports
519 (goto-char (point-min))
520 (while (re-search-forward
521 "^\\s-*import\\s-+\\(static\\s-+\\)?\\([._[:word:]]+\\)"
522 nil t)
523 (if (null (match-string 1))
524 (setq import-groups
525 (javaimp-add-to-import-groups (match-string 2)
526 import-groups))
527 (setq static-import-groups
528 (javaimp-add-to-import-groups (match-string 2)
529 static-import-groups)))
530 (beginning-of-line)
531 (unless old-imports-start (setq old-imports-start (point)))
532 (kill-line)
533 ;; delete whatever happened to be between import statements
534 (when (not (equal (point) old-imports-start))
535 (delete-region old-imports-start (point))))
536 ;; new imports
537 (dolist (class new-classes)
538 (setq import-groups (javaimp-add-to-import-groups class import-groups)))
539 ;; insert all
540 (if (or import-groups static-import-groups)
541 (progn
542 ;; prepare the position
543 (cond (old-imports-start
544 ;; here we do not mangle with empty lines at all
545 (goto-char old-imports-start))
546 ((re-search-forward "^\\s-*package\\s-" nil t)
547 ;; try to preserve all empty lines (if any) before the
548 ;; following text
549 (when (equal (forward-line) 1) (insert ?\n)) ;; last line?
550 (insert ?\n))
551 (t
552 ;; start from the bob; add one line after the insert pos
553 (goto-char (point-min))
554 (insert ?\n)
555 (backward-char)))
556 (javaimp-insert-import-groups import-groups nil)
557 (and import-groups static-import-groups (insert ?\n))
558 (javaimp-insert-import-groups static-import-groups t))
559 (message "Nothing to organize")))))
560
561 ;;;###autoload
562 (defun javaimp-invalidate-jar-classes-cache ()
563 "Resets jar classes cache (debugging only)"
564 (interactive)
565 (setq javaimp-jar-classes-cache nil))
566
567 ;;;###autoload
568 (defun javaimp-forget-all-visited-modules ()
569 "Resets `javaimp-maven-root-modules' (debugging only)"
570 (interactive)
571 (setq javaimp-maven-root-modules nil))
572
573 ;;;###autoload
574 (defun javaimp-reset ()
575 "Resets all data (debugging only)"
576 (interactive)
577 (javaimp-forget-all-visited-modules)
578 (javaimp-invalidate-jar-classes-cache))
579
580 (provide 'javaimp)
581
582 ;;; javaimp.el ends here