]> code.delx.au - gnu-emacs-elpa/blob - packages/javaimp/javaimp.el
Merge commit '97eaa5778ce0cd596a0807ef2e676d2681aabf84'
[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, 2015, 2016 Free Software Foundation, Inc.
4
5 ;; Author: Filipp Gunbin <fgunbin@fastmail.fm>
6 ;; Maintainer: Filipp Gunbin <fgunbin@fastmail.fm>
7 ;; Version: 0.6
8 ;; Keywords: java, maven, programming
9
10 ;;; Commentary:
11
12 ;; Allows to manage Java import statements in Maven projects.
13 ;;
14 ;; Quick start:
15 ;;
16 ;; - customize `javaimp-import-group-alist'
17 ;; - call `javaimp-maven-visit-project', giving it the top-level project
18 ;; directory where pom.xml resides
19 ;;
20 ;; Then in a Java buffer visiting a file under that project or one of its
21 ;; submodules call `javaimp-organize-imports' or `javaimp-add-import'.
22 ;;
23 ;; This module does not add all needed imports automatically! It only helps
24 ;; you to quickly add imports when stepping through compilation errors.
25 ;;
26 ;; Some details:
27 ;;
28 ;; If Maven failed, you can see its output in the buffer named by
29 ;; `javaimp-debug-buf-name' (default is "*javaimp-debug*").
30 ;;
31 ;; Contents of jar files and Maven project structures (pom.xml) are cached,
32 ;; so usually only first command should take a considerable amount of time
33 ;; to complete. If a modules's pom.xml or any of its parents' pom.xml was
34 ;; changed (i.e. any of them was modified after information was loaded),
35 ;; `mvn dependency:build-classpath' is re-run on the current module. If a
36 ;; jar file was changed, its contents are re-read.
37 ;;
38 ;; If you make some changes which change project hierarchy, you should
39 ;; re-visit the parent again with `javaimp-maven-visit-project'.
40 ;;
41 ;; Currently inner classes are filtered out from completion alternatives.
42 ;; You can always import top-level class and use qualified name.
43 ;;
44 ;;
45 ;; Example of initialization:
46 ;;
47 ;; (require 'javaimp)
48 ;;
49 ;; (add-to-list 'javaimp-import-group-alist
50 ;; '("\\`\\(my\\.company\\.\\|my\\.company2\\.\\)" . 80))
51 ;;
52 ;; (setq javaimp-additional-source-dirs '("generated-sources/thrift"))
53 ;;
54 ;; (add-hook 'java-mode-hook
55 ;; (lambda ()
56 ;; (local-set-key "\C-ci" 'javaimp-add-import)
57 ;; (local-set-key "\C-co" 'javaimp-organize-imports)))
58 ;;
59 ;;
60 ;; TODO:
61 ;;
62 ;; - use functions `cygwin-convert-file-name-from-windows' and
63 ;; `cygwin-convert-file-name-to-windows' when they are available instead of
64 ;; calling `cygpath'. See https://cygwin.com/ml/cygwin/2013-03/msg00228.html.
65 ;; - save/restore state
66 ;; - `javaimp-add-import': without prefix arg narrow alternatives by local name;
67 ;; with prefix arg include all classes in alternatives
68 ;; - types for defcustom
69
70 ;;; Code:
71
72 (require 'cl-lib)
73 (require 'seq)
74 (require 'xml)
75
76 \f
77 ;; User options
78
79 (defgroup javaimp ()
80 "Add and reorder Java import statements in Maven projects")
81
82 (defcustom javaimp-import-group-alist '(("\\`javax?\\." . 10))
83 "Specifies how to group classes and how to order resulting
84 groups in the imports list.
85
86 Each element should be of the form `(CLASSNAME-REGEXP . ORDER)'
87 where `CLASSNAME-REGEXP' is a regexp matching the fully qualified
88 class name. Lowest-order groups are placed earlier.
89
90 The order of classes which were not matched is defined by
91 `javaimp-import-default-order'.")
92
93 (defcustom javaimp-import-default-order 50
94 "Defines the order of classes which were not matched by
95 `javaimp-import-group-alist'")
96
97 (defcustom javaimp-jdk-home (getenv "JAVA_HOME")
98 "Path to the JDK. It is used to find JDK jars to scan. By
99 default, it is set from the JAVA_HOME environment variable.")
100
101 (defcustom javaimp-additional-source-dirs nil
102 "List of directories where additional (e.g. generated)
103 source files reside.
104
105 Each directory is a relative path from ${project.build.directory} project
106 property value.
107
108 Typically you would check documentation for a Maven plugin, look
109 at the parameter's default value there and add it to this list.
110
111 E.g. \"${project.build.directory}/generated-sources/<plugin_name>\"
112 becomes \"generated-sources/<plugin_name>\" (note the absence
113 of the leading slash.
114
115 Custom values set in plugin configuration in pom.xml are not
116 supported yet.")
117
118 (defcustom javaimp-mvn-program "mvn"
119 "Path to the `mvn' program. Customize it if the program is not
120 on `exec-path'.")
121
122 (defcustom javaimp-cygpath-program
123 (if (eq system-type 'cygwin) "cygpath")
124 "Path to the `cygpath' program (Cygwin only). Customize it if
125 the program is not on `exec-path'.")
126
127 (defcustom javaimp-jar-program "jar"
128 "Path to the `jar' program used to read contents of jar files.
129 Customize it if the program is not on `exec-path'.")
130
131 (defcustom javaimp-include-current-module-classes t
132 "If non-nil, current module's classes are included into
133 completion alternatives. `javaimp-add-import' will find all java
134 files in the current project and add their fully-qualified names
135 to the completion alternatives list.")
136
137 \f
138 ;; Variables and constants
139
140 (defvar javaimp-project-forest nil
141 "Visited projects.")
142
143 (defvar javaimp-cached-jars nil
144 "Alist of cached jars. Each element is of the form (FILE
145 . CACHED-JAR).")
146
147 (defconst javaimp-debug-buf-name "*javaimp-debug*")
148
149 ;; Structs
150
151 (cl-defstruct javaimp-node
152 parent children contents)
153
154 (cl-defstruct javaimp-module
155 id parent-id
156 file
157 final-name
158 packaging
159 source-dir test-source-dir build-dir
160 modules
161 dep-jars
162 load-ts)
163
164 (cl-defstruct javaimp-id
165 group artifact version)
166
167 (cl-defstruct javaimp-cached-jar
168 file read-ts classes)
169
170 \f
171 ;; Utilities
172
173 (defun javaimp--xml-children (xml-tree child-name)
174 "Returns list of children of XML-TREE filtered by CHILD-NAME"
175 (seq-filter (lambda (child)
176 (and (consp child)
177 (eq (car child) child-name)))
178 (cddr xml-tree)))
179
180 (defun javaimp--xml-child (name el)
181 "Returns a child of EL named by symbol NAME"
182 (assq name (cddr el)))
183
184 (defun javaimp--xml-first-child (el)
185 "Returns a first child of EL"
186 (car (cddr el)))
187
188 (defun javaimp--get-file-ts (file)
189 (nth 5 (file-attributes file)))
190
191 (defun javaimp--get-jdk-jars ()
192 (if javaimp-jdk-home
193 (let ((jre-lib-dir
194 (concat (file-name-as-directory javaimp-jdk-home)
195 (file-name-as-directory "jre")
196 (file-name-as-directory "lib"))))
197 (directory-files jre-lib-dir t "\\.jar\\'"))))
198
199 (defun javaimp-cygpath-convert-maybe (path &optional mode is-really-path)
200 "On Cygwin, converts PATH using cygpath according to MODE and
201 IS-REALLY-PATH. If MODE is `unix' (the default), adds -u switch.
202 If MODE is `windows', adds -m switch. If `is-really-path' is
203 non-nil, adds `-p' switch. On other systems, PATH is returned
204 unchanged."
205 (if (eq system-type 'cygwin)
206 (progn
207 (unless mode (setq mode 'unix))
208 (let (args)
209 (push (cond ((eq mode 'unix) "-u")
210 ((eq mode 'windows) "-m")
211 (t (error "Invalid mode: %s" mode)))
212 args)
213 (and is-really-path (push "-p" args))
214 (push path args)
215 (car (apply #'process-lines javaimp-cygpath-program args))))
216 path))
217
218 \f
219 ;; Project loading
220
221 ;;;###autoload
222 (defun javaimp-maven-visit-project (path)
223 "Loads a project and its submodules. PATH should point to a
224 directory containing pom.xml.
225
226 Calls `mvn help:effective-pom' on the pom.xml in the PATH, reads
227 project structure from the output and records which files belong
228 to which modules and other module information.
229
230 After being processed by this command, the module tree becomes
231 known to javaimp and `javaimp-add-import' maybe called inside any
232 module file."
233 (interactive "DVisit maven project: ")
234 (let ((file (expand-file-name
235 (concat (file-name-as-directory path) "pom.xml"))))
236 (unless (file-readable-p file)
237 (error "Cannot read file: %s" file))
238 ;; delete previous loaded tree, if any
239 (setq javaimp-project-forest
240 (seq-remove (lambda (tree)
241 (equal (javaimp-module-file (javaimp-node-contents tree))
242 file))
243 javaimp-project-forest))
244 (let ((tree (javaimp--maven-xml-load-tree file)))
245 (if tree
246 (push tree javaimp-project-forest)))
247 (message "Loaded tree for %s" file)))
248
249 \f
250 ;; Maven XML routines
251
252 (defun javaimp--maven-xml-load-tree (file)
253 "Invokes `mvn help:effective-pom' on FILE and using its output
254 creates a tree of Maven projects starting from FILE. Children
255 which link to the parent via the <parent> element are inheriting
256 children and are also included. Subordinate modules with no
257 inheritance are not included."
258 (let ((xml-tree (javaimp--maven-xml-read-effective-pom file)))
259 (cond ((assq 'project xml-tree)
260 (let ((project-elt (assq 'project xml-tree))
261 (submodules (javaimp--xml-children
262 (javaimp--xml-child 'modules project-elt) 'module)))
263 (and submodules
264 ;; no real children
265 (message "Independent submodules: %s"
266 (mapconcat #'javaimp--xml-first-child submodules ", ")))
267 (let ((module (javaimp--maven-xml-parse-module project-elt)))
268 (javaimp--maven-build-tree
269 (javaimp-module-id module) nil (list module) file))))
270 ((assq 'projects xml-tree)
271 ;; we have are inheriting children - they and their children, if
272 ;; any, are listed in a linear list
273 (let* ((project-elts (javaimp--xml-children
274 (assq 'projects xml-tree) 'project))
275 (all-modules (mapcar #'javaimp--maven-xml-parse-module project-elts)))
276 (message "Total modules: %d" (length all-modules))
277 (javaimp--maven-build-tree
278 (javaimp-module-id (car all-modules)) nil all-modules file)))
279 (t
280 ;; neither <project> nor <projects> - error
281 (error "Invalid `help:effective-pom' output")))))
282
283 (defun javaimp--maven-xml-read-effective-pom (pom)
284 "Calls `mvn help:effective:pom and returns XML parse tree"
285 (message "Loading root pom %s..." pom)
286 (javaimp--maven-call
287 pom "help:effective-pom"
288 (lambda ()
289 (let ((xml-start-pos
290 (save-excursion
291 (progn
292 (goto-char (point-min))
293 (re-search-forward "<\\?xml\\|<projects?")
294 (match-beginning 0))))
295 (xml-end-pos
296 (save-excursion
297 (progn
298 (goto-char (point-min))
299 (re-search-forward "<\\(projects?\\)")
300 ;; corresponding closing tag is the end of parse region
301 (search-forward (concat "</" (match-string 1) ">"))
302 (match-end 0)))))
303 (xml-parse-region xml-start-pos xml-end-pos)))))
304
305 (defun javaimp--maven-xml-parse-module (project-elt)
306 (let ((build-elt (javaimp--xml-child 'build project-elt)))
307 (make-javaimp-module
308 :id (javaimp--maven-xml-extract-id project-elt)
309 :parent-id (javaimp--maven-xml-extract-id (javaimp--xml-child 'parent project-elt))
310 ;; we set `file' slot later because raw <project> element does not contain
311 ;; pom file path, so we need to construct it during tree construction
312 :file nil
313 :final-name (javaimp--xml-first-child
314 (javaimp--xml-child 'finalName build-elt))
315 :packaging (javaimp--xml-first-child
316 (javaimp--xml-child 'packaging project-elt))
317 :source-dir (file-name-as-directory
318 (javaimp-cygpath-convert-maybe
319 (javaimp--xml-first-child
320 (javaimp--xml-child 'sourceDirectory build-elt))))
321 :test-source-dir (file-name-as-directory
322 (javaimp-cygpath-convert-maybe
323 (javaimp--xml-first-child
324 (javaimp--xml-child 'testSourceDirectory build-elt))))
325 :build-dir (file-name-as-directory
326 (javaimp-cygpath-convert-maybe
327 (javaimp--xml-first-child (javaimp--xml-child 'directory build-elt))))
328 :modules (mapcar (lambda (module-elt)
329 (javaimp--xml-first-child module-elt))
330 (javaimp--xml-children (javaimp--xml-child 'modules project-elt) 'module))
331 :dep-jars nil ; dep-jars is initialized lazily on demand
332 :load-ts (current-time))))
333
334 (defun javaimp--maven-xml-extract-id (elt)
335 (make-javaimp-id
336 :group (javaimp--xml-first-child (javaimp--xml-child 'groupId elt))
337 :artifact (javaimp--xml-first-child (javaimp--xml-child 'artifactId elt))
338 :version (javaimp--xml-first-child (javaimp--xml-child 'version elt))))
339
340 (defun javaimp--maven-xml-file-matches (file id parent-id)
341 (let* ((xml-tree (with-temp-buffer
342 (insert-file-contents file)
343 (xml-parse-region (point-min) (point-max))))
344 (project-elt (assq 'project xml-tree))
345 (tested-id (javaimp--maven-xml-extract-id project-elt))
346 (tested-parent-id (javaimp--maven-xml-extract-id (assq 'parent project-elt))))
347 ;; seems that the only mandatory component in tested ids is artifact, while
348 ;; group and version may be inherited and thus not presented in pom.xml
349 (let ((test (if (or (null (javaimp-id-group tested-id))
350 (null (javaimp-id-version tested-id))
351 (null (javaimp-id-group tested-parent-id))
352 (null (javaimp-id-version tested-parent-id)))
353 (progn
354 (message "File %s contains incomplete id, using lax match" file)
355 (lambda (first second)
356 (equal (javaimp-id-artifact first) (javaimp-id-artifact second))))
357 #'equal)))
358 (and (funcall test tested-id id)
359 (funcall test tested-parent-id parent-id)))))
360
361 \f
362 ;; Maven routines
363
364 (defun javaimp--maven-call (pom-file target handler)
365 "Runs Maven target TARGET on POM-FILE, then calls HANDLER in
366 the temporary buffer and returns its result"
367 (message "Calling \"mvn %s\" on pom: %s" target pom-file)
368 (with-temp-buffer
369 (let* ((pom-file (javaimp-cygpath-convert-maybe pom-file))
370 (status
371 ;; TODO check in Maven output on Gnu/Linux
372 (let ((coding-system-for-read
373 (if (eq system-type 'cygwin) 'utf-8-dos)))
374 (process-file javaimp-mvn-program nil t nil "-f" pom-file target)))
375 (buf (current-buffer)))
376 (with-current-buffer (get-buffer-create javaimp-debug-buf-name)
377 (erase-buffer)
378 (insert-buffer-substring buf))
379 (or (and (numberp status) (= status 0))
380 (error "Maven target \"%s\" failed with status \"%s\"" target status))
381 (goto-char (point-min))
382 (funcall handler))))
383
384 (defun javaimp--maven-build-tree (id parent-node all-modules file)
385 (message "Building tree for project: %s" id)
386 (let ((this (or (seq-find (lambda (m) (equal (javaimp-module-id m) id))
387 all-modules)
388 (error "Cannot find module %s!" id)))
389 ;; although each real parent has <modules> section, more reliable
390 ;; way to build hirarchy is to analyze <parent> node in each child
391 (children (seq-filter (lambda (m) (equal (javaimp-module-parent-id m) id))
392 all-modules)))
393 (if (and (null children)
394 (string= (javaimp-module-packaging this) "pom"))
395 (progn (message "Skipping empty aggregate module %s" (javaimp-module-id this))
396 nil)
397 ;; here we can finally set the `file' slot as the path is known at
398 ;; this time
399 (setf (javaimp-module-file this) file)
400 ;; make node
401 (let ((this-node (make-javaimp-node
402 :parent parent-node
403 :children nil
404 :contents this)))
405 (setf (javaimp-node-children this-node)
406 (mapcar (lambda (child)
407 (let ((child-file
408 (javaimp--maven-get-submodule-file
409 child file (javaimp-module-modules this))))
410 (javaimp--maven-build-tree
411 (javaimp-module-id child) this-node all-modules child-file)))
412 children))
413 this-node))))
414
415 (defun javaimp--maven-get-submodule-file (submodule parent-file rel-paths-from-parent)
416 ;; seems that the only reliable way to match a module parsed from
417 ;; <project> element with module relative path taken from <modules> is to
418 ;; visit pom and check that id and parent-id matches
419 (let* ((parent-dir (file-name-directory parent-file))
420 (files (mapcar (lambda (rel-path)
421 (concat parent-dir
422 (file-name-as-directory rel-path)
423 "pom.xml"))
424 rel-paths-from-parent)))
425 (or (seq-find
426 (lambda (file)
427 (javaimp--maven-xml-file-matches
428 file (javaimp-module-id submodule) (javaimp-module-parent-id submodule)))
429 files)
430 (error "Cannot find file for module: %s" (javaimp-module-id submodule)))))
431
432 \f
433 ;;; Loading dep-jars
434
435 (defun javaimp--maven-update-module-maybe (node)
436 (let (need-update)
437 ;; are deps not initialized?
438 (let ((module (javaimp-node-contents node)))
439 (if (null (javaimp-module-dep-jars module))
440 (setq need-update t)))
441 ;; were any pom.xml files updated after last load?
442 (let ((tmp node))
443 (while (and tmp
444 (not need-update))
445 (let ((module (javaimp-node-contents tmp)))
446 (if (> (float-time (javaimp--get-file-ts (javaimp-module-file module)))
447 (float-time (javaimp-module-load-ts module)))
448 (setq need-update t)))
449 (setq tmp (javaimp-node-parent tmp))))
450 (when need-update
451 ;; update current module
452 (let ((module (javaimp-node-contents node)))
453 ;; reload & update dep-jars
454 (setf (javaimp-module-dep-jars module)
455 (javaimp--maven-fetch-dep-jars module))
456 ;; update load-ts
457 (setf (javaimp-module-load-ts module) (current-time))))))
458
459 (defun javaimp--maven-fetch-dep-jars (module)
460 (let ((raw-line
461 (javaimp--maven-call
462 (javaimp-module-file module) "dependency:build-classpath"
463 (lambda ()
464 (goto-char (point-min))
465 (search-forward "Dependencies classpath:")
466 (forward-line 1)
467 (thing-at-point 'line))))
468 (separator-regex (concat "[" path-separator "\n" "]+")))
469 (split-string (javaimp-cygpath-convert-maybe raw-line 'unix t) separator-regex t)))
470
471
472 \f
473 ;; Working with jar classes
474
475 (defun javaimp--get-jar-classes (file)
476 (let ((cached (cdr (assoc file javaimp-cached-jars))))
477 (cond ((null cached)
478 ;; create, load & put into cache
479 (setq cached
480 (make-javaimp-cached-jar
481 :file file
482 :read-ts (javaimp--get-file-ts file)
483 :classes (javaimp--fetch-jar-classes file)))
484 (push (cons file cached) javaimp-cached-jars))
485 ((> (float-time (javaimp--get-file-ts (javaimp-cached-jar-file cached)))
486 (float-time (javaimp-cached-jar-read-ts cached)))
487 ;; reload
488 (setf (javaimp-cached-jar-classes cached) (javaimp--fetch-jar-classes file))
489 ;; update read-ts
490 (setf (javaimp-cached-jar-read-ts cached) (current-time))))
491 ;; return from cached
492 (javaimp-cached-jar-classes cached)))
493
494 (defun javaimp--fetch-jar-classes (file)
495 (message "Reading classes in file: %s" file)
496 (with-temp-buffer
497 (let ((coding-system-for-read (and (eq system-type 'cygwin) 'utf-8-dos)))
498 ;; on cygwin, "jar" is a windows program, so file path needs to be
499 ;; converted appropriately.
500 (process-file javaimp-jar-program nil t nil
501 ;; `jar' accepts commands/options as a single string
502 "tf" (javaimp-cygpath-convert-maybe file 'windows))
503 (goto-char (point-min))
504 (while (search-forward "/" nil t)
505 (replace-match "."))
506 (goto-char (point-min))
507 (let (result)
508 (while (re-search-forward "\\(^[[:alnum:]._]+\\)\\.class$" nil t)
509 (push (match-string 1) result))
510 result))))
511
512 \f
513 ;; Tree search routines
514
515 (defun javaimp--find-node (predicate)
516 (javaimp--find-node-in-forest javaimp-project-forest predicate))
517
518 (defun javaimp--select-nodes (predicate)
519 (javaimp--select-nodes-from-forest javaimp-project-forest predicate))
520
521 (defun javaimp--find-node-in-forest (forest predicate)
522 (catch 'found
523 (dolist (tree forest)
524 (javaimp--find-node-in-tree tree predicate))))
525
526 (defun javaimp--find-node-in-tree (tree predicate)
527 (if tree
528 (progn (if (funcall predicate (javaimp-node-contents tree))
529 (throw 'found tree))
530 (dolist (child (javaimp-node-children tree))
531 (javaimp--find-node-in-tree child predicate)))))
532
533 (defun javaimp--select-nodes-from-forest (forest predicate)
534 (apply #'seq-concatenate 'list
535 (mapcar (lambda (tree)
536 (javaimp--select-nodes-from-tree tree predicate))
537 forest)))
538
539 (defun javaimp--select-nodes-from-tree (tree predicate)
540 (if tree
541 (append (if (funcall predicate (javaimp-node-contents tree))
542 (list tree))
543 (apply #'seq-concatenate 'list
544 (mapcar (lambda (child)
545 (javaimp--select-nodes-from-tree child predicate))
546 (javaimp-node-children tree))))))
547
548 \f
549 ;; Some API functions
550
551 ;; do not expose tree structure, return only modules
552
553 (defun javaimp-find-module (predicate)
554 (let ((node (javaimp--find-node predicate)))
555 (and node
556 (javaimp-node-contents node))))
557
558 (defun javaimp-select-modules (predicate)
559 (mapcar #'javaimp-node-contents
560 (javaimp--select-nodes predicate)))
561
562 \f
563 ;;; Adding imports
564
565 ;;;###autoload
566 (defun javaimp-add-import (classname)
567 "Imports classname in the current file. Interactively,
568 asks for a class to import, adds import statement and calls
569 `javaimp-organize-imports'. Import statements are not
570 duplicated. Completion alternatives are constructed based on
571 this module's dependencies' classes, jdk classes and top-level
572 classes in the current module."
573 (interactive
574 (progn
575 (barf-if-buffer-read-only)
576 (let* ((file (expand-file-name
577 (or buffer-file-name
578 (error "Buffer is not visiting a file!"))))
579 (node (or (javaimp--find-node
580 (lambda (m)
581 (or (string-prefix-p (javaimp-module-source-dir m) file)
582 (string-prefix-p (javaimp-module-test-source-dir m) file))))
583 (error "Cannot find module by file: %s" file))))
584 (javaimp--maven-update-module-maybe node)
585 (let ((module (javaimp-node-contents node)))
586 (list (completing-read
587 "Import: "
588 (append
589 ;; we're not caching full list of classes coming from module
590 ;; dependencies because jars may change and we need to reload
591 ;; them
592 (let ((jars (append (javaimp-module-dep-jars module)
593 (javaimp--get-jdk-jars))))
594 (apply #'seq-concatenate 'list
595 (mapcar #'javaimp--get-jar-classes jars)))
596 (and javaimp-include-current-module-classes
597 (javaimp--get-module-classes module)))
598 nil t nil nil (symbol-name (symbol-at-point))))))))
599 (javaimp-organize-imports (cons classname 'ordinary)))
600
601 (defun javaimp--get-module-classes (module)
602 "Returns list of top-level classes in current module"
603 (append
604 (let ((build-dir (javaimp-module-build-dir module)))
605 ;; additional source dirs
606 (and (seq-mapcat
607 (lambda (rel-dir)
608 (let ((dir (concat build-dir (file-name-as-directory rel-dir))))
609 (and (file-accessible-directory-p dir)
610 (javaimp--get-directory-classes dir nil))))
611 javaimp-additional-source-dirs)))
612 ;; source dir
613 (let ((dir (javaimp-module-source-dir module)))
614 (and (file-accessible-directory-p dir)
615 (javaimp--get-directory-classes dir nil)))
616 ;; test source dir
617 (let ((dir (javaimp-module-test-source-dir module)))
618 (and (file-accessible-directory-p dir)
619 (javaimp--get-directory-classes dir nil)))))
620
621 (defun javaimp--get-directory-classes (dir prefix)
622 (append
623 ;; .java files in current directory
624 (mapcar (lambda (file)
625 (concat prefix (file-name-sans-extension (car file))))
626 (seq-filter (lambda (file) (null (cadr file))) ;only files
627 (directory-files-and-attributes dir nil "\\.java\\'" t)))
628 ;; descend into subdirectories
629 (apply #'seq-concatenate 'list
630 (mapcar (lambda (subdir)
631 (let ((name (car subdir)))
632 (javaimp--get-directory-classes
633 (concat dir (file-name-as-directory name)) (concat prefix name "."))))
634 (seq-filter (lambda (file)
635 (and (eq (cadr file) t) ;only directories
636 (null (member (car file) '("." "..")))))
637 (directory-files-and-attributes dir nil nil t))))))
638
639 \f
640 ;; Organizing imports
641
642 ;;;###autoload
643 (defun javaimp-organize-imports (&rest new-imports)
644 "Groups import statements according to the value of
645 `javaimp-import-group-alist' (which see) and prints resulting
646 groups leaving one blank line in between.
647
648 Classes within a single group are ordered in a lexicographic
649 order.
650
651 Imports not matched by any regexp in `javaimp-import-group-alist'
652 are assigned a default order defined by
653 `javaimp-import-default-order'.
654
655 NEW-IMPORTS is a list of additional imports; each element should
656 be of the form (CLASS . TYPE), where CLASS is a string and TYPE
657 is `'ordinary' or `'static'. Interactively, NEW-IMPORTS is nil."
658 (interactive)
659 (barf-if-buffer-read-only)
660 (save-excursion
661 (goto-char (point-min))
662 (let* ((old-data (javaimp--parse-imports))
663 (first (car old-data))
664 (last (cadr old-data))
665 (all-imports (append new-imports (cddr old-data))))
666 (if all-imports
667 (progn
668 ;; delete old imports, if any
669 (if first
670 (progn
671 (goto-char last)
672 (forward-line)
673 (delete-region first (point))))
674 (javaimp--prepare-for-insertion first)
675 (setq all-imports
676 (delete-duplicates all-imports
677 :test (lambda (first second)
678 (equal (car first) (car second)))))
679 ;; assign order
680 (let ((with-order
681 (mapcar
682 (lambda (import)
683 (let ((order (or (assoc-default (car import)
684 javaimp-import-group-alist
685 'string-match)
686 javaimp-import-default-order)))
687 (cons import order)))
688 all-imports)))
689 (setq with-order
690 (sort with-order
691 (lambda (first second)
692 ;; sort by order, name
693 (if (= (cdr first) (cdr second))
694 (string< (caar first) (caar second))
695 (< (cdr first) (cdr second))))))
696 (javaimp--insert-imports with-order)))
697 (message "Nothing to organize!")))))
698
699 (defun javaimp--parse-imports ()
700 (let (first last list)
701 (while (re-search-forward "^\\s-*import\\s-+\\(static\\s-+\\)?\\([._[:word:]]+\\)" nil t)
702 (push (cons (match-string 2) (if (match-string 1) 'static 'ordinary)) list)
703 (setq last (line-beginning-position))
704 (or first (setq first last)))
705 (cons first (cons last list))))
706
707 (defun javaimp--prepare-for-insertion (start)
708 (cond (start
709 ;; if there were any imports, we start inserting at the same place
710 (goto-char start))
711 ((re-search-forward "^\\s-*package\\s-" nil t)
712 ;; if there's a package directive, move to the next line, creating it
713 ;; if needed
714 (end-of-line)
715 (if (eobp)
716 (insert ?\n)
717 (forward-line))
718 ;; then insert one blank line and we're done
719 (insert ?\n))
720 (t
721 ;; otherwise, just go to bob
722 (goto-char (point-min)))))
723
724 (defun javaimp--insert-imports (imports)
725 (let ((static (seq-filter (lambda (elt)
726 (eq (cdar elt) 'static))
727 imports))
728 (ordinary (seq-filter (lambda (elt)
729 (eq (cdar elt) 'ordinary))
730 imports)))
731 (javaimp--insert-import-group "import static %s;" static)
732 (and static ordinary (insert ?\n))
733 (javaimp--insert-import-group "import %s;" ordinary)))
734
735 (defun javaimp--insert-import-group (pattern imports)
736 (let (last-order)
737 (dolist (import imports)
738 ;; if adjacent imports have different order value, insert a newline
739 ;; between them
740 (let ((order (cdr import)))
741 (and last-order
742 (/= order last-order)
743 (insert ?\n))
744 (insert (format pattern (caar import)) ?\n)
745 (setq last-order order)))))
746
747 (provide 'javaimp)
748
749 ;;; javaimp.el ends here