org.fife.rsta.ac.java.SourceCompletionProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of languagesupport Show documentation
Show all versions of languagesupport Show documentation
A library adding code completion and other advanced features for Java, JavaScript, Perl, and other languages to RSyntaxTextArea.
/* * 03/21/2010 * * Copyright (C) 2010 Robert Futrell * robert_futrell at users.sourceforge.net * http://fifesoft.com/rsyntaxtextarea * * 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 java.awt.Cursor; import java.awt.Point; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; 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.ClassFile; import org.fife.rsta.ac.java.classreader.FieldInfo; import org.fife.rsta.ac.java.classreader.MemberInfo; import org.fife.rsta.ac.java.classreader.MethodInfo; import org.fife.rsta.ac.java.rjc.ast.CodeBlock; import org.fife.rsta.ac.java.rjc.ast.CompilationUnit; import org.fife.rsta.ac.java.rjc.ast.Field; import org.fife.rsta.ac.java.rjc.ast.FormalParameter; import org.fife.rsta.ac.java.rjc.ast.ImportDeclaration; import org.fife.rsta.ac.java.rjc.ast.LocalVariable; import org.fife.rsta.ac.java.rjc.ast.Member; import org.fife.rsta.ac.java.rjc.ast.Method; import org.fife.rsta.ac.java.rjc.ast.NormalClassDeclaration; import org.fife.rsta.ac.java.rjc.ast.TypeDeclaration; 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.DefaultCompletionProvider; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; import org.fife.ui.rsyntaxtextarea.Token; /** * Parses a Java AST for code completions. It currently scans the following: * *
if **
* * Also, if the caret is inside a method, local variables up to the caret * position are also returned. * * @author Robert Futrell * @version 1.0 */ class SourceCompletionProvider extends DefaultCompletionProvider { /** * The parent completion provider. */ private JavaCompletionProvider javaProvider; /** * Used to get information about what classes match imports. */ private JarManager jarManager; private static final String JAVA_LANG_PACKAGE = "java.lang.*"; private static final String THIS = "this"; //Shorthand completions (templates and comments) private ShorthandCompletionCache shorthandCache; /** * Constructor. */ public SourceCompletionProvider() { this(null); } /** * Constructor. * * @param jarManager The jar manager for this provider. */ public SourceCompletionProvider(JarManager jarManager) { if (jarManager==null) { jarManager = new JarManager(); } this.jarManager = jarManager; setParameterizedCompletionParams('(', ", ", ')'); setAutoActivationRules(false, "."); // Default - only activate after '.' setParameterChoicesProvider(new SourceParamChoicesProvider()); } private void addCompletionsForStaticMembers(Set set, CompilationUnit cu, ClassFile cf, String pkg) { // Check us first, so if we override anything, we get the "newest" // version. int methodCount = cf.getMethodCount(); for (int i=0; i- Import statements *
- Method names *
- Field names *
null cf
representsjava.lang.Object
(or * if the super class could not be determined). */ private ClassFile getClassFileFor(CompilationUnit cu, String className) { //System.err.println(">>> Getting class file for: " + className); if (className==null) { return null; } ClassFile superClass = null; // Determine the fully qualified class to grab if (!Util.isFullyQualified(className)) { // Check in this source file's package first String pkg = cu.getPackageName(); if (pkg!=null) { String temp = pkg + "." + className; superClass = jarManager.getClassEntry(temp); } // Next, go through the imports (order is important) if (superClass==null) { for (Iterator i=cu.getImportIterator(); i.hasNext(); ) { ImportDeclaration id = (ImportDeclaration)i.next(); String imported = id.getName(); if (imported.endsWith(".*")) { String temp = imported.substring( 0, imported.length()-1) + className; superClass = jarManager.getClassEntry(temp); if (superClass!=null) { break; } } else if (imported.endsWith("." + className)) { superClass = jarManager.getClassEntry(imported); break; } } } // Finally, try java.lang if (superClass==null) { String temp = "java.lang." + className; superClass = jarManager.getClassEntry(temp); } } else { superClass = jarManager.getClassEntry(className); } return superClass; } /** * Adds completions for local variables in a method. * * @param set * @param method * @param offs The caret's offset into the source. This should be inside * ofmethod
. */ private void addLocalVarCompletions(Set set, Method method, int offs) { for (int i=0; iblock. */ private void addLocalVarCompletions(Set set, CodeBlock block, int offs) { for (int i=0; i offs) { break; } } } /** * Adds a jar to read from. * * @param info The jar to add. If this is null
, then * the current JVM's main JRE jar (rt.jar, or classes.jar on OS X) * will be added. If this jar has already been added, adding it * again will do nothing (except possibly update its attached source * location). * @throws IOException If an IO error occurs. * @see #getJars() * @see #removeJar(File) */ public void addJar(LibraryInfo info) throws IOException { jarManager.addClassFileSource(info); } /** * Checks whether the user is typing a completion for a String member after * a String literal. * * @param comp The text component. * @param alreadyEntered The text already entered. * @param cu The compilation unit being parsed. * @param set The set to add possible completions to. * @return Whether the user is indeed typing a completion for a String * literal member. */ private boolean checkStringLiteralMember(JTextComponent comp, String alreadyEntered, CompilationUnit cu, Set set) { boolean stringLiteralMember = false; int offs = comp.getCaretPosition() - alreadyEntered.length() - 1; if (offs>1) { RSyntaxTextArea textArea = (RSyntaxTextArea)comp; RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument(); try { //System.out.println(doc.charAt(offs) + ", " + doc.charAt(offs+1)); if (doc.charAt(offs)=='"' && doc.charAt(offs+1)=='.') { int curLine = textArea.getLineOfOffset(offs); Token list = textArea.getTokenListForLine(curLine); Token prevToken = RSyntaxUtilities.getTokenAtOffset(list, offs); if (prevToken!=null && prevToken.type==Token.LITERAL_STRING_DOUBLE_QUOTE) { ClassFile cf = getClassFileFor(cu, "java.lang.String"); addCompletionsForExtendedClass(set, cu, cf, cu.getPackageName(), null); stringLiteralMember = true; } else { System.out.println(prevToken); } } } catch (BadLocationException ble) { // Never happens ble.printStackTrace(); } } return stringLiteralMember; } /** * Removes all jars from the "build path." * * @see #removeJar(File) * @see #addClassFileSource(JarInfo) * @see #getJars() */ public void clearJars() { jarManager.clearClassFileSources(); // The memory used by the completions can be quite large, so go ahead // and clear out the completions list so no-longer-needed ones are // eligible for GC. clear(); } /** * Creates and returns a mapping of type parameters to type arguments. * * @param type The type of a variable/field/etc. whose fields/methods/etc. * are being code completed, as declared in the source. This * includes type arguments. * @param cf TheClassFile
representing the actual type of * the variable/field/etc. being code completed * @return A mapping of type parameter names to type arguments (both * Strings). */ private Map createTypeParamMap(Type type, ClassFile cf) { Map typeParamMap = null; List typeArgs = type.getTypeArguments(type.getIdentifierCount()-1); if (typeArgs!=null) { typeParamMap = new HashMap(); List paramTypes = cf.getParamTypes(); // Should be the same size! Otherwise, the source code has // too many/too few type arguments listed for this type. int min = Math.min(paramTypes==null ? 0 : paramTypes.size(), typeArgs.size()); for (int i=0; i0 && comparator.compare(completions.get(start-1), text)==0) { start--; } } int end = Collections.binarySearch(completions, text+'{', comparator); end = -(end+1); return completions.subList(start, end); } finally { comp.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); } } /** * Returns the jars on the "build path." * * @return A list of {@link JarInfo}s. Modifying a JarInfo in * this list will have no effect on this completion provider; in * order to do that, you must re-add the jar via * {@link #addClassFileSource(JarInfo)}. If there are no jars on the * "build path," this will be an empty list. * @see #addClassFileSource(JarInfo) */ public List getJars() { return jarManager.getClassFileSources(); } public SourceLocation getSourceLocForClass(String className) { return jarManager.getSourceLocForClass(className); } /** * Returns whether a method defined by a super class is accessible to * this class. * * @param info Information about the member. * @param pkg The package of the source currently being parsed. * @return Whether or not the method is accessible. */ private boolean isAccessible(MemberInfo info, String pkg) { boolean accessible = false; int access = info.getAccessFlags(); if (org.fife.rsta.ac.java.classreader.Util.isPublic(access) || org.fife.rsta.ac.java.classreader.Util.isProtected(access)) { accessible = true; } else if (org.fife.rsta.ac.java.classreader.Util.isDefault(access)) { String pkg2 = info.getClassFile().getPackageName(); accessible = (pkg==null && pkg2==null) || (pkg!=null && pkg.equals(pkg2)); } return accessible; } /** * {@inheritDoc} */ protected boolean isValidChar(char ch) { return Character.isJavaIdentifierPart(ch) || ch=='.'; } /** * Loads completions based on the current caret location in the source. In * other words: * * *
* * @param cu * @param comp * @param alreadyEntered * @param retVal */ private void loadCompletionsForCaretPosition(CompilationUnit cu, JTextComponent comp, String alreadyEntered, Set retVal) { // Get completions for all fields and methods of all type declarations. //long startTime = System.currentTimeMillis(); int caret = comp.getCaretPosition(); //List temp = new ArrayList(); int start, end; int lastDot = alreadyEntered.lastIndexOf('.'); boolean qualified = lastDot>-1; String prefix = qualified ? alreadyEntered.substring(0, lastDot) : null; for (Iterator i=cu.getTypeDeclarationIterator(); i.hasNext(); ) { TypeDeclaration td = (TypeDeclaration)i.next(); start = td.getBodyStartOffset(); end = td.getBodyEndOffset(); if (caret>start && caret<=end) { loadCompletionsForCaretPosition(cu, comp, alreadyEntered, retVal, td, prefix, caret); } else if (caret- If the caret is anywhere in a class, the names of all methods and * fields in the class are loaded. Methods and fields in super * classes are also loaded. TODO: Get super methods/fields added * correctly by access! *
- If the caret is in a field, local variables currently accessible * are loaded. *
* If the caret is anywhere in a class, the names of all methods and * fields in the class are loaded. Methods and fields in super * classes are also loaded. TODO: Get super methods/fields added * correctly by access! * If the caret is in a field, local variables currently accessible * are loaded. * * * @param cu * @param comp * @param alreadyEntered * @param retVal */ private void loadCompletionsForCaretPosition(CompilationUnit cu, JTextComponent comp, String alreadyEntered, Set retVal, TypeDeclaration td, String prefix, int caret) { // Do any child types first, so if any vars, etc. have duplicate names, // we pick up the one "closest" to us first. for (int i=0; i =method.getBodyStartOffset() && caret null if * none. * @param prefix The text up to the current caret position. This is * guaranteed to be non- null
not equal to * "this". * @param offs The offset of the caret in the document. */ private void loadCompletionsForCaretPositionQualified(CompilationUnit cu, String alreadyEntered, Set retVal, TypeDeclaration td, Method currentMethod, String prefix, int offs) { // TODO: Remove this restriction. int dot = prefix.indexOf('.'); if (dot>-1) { System.out.println("[DEBUG]: Qualified non-this completions currently only go 1 level deep"); return; } // TODO: Remove this restriction. else if (!prefix.matches("[A-Za-z_][A-Za-z0-9_\\$]*")) { System.out.println("[DEBUG]: Only identifier non-this completions are currently supported"); return; } String pkg = cu.getPackageName(); boolean matched = false; for (Iterator j=td.getMemberIterator(); j.hasNext(); ) { Member m = (Member)j.next(); // The prefix might be a field in the local class. if (m instanceof Field) { Field field = (Field)m; if (field.getName().equals(prefix)) { //System.out.println("FOUND: " + prefix + " (" + pkg + ")"); Type type = field.getType(); if (type.isArray()) { ClassFile cf = getClassFileFor(cu, "java.lang.Object"); addCompletionsForExtendedClass(retVal, cu, cf, pkg, null); FieldCompletion fc = FieldCompletion. createLengthCompletion(this, type); retVal.add(fc); } else if (!type.isBasicType()) { String typeStr = type.getName(true, false); ClassFile cf = getClassFileFor(cu, typeStr); // Add completions for extended class type chain if (cf!=null) { Map typeParamMap = createTypeParamMap(type, cf); addCompletionsForExtendedClass(retVal, cu, cf, pkg, typeParamMap); // Add completions for all implemented interfaces // TODO: Only do this if type is abstract! for (int i=0; ioffs) { break; } } } /** * Loads completions for a single import statement. * * @param importStr The import statement. * @param pkgName The package of the source currently being parsed. */ private void loadCompletionsForImport(Set set, String importStr, String pkgName) { if (importStr.endsWith(".*")) { String pkg = importStr.substring(0, importStr.length()-2); boolean inPkg = pkg.equals(pkgName); List classes = jarManager.getClassesInPackage(pkg, inPkg); for (Iterator i=classes.iterator(); i.hasNext(); ) { ClassFile cf = (ClassFile)i.next(); set.add(new ClassCompletion(this, cf)); } } else { ClassFile cf = jarManager.getClassEntry(importStr); if (cf!=null) { set.add(new ClassCompletion(this, cf)); } } } /** * Loads completions for all import statements. * * @param cu The compilation unit being parsed. */ private void loadImportCompletions(Set set, String text, CompilationUnit cu) { // Fully-qualified completions are handled elsewhere, so no need to // duplicate the work here if (text.indexOf('.')>-1) { return; } //long startTime = System.currentTimeMillis(); String pkgName = cu.getPackageName(); loadCompletionsForImport(set, JAVA_LANG_PACKAGE, pkgName); for (Iterator i=cu.getImportIterator(); i.hasNext(); ) { ImportDeclaration id = (ImportDeclaration)i.next(); String name = id.getName(); if (!JAVA_LANG_PACKAGE.equals(name)) { loadCompletionsForImport(set, name, pkgName); } } // Collections.sort(completions); //long time = System.currentTimeMillis() - startTime; //System.out.println("imports loaded in: " + time); } /** * Removes a jar from the "build path." * * @param jar The jar to remove. * @return Whether the jar was removed. This will be false
* if the jar was not on the build path. * @see #addClassFileSource(JarInfo) * @see #getJars() * @see #clearJars() */ public boolean removeJar(File jar) { boolean removed = jarManager.removeClassFileSource(jar); // The memory used by the completions can be quite large, so go ahead // and clear out the completions list so no-longer-needed ones are // eligible for GC. if (removed) { clear(); } return removed; } /** * Sets the parent Java provider. * * @param javaProvider The parent completion provider. */ void setJavaProvider(JavaCompletionProvider javaProvider) { this.javaProvider = javaProvider; } }