emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/javaimp 894f428: Support imenu, first working version


From: Filipp Gunbin
Subject: [elpa] externals/javaimp 894f428: Support imenu, first working version
Date: Thu, 5 Aug 2021 16:30:03 -0400 (EDT)

branch: externals/javaimp
commit 894f4284b6d9fbf728fd7e9767925b757a56d94f
Author: Filipp Gunbin <fgunbin@fastmail.fm>
Commit: Filipp Gunbin <fgunbin@fastmail.fm>

    Support imenu, first working version
---
 javaimp-gradle.el                     |  17 +-
 javaimp-maven.el                      |  18 +-
 javaimp-parse.el                      | 226 ++++++++--------
 javaimp-tests.el                      | 468 ++++++++++++++++++++++++----------
 javaimp-util.el                       | 262 ++++++++++++++-----
 javaimp.el                            | 195 +++++++++++---
 testdata/test-get-file-classes-1.java |  60 -----
 testdata/test1-misc-classes.java      | 125 +++++++++
 8 files changed, 955 insertions(+), 416 deletions(-)

diff --git a/javaimp-gradle.el b/javaimp-gradle.el
index 85e3f2e..af3fd5a 100644
--- a/javaimp-gradle.el
+++ b/javaimp-gradle.el
@@ -20,13 +20,6 @@
 
 (require 'javaimp-util)
 
-(defcustom javaimp-gradle-program "gradle"
-  "Path to the `gradle' program.  Customize it if the program is
-not on `exec-path'.  If the visited project's directory contains
-gradlew program, it is used in preference."
-  :group 'javaimp
-  :type 'string)
-
 (defconst javaimp--gradle-task-body
   (with-temp-buffer
     (insert-file-contents (expand-file-name "gradleTaskBody.inc.kts" 
javaimp--basedir))
@@ -48,7 +41,15 @@ information."
                             (javaimp--gradle-module-from-alist alist file))
                           alists)))
     ;; first module is always root
-    (javaimp--build-tree (car modules) nil modules)))
+    (message "Building tree for root: %s"
+             (javaimp-print-id (javaimp-module-id (car modules))))
+    (javaimp--build-tree (car modules) modules
+                        ;; more or less reliable way to find children
+                        ;; is to look for modules with "this" as the
+                        ;; parent
+                         (lambda (el tested)
+                           (equal (javaimp-module-parent-id tested)
+                                  (javaimp-module-id el))))))
 
 (defun javaimp--gradle-handler ()
   (goto-char (point-min))
diff --git a/javaimp-maven.el b/javaimp-maven.el
index eee886f..73d914d 100644
--- a/javaimp-maven.el
+++ b/javaimp-maven.el
@@ -23,13 +23,6 @@
 
 (require 'javaimp-util)
 
-(defcustom javaimp-mvn-program "mvn"
-  "Path to the `mvn' program.  Customize it if the program is not
-on `exec-path'."
-  :group 'javaimp
-  :type 'string)
-
-
 (defun javaimp--maven-visit (file)
   "Calls \"mvn help:effective-pom\" on FILE,
 reads project structure from the output and records which files
@@ -87,7 +80,16 @@ resulting module trees."
                                          modules)))
                        (cdr modules)))))
       (mapcar (lambda (root)
-                (javaimp--build-tree root nil modules))
+                (message "Building tree for root: %s"
+                         (javaimp-print-id (javaimp-module-id root)))
+                (javaimp--build-tree
+                 root modules
+                ;; more or less reliable way to find
+                ;; children is to look for modules with
+                ;; "this" as the parent
+                 (lambda (el tested)
+                   (equal (javaimp-module-parent-id tested)
+                          (javaimp-module-id el)))))
               roots))))
 
 (defun javaimp--maven-effective-pom-handler ()
diff --git a/javaimp-parse.el b/javaimp-parse.el
index fca0369..0c31506 100644
--- a/javaimp-parse.el
+++ b/javaimp-parse.el
@@ -18,52 +18,31 @@
 ;; You should have received a copy of the GNU General Public License
 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-(require 'cl-lib)
-(require 'seq)
-(require 'cc-mode)                      ;for java-mode-syntax-table
 (require 'javaimp-util)
 
-(defcustom javaimp-parse-format-method-name
-  #'javaimp--parse-format-method-name-full
-  "Function to format method name, invoked with 3 arguments:
-NAME, ARGS and THROWS-ARGS.  The last two are lists with elements
-of the form (TYPE . NAME).  For THROWS-ARGS, only TYPE is
-present."
-  :group 'javaimp
-  :type 'function)
-
-(cl-defstruct javaimp-scope
-  type ; one of anonymous-class, class, interface, enum, local-class,
-       ; method, statement, simple-statement, array, unknown
-  name
-  start
-  open-brace)
-
-(defconst javaimp--parse-class-keywords
-  '("class" "interface" "enum"))
+(defconst javaimp--parse-classlike-keywords
+  (mapcar #'symbol-name
+          javaimp--classlike-scope-types))
+
 (defconst javaimp--parse-stmt-keywords
   '("if" "for" "while" "switch" "try" "catch" "finally"
-    "static"                            ;static initializer block
+    "static"                            ; static initializer block
     ))
 
-(defsubst javaimp--parse-is-class (scope)
-  (member (symbol-name (javaimp-scope-type scope)) 
javaimp--parse-class-keywords))
+(defvar-local javaimp--parse-dirty-pos nil
+  "Buffer position after which all parsed information should be
+considered as stale.  Usually set by modification change hooks.
+Should be set to (point-min) in major mode hook.")
 
-(defvar javaimp--arglist-syntax-table
-  (let ((st (make-syntax-table java-mode-syntax-table))) ;TODO don't depend
-    (modify-syntax-entry ?< "(>" st)
-    (modify-syntax-entry ?> ")<" st)
-    (modify-syntax-entry ?. "_" st) ; separates parts of fully-qualified type
-    st)
-  "Enables parsing angle brackets as lists")
 
-(defmacro javaimp--parse-with-arglist-syntax (beg &rest body)
-  (declare (debug t))
+(defmacro javaimp--parse-with-syntax-table (syntax-table beg &rest body)
+  (declare (debug t)
+           (indent 2))
   (let ((begin (make-symbol "begin")))
     `(let ((,begin ,beg))
        (syntax-ppss-flush-cache ,begin)
        (unwind-protect
-           (with-syntax-table javaimp--arglist-syntax-table
+           (with-syntax-table ,syntax-table
              ,@body)
          (syntax-ppss-flush-cache ,begin)))))
 
@@ -90,7 +69,7 @@ point is outside of any context initially."
   "Parse arg list between BEG and END, of the form 'TYPE NAME,
 ...'.  Return list of conses (TYPE . NAME).  If ONLY-TYPE is
 non-nil, then name parsing is skipped."
-  (javaimp--parse-with-arglist-syntax beg
+  (javaimp--parse-with-syntax-table javaimp--arglist-syntax-table beg
     (save-excursion
       (save-restriction
         (narrow-to-region beg end)
@@ -207,7 +186,7 @@ is unchanged."
     (catch 'found
       (while (javaimp--parse-rsb-keyword regexp bound t)
         (let ((scan-pos (match-end 0)))
-          (javaimp--parse-with-arglist-syntax scan-pos
+          (javaimp--parse-with-syntax-table javaimp--arglist-syntax-table 
scan-pos
             (while (and scan-pos (<= scan-pos (nth 1 state)))
               (if (ignore-errors
                     (= (scan-lists scan-pos 1 -1) ;As in 
javaimp--parse-preceding
@@ -221,32 +200,6 @@ is unchanged."
       (goto-char pos)
       nil)))
 
-
-
-;;; Formatting
-
-(defsubst javaimp--parse-format-method-name-full (name args throws-args)
-  "Outputs NAME, ARGS (name and type) and THROWS-ARGS (only type)."
-  (concat name
-          "("
-          (mapconcat (lambda (arg)
-                       (concat (car arg) " " (cdr arg)))
-                     args
-                     ", ")
-          ")"
-          (if throws-args
-              (concat " throws "
-                      (mapconcat #'car throws-args ", ")))
-          ))
-
-(defsubst javaimp--parse-format-method-name-types (name args _throws-args)
-  "Outputs NAME and ARGS (only type)."
-  (concat name
-          "("
-          (mapconcat #'car args ", ")
-          ")"
-          ))
-
 
 ;;; Scopes
 
@@ -265,7 +218,7 @@ is unchanged."
   "Attempts to parse 'class' / 'interface' / 'enum' scope.  Some of
 those may later become 'local-class' (see `javaimp--parse-scopes')."
   (save-excursion
-    (if (javaimp--parse-preceding (regexp-opt javaimp--parse-class-keywords 
'words)
+    (if (javaimp--parse-preceding (regexp-opt 
javaimp--parse-classlike-keywords 'words)
                                   (nth 1 state))
         (let* ((keyword-start (match-beginning 1))
                (keyword-end (match-end 1))
@@ -335,6 +288,7 @@ those may later become 'local-class' (see 
`javaimp--parse-scopes')."
                    (javaimp--parse-skip-back-until)
                    (= (char-before) ?\)))
                  (ignore-errors
+                   ;; for method this is arglist
                    (goto-char
                     (scan-lists (point) -1 0))))
         (let* (;; leave open/close parens out
@@ -355,7 +309,7 @@ those may later become 'local-class' (see 
`javaimp--parse-scopes')."
              :type type
              :name (if (eq type 'statement)
                        name
-                     (funcall javaimp-parse-format-method-name
+                     (funcall javaimp-format-method-name
                               name
                               (javaimp--parse-arglist (car arglist-region)
                                                       (cdr arglist-region))
@@ -367,14 +321,14 @@ those may later become 'local-class' (see 
`javaimp--parse-scopes')."
   "Attempts to parse 'array' scope."
   (save-excursion
     (and (javaimp--parse-skip-back-until)
-         (member (char-before) '(?, ?{ ?\]))
+         (member (char-before) '(?, ?\]))
          (make-javaimp-scope :type 'array
                              :name ""
                              :start nil
                              :open-brace (nth 1 state)))))
 
 (defun javaimp--parse-scope-unknown (state)
-  "Catch-all parser which produces `unknown' scope."
+  "Catch-all parser which produces 'unknown' scope."
   (make-javaimp-scope :type 'unknown
                       :name "unknown"
                       :start nil
@@ -382,58 +336,122 @@ those may later become 'local-class' (see 
`javaimp--parse-scopes')."
 
 (defun javaimp--parse-scopes (count)
   "Attempts to parse COUNT enclosing scopes at point.  If COUNT is
-nil then goes all the way up."
-  (let ((state (syntax-ppss)) res)
+nil then goes all the way up.  Examines and sets property
+'javaimp-parse-scope' at each scope's open brace."
+  (let ((state (syntax-ppss))
+        res)
     (unless (syntax-ppss-context state)
-      (save-excursion
-        (while (and (nth 1 state)
-                    (or (not count)
-                        (>= (setq count (1- count)) 0)))
-          ;; find innermost enclosing open-bracket
-          (goto-char (nth 1 state))
-          (when (= (char-after) ?{)
-            (let ((scope (run-hook-with-args-until-success
-                          'javaimp--parse-scope-hook state)))
-              (push scope res)
-              (if (javaimp-scope-start scope)
-                  (goto-char (javaimp-scope-start scope)))))
-          (setq state (syntax-ppss)))))
+      (while (and (nth 1 state)
+                  (or (not count)
+                      (>= (setq count (1- count)) 0)))
+        ;; find innermost enclosing open-bracket
+        (goto-char (nth 1 state))
+        (when (= (char-after) ?{)
+          (let ((scope (get-text-property (point) 'javaimp-parse-scope)))
+            (unless scope
+              (setq scope (run-hook-with-args-until-success
+                           'javaimp--parse-scope-hook state))
+              (put-text-property (point) (1+ (point))
+                                 'javaimp-parse-scope scope))
+            (push scope res)
+            (if (javaimp-scope-start scope)
+                (goto-char (javaimp-scope-start scope)))))
+        (setq state (syntax-ppss))))
     ;; if a class is enclosed in anything other than a class, then it
     ;; should be local
     (let ((tmp res)
-          in-local)
+          in-local parent)
       (while tmp
-        (if (javaimp--parse-is-class (car tmp))
-            (if in-local
-                (setf (javaimp-scope-type (car tmp)) 'local-class))
+        (if (javaimp--is-classlike (car tmp))
+            (when in-local
+              (setf (javaimp-scope-type (car tmp)) 'local-class))
           (setq in-local t))
+        (setf (javaimp-scope-parent (car tmp)) parent)
+        (setq parent (car tmp))
         (setq tmp (cdr tmp))))
     res))
 
+(defun javaimp--parse-all-scopes ()
+  "Entry point to the scope parsing.  Parses scopes in this buffer
+which are after `javaimp--parse-dirty-pos', if it is non-nil.
+Resets this variable after parsing is done."
+  (when javaimp--parse-dirty-pos
+    (remove-text-properties javaimp--parse-dirty-pos (point-max)
+                            '(javaimp-parse-scope nil))
+    (goto-char (point-max))
+    ;; FIXME With major mode we could set these, as well as syntax
+    ;; table, in mode function.
+    (let ((parse-sexp-ignore-comments t)
+          (parse-sexp-lookup-properties nil))
+      (javaimp--parse-with-syntax-table javaimp-syntax-table (point-min)
+        (while (javaimp--parse-rsb-keyword "{" javaimp--parse-dirty-pos t)
+          (save-excursion
+            (forward-char)
+            ;; Set props at this brace and all the way up
+            (javaimp--parse-scopes nil)))))
+    (setq javaimp--parse-dirty-pos nil)))
+
 
 
-;; Main
+;; Functions intended to be called from other parts of javaimp.
 
 (defun javaimp--parse-get-package ()
   (goto-char (point-max))
-  (when (javaimp--parse-rsb-keyword
-         "^\\s-*package\\s-+\\([^;\n]+\\)\\s-*;" nil t 1)
-    (match-string 1)))
-
-(defun javaimp--parse-get-file-classes ()
-  (goto-char (point-max))
-  (let (res)
-    (while (javaimp--parse-rsb-keyword
-            (regexp-opt javaimp--parse-class-keywords 'words) nil t)
-      (save-excursion
-        (let ((parse-sexp-ignore-comments t) ; FIXME remove with major mode
-              (parse-sexp-lookup-properties nil))
-          (when (and (ignore-errors
-                       (goto-char (scan-lists (point) 1 -1)))
-                     (= (char-before) ?{))
-            (let ((scopes (javaimp--parse-scopes nil)))
-              (when (seq-every-p #'javaimp--parse-is-class scopes)
-                (push (mapconcat #'javaimp-scope-name scopes ".") res)))))))
+  (javaimp--parse-with-syntax-table javaimp-syntax-table (point-min)
+    (when (javaimp--parse-rsb-keyword
+           "^[ \t]*package[ \t]+\\([^ \t;\n]+\\)[ \t]*;" nil t 1)
+      (match-string 1))))
+
+(defun javaimp--parse-get-all-classlikes ()
+  (mapcar (lambda (scope)
+            (let ((name (javaimp-scope-name scope))
+                  (parent-names (javaimp--concat-scope-parents scope)))
+              (if (string-empty-p parent-names)
+                  name
+                (concat parent-names "." name))))
+          (javaimp--parse-get-all-scopes #'javaimp--is-classlike)))
+
+(defun javaimp--parse-get-imenu-forest ()
+  (let* ((methods (javaimp--parse-get-all-scopes
+                   #'javaimp--is-imenu-included-method 
#'javaimp--is-classlike))
+         (classes (javaimp--parse-get-all-scopes #'javaimp--is-classlike))
+         (top-classes (seq-filter (lambda (s)
+                                    (null (javaimp-scope-parent s)))
+                                  classes)))
+    (mapcar
+     (lambda (top-class)
+       (message "Building tree for top-level class-like scope: %s"
+                (javaimp-scope-name top-class))
+       (javaimp--build-tree top-class (append methods classes)
+                            (lambda (el tested)
+                              (equal el (javaimp-scope-parent tested)))
+                            nil
+                            (lambda (s1 s2)
+                              (< (javaimp-scope-start s1)
+                                 (javaimp-scope-start s2)))))
+     top-classes)))
+
+(defun javaimp--parse-get-all-scopes (&optional pred parent-pred)
+  "Return all scopes in the current buffer, optionally filtering
+them with PRED, and their parents with PARENT-PRED."
+  (javaimp--parse-all-scopes)
+  (let ((pos (point-max))
+        scope res)
+    (while (setq pos (previous-single-property-change pos 
'javaimp-parse-scope))
+      (setq scope (get-text-property pos 'javaimp-parse-scope))
+      (when (and scope
+                 (or (null pred)
+                     (funcall pred scope)))
+        (setq scope (javaimp--copy-scope scope))
+        (when parent-pred
+          (javaimp--filter-scope-parents scope parent-pred))
+        (push scope res)))
     res))
 
+(defun javaimp--parse-update-dirty-pos (beg _end _old-len)
+  "Function to add to `after-change-functions' hook."
+  (when (or (not javaimp--parse-dirty-pos)
+            (< beg javaimp--parse-dirty-pos))
+    (setq javaimp--parse-dirty-pos beg)))
+
 (provide 'javaimp-parse)
diff --git a/javaimp-tests.el b/javaimp-tests.el
index 0ca4593..755e644 100644
--- a/javaimp-tests.el
+++ b/javaimp-tests.el
@@ -8,119 +8,7 @@
 (require 'ert)
 (require 'javaimp)
 
-;; (ert-deftest javaimp-test--maven-projects-from-xml--project ()
-;;   (with-temp-buffer
-;;     (insert "<project/>")
-;;     (let ((projects (javaimp--maven-projects-from-xml
-;;                  (xml-parse-region (point-min) (point-max)))))
-;;       (should (eql (length projects) 1)))))
-
-;; (ert-deftest javaimp-test--maven-projects-from-xml--projects ()
-;;   (with-temp-buffer
-;;     (insert "<projects><project/><project/></projects>")
-;;     (let ((projects (javaimp--maven-projects-from-xml
-;;                  (xml-parse-region (point-min) (point-max)))))
-;;       (should (eql (length projects) 2)))))
-
-
-(defun javaimp-test--check-scope (parse-hook &rest test-items)
-  (declare (indent 1))
-  (dolist (item test-items)
-    (with-temp-buffer
-      (insert (nth 0 item))
-      (java-mode)
-      (let* ((parse-sexp-ignore-comments t) ;FIXME remove with major mode
-             (parse-sexp-lookup-properties nil)
-             (javaimp--parse-scope-hook parse-hook)
-             (scope (car (javaimp--parse-scopes 1))))
-        (should-not (null scope))
-        (should (eq (javaimp-scope-type scope) (nth 1 item)))
-        (should (equal (javaimp-scope-name scope) (nth 2 item)))))))
-
-(ert-deftest javaimp-test--parse-scope-class ()
-  (javaimp-test--check-scope #'javaimp--parse-scope-class
-    '("class Foo {"
-      class "Foo")
-    '("class Foo extends Bar {"
-      class "Foo")
-    '("class Foo implements Bar {"
-      class "Foo")
-    '("class Foo implements Bar, Baz {"
-      class "Foo")
-    '("public class Foo extends Bar implements Baz1 , Baz2 {"
-      class "Foo")
-    `(,(subst-char-in-string
-        ?  ?\n
-        "public class Foo extends Bar implements Baz1 , Baz2 {")
-      class "Foo")
-    '("class Foo<Bar, Baz> extends FooSuper<Bar, Baz> \
-implements Interface1<Bar, Baz>, Interface2 {"
-      class "Foo")
-    '("class Foo<E extends Bar> {"
-      class "Foo")
-    '("class Foo<Enum<?>> {"
-      class "Foo")
-    '("class Foo<T extends Baz<? extends Baz2>> \
-extends Bar<? extends Baz<? extends Baz2>> {"
-      class "Foo")
-    '("interface Foo<Bar, Baz> {"
-      interface "Foo")
-    '("private enum Foo {"
-      enum "Foo")))
-
-(ert-deftest javaimp-test--parse-scope-anonymous-class ()
-  (javaimp-test--check-scope #'javaimp--parse-scope-anonymous-class
-    '(" = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {"
-      anonymous-class "Object")
-    `(,(subst-char-in-string
-        ?  ?\n
-        " = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {")
-      anonymous-class "Object")
-    '(" = (obj.getField()).new Object<Class1, Class2>(1, baz) {"
-      anonymous-class "Object")
-    '(" = obj.new Object<>(1, baz) {"
-      anonymous-class "Object")))
-
-(ert-deftest javaimp-test--parse-scope-method-or-stmt ()
-  (javaimp-test--check-scope #'javaimp--parse-scope-method-or-stmt
-    '("static void foo_bar ( String a , int b ) {"
-      method "foo_bar(String a, int b)")
-    `(,(subst-char-in-string
-        ?  ?\n
-        "static void foo_bar ( String a , int b ) {")
-      method "foo_bar(String a, int b)")
-    '("void foo_bar(String a, int b) throws E1, E2 {"
-      method "foo_bar(String a, int b) throws E1, E2")
-    '("void foo_bar()
-throws E1 {"
-      method "foo_bar() throws E1")
-    '("if (foo_bar(a, b) < 2) {"
-      statement "if")))
-
-(ert-deftest javaimp-test--parse-scope-simple-stmt ()
-  (javaimp-test--check-scope #'javaimp--parse-scope-simple-stmt
-    '(" try {"
-      simple-statement "try")
-    `(,(subst-char-in-string ?  ?\n " try {")
-      simple-statement "try")
-    ;; static initializer
-    '("static {"
-      simple-statement "static")
-    ;; lambda
-    '("it -> {"
-      simple-statement "lambda")
-    '("(x, y) -> {"
-      simple-statement "lambda")
-    ))
-
-(ert-deftest javaimp-test--parse-scope-array ()
-  (javaimp-test--check-scope #'javaimp--parse-scope-array
-    '("new String[] {"
-      array "")
-    '("new Object[][] { {"
-      array "")
-    '("new int[] {{1, 2}, {"
-      array "")))
+;; Tests for low-level helpers of scope parsers.
 
 (ert-deftest javaimp-test--parse-arglist ()
   (dolist (data '(("")
@@ -146,7 +34,6 @@ throws E1 {"
                    ("String[][]" . "arr"))
                   ))
     (with-temp-buffer
-      (java-mode)
       (insert (car data))
       (should (equal (javaimp--parse-arglist (point-min) (point-max))
                      (cdr data))))))
@@ -169,30 +56,347 @@ Exception4<? super Exception5>>")
                    ("Exception6")
                    ("Exception7<Exception8>"))))
     (with-temp-buffer
-      (java-mode)
       (insert (car data))
       (should (equal (javaimp--parse-arglist (point-min) (point-max) t)
                      (cdr data))))))
 
 
+
+;; Tests for scope parsers, which should be in
+;; `javaimp--parse-scope-hook'.
+
+(ert-deftest javaimp-test--parse-scope-class ()
+  (let ((javaimp--parse-scope-hook #'javaimp--parse-scope-class))
+    (javaimp-test--check-single-scope
+     '("class Foo {"
+       class "Foo")
+     '("class Foo extends Bar {"
+       class "Foo")
+     '("class Foo implements Bar {"
+       class "Foo")
+     '("class Foo implements Bar, Baz {"
+       class "Foo")
+     '("public class Foo extends Bar implements Baz1 , Baz2 {"
+       class "Foo")
+     `(,(subst-char-in-string
+         ?  ?\n
+         "public class Foo extends Bar implements Baz1 , Baz2 {")
+       class "Foo")
+     '("class Foo<Bar, Baz> extends FooSuper<Bar, Baz> \
+implements Interface1<Bar, Baz>, Interface2 {"
+       class "Foo")
+     '("class Foo<E extends Bar> {"
+       class "Foo")
+     '("class Foo<Enum<?>> {"
+       class "Foo")
+     '("class Foo<T extends Baz<? extends Baz2>> \
+extends Bar<? extends Baz<? extends Baz2>> {"
+       class "Foo")
+     '("interface Foo<Bar, Baz> {"
+       interface "Foo")
+     '("private enum Foo {"
+       enum "Foo")
+     )))
+
+(ert-deftest javaimp-test--parse-scope-anonymous-class ()
+  (let ((javaimp--parse-scope-hook #'javaimp--parse-scope-anonymous-class))
+    (javaimp-test--check-single-scope
+     '(" = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {"
+       anonymous-class "Object")
+     `(,(subst-char-in-string
+         ?  ?\n
+         " = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {")
+       anonymous-class "Object")
+     '(" = (obj.getField()).new Object<Class1, Class2>(1, baz) {"
+       anonymous-class "Object")
+     '(" = obj.new Object<>(1, baz) {"
+       anonymous-class "Object")
+     )))
+
+(ert-deftest javaimp-test--parse-scope-method-or-stmt ()
+  (let ((javaimp--parse-scope-hook #'javaimp--parse-scope-method-or-stmt)
+        (javaimp-format-method-name #'javaimp-format-method-name-full))
+    (javaimp-test--check-single-scope
+     '("static void foo_bar ( String a , int b ) {"
+       method "foo_bar(String a, int b)")
+     `(,(subst-char-in-string
+         ?  ?\n
+         "static void foo_bar ( String a , int b ) {")
+       method "foo_bar(String a, int b)")
+     '("void foo_bar(String a, int b) throws E1, E2 {"
+       method "foo_bar(String a, int b) throws E1, E2")
+     '("void foo_bar()
+throws E1 {"
+       method "foo_bar() throws E1")
+     '("if (foo_bar(a, b) < 2) {"
+       statement "if")
+     )))
+
+(ert-deftest javaimp-test--parse-scope-simple-stmt ()
+  (let ((javaimp--parse-scope-hook #'javaimp--parse-scope-simple-stmt))
+    (javaimp-test--check-single-scope
+     '(" try {"
+       simple-statement "try")
+     `(,(subst-char-in-string ?  ?\n " try {")
+       simple-statement "try")
+     ;; static initializer
+     '("static {"
+       simple-statement "static")
+     ;; lambda
+     '("it -> {"
+       simple-statement "lambda")
+     '("(x, y) -> {"
+       simple-statement "lambda")
+     )))
+
+(ert-deftest javaimp-test--parse-scope-array ()
+  (let ((javaimp--parse-scope-hook #'javaimp--parse-scope-array))
+    (javaimp-test--check-single-scope
+     '("new String[] {"
+       array "")
+     ;; TODO fix
+     ;; '("new Object[][] { {"
+     ;;   array "")
+     ;; '("new int[] {{1, 2}, {"
+     ;;   array "")
+     )))
+
+(defun javaimp-test--check-single-scope (&rest test-items)
+  (dolist (item test-items)
+    (with-temp-buffer
+      (insert (nth 0 item))
+      (setq javaimp--parse-dirty-pos (point-min))
+      (let ((scopes (javaimp--parse-get-all-scopes)))
+        (should (= 1 (length scopes)))
+        (should (eq (javaimp-scope-type (car scopes)) (nth 1 item)))
+        (should (equal (javaimp-scope-name (car scopes)) (nth 2 item)))))))
+
+
+;; Tests for javaimp-parse.el "package-private" API.
+
 (ert-deftest javaimp-test--parse-get-package ()
   (with-temp-buffer
-    (insert "//package org.commented1;
-/*package org.commented2;*/
-  package org.foo;")
-    (should (equal (javaimp--parse-get-package) "org.foo"))))
-
-
-(ert-deftest javaimp-test--get-file-classes ()
-  (should (equal (javaimp--get-file-classes
-                  (concat javaimp--basedir 
"testdata/test-get-file-classes-1.java"))
-                 '("org.foo.Top"
-                   "org.foo.Top.CInner1"
-                   "org.foo.Top.CInner1.CInner1_CInner1"
-                   "org.foo.Top.IInner1"
-                   "org.foo.Top.IInner1.IInner1_IInner1"
-                   "org.foo.Top.IInner1.IInner1_CInner1"
-                   "org.foo.Top.EInner1"
-                   "org.foo.Top.EInner1.EInner1_EInner1"))))
+    (insert "  package  foo.bar.baz  ;
+//package commented.line;
+/*
+package commented.block;
+*/")
+    (should (equal (javaimp--parse-get-package) "foo.bar.baz"))))
+
+(ert-deftest javaimp-test--parse-get-all-classlikes ()
+  (with-temp-buffer
+    (insert-file-contents
+     (concat javaimp--basedir "testdata/test1-misc-classes.java"))
+    (setq javaimp--parse-dirty-pos (point-min))
+    (should (equal (javaimp--parse-get-all-classlikes)
+                   '("Top"
+                     "Top.CInner1"
+                     "Top.CInner1.CInner1_CInner1"
+                     "Top.IInner1"
+                     "Top.IInner1.IInner1_CInner1"
+                     "Top.IInner1.IInner1_IInner1"
+                     "Top.EnumInner1"
+                     "Top.EnumInner1.EnumInner1_EInner1"
+                     "ColocatedTop")))))
+
+(ert-deftest javaimp-test--parse-get-all-scopes ()
+  (with-temp-buffer
+    (insert-file-contents
+     (concat javaimp--basedir "testdata/test1-misc-classes.java"))
+    (let ((javaimp-format-method-name #'javaimp-format-method-name-types))
+      ;;
+      ;; parse full buffer
+      (setq javaimp--parse-dirty-pos (point-min))
+      (javaimp-test--check-named-scopes
+       (javaimp--parse-get-all-scopes
+        #'javaimp--is-named #'javaimp--is-named))
+      ;;
+      ;; reparse half of buffer
+      (setq javaimp--parse-dirty-pos (/ (- (point-max) (point-min)) 2))
+      (javaimp-test--check-named-scopes
+       (javaimp--parse-get-all-scopes
+        #'javaimp--is-named #'javaimp--is-named))
+      ;;
+      ;; don't reparse
+      (javaimp-test--check-named-scopes
+       (javaimp--parse-get-all-scopes
+        #'javaimp--is-named #'javaimp--is-named)))))
+
+(defun javaimp-test--check-named-scopes (scopes)
+  (let ((actual
+         (mapcar (lambda (s)
+                   (let (res)
+                     (while s
+                       (push (list (javaimp-scope-type s)
+                                   (javaimp-scope-name s))
+                             res)
+                       (setq s (javaimp-scope-parent s)))
+                     (nreverse res)))
+                 scopes))
+        (expected
+         '(((class "Top"))
+           ((class "CInner1") (class "Top"))
+           ((method "foo()") (class "CInner1") (class "Top"))
+           ((local-class "CInner1_CLocal1")
+            (method "foo()") (class "CInner1") (class "Top"))
+           ((method "foo()")
+            (local-class "CInner1_CLocal1")
+            (method "foo()") (class "CInner1") (class "Top"))
+           ((local-class "CInner1_CLocal1_CLocal1")
+            (method "foo()")
+            (local-class "CInner1_CLocal1")
+            (method "foo()") (class "CInner1") (class "Top"))
+           ((method "foo()")
+            (local-class "CInner1_CLocal1_CLocal1")
+            (method "foo()")
+            (local-class "CInner1_CLocal1")
+            (method "foo()") (class "CInner1") (class "Top"))
+
+           ((local-class "CInner1_CLocal2")
+            (method "foo()") (class "CInner1") (class "Top"))
+           ((method "foo()")
+            (local-class "CInner1_CLocal2")
+            (method "foo()") (class "CInner1") (class "Top"))
+
+           ((method "toString()")
+            (class "CInner1") (class "Top"))
+
+           ((class "CInner1_CInner1") (class "CInner1") (class "Top"))
+           ((method "foo()")
+            (class "CInner1_CInner1") (class "CInner1") (class "Top"))
+           ((method "bar()")
+            (class "CInner1_CInner1") (class "CInner1") (class "Top"))
+
+           ((interface "IInner1") (class "Top"))
+           ((method "foo()") (interface "IInner1") (class "Top"))
+           ((class "IInner1_CInner1") (interface "IInner1") (class "Top"))
+           ((method "foo()")
+            (class "IInner1_CInner1") (interface "IInner1") (class "Top"))
+           ((method "defaultMethod(String)")
+            (interface "IInner1") (class "Top"))
+
+           ((interface "IInner1_IInner1") (interface "IInner1") (class "Top"))
+           ((method "defaultMethod(String)")
+            (interface "IInner1_IInner1") (interface "IInner1") (class "Top"))
+
+           ((enum "EnumInner1") (class "Top"))
+           ((method "EnumInner1()") (enum "EnumInner1") (class "Top"))
+           ((method "foo()") (enum "EnumInner1") (class "Top"))
+           ((enum "EnumInner1_EInner1") (enum "EnumInner1") (class "Top"))
+
+           ((class "ColocatedTop"))
+           ((method "foo()") (class "ColocatedTop"))
+           ((method "bar(String, String)") (class "ColocatedTop")))))
+    (should (= (length expected) (length actual)))
+    (dotimes (i (length expected))
+      (should (equal (nth i expected) (nth i actual)))))
+  ;;
+  (let ((data
+         `((,(nth 0 scopes) "Top" 26 36)
+           (,(nth 16 scopes) "foo()" 1798 1804)
+           (,(nth 23 scopes) "EnumInner1_EInner1" 2462 2486)
+           (,(nth 25 scopes) "foo()" 2554 2560))))
+    (dolist (elt data)
+      (let ((scope (nth 0 elt)))
+        (should (equal (nth 1 elt) (javaimp-scope-name scope)))
+        (should (equal (nth 2 elt) (javaimp-scope-start scope)))
+        (should (equal (nth 3 elt) (javaimp-scope-open-brace scope)))))))
+
+
+
+;; Tests for imenu function
+
+(ert-deftest javaimp-test--imenu-group ()
+  (let* ((javaimp-imenu-group-methods t)
+         (javaimp-format-method-name #'javaimp-format-method-name-types)
+         (actual (with-temp-buffer
+                   (insert-file-contents
+                    (concat javaimp--basedir 
"testdata/test1-misc-classes.java"))
+                   (setq javaimp--parse-dirty-pos (point-min))
+                   (javaimp-imenu-create-index))))
+    (javaimp-test--imenu-simplify-entries actual)
+    (should
+     (equal
+      '(("Top"
+         ("CInner1"
+          ("foo()" . 98)
+          ("CInner1_CInner1"
+           ("foo()" . 1099)
+           ("bar()" . 1192)))
+         ("IInner1"
+          ("foo()" . 1603)
+          ("IInner1_CInner1"
+           ("foo()" . 1798))
+          ("defaultMethod(String)" . 1963)
+          ("IInner1_IInner1"
+           ("defaultMethod(String)" . 2157)))
+         ("EnumInner1"
+          ("EnumInner1()" . 2353)
+          ("foo()" . 2399)
+          ;; "EnumInner1_EInner1" omitted because no methods inside
+          ))
+        ("ColocatedTop"
+         ("foo()" . 2554)
+         ("bar(String, String)" . 2578)))
+      actual))))
+
+(defun javaimp-test--imenu-simplify-entries (alist)
+  (dolist (elt alist)
+    (if (and (= (length elt) 4)
+             (functionp (nth 2 elt)))
+        (setcdr elt (nth 1 elt))
+      (javaimp-test--imenu-simplify-entries (cdr elt)))))
+
+
+(ert-deftest javaimp-test--imenu-simple ()
+  (let ((javaimp-format-method-name #'javaimp-format-method-name-types)
+        (javaimp-imenu-group-methods nil))
+    (javaimp-test--imenu-method-list 0)))
+
+(ert-deftest javaimp-test--imenu-qualified ()
+  (let ((javaimp-format-method-name #'javaimp-format-method-name-types)
+        (javaimp-imenu-group-methods 'qualified))
+    (javaimp-test--imenu-method-list 1)))
+
+(defconst javaimp-test--imenu-method-list-expected
+  '(("foo() [Top.CInner1]"
+     "Top.CInner1.foo()" 98)
+    ("foo() [Top.CInner1.CInner1_CInner1]"
+     "Top.CInner1.CInner1_CInner1.foo()" 1099)
+    ("bar()"
+     "Top.CInner1.CInner1_CInner1.bar()" 1192)
+    ("foo() [Top.IInner1]"
+     "Top.IInner1.foo()" 1603)
+    ("foo() [Top.IInner1.IInner1_CInner1]"
+     "Top.IInner1.IInner1_CInner1.foo()" 1798)
+    ("defaultMethod(String) [Top.IInner1]"
+     "Top.IInner1.defaultMethod(String)" 1963)
+    ("defaultMethod(String) [Top.IInner1.IInner1_IInner1]"
+     "Top.IInner1.IInner1_IInner1.defaultMethod(String)" 2157)
+    ("EnumInner1()"
+     "Top.EnumInner1.EnumInner1()" 2353)
+    ("foo() [Top.EnumInner1]"
+     "Top.EnumInner1.foo()" 2399)
+    ("foo() [ColocatedTop]"
+     "ColocatedTop.foo()" 2554)
+    ("bar(String, String)"
+     "ColocatedTop.bar(String, String)" 2578)))
+
+(defun javaimp-test--imenu-method-list (exp-name-idx)
+  (let ((actual
+         (with-temp-buffer
+           (insert-file-contents
+            (concat javaimp--basedir "testdata/test1-misc-classes.java"))
+           (setq javaimp--parse-dirty-pos (point-min))
+           (javaimp-imenu-create-index)))
+        (expected javaimp-test--imenu-method-list-expected))
+    (should (= (length expected) (length actual)))
+    (dotimes (i (length expected))
+      (let ((exp (nth i expected))
+            (act (nth i actual)))
+        ;; name
+        (should (equal (nth exp-name-idx exp) (nth 0 act)))
+        ;; pos
+        (should (= (nth 2 exp) (nth 1 act)))))))
 
 (provide 'javaimp-tests)
diff --git a/javaimp-util.el b/javaimp-util.el
index 1544cc4..d058e82 100644
--- a/javaimp-util.el
+++ b/javaimp-util.el
@@ -25,17 +25,25 @@
 (require 'cl-lib)
 (require 'seq)
 
-(defcustom javaimp-cygpath-program
-  (if (eq system-type 'cygwin) "cygpath")
-  "Path to the `cygpath' program (Cygwin only).  Customize it if
-the program is not on `exec-path'."
-  :group 'javaimp
-  :type 'string)
-
 (defconst javaimp-debug-buf-name "*javaimp-debug*")
 
 (defconst javaimp--basedir (file-name-directory load-file-name))
 
+(defconst javaimp--classlike-scope-types
+  '(class interface enum))
+
+(defconst javaimp--named-scope-types
+  (append
+   '(local-class method)
+   javaimp--classlike-scope-types))
+
+(defconst javaimp--all-scope-types
+  (append
+   '(anonymous-class statement simple-statement array unknown)
+   javaimp--named-scope-types))
+
+
+
 ;; Structs
 
 (cl-defstruct javaimp-node
@@ -59,6 +67,16 @@ the program is not on `exec-path'."
 (cl-defstruct javaimp-cached-jar        ;jar or jmod
   file read-ts classes)
 
+(cl-defstruct javaimp-scope
+  type                                  ; see javaimp--all-scope-types
+  name
+  start
+  open-brace
+  parent)
+
+
+
+;; Xml
 
 (defun javaimp--xml-children (xml-tree child-name)
   "Returns list of children of XML-TREE filtered by CHILD-NAME"
@@ -76,15 +94,181 @@ the program is not on `exec-path'."
   (car (cddr el)))
 
 
-(defun javaimp--get-file-ts (file)
-  (nth 5 (file-attributes file)))
+
+;; Scopes
+
+(defsubst javaimp--is-classlike (scope)
+  (and scope
+       (memq (javaimp-scope-type scope)
+             javaimp--classlike-scope-types)))
+
+(defsubst javaimp--is-named (scope)
+  (and scope
+       (memq (javaimp-scope-type scope)
+             javaimp--named-scope-types)))
+
+(defsubst javaimp--is-imenu-included-method (scope)
+  (and (eq (javaimp-scope-type scope) 'method)
+       (javaimp--is-classlike (javaimp-scope-parent scope))))
+
+(defun javaimp--copy-scope (scope)
+  "Recursively copies SCOPE and its parents."
+  (let* ((res (copy-javaimp-scope scope))
+         (tmp res)
+         orig-parent)
+    (while (setq orig-parent (javaimp-scope-parent tmp))
+      (setf (javaimp-scope-parent tmp) (copy-javaimp-scope orig-parent))
+      (setq tmp (javaimp-scope-parent tmp)))
+    res))
+
+(defun javaimp--filter-scope-parents (scope pred)
+  "Rewrite SCOPE's parents so that only those matching PRED are
+left."
+  (while scope
+    (let ((parent (javaimp-scope-parent scope)))
+      (if (and parent
+               (not (funcall pred parent)))
+          ;; leave out this parent
+          (setf (javaimp-scope-parent scope) (javaimp-scope-parent parent))
+        (setq scope (javaimp-scope-parent scope))))))
+
+(defun javaimp--concat-scope-parents (scope)
+  (let (parents)
+    (while (setq scope (javaimp-scope-parent scope))
+      (push scope parents))
+    (mapconcat #'javaimp-scope-name parents ".")))
+
+
+
+;;; Formatting
+
+(defsubst javaimp-format-method-name-full (name args throws-args)
+  "Outputs NAME, ARGS (name and type) and THROWS-ARGS (only type)."
+  (concat name
+          "("
+          (mapconcat (lambda (arg)
+                       (concat (car arg) " " (cdr arg)))
+                     args
+                     ", ")
+          ")"
+          (if throws-args
+              (concat " throws "
+                      (mapconcat #'car throws-args ", ")))
+          ))
+
+(defsubst javaimp-format-method-name-types (name args _throws-args)
+  "Outputs NAME and ARGS (only type)."
+  (concat name
+          "("
+          (mapconcat #'car args ", ")
+          ")"
+          ))
+
+
+
+;; Tree
+
+(defun javaimp--build-tree (this all child-p &optional parent-node sort-pred)
+  "Recursively builds tree for element THIS and its children.
+Children are those elements from ALL for which CHILD-P invoked
+with this element and tested element returns non-nil.  Children
+are sorted by SORT-PRED, if given.  PARENT-NODE is indented for
+recursive calls."
+  (let ((children (seq-filter (apply-partially child-p this)
+                              all)))
+    (if sort-pred
+        (setq children (sort children sort-pred)))
+    (let* ((this-node (make-javaimp-node
+                      :parent parent-node
+                      :children nil
+                      :contents this))
+          (child-nodes
+           (mapcar (lambda (child)
+                     (javaimp--build-tree
+                       child all child-p this-node sort-pred))
+                   children)))
+      (setf (javaimp-node-children this-node) child-nodes)
+      this-node)))
+
+(defun javaimp--find-node (pred forest &optional unwrap)
+  (catch 'found
+    (dolist (tree forest)
+      (javaimp--find-node-in-tree tree pred unwrap))))
+
+(defun javaimp--find-node-in-tree (tree pred unwrap)
+  (when tree
+    (if (funcall pred (javaimp-node-contents tree))
+       (throw 'found
+               (if unwrap
+                   (javaimp-node-contents tree)
+                 tree)))
+    (dolist (child (javaimp-node-children tree))
+      (javaimp--find-node-in-tree child pred unwrap))))
+
+
+(defun javaimp--collect-nodes (pred forest)
+  (apply #'seq-concatenate 'list
+        (mapcar (lambda (tree)
+                   (delq nil
+                        (javaimp--collect-nodes-from-tree tree pred)))
+                forest)))
+
+(defun javaimp--collect-nodes-from-tree (tree pred)
+  (when tree
+    (cons (and (funcall pred (javaimp-node-contents tree))
+               (javaimp-node-contents tree))
+         (apply #'seq-concatenate 'list
+                (mapcar (lambda (child)
+                           (delq nil
+                                (javaimp--collect-nodes-from-tree child pred)))
+                        (javaimp-node-children tree))))))
+
+
+(defun javaimp--map-nodes (function pred forest)
+  "Recursively applies FUNCTION to each node's contents in FOREST
+and returns new tree.  FUNCTION should return (t . VALUE) if the
+result for this node should be made a list of the form (VALUE
+. CHILDREN), or (nil . VALUE) for plain VALUE as the result (in
+this case children are discarded).  The result for each node is
+additionally tested by PRED."
+  (delq nil
+        (mapcar (lambda (tree)
+                  (javaimp--map-nodes-from-tree tree function pred))
+                forest)))
+
+(defun javaimp--map-nodes-from-tree (tree function pred)
+  (when tree
+    (let* ((cell (funcall function (javaimp-node-contents tree)))
+           (res
+            (if (car cell)
+                (let ((children
+                       (delq nil
+                             (mapcar (lambda (child)
+                                       (javaimp--map-nodes-from-tree
+                                        child function pred))
+                                     (javaimp-node-children tree)))))
+                  (cons (cdr cell) children))
+              (cdr cell))))
+      (and (funcall pred res)
+           res))))
 
-(defun javaimp-print-id (id)
+(defun javaimp--get-root (node)
+  (while (javaimp-node-parent node)
+    (setq node (javaimp-node-parent node)))
+  node)
+
+
+
+;; Other
+
+(defsubst javaimp-print-id (id)
   (format "%s:%s:%s"
           (javaimp-id-artifact id)
           (javaimp-id-group id)
           (javaimp-id-version id)))
 
+(defsubst javaimp--get-file-ts (file)
+  (nth 5 (file-attributes file)))
 
 ;; TODO use functions `cygwin-convert-file-name-from-windows' and
 ;; `cygwin-convert-file-name-to-windows' when they are available
@@ -110,7 +294,6 @@ unchanged."
          (car (apply #'process-lines javaimp-cygpath-program args))))
     path))
 
-
 (defun javaimp--call-build-tool (program handler &rest args)
   "Runs PROGRAM with ARGS, then calls HANDLER in the temporary
 buffer and returns its result"
@@ -137,61 +320,4 @@ buffer and returns its result"
                   (concat "[" path-separator "\n]+")
                   t)))
 
-
-;; Tree building & search
-
-(defun javaimp--build-tree (this parent-node all)
-  (message "Building tree for module: %s" (javaimp-print-id (javaimp-module-id 
this)))
-  (let ((children
-        ;; more or less reliable way to find children is to look for
-        ;; modules with "this" as the parent
-        (seq-filter (lambda (m)
-                      (equal (javaimp-module-parent-id m) (javaimp-module-id 
this)))
-                    all)))
-    (let* ((this-node (make-javaimp-node
-                      :parent parent-node
-                      :children nil
-                      :contents this))
-          ;; recursively build child nodes
-          (child-nodes
-           (mapcar (lambda (child)
-                     (javaimp--build-tree child this-node all))
-                   children)))
-      (setf (javaimp-node-children this-node) child-nodes)
-      this-node)))
-
-(defun javaimp--find-node (predicate forest)
-  (catch 'found
-    (dolist (tree forest)
-      (javaimp--find-node-in-tree-1 tree predicate))))
-
-(defun javaimp--find-node-in-tree-1 (tree predicate)
-  (when tree
-    (if (funcall predicate (javaimp-node-contents tree))
-       (throw 'found tree))
-    (dolist (child (javaimp-node-children tree))
-      (javaimp--find-node-in-tree-1 child predicate))))
-
-
-(defun javaimp--collect-nodes (predicate forest)
-  (apply #'seq-concatenate 'list
-        (mapcar (lambda (tree)
-                  (javaimp--collect-nodes-from-tree tree predicate))
-                forest)))
-
-(defun javaimp--collect-nodes-from-tree (tree &optional predicate)
-  (when tree
-    (append (when (or (not predicate)
-                      (funcall predicate (javaimp-node-contents tree)))
-             (list tree))
-           (apply #'seq-concatenate 'list
-                  (mapcar (lambda (child)
-                            (javaimp--collect-nodes-from-tree child predicate))
-                          (javaimp-node-children tree))))))
-
-(defun javaimp--get-root (node)
-  (while (javaimp-node-parent node)
-    (setq node (javaimp-node-parent node)))
-  node)
-
 (provide 'javaimp-util)
diff --git a/javaimp.el b/javaimp.el
index 8309d50..c1ba563 100644
--- a/javaimp.el
+++ b/javaimp.el
@@ -78,6 +78,7 @@
 (require 'javaimp-maven)
 (require 'javaimp-gradle)
 (require 'javaimp-parse)
+(require 'cc-mode)                      ;for java-mode-syntax-table
 
 
 
@@ -148,6 +149,42 @@ files in the current project and add their fully-qualified 
names
 to the completion alternatives list."
   :type 'boolean)
 
+(defcustom javaimp-imenu-group-methods t
+  "How to lay out methods in Imenu index.
+If t (the default), methods are grouped in their enclosing
+scopes.  nil means use just flat list of simple method names.
+`qualified' means use flat list where each method name is
+prepended with nested scopes.  See also
+`javaimp-format-method-name'."
+  :type '(choice :tag "Group methods"
+                (const :tag "Group into enclosing scopes" t)
+                (const :tag "Flat list of simple name" nil)
+                (const :tag "Flat list of qualified names" qualified)))
+
+(defcustom javaimp-cygpath-program
+  (if (eq system-type 'cygwin) "cygpath")
+  "Path to the `cygpath' program (Cygwin only).  Customize it if
+the program is not on `exec-path'."
+  :type 'string)
+
+(defcustom javaimp-format-method-name #'javaimp-format-method-name-full
+  "Function to format method name, invoked with 3 arguments:
+NAME, ARGS and THROWS-ARGS.  The last two are lists with elements
+of the form (TYPE . NAME).  For THROWS-ARGS, only TYPE is
+present."
+  :type 'function)
+
+(defcustom javaimp-mvn-program "mvn"
+  "Path to the `mvn' program.  Customize it if the program is not
+on `exec-path'."
+  :type 'string)
+
+(defcustom javaimp-gradle-program "gradle"
+  "Path to the `gradle' program.  Customize it if the program is
+not on `exec-path'.  If the visited project's directory contains
+gradlew program, it is used in preference."
+  :type 'string)
+
 
 ;; Variables
 
@@ -160,6 +197,17 @@ to the completion alternatives list."
 
 (defvar javaimp--jdk-classes 'need-init)
 
+(defvar javaimp-syntax-table
+  (make-syntax-table java-mode-syntax-table) ;TODO don't depend
+  "Javaimp syntax table")
+
+(defvar javaimp--arglist-syntax-table
+  (let ((st (make-syntax-table javaimp-syntax-table)))
+    (modify-syntax-entry ?< "(>" st)
+    (modify-syntax-entry ?> ")<" st)
+    (modify-syntax-entry ?. "_" st) ; separates parts of fully-qualified type
+    st)
+  "Enables parsing angle brackets as lists")
 
 
 ;;;###autoload
@@ -291,13 +339,17 @@ any module file."
 ;; do not expose tree structure, return only modules
 
 (defun javaimp-find-module (predicate)
-  (let ((node (javaimp--find-node predicate javaimp-project-forest)))
-    (and node
-        (javaimp-node-contents node))))
+  "Returns first module in `javaimp-project-forest' for which
+PREDICATE returns non-nil."
+  (javaimp--find-node predicate javaimp-project-forest t))
 
 (defun javaimp-collect-modules (predicate)
-  (mapcar #'javaimp-node-contents
-         (javaimp--collect-nodes predicate javaimp-project-forest)))
+  "Returns all modules in `javaimp-project-forest' for which
+PREDICATE returns non-nil."
+  (javaimp--collect-nodes predicate javaimp-project-forest))
+
+(defun javaimp-map-modules (function)
+  (javaimp--map-nodes function #'always javaimp-project-forest))
 
 
 ;;; Adding imports
@@ -425,11 +477,12 @@ prefix arg is given, don't do this filtering."
 (defun javaimp--get-file-classes (file)
   (with-temp-buffer
     (insert-file-contents file)
+    (setq javaimp--parse-dirty-pos (point-min))
     (let ((package (javaimp--parse-get-package)))
       (mapcar (lambda (class)
                 (if package
                     (concat package "." class)))
-              (javaimp--parse-get-file-classes)))))
+              (javaimp--parse-get-all-classlikes)))))
 
 
 ;; Organizing imports
@@ -549,17 +602,73 @@ is `ordinary' or `static'.  Interactively, NEW-IMPORTS is 
nil."
 
 
 
-;; Misc
-
-(defun javaimp-reset (arg)
-  "Forget loaded trees state.  With prefix arg, also reset jars
-cache."
-  (interactive "P")
-  (setq javaimp-project-forest nil
-        javaimp--jdk-classes 'need-init)
-  (when arg
-    (setq javaimp-cached-jars nil)))
+;; Imenu support
 
+;;;###autoload
+(defun javaimp-imenu-create-index ()
+  "Function to use as `imenu-create-index-function'."
+  (let ((forest (save-excursion
+                  (javaimp--parse-get-imenu-forest))))
+    (cond ((not javaimp-imenu-group-methods)
+           ;; plain list of methods
+           (let ((entries
+                  (mapcar #'javaimp-imenu--make-entry
+                          (seq-sort-by
+                           #'javaimp-scope-start #'<
+                           (javaimp--collect-nodes
+                            #'javaimp--is-imenu-included-method forest))))
+                 name-alist)
+             (mapc (lambda (entry)
+                     (setf (alist-get (car entry) name-alist 0 nil #'equal)
+                           (1+ (alist-get (car entry) name-alist 0 nil 
#'equal))))
+                   entries)
+             (mapc (lambda (entry)
+                     ;; disambiguate same method names
+                     (when (> (alist-get (car entry) name-alist 0 nil #'equal) 
1)
+                       (setcar entry
+                               (format "%s [%s]"
+                                       (car entry)
+                                       (javaimp--concat-scope-parents
+                                        (nth 3 entry))))))
+                   entries)))
+          ((eq javaimp-imenu-group-methods 'qualified)
+           ;; list of qualified methods
+           (mapc (lambda (entry)
+                   ;; prepend parents to name
+                   (setcar entry (concat (javaimp--concat-scope-parents
+                                          (nth 3 entry))
+                                         "."
+                                         (car entry))))
+                 (mapcar #'javaimp-imenu--make-entry
+                         (seq-sort-by
+                          #'javaimp-scope-start #'<
+                          (javaimp--collect-nodes
+                           #'javaimp--is-imenu-included-method forest)))))
+
+          (t
+           ;; group methods inside their enclosing class
+           (javaimp--map-nodes
+            (lambda (scope)
+              (cond ((javaimp--is-classlike scope)
+                     ;; sub-alist
+                     (cons t (javaimp-scope-name scope)))
+                    ((javaimp--is-imenu-included-method scope)
+                     ;; entry
+                     (cons nil (javaimp-imenu--make-entry scope)))))
+            (lambda (res)
+              (or (functionp (nth 2 res)) ;entry
+                  (cdr res)))             ;non-empty sub-alist
+            forest)))))
+
+(defsubst javaimp-imenu--make-entry (scope)
+  (list (javaimp-scope-name scope)
+        (javaimp-scope-start scope)
+        #'javaimp-imenu--function
+        scope))
+
+(defun javaimp-imenu--function (index-name index-position scope)
+  (goto-char index-position)
+  (back-to-indentation))
 
 
 ;; Help
@@ -597,35 +706,49 @@ start (`javaimp-scope-start') instead."
                        (javaimp-scope-start scope)
                      (javaimp-scope-open-brace scope)))))))
 
-(defun javaimp-help-scopes-at-point ()
-  "Shows enclosing scopes at point in a *javaimp-scopes* buffer,
-which is first cleared."
+
+(defun javaimp-help-reparse-and-show-scopes ()
+  "Reparse scopes and show them in a *javaimp-scopes* buffer."
   (interactive)
-  (let* ((parse-sexp-ignore-comments t) ; FIXME remove with major mode
-         (parse-sexp-lookup-properties nil)
-         (scopes (javaimp--parse-scopes nil))
-         (file buffer-file-name)
-         (pos (point))
-         (buf (get-buffer-create "*javaimp-scopes*")))
+  (setq javaimp--parse-dirty-pos (point-min))
+  (let ((scopes (save-excursion
+                  (javaimp--parse-get-all-scopes)))
+        (file buffer-file-name)
+        (buf (get-buffer-create "*javaimp-scopes*")))
     (with-current-buffer buf
       (setq buffer-read-only nil)
       (erase-buffer)
-      (insert (propertize (format "Scopes at position %d in file:\n  %s\n\n"
-                                  pos file)
+      (insert (propertize (format "%s\n\n" file)
                           'javaimp-help-file file))
       (dolist (scope scopes)
-        (insert (propertize
-                 (concat (symbol-name (javaimp-scope-type scope))
-                         " "
-                         (javaimp-scope-name scope)
-                         "\n")
-                 'mouse-face 'highlight
-                 'help-echo "mouse-2: go to this scope"
-                 'javaimp-help-scope scope
-                 'keymap javaimp-help-keymap)))
+        (let ((depth 0)
+              (tmp scope))
+          (while (setq tmp (javaimp-scope-parent tmp))
+            (setq depth (1+ depth)))
+          (insert (propertize
+                   (format "%d: %010s %s\n"
+                           depth
+                           (symbol-name (javaimp-scope-type scope))
+                           (javaimp-scope-name scope))
+                   'mouse-face 'highlight
+                   'help-echo "mouse-2: go to this scope"
+                   'javaimp-help-scope scope
+                   'keymap javaimp-help-keymap))))
       (setq buffer-read-only t))
     (display-buffer buf)))
 
+
+;; Misc
+
+(defun javaimp-reset (arg)
+  "Forget loaded trees state.  With prefix arg, also reset jars
+cache."
+  (interactive "P")
+  (setq javaimp-project-forest nil
+        javaimp--jdk-classes 'need-init)
+  (when arg
+    (setq javaimp-cached-jars nil)))
+
 (provide 'javaimp)
 
 ;;; javaimp.el ends here
diff --git a/testdata/test-get-file-classes-1.java 
b/testdata/test-get-file-classes-1.java
deleted file mode 100644
index ba36d31..0000000
--- a/testdata/test-get-file-classes-1.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.foo;
-
-public class Top {
-    public static class CInner1<T, S> {
-        public void foo_bar() {
-            if (true) {
-                class CInner1_CLocal1 implements Serializable {
-                    void foo_bar() {
-                        System.out.println("");
-                        if (true) {
-                            class CInner1_CLocal1_CLocal1 extends Object {
-                                public void foo_bar() {
-                                    System.out.println("");
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-
-            class CInner1_CLocal2 extends Object {
-                void foo_bar() {
-                    System.out.println("");
-                }
-            }
-        }
-
-        private Object obj = new Object();
-        class CInner1_CInner1 {
-        }
-    }
-
-    interface IInner1 {
-        interface IInner1_IInner1 extends A<B, C>, Serializable {
-            void foo_bar();
-        }
-
-        static class IInner1_CInner1 {
-            public void foo_bar() {
-                if (true) {
-                    System.out.println("");
-                }
-            }
-        }
-    }
-
-    enum EInner1 {
-        A("a"),
-        B("b");
-        private EInner1() {
-        }
-        public void foo_bar() {
-            System.out.println("");
-        }
-
-        enum EInner1_EInner1 {
-            C, D
-        }
-    }
-}
diff --git a/testdata/test1-misc-classes.java b/testdata/test1-misc-classes.java
new file mode 100644
index 0000000..c010604
--- /dev/null
+++ b/testdata/test1-misc-classes.java
@@ -0,0 +1,125 @@
+package org.foo;
+
+public class Top {
+    public static class CInner1<T, S> {
+        public void foo() {
+            if (true) {
+                class CInner1_CLocal1 implements Serializable {
+                    void foo() {
+                        System.out.println("");
+                        if (true) {
+                            class CInner1_CLocal1_CLocal1 extends Object {
+                                public void foo() {
+                                    System.out.println("");
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            class CInner1_CLocal2 extends Object {
+                void foo() {
+                    System.out.println("");
+                }
+            }
+        }
+
+        private Object obj = new Object();
+        private Object obj = new Object() {
+                @Override
+                public String toString() {
+                    return "asdf";
+                }
+            };
+        abstract class CInner1_CInner1 {
+            public void foo() {
+            }
+
+            abstract void abstract_method();
+
+            public void bar() {
+                System.out.println("");
+            }
+
+            abstract void baz();
+        }
+    }
+
+    // public class LineCommentedClass {
+    // }
+
+    /*
+      public class BlockCommentedClass {
+      }
+    */
+
+    /*
+     * public class BlockCommentedClass2 {
+     * }
+     */
+
+    /**
+     * public class JavadocCommentedClass {
+     * }
+     */
+
+
+    interface IInner1 {
+        static void foo() {
+            if (true) {
+                System.out.println("");
+            }
+        }
+
+        String abstract_method();
+
+        static class IInner1_CInner1 {
+            public void foo() {
+                if (true) {
+                    System.out.println("");
+                }
+            }
+        }
+
+        void baz();
+
+        default void defaultMethod(String arg1) {
+            System.out.println("");
+        }
+
+        interface IInner1_IInner1 extends A<B, C>, Serializable {
+            void foo();
+
+            default String defaultMethod(String arg2) {
+                System.out.println("");
+            }
+
+            void baz();
+        }
+    }
+
+    enum EnumInner1 {
+        A("a"),
+        B("b");
+
+        private EnumInner1() {
+        }
+
+        public void foo() {
+            System.out.println("");
+        }
+
+        enum EnumInner1_EInner1 {
+            C, D
+        }
+    }
+}
+
+class ColocatedTop {
+    void foo() {
+    }
+
+    void bar(String arg1, String arg2) {
+    }
+}



reply via email to

[Prev in Thread] Current Thread [Next in Thread]