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