branch: externals/javaimp commit 150b592b3512b42929ed7e07f47e6fa1db6e6477 Author: Filipp Gunbin <fgun...@fastmail.fm> Commit: Filipp Gunbin <fgun...@fastmail.fm>
Improve flat imenu * Also show non-method items. * Include anon/local classes and their methods. * Refactor abstract method parsing. --- javaimp-parse.el | 137 ++++++++++++++++++++++++++-------------------------- javaimp-util.el | 4 +- javaimp.el | 145 +++++++++++++++++++++++++++++-------------------------- tests/imenu.el | 29 ++++++++--- tests/parse.el | 46 ++++++++++-------- 5 files changed, 196 insertions(+), 165 deletions(-) diff --git a/javaimp-parse.el b/javaimp-parse.el index 867ea99521..1178bc5606 100644 --- a/javaimp-parse.el +++ b/javaimp-parse.el @@ -18,6 +18,17 @@ ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <http://www.gnu.org/licenses/>. +;; When changing parsing, it may make sense to benchmark performance +;; on a large source tree with something like this: +;; +;; (benchmark-run 1 +;; (javaimp-flush-cache) +;; (javaimp--read-dir-source-idents +;; (javaimp-scope-defun-p t) +;; "<path-to-jdk-source>/src/java.base/share/classes/" +;; "bench")) + + (require 'cc-mode) ;for java-mode-syntax-table (require 'cl-lib) (require 'seq) @@ -32,7 +43,10 @@ open-brace (parent nil - :documentation "Reference to parent scope struct.")) + :documentation "Reference to parent scope struct.") + (attrs + nil + :documentation "Attributes plist")) (defconst javaimp-scope-all-types @@ -343,12 +357,13 @@ PARENT-PRED." (push scope parents)) (mapconcat #'javaimp-scope-name parents "."))) -(defsubst javaimp-scope-test-type (scope leaf-types parent-types) +(defsubst javaimp-scope-test-type (scope leaf-types &optional parent-types) (declare (indent 1)) (let ((res (memq (javaimp-scope-type scope) leaf-types))) - (while (and res - (setq scope (javaimp-scope-parent scope))) - (setq res (memq (javaimp-scope-type scope) parent-types))) + (when parent-types + (while (and res + (setq scope (javaimp-scope-parent scope))) + (setq res (memq (javaimp-scope-type scope) parent-types)))) res)) (defun javaimp-scope-defun-p (&optional include-also) @@ -364,6 +379,8 @@ methods. If INCLUDE-ALSO is t, include methods, as well as local '(anon-class local-class)))) (parent-types (if (eq include-also t) + ;; Anonymous / local classes may be inside non-defun + ;; scopes javaimp-scope-all-types '(class interface enum)))) (lambda (s) @@ -426,11 +443,18 @@ brace.") ;; or there weren't any of them. (setq arglist (javaimp-parse--arglist keyword-end (point) t)) (when (= (length arglist) 1) - (make-javaimp-scope :type (intern - (buffer-substring-no-properties - keyword-start keyword-end)) - :name (javaimp-parse--substr-before-< (caar arglist)) - :start keyword-start))))) + (let* ((type (intern (buffer-substring-no-properties + keyword-start keyword-end))) + (attrs + (when-let (((eq type 'class)) + (decl-start (javaimp-parse--decl-prefix))) + (goto-char brace-pos) + (when (javaimp-parse--rsb-keyword "\\_<abstract\\_>" decl-start t) + '(abstract t))))) + (make-javaimp-scope :type type + :name (javaimp-parse--substr-before-< (caar arglist)) + :start keyword-start + :attrs attrs)))))) (defun javaimp-parse--scope-simple-stmt (_brace-pos) "Attempts to parse `simple-statement' scope. Currently block @@ -449,7 +473,7 @@ lambdas are also recognized as such." :start (match-beginning 1))))) (defun javaimp-parse--scope-anon-class (brace-pos) - "Attempts to parse anonymous class scope." + "Attempts to parse `anon-class' scope." ;; skip arg-list and ws (when (and (progn (javaimp-parse--skip-back-until) @@ -464,9 +488,10 @@ lambdas are also recognized as such." arglist (javaimp-parse--arglist (match-end 0) end t)) (when (= (length arglist) 1) (make-javaimp-scope :type 'anon-class - :name - (concat "<anon>" - (javaimp-parse--substr-before-< (caar arglist))) + :name (format "<anon%d>%s" + brace-pos + (javaimp-parse--substr-before-< + (caar arglist))) :start start)))))) (defun javaimp-parse--scope-method-or-stmt (brace-pos) @@ -544,7 +569,8 @@ we have full chain of scope nesting: - If a `class' is nested in `method' (possibly within statements), then it's changed to `local-class'. - If an unrecognized scope (with nil type) is nested directly in -`array-init' then it's changed to `array-init'. +`array-init' then it's changed to `array-init'. This applies +recursively. If point is inside of any comment/string then this function does nothing." @@ -577,8 +603,12 @@ nothing." ;; once. (cond ((and (eq 'class (javaimp-scope-type curr)) in-method) - ;; Class nested in method is "local class" - (setf (javaimp-scope-type curr) 'local-class)) + ;; Change to local-class + (setf (javaimp-scope-type curr) 'local-class + (javaimp-scope-name curr) + (format "<local%d>%s" + (javaimp-scope-open-brace curr) + (javaimp-scope-name curr)))) ((and parent (eq 'array-init (javaimp-scope-type parent)) (not (javaimp-scope-type curr))) @@ -607,7 +637,8 @@ call this function first." (remove-text-properties javaimp-parse--dirty-pos (point-max) '(javaimp-parse-scope nil)) (goto-char (point-max)) - (let (;; Can be removed when we no longer rely on cc-mode + (let (;; Ignore syntax-table properties set by cc-mode. Can be + ;; removed when we no longer rely on cc-mode. (parse-sexp-lookup-properties nil)) (while (javaimp-parse--rsb-keyword "{" javaimp-parse--dirty-pos t) (save-excursion @@ -643,44 +674,24 @@ call this function first." (setq state (syntax-ppss))) (throw 'found nil)))))) -(defun javaimp-parse--class-abstract-methods () - (goto-char (point-max)) - (let (res) - (while (javaimp-parse--rsb-keyword "\\_<abstract\\_>" nil t) - (save-excursion - (let ((enclosing (nth 1 (syntax-ppss)))) - (when (and enclosing - (javaimp-parse--rsb-keyword ";" nil t -1) - ;; are we in the same nest? - (= (nth 1 (syntax-ppss)) enclosing)) - (backward-char) ;skip semicolon - ;; now parse as normal method scope - (when-let ((scope (javaimp-parse--scope-method-or-stmt (point))) - ;; note that an abstract method with no parents - ;; will be ignored - (parent (javaimp-parse--scopes nil))) - (setf (javaimp-scope-parent scope) (javaimp-scope-copy parent)) - (push scope res)))))) - res)) - -(defun javaimp-parse--interface-abstract-methods (int-scope) - (let ((start (1+ (javaimp-scope-open-brace int-scope))) +(defun javaimp-parse--abstract-methods (parent-scope) + (let ((start (1+ (javaimp-scope-open-brace parent-scope))) (end (ignore-errors - (1- (scan-lists (javaimp-scope-open-brace int-scope) 1 0)))) + (1- (scan-lists (javaimp-scope-open-brace parent-scope) 1 0)))) res) - (when (and start end) - (goto-char end) - (while (and (> (point) start) - (javaimp-parse--rsb-keyword ";" start t)) - ;; are we in the same nest? - (if (= (nth 1 (syntax-ppss)) (javaimp-scope-open-brace int-scope)) - (save-excursion - ;; now parse as normal method scope - (when-let ((scope (javaimp-parse--scope-method-or-stmt (point)))) - (setf (javaimp-scope-parent scope) int-scope) - (push scope res))) - ;; we've entered another nest, go back to its start - (goto-char (nth 1 (syntax-ppss)))))) + (goto-char (or end (point-max))) + (while (and (> (point) start) + (javaimp-parse--rsb-keyword ";" start t)) + ;; Are we in the same nest? + (if (= (nth 1 (syntax-ppss)) (javaimp-scope-open-brace parent-scope)) + (save-excursion + ;; Parse as normal method scope, but don't set open-brace + ;; because there's no body + (when-let ((scope (javaimp-parse--scope-method-or-stmt (point)))) + (setf (javaimp-scope-parent scope) parent-scope) + (push scope res))) + ;; We've entered another nest, skip to its start + (goto-char (nth 1 (syntax-ppss))))) res)) @@ -778,22 +789,10 @@ outside paren constructs like arg-list." (javaimp-parse--all-scopes)) (javaimp-parse--decl-prefix bound)) -(defun javaimp-parse-get-class-abstract-methods () - "Return all scopes which are abstract methods in classes." +(defun javaimp-parse-get-abstract-methods (scope) + "Return abstract methods defined in SCOPE." (javaimp-parse--all-scopes) - (javaimp-parse--class-abstract-methods)) - -(defun javaimp-parse-get-interface-abstract-methods () - "Return all scopes which are abstract methods in interfaces." - (javaimp-parse--all-scopes) - (let ((interfaces (javaimp-parse-get-all-scopes - nil nil - (lambda (s) - (javaimp-scope-test-type s - '(interface) '(class interface enum)))))) - (seq-mapcat #'javaimp-parse--interface-abstract-methods - interfaces))) - + (javaimp-parse--abstract-methods scope)) (defun javaimp-parse-fully-parsed-p () "Return non-nil if current buffer is fully parsed." diff --git a/javaimp-util.el b/javaimp-util.el index 935c033f4a..a7113617df 100644 --- a/javaimp-util.el +++ b/javaimp-util.el @@ -125,8 +125,8 @@ return a list of strings - jar file names.") (defun javaimp-tree-build (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 +with THIS 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))) diff --git a/javaimp.el b/javaimp.el index 6134d141fa..6845786719 100644 --- a/javaimp.el +++ b/javaimp.el @@ -184,7 +184,10 @@ the leading slash)." (defcustom javaimp-imenu-use-sub-alists nil "If non-nil, make sub-alist for each containing scope (e.g. a -class)." +class). In this case, scopes nested inside methods (local +classes, anonymous classes and their methods) are not shown, +because otherwise it won't be possible to go to the method +itself. Non-method scopes with no children are also omitted." :type 'boolean) (defcustom javaimp-jar-program "jar" @@ -407,7 +410,7 @@ then just return `default-directory'." (defun javaimp--read-dir-source-idents (scope-pred dir what-desc) (javaimp--collect-from-source-dir (apply-partially #'javaimp--collect-idents scope-pred) - dir 'javaimp--source-idents-cache what-desc)) + dir 'javaimp--source-idents-cache what-desc)) (defun javaimp--collect-idents (scope-pred buf file) "Return all identifiers satisfying SCOPE-PRED in buffer BUF, @@ -790,80 +793,84 @@ form as CLASS-ALIST in return value of ;;;###autoload (defun javaimp-imenu-create-index () - "Function to use as `imenu-create-index-function', can be set -in a major mode hook." - (let ((forest (javaimp-imenu--get-forest))) - (if javaimp-imenu-use-sub-alists - (javaimp-tree-map-nodes - (lambda (scope) - (if (eq (javaimp-scope-type scope) 'method) - ;; leaf entry for method - (cons nil (javaimp-imenu--make-entry scope)) - ;; sub-alist for class-like - (cons t (javaimp-scope-name scope)))) - (lambda (res) - (or (functionp (nth 2 res)) ; leaf imenu entry - (cdr res))) ; non-empty sub-alist - forest) - ;; Flat list - currently only methods - (let* ((methods (javaimp-tree-collect-nodes - (lambda (scope) - (eq (javaimp-scope-type scope) 'method)) - forest)) - (entries - (mapcar #'javaimp-imenu--make-entry - (seq-sort-by #'javaimp-scope-start #'< methods))) - alist) - (mapc (lambda (entry) - (setf (alist-get (car entry) alist 0 nil #'equal) - (1+ (alist-get (car entry) alist 0 nil #'equal)))) - entries) - ;; Append parents to equal names to disambiguate them - (mapc (lambda (entry) - (when (> (alist-get (car entry) alist 0 nil #'equal) 1) - (setcar entry - (format "%s [%s]" - (car entry) - (javaimp-scope-concat-parents - (nth 3 entry)))))) - entries))))) - -(defun javaimp-imenu--get-forest () - "Subroutine of `javaimp-imenu-create-index'." - (let* ((scopes - (javaimp-parse-get-all-scopes nil nil - (javaimp-scope-defun-p 'method))) - (abstract-methods (append - (javaimp-parse-get-class-abstract-methods) - (javaimp-parse-get-interface-abstract-methods))) - - methods classes top-classes) - (dolist (s scopes) - (if (eq (javaimp-scope-type s) 'method) - (push s methods) - (push s classes) - (when (null (javaimp-scope-parent s)) - (push s top-classes)))) - (setq methods (nreverse methods) - classes (nreverse classes) - top-classes (nreverse top-classes)) + "Function to use as `imenu-create-index-function'. +How to show the index is determined by +`javaimp-imenu-use-sub-alists', which see." + (if javaimp-imenu-use-sub-alists + (javaimp-imenu--create-index-nested) + (javaimp-imenu--create-index-flat))) + +(defun javaimp-imenu--create-index-nested () + "Build nested index for `javaimp-imenu-create-index'. Scopes +nested in methods are not included, see +`javaimp-imenu-use-sub-alists' for explanation." + (let ((forest (javaimp-imenu--get-forest + (javaimp-scope-defun-p 'method)))) + (javaimp-tree-map-nodes + (lambda (scope) + (if (eq (javaimp-scope-type scope) 'method) + ;; Leaf entry for method + (cons nil (javaimp-imenu--make-entry scope)) + ;; Sub-alist for container defuns - classes etc. + (cons t (javaimp-scope-name scope)))) + (lambda (res) + (or (functionp (nth 2 res)) ; leaf imenu entry + (cdr res))) ; non-empty sub-alist + forest))) + +(defun javaimp-imenu--create-index-flat () + "Build flat index for `javaimp-imenu-create-index'." + (let* ((forest (javaimp-imenu--get-forest + (javaimp-scope-defun-p t))) + (entries + (mapcar #'javaimp-imenu--make-entry + (seq-sort-by #'javaimp-scope-start #'< + (javaimp-tree-collect-nodes #'always forest)))) + alist) + (mapc (lambda (entry) + (setf (alist-get (car entry) alist 0 nil #'equal) + (1+ (alist-get (car entry) alist 0 nil #'equal)))) + entries) + ;; Append parents to equal names to disambiguate them + (mapc (lambda (entry) + (when (> (alist-get (car entry) alist 0 nil #'equal) 1) + (setcar entry + (format "%s [%s]" + (car entry) + (javaimp-scope-concat-parents + (nth 3 entry)))))) + entries))) + +(defun javaimp-imenu--get-forest (scope-pred) + "Build forest for imenu from scopes matching SCOPE-PRED." + (let* ((scopes (javaimp-parse-get-all-scopes nil nil scope-pred)) + ;; Note that as these are abstract methods, open-brace is nil + ;; for them. Represent them as scopes for convenience. + (abstract-methods + (mapcan + #'javaimp-parse-get-abstract-methods + (seq-filter + (lambda (s) + (or (eq (javaimp-scope-type s) 'interface) + (plist-get (javaimp-scope-attrs s) 'abstract))) + scopes)))) (mapcar - (lambda (top-class) + (lambda (top-defun) (when javaimp-verbose (message "Building tree for top-level %s %s" - (javaimp-scope-type top-class) - (javaimp-scope-name top-class))) - (javaimp-tree-build top-class - (append methods - classes - abstract-methods) + (javaimp-scope-type top-defun) + (javaimp-scope-name top-defun))) + (javaimp-tree-build top-defun + (append scopes abstract-methods) (lambda (el tested) - (equal el (javaimp-scope-parent tested))) + (eq el (javaimp-scope-parent tested))) nil (lambda (s1 s2) (< (javaimp-scope-start s1) (javaimp-scope-start s2))))) - top-classes))) + (seq-filter (lambda (s) + (not (javaimp-scope-parent s))) + scopes)))) (defun javaimp-imenu--function (_index-name index-position _scope) (let ((decl-beg (javaimp--beg-of-defun-decl index-position))) @@ -879,6 +886,7 @@ in a major mode hook." (defun javaimp-xref--backend () 'javaimp) (defun javaimp-xref--ident-completion-table () + ;; FIXME: Include local classes and abstract methods (let ((scope-pred (javaimp-scope-defun-p 'method)) (module (javaimp--detect-module))) (if module @@ -1273,6 +1281,7 @@ defun javadoc to be included in the narrowed region when using (setq-local multibyte-syntax-as-symbol t) ;; Discard parse state, if any (setq javaimp-parse--dirty-pos nil) + (setq syntax-ppss-table java-mode-syntax-table) ;; There're spaces within generic types, just show them (setq-local imenu-space-replacement nil)) (remove-function (local 'imenu-create-index-function) diff --git a/tests/imenu.el b/tests/imenu.el index 0ae4f3c64d..4f911a4e98 100644 --- a/tests/imenu.el +++ b/tests/imenu.el @@ -17,35 +17,52 @@ (javaimp-test-imenu--simplify-entries (cdr elt))))) -(ert-deftest javaimp-imenu-create-index () +(ert-deftest javaimp-imenu-create-index-flat () (let ((actual (javaimp-with-temp-buffer "test1.java" - (javaimp-imenu-create-index))) + (javaimp-imenu--create-index-flat))) (expected-names - '("foo() [Top.CInner1]" + '("Top" + "CInner1" + "foo() [Top.CInner1]" + "<local192>CInner1_CLocal1" + "foo() [Top.CInner1.foo().<local192>CInner1_CLocal1]" + "<local384>CInner1_CLocal1_CLocal1" + "foo() [Top.CInner1.foo().<local192>CInner1_CLocal1.foo()\ +.<local384>CInner1_CLocal1_CLocal1]" + "<local692>CInner1_CLocal2" + "foo() [Top.CInner1.foo().<local692>CInner1_CLocal2]" + "<anon895>Object" + "toString()" + "CInner1_CInner1" "foo() [Top.CInner1.CInner1_CInner1]" "abstract_method() [Top.CInner1.CInner1_CInner1]" "bar()" "baz() [Top.CInner1.CInner1_CInner1]" + "IInner1" "foo() [Top.IInner1]" "abstract_method() [Top.IInner1]" + "IInner1_CInner1" "foo() [Top.IInner1.IInner1_CInner1]" "baz() [Top.IInner1]" "defaultMethod(String) [Top.IInner1]" + "IInner1_IInner1" "foo() [Top.IInner1.IInner1_IInner1]" "defaultMethod(String) [Top.IInner1.IInner1_IInner1]" "baz() [Top.IInner1.IInner1_IInner1]" + "EnumInner1" "EnumInner1()" "foo() [Top.EnumInner1]" + "EnumInner1_EInner1" + "ColocatedTop" "foo() [ColocatedTop]" "bar(String,String)"))) (should (= (length expected-names) (length actual))) (dotimes (i (length expected-names)) (should (equal (nth i expected-names) (car (nth i actual))))))) -(ert-deftest javaimp-imenu-create-index-use-sub-alists () +(ert-deftest javaimp-imenu-create-index-nested () (let ((actual (javaimp-with-temp-buffer "test1.java" - (let ((javaimp-imenu-use-sub-alists t)) - (javaimp-imenu-create-index)))) + (javaimp-imenu--create-index-nested))) (expected '(("Top" ("CInner1" diff --git a/tests/parse.el b/tests/parse.el index 84d4a32454..daaccad764 100644 --- a/tests/parse.el +++ b/tests/parse.el @@ -121,7 +121,7 @@ Exception4<? super Exception5>>") (declare (indent 1)) (dolist (item test-items) (javaimp-with-temp-buffer nil - (insert (nth 0 item)) + (insert (car item)) (let* ((javaimp-parse--scope-hook (javaimp-parse--wrap-parser parser)) (scopes (mapcar #'javaimp-test-parse--simplify-scope (javaimp-parse-get-all-scopes nil nil nil t)))) @@ -150,6 +150,8 @@ implements Interface1<Bar, Baz>, Interface2 {" '("class Foo<T extends Baz<? extends Baz2>> \ extends Bar<? extends Baz<? extends Baz2>> {" ((class "Foo"))) + '("protected abstract class Foo {" + ((class "Foo" abstract t))) '("interface Foo<Bar, Baz> {" ((interface "Foo"))) '("private enum Foo {" @@ -159,17 +161,17 @@ extends Bar<? extends Baz<? extends Baz2>> {" (ert-deftest javaimp-parse-scope-anon-class () (javaimp-test-parse--scope #'javaimp-parse--scope-anon-class '(" = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {" - ((anon-class "<anon>Object"))) + ((anon-class "<anon51>Object"))) `(,(subst-char-in-string ? ?\n " = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {") - ((anon-class "<anon>Object"))) + ((anon-class "<anon51>Object"))) '("new Object(foo()) {" - ((anon-class "<anon>Object"))) + ((anon-class "<anon19>Object"))) '(" = (obj.getField()) .new Object<Class1, Class2>(1, baz) {" - ((anon-class "<anon>Object"))) + ((anon-class "<anon57>Object"))) '(" = obj.new Object<>(1, baz) {" - ((anon-class "<anon>Object"))) + ((anon-class "<anon29>Object"))) )) (ert-deftest javaimp-parse-scope-method-or-stmt () @@ -241,44 +243,44 @@ throws E1 {" ((method "foo()") (class "CInner1") (class "Top")) - ((local-class "CInner1_CLocal1") + ((local-class "<local192>CInner1_CLocal1") (method "foo()") (class "CInner1") (class "Top")) ((method "foo()") - (local-class "CInner1_CLocal1") + (local-class "<local192>CInner1_CLocal1") (method "foo()") (class "CInner1") (class "Top")) - ((local-class "CInner1_CLocal1_CLocal1") + ((local-class "<local384>CInner1_CLocal1_CLocal1") (method "foo()") - (local-class "CInner1_CLocal1") + (local-class "<local192>CInner1_CLocal1") (method "foo()") (class "CInner1") (class "Top")) ((method "foo()") - (local-class "CInner1_CLocal1_CLocal1") + (local-class "<local384>CInner1_CLocal1_CLocal1") (method "foo()") - (local-class "CInner1_CLocal1") + (local-class "<local192>CInner1_CLocal1") (method "foo()") (class "CInner1") (class "Top")) - ((local-class "CInner1_CLocal2") + ((local-class "<local692>CInner1_CLocal2") (method "foo()") (class "CInner1") (class "Top")) ((method "foo()") - (local-class "CInner1_CLocal2") + (local-class "<local692>CInner1_CLocal2") (method "foo()") (class "CInner1") (class "Top")) - ((anon-class "<anon>Object") + ((anon-class "<anon895>Object") (class "CInner1") (class "Top")) ((method "toString()") - (anon-class "<anon>Object") (class "CInner1") (class "Top")) + (anon-class "<anon895>Object") (class "CInner1") (class "Top")) - ((class "CInner1_CInner1") (class "CInner1") (class "Top")) + ((class "CInner1_CInner1" abstract t) (class "CInner1") (class "Top")) ((method "foo()") - (class "CInner1_CInner1") (class "CInner1") (class "Top")) + (class "CInner1_CInner1" abstract t) (class "CInner1") (class "Top")) ((method "bar()") - (class "CInner1_CInner1") (class "CInner1") (class "Top")) + (class "CInner1_CInner1" abstract t) (class "CInner1") (class "Top")) ((interface "IInner1") (class "Top")) @@ -329,7 +331,11 @@ throws E1 {" (defun javaimp-test-parse--simplify-scope (s) (let (res) (while s - (push (list (javaimp-scope-type s) (javaimp-scope-name s)) res) + (push (append + (list (javaimp-scope-type s) + (javaimp-scope-name s)) + (javaimp-scope-attrs s)) + res) (setq s (javaimp-scope-parent s))) (nreverse res)))