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