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