Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RSTALanguageSupport/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ dependencies {
api 'com.fifesoft:rsyntaxtextarea:3.6.1'
api 'com.fifesoft:autocomplete:3.3.3'
implementation 'org.mozilla:rhino-all:1.9.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

base {
Expand All @@ -30,6 +32,7 @@ jar {
}
}
test {
useJUnitPlatform()
testLogging {
events 'failed' //, 'passed', 'skipped', 'standardOut', 'standardError'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ public String getSummary() {
}


/**
* Returns whether this class is an annotation type (has ACC_ANNOTATION flag).
*
* @return Whether this is an annotation type.
*/
public boolean isAnnotationType() {
return (cf.getAccessFlags() & AccessFlags.ACC_ANNOTATION) != 0;
}


@Override
public String getToolTipText() {
return "class " + getReplacementText();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.fife.rsta.ac.ShorthandCompletionCache;
import org.fife.rsta.ac.java.buildpath.LibraryInfo;
import org.fife.rsta.ac.java.buildpath.SourceLocation;
import org.fife.rsta.ac.java.classreader.AccessFlags;
import org.fife.rsta.ac.java.classreader.ClassFile;
import org.fife.rsta.ac.java.classreader.FieldInfo;
import org.fife.rsta.ac.java.classreader.MemberInfo;
Expand All @@ -46,6 +47,7 @@
import org.fife.rsta.ac.java.rjc.lang.Type;
import org.fife.rsta.ac.java.rjc.lang.TypeArgument;
import org.fife.rsta.ac.java.rjc.lang.TypeParameter;
import org.fife.ui.autocomplete.BasicCompletion;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.autocomplete.DefaultCompletionProvider;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
Expand Down Expand Up @@ -261,6 +263,93 @@ public void setShorthandCache(ShorthandCompletionCache shorthandCache) {
}


/**
* Adds completions for annotation element parameters when the caret
* is inside an annotation's parentheses (e.g. {@code @Serialize(nam|)}).
*
* @param set The set to add completions to.
* @param cu The compilation unit being parsed.
* @param annotationClassName The simple name of the annotation type.
* @return {@code true} if annotation completions were added (caller
* should return them directly), {@code false} if not in
* annotation context.
* @since 3.3.0
*/
private boolean addAnnotationElementCompletions(Set<Completion> set,
CompilationUnit cu, String annotationClassName) {

ClassFile cf = getClassFileFor(cu, annotationClassName);
if (cf == null ||
(cf.getAccessFlags() & AccessFlags.ACC_ANNOTATION) == 0) {
return false;
}

// Add annotation element methods as completions
int methodCount = cf.getMethodCount();
for (int i=0; i<methodCount; i++) {
MethodInfo method = cf.getMethodInfo(i);
String name = method.getName();

// Skip synthetic methods and inherited Object/Annotation methods
if (name.startsWith("<") || name.equals("values") ||
name.equals("valueOf") || name.equals("hashCode") ||
name.equals("toString") || name.equals("annotationType") ||
name.equals("equals")) {
continue;
}

// Display shows "name" but insertion produces "name="
BasicCompletion bc = new BasicCompletion(this, name + "=") {
@Override
public String getInputText() {
return name;
}
};
bc.setShortDescription(method.getReturnTypeString(false));

// Build summary from source Javadoc if available
SourceLocation loc = getSourceLocForClass(
cf.getClassName(true));
if (loc != null) {
CompilationUnit annotCu =
Util.getCompilationUnitFromDisk(loc, cf);
if (annotCu != null) {
Iterator<TypeDeclaration> tdi =
annotCu.getTypeDeclarationIterator();
while (tdi.hasNext()) {
TypeDeclaration td = tdi.next();
if (td.getName().equals(
cf.getClassName(false))) {
Iterator<Member> members =
td.getMemberIterator();
while (members.hasNext()) {
Member m = members.next();
if (m instanceof Method &&
((Method) m).getName()
.equals(name)) {
String doc =
((Method) m).getDocComment();
if (doc != null &&
doc.startsWith("/**")) {
bc.setSummary(Util
.docCommentToHtml(doc));
}
break;
}
}
break;
}
}
}
}

set.add(bc);
}

return !set.isEmpty();
}


/**
* Gets the {@link ClassFile} for a class.
*
Expand Down Expand Up @@ -523,6 +612,34 @@ protected List<Completion> getCompletionsImpl(JTextComponent comp) {
// Note: getAlreadyEnteredText() never returns null
String text = getAlreadyEnteredText(comp);

// Check for annotation parameter context: @Annotation(param|
String annotationClassName = getAnnotationClassName(comp);
if (annotationClassName != null && text.indexOf('.') == -1) {
if (addAnnotationElementCompletions(set, cu,
annotationClassName)) {
completions = new ArrayList<>(set);
Collections.sort(completions);
text = text.substring(text.lastIndexOf('.') + 1);
@SuppressWarnings("unchecked")
int startIdx = Collections.binarySearch(completions,
text, comparator);
if (startIdx < 0) {
startIdx = -(startIdx + 1);
}
else {
while (startIdx > 0 && comparator.compare(
completions.get(startIdx - 1), text) == 0) {
startIdx--;
}
}
@SuppressWarnings("unchecked")
int endIdx = Collections.binarySearch(completions,
text + '{', comparator);
endIdx = -(endIdx + 1);
return completions.subList(startIdx, endIdx);
}
}

// Special case - end of a String literal
boolean stringLiteralMember = checkStringLiteralMember(comp, text, cu,
set);
Expand Down Expand Up @@ -552,6 +669,20 @@ protected List<Completion> getCompletionsImpl(JTextComponent comp) {

// Do a final sort of all of our completions and we're good to go!
completions = new ArrayList<>(set);

// If in annotation context (@), filter to only annotation types
boolean annotationContext = isAnnotationContext(comp, text);
if (annotationContext) {
Iterator<Completion> it = completions.iterator();
while (it.hasNext()) {
Completion c = it.next();
if (!(c instanceof ClassCompletion) ||
!((ClassCompletion) c).isAnnotationType()) {
it.remove();
}
}
}

Collections.sort(completions);

// Only match based on stuff after the final '.', since that's what is
Expand Down Expand Up @@ -600,9 +731,109 @@ public List<LibraryInfo> getJars() {



public SourceLocation getSourceLocForClass(String className) {
return jarManager.getSourceLocForClass(className);
}
/**
* Returns the source location for the specified class, if available.
*
* @param className The fully qualified class name.
* @return The source location, or {@code null} if not found.
* @since 3.3.0
*/
public SourceLocation getSourceLocForClass(String className) {
return jarManager.getSourceLocForClass(className);
}


/**
* Checks if the character immediately before the already-entered text
* is '@', indicating an annotation context.
*/
private boolean isAnnotationContext(JTextComponent comp,
String alreadyEntered) {
try {
int caret = comp.getCaretPosition();
int textStart = caret - alreadyEntered.length();
if (textStart > 0) {
String prev = comp.getDocument().getText(
textStart - 1, 1);
return prev.charAt(0) == '@';
}
}
catch (BadLocationException e) {
// ignore
}
return false;
}


/**
* If the caret is inside an annotation's parentheses, returns the
* annotation class name (simple name, to be resolved via imports).
* Returns null if not in annotation parameter context.
*/
private String getAnnotationClassName(JTextComponent comp) {
try {
javax.swing.text.Document doc = comp.getDocument();
int caret = comp.getCaretPosition();
javax.swing.text.Element root = doc.getDefaultRootElement();
// Scan up to 3 lines back (annotation params may span lines)
int lineIdx = root.getElementIndex(caret);
int scanStart = root.getElement(
Math.max(0, lineIdx - 3)).getStartOffset();
String text = doc.getText(scanStart, caret - scanStart);

// Find unmatched '(' scanning backward
int depth = 0;
int i = text.length() - 1;
int parenPos = -1;
while (i >= 0) {
char c = text.charAt(i);
if (c == ')') {
depth++;
}
else if (c == '(') {
if (depth == 0) {
parenPos = i;
break;
}
depth--;
}
i--;
}
if (parenPos < 0) {
return null;
}

// Extract identifier before '('
int nameEnd = parenPos;
int nameStart = nameEnd - 1;
while (nameStart >= 0 &&
Character.isJavaIdentifierPart(
text.charAt(nameStart))) {
nameStart--;
}
nameStart++;
if (nameStart >= nameEnd) {
return null;
}

// Check for '@' before the identifier
int atPos = nameStart - 1;
// Skip whitespace between @ and name
while (atPos >= 0 &&
Character.isWhitespace(text.charAt(atPos))) {
atPos--;
}
if (atPos < 0 || text.charAt(atPos) != '@') {
return null;
}

return text.substring(nameStart, nameEnd);
}
catch (BadLocationException e) {
return null;
}
}


/**
* Returns whether a method defined by a super class is accessible to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,11 @@ private TypeDeclaration getClassOrInterfaceDeclaration(CompilationUnit cu,
break;

case ANNOTATION_START:
// TODO: AnnotationTypeDeclaration, implement me.
throw new IOException(
"AnnotationTypeDeclaration not implemented");
// @interface — annotation type declaration.
// The '@' token has been consumed; now consume the 'interface' keyword.
s.yylexNonNull(KEYWORD_INTERFACE, "'interface' expected after '@'");
td = getNormalInterfaceDeclaration(cu, s, addTo);
break;

default:
ParserNotice notice = new ParserNotice(t,
Expand Down Expand Up @@ -873,9 +875,10 @@ else if (methodDecl) {
type.incrementBracketPairCount(s.skipBracketPairs());
}
List<String> thrownTypeNames = getThrownTypeNames(cu, s);
t = s.yylexNonNull("'{' or ';' expected");
t = s.yylexNonNull("';' expected");
if (t.getType() != SEPARATOR_SEMICOLON) {
throw new IOException("';' expected");
// Could be 'default' clause in annotation type element — skip to ';'
s.eatThroughNextSkippingBlocks(SEPARATOR_SEMICOLON);
}
Method m = new Method(s, modList, type, methodNameToken, formalParams,
thrownTypeNames);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* This library is distributed under a modified BSD license. See the included
* RSTALanguageSupport.License.txt file for details.
*/
package org.fife.rsta.ac.java;

import static org.junit.jupiter.api.Assertions.*;

import javax.swing.JTextArea;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Unit tests for annotation-related autocomplete features in
* {@link SourceCompletionProvider}.
*/
class SourceCompletionProviderTest {

private SourceCompletionProvider provider;

@BeforeEach
void setUp() {
provider = new SourceCompletionProvider();
}

/**
* Helper: creates a JTextArea with the given text and caret at the end,
* then returns getAlreadyEnteredText result.
*/
private String getEnteredText(String text) {
JTextArea textArea = new JTextArea(text);
textArea.setCaretPosition(text.length());
return provider.getAlreadyEnteredText(textArea);
}

// --- annotation prefix tests ---

@Test
void testAnnotationPrefix() {
// @ is not a Java identifier part, so it should NOT be included
assertEquals("Serialize", getEnteredText("@Serialize"));
}

@Test
void testAnnotationPrefixPartial() {
assertEquals("Ser", getEnteredText("@Ser"));
}

}
Loading