|
29 | 29 | import org.fife.rsta.ac.ShorthandCompletionCache; |
30 | 30 | import org.fife.rsta.ac.java.buildpath.LibraryInfo; |
31 | 31 | import org.fife.rsta.ac.java.buildpath.SourceLocation; |
| 32 | +import org.fife.rsta.ac.java.classreader.AccessFlags; |
32 | 33 | import org.fife.rsta.ac.java.classreader.ClassFile; |
33 | 34 | import org.fife.rsta.ac.java.classreader.FieldInfo; |
34 | 35 | import org.fife.rsta.ac.java.classreader.MemberInfo; |
|
46 | 47 | import org.fife.rsta.ac.java.rjc.lang.Type; |
47 | 48 | import org.fife.rsta.ac.java.rjc.lang.TypeArgument; |
48 | 49 | import org.fife.rsta.ac.java.rjc.lang.TypeParameter; |
| 50 | +import org.fife.ui.autocomplete.BasicCompletion; |
49 | 51 | import org.fife.ui.autocomplete.Completion; |
50 | 52 | import org.fife.ui.autocomplete.DefaultCompletionProvider; |
51 | 53 | import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; |
@@ -261,6 +263,93 @@ public void setShorthandCache(ShorthandCompletionCache shorthandCache) { |
261 | 263 | } |
262 | 264 |
|
263 | 265 |
|
| 266 | + /** |
| 267 | + * Adds completions for annotation element parameters when the caret |
| 268 | + * is inside an annotation's parentheses (e.g. {@code @Serialize(nam|)}). |
| 269 | + * |
| 270 | + * @param set The set to add completions to. |
| 271 | + * @param cu The compilation unit being parsed. |
| 272 | + * @param annotationClassName The simple name of the annotation type. |
| 273 | + * @return {@code true} if annotation completions were added (caller |
| 274 | + * should return them directly), {@code false} if not in |
| 275 | + * annotation context. |
| 276 | + * @since 3.3.0 |
| 277 | + */ |
| 278 | + private boolean addAnnotationElementCompletions(Set<Completion> set, |
| 279 | + CompilationUnit cu, String annotationClassName) { |
| 280 | + |
| 281 | + ClassFile cf = getClassFileFor(cu, annotationClassName); |
| 282 | + if (cf == null || |
| 283 | + (cf.getAccessFlags() & AccessFlags.ACC_ANNOTATION) == 0) { |
| 284 | + return false; |
| 285 | + } |
| 286 | + |
| 287 | + // Add annotation element methods as completions |
| 288 | + int methodCount = cf.getMethodCount(); |
| 289 | + for (int i=0; i<methodCount; i++) { |
| 290 | + MethodInfo method = cf.getMethodInfo(i); |
| 291 | + String name = method.getName(); |
| 292 | + |
| 293 | + // Skip synthetic methods and inherited Object/Annotation methods |
| 294 | + if (name.startsWith("<") || name.equals("values") || |
| 295 | + name.equals("valueOf") || name.equals("hashCode") || |
| 296 | + name.equals("toString") || name.equals("annotationType") || |
| 297 | + name.equals("equals")) { |
| 298 | + continue; |
| 299 | + } |
| 300 | + |
| 301 | + // Display shows "name" but insertion produces "name=" |
| 302 | + BasicCompletion bc = new BasicCompletion(this, name + "=") { |
| 303 | + @Override |
| 304 | + public String getInputText() { |
| 305 | + return name; |
| 306 | + } |
| 307 | + }; |
| 308 | + bc.setShortDescription(method.getReturnTypeString(false)); |
| 309 | + |
| 310 | + // Build summary from source Javadoc if available |
| 311 | + SourceLocation loc = getSourceLocForClass( |
| 312 | + cf.getClassName(true)); |
| 313 | + if (loc != null) { |
| 314 | + CompilationUnit annotCu = |
| 315 | + Util.getCompilationUnitFromDisk(loc, cf); |
| 316 | + if (annotCu != null) { |
| 317 | + Iterator<TypeDeclaration> tdi = |
| 318 | + annotCu.getTypeDeclarationIterator(); |
| 319 | + while (tdi.hasNext()) { |
| 320 | + TypeDeclaration td = tdi.next(); |
| 321 | + if (td.getName().equals( |
| 322 | + cf.getClassName(false))) { |
| 323 | + Iterator<Member> members = |
| 324 | + td.getMemberIterator(); |
| 325 | + while (members.hasNext()) { |
| 326 | + Member m = members.next(); |
| 327 | + if (m instanceof Method && |
| 328 | + ((Method) m).getName() |
| 329 | + .equals(name)) { |
| 330 | + String doc = |
| 331 | + ((Method) m).getDocComment(); |
| 332 | + if (doc != null && |
| 333 | + doc.startsWith("/**")) { |
| 334 | + bc.setSummary(Util |
| 335 | + .docCommentToHtml(doc)); |
| 336 | + } |
| 337 | + break; |
| 338 | + } |
| 339 | + } |
| 340 | + break; |
| 341 | + } |
| 342 | + } |
| 343 | + } |
| 344 | + } |
| 345 | + |
| 346 | + set.add(bc); |
| 347 | + } |
| 348 | + |
| 349 | + return !set.isEmpty(); |
| 350 | + } |
| 351 | + |
| 352 | + |
264 | 353 | /** |
265 | 354 | * Gets the {@link ClassFile} for a class. |
266 | 355 | * |
@@ -523,6 +612,34 @@ protected List<Completion> getCompletionsImpl(JTextComponent comp) { |
523 | 612 | // Note: getAlreadyEnteredText() never returns null |
524 | 613 | String text = getAlreadyEnteredText(comp); |
525 | 614 |
|
| 615 | + // Check for annotation parameter context: @Annotation(param| |
| 616 | + String annotationClassName = getAnnotationClassName(comp); |
| 617 | + if (annotationClassName != null && text.indexOf('.') == -1) { |
| 618 | + if (addAnnotationElementCompletions(set, cu, |
| 619 | + annotationClassName)) { |
| 620 | + completions = new ArrayList<>(set); |
| 621 | + Collections.sort(completions); |
| 622 | + text = text.substring(text.lastIndexOf('.') + 1); |
| 623 | + @SuppressWarnings("unchecked") |
| 624 | + int startIdx = Collections.binarySearch(completions, |
| 625 | + text, comparator); |
| 626 | + if (startIdx < 0) { |
| 627 | + startIdx = -(startIdx + 1); |
| 628 | + } |
| 629 | + else { |
| 630 | + while (startIdx > 0 && comparator.compare( |
| 631 | + completions.get(startIdx - 1), text) == 0) { |
| 632 | + startIdx--; |
| 633 | + } |
| 634 | + } |
| 635 | + @SuppressWarnings("unchecked") |
| 636 | + int endIdx = Collections.binarySearch(completions, |
| 637 | + text + '{', comparator); |
| 638 | + endIdx = -(endIdx + 1); |
| 639 | + return completions.subList(startIdx, endIdx); |
| 640 | + } |
| 641 | + } |
| 642 | + |
526 | 643 | // Special case - end of a String literal |
527 | 644 | boolean stringLiteralMember = checkStringLiteralMember(comp, text, cu, |
528 | 645 | set); |
@@ -552,6 +669,20 @@ protected List<Completion> getCompletionsImpl(JTextComponent comp) { |
552 | 669 |
|
553 | 670 | // Do a final sort of all of our completions and we're good to go! |
554 | 671 | completions = new ArrayList<>(set); |
| 672 | + |
| 673 | + // If in annotation context (@), filter to only annotation types |
| 674 | + boolean annotationContext = isAnnotationContext(comp, text); |
| 675 | + if (annotationContext) { |
| 676 | + Iterator<Completion> it = completions.iterator(); |
| 677 | + while (it.hasNext()) { |
| 678 | + Completion c = it.next(); |
| 679 | + if (!(c instanceof ClassCompletion) || |
| 680 | + !((ClassCompletion) c).isAnnotationType()) { |
| 681 | + it.remove(); |
| 682 | + } |
| 683 | + } |
| 684 | + } |
| 685 | + |
555 | 686 | Collections.sort(completions); |
556 | 687 |
|
557 | 688 | // Only match based on stuff after the final '.', since that's what is |
@@ -600,9 +731,109 @@ public List<LibraryInfo> getJars() { |
600 | 731 |
|
601 | 732 |
|
602 | 733 |
|
603 | | -public SourceLocation getSourceLocForClass(String className) { |
604 | | - return jarManager.getSourceLocForClass(className); |
605 | | -} |
| 734 | + /** |
| 735 | + * Returns the source location for the specified class, if available. |
| 736 | + * |
| 737 | + * @param className The fully qualified class name. |
| 738 | + * @return The source location, or {@code null} if not found. |
| 739 | + * @since 3.3.0 |
| 740 | + */ |
| 741 | + public SourceLocation getSourceLocForClass(String className) { |
| 742 | + return jarManager.getSourceLocForClass(className); |
| 743 | + } |
| 744 | + |
| 745 | + |
| 746 | + /** |
| 747 | + * Checks if the character immediately before the already-entered text |
| 748 | + * is '@', indicating an annotation context. |
| 749 | + */ |
| 750 | + private boolean isAnnotationContext(JTextComponent comp, |
| 751 | + String alreadyEntered) { |
| 752 | + try { |
| 753 | + int caret = comp.getCaretPosition(); |
| 754 | + int textStart = caret - alreadyEntered.length(); |
| 755 | + if (textStart > 0) { |
| 756 | + String prev = comp.getDocument().getText( |
| 757 | + textStart - 1, 1); |
| 758 | + return prev.charAt(0) == '@'; |
| 759 | + } |
| 760 | + } |
| 761 | + catch (BadLocationException e) { |
| 762 | + // ignore |
| 763 | + } |
| 764 | + return false; |
| 765 | + } |
| 766 | + |
| 767 | + |
| 768 | + /** |
| 769 | + * If the caret is inside an annotation's parentheses, returns the |
| 770 | + * annotation class name (simple name, to be resolved via imports). |
| 771 | + * Returns null if not in annotation parameter context. |
| 772 | + */ |
| 773 | + private String getAnnotationClassName(JTextComponent comp) { |
| 774 | + try { |
| 775 | + javax.swing.text.Document doc = comp.getDocument(); |
| 776 | + int caret = comp.getCaretPosition(); |
| 777 | + javax.swing.text.Element root = doc.getDefaultRootElement(); |
| 778 | + // Scan up to 3 lines back (annotation params may span lines) |
| 779 | + int lineIdx = root.getElementIndex(caret); |
| 780 | + int scanStart = root.getElement( |
| 781 | + Math.max(0, lineIdx - 3)).getStartOffset(); |
| 782 | + String text = doc.getText(scanStart, caret - scanStart); |
| 783 | + |
| 784 | + // Find unmatched '(' scanning backward |
| 785 | + int depth = 0; |
| 786 | + int i = text.length() - 1; |
| 787 | + int parenPos = -1; |
| 788 | + while (i >= 0) { |
| 789 | + char c = text.charAt(i); |
| 790 | + if (c == ')') { |
| 791 | + depth++; |
| 792 | + } |
| 793 | + else if (c == '(') { |
| 794 | + if (depth == 0) { |
| 795 | + parenPos = i; |
| 796 | + break; |
| 797 | + } |
| 798 | + depth--; |
| 799 | + } |
| 800 | + i--; |
| 801 | + } |
| 802 | + if (parenPos < 0) { |
| 803 | + return null; |
| 804 | + } |
| 805 | + |
| 806 | + // Extract identifier before '(' |
| 807 | + int nameEnd = parenPos; |
| 808 | + int nameStart = nameEnd - 1; |
| 809 | + while (nameStart >= 0 && |
| 810 | + Character.isJavaIdentifierPart( |
| 811 | + text.charAt(nameStart))) { |
| 812 | + nameStart--; |
| 813 | + } |
| 814 | + nameStart++; |
| 815 | + if (nameStart >= nameEnd) { |
| 816 | + return null; |
| 817 | + } |
| 818 | + |
| 819 | + // Check for '@' before the identifier |
| 820 | + int atPos = nameStart - 1; |
| 821 | + // Skip whitespace between @ and name |
| 822 | + while (atPos >= 0 && |
| 823 | + Character.isWhitespace(text.charAt(atPos))) { |
| 824 | + atPos--; |
| 825 | + } |
| 826 | + if (atPos < 0 || text.charAt(atPos) != '@') { |
| 827 | + return null; |
| 828 | + } |
| 829 | + |
| 830 | + return text.substring(nameStart, nameEnd); |
| 831 | + } |
| 832 | + catch (BadLocationException e) { |
| 833 | + return null; |
| 834 | + } |
| 835 | + } |
| 836 | + |
606 | 837 |
|
607 | 838 | /** |
608 | 839 | * Returns whether a method defined by a super class is accessible to |
|
0 commit comments