org.fife.rsta.ac.java.JavaLanguageSupport Maven / Gradle / Ivy
/*
* 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.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import org.fife.rsta.ac.AbstractLanguageSupport;
import org.fife.rsta.ac.GoToMemberAction;
import org.fife.rsta.ac.java.rjc.ast.CompilationUnit;
import org.fife.rsta.ac.java.rjc.ast.ImportDeclaration;
import org.fife.rsta.ac.java.rjc.ast.Package;
import org.fife.rsta.ac.java.tree.JavaOutlineTree;
import org.fife.ui.autocomplete.AutoCompletion;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
/**
* Language support for Java.
*
* @author Robert Futrell
* @version 1.0
*/
public class JavaLanguageSupport extends AbstractLanguageSupport {
/**
* Maps JavaParsers to Info instances about them.
*/
private Map parserToInfoMap;
/**
* The shared jar manager to use with all {@link JavaCompletionProvider}s,
* or null
if each one should have a unique jar manager.
*/
private JarManager jarManager;
/**
* Client property installed on text areas that points to a listener.
*/
private static final String PROPERTY_LISTENER =
"org.fife.rsta.ac.java.JavaLanguageSupport.Listener";
/**
* Constructor.
*/
public JavaLanguageSupport() {
parserToInfoMap = new HashMap();
jarManager = new JarManager();
setAutoActivationEnabled(true);
setParameterAssistanceEnabled(true);
setShowDescWindow(true);
}
/**
* Returns the completion provider running on a text area with this Java
* language support installed.
*
* @param textArea The text area.
* @return The completion provider. This will be null
if
* the text area does not have this JavaLanguageSupport
* installed.
*/
public JavaCompletionProvider getCompletionProvider(
RSyntaxTextArea textArea) {
AutoCompletion ac = getAutoCompletionFor(textArea);
return (JavaCompletionProvider)ac.getCompletionProvider();
}
/**
* Returns the shared jar manager instance.
* NOTE: This method will be removed over time, as the Java support becomes
* more robust!
*
* @return The shared jar manager.
*/
public JarManager getJarManager() {
return jarManager;
}
/**
* Returns the Java parser running on a text area with this Java language
* support installed.
*
* @param textArea The text area.
* @return The Java parser. This will be null
if the text
* area does not have this JavaLanguageSupport installed.
*/
public JavaParser getParser(RSyntaxTextArea textArea) {
// Could be a parser for another language.
Object parser = textArea.getClientProperty(PROPERTY_LANGUAGE_PARSER);
if (parser instanceof JavaParser) {
return (JavaParser)parser;
}
return null;
}
/**
* {@inheritDoc}
*/
public void install(RSyntaxTextArea textArea) {
JavaCompletionProvider p = new JavaCompletionProvider(jarManager);
// Can't use createAutoCompletion(), as Java's is "special."
AutoCompletion ac = new JavaAutoCompletion(p, textArea);
ac.setListCellRenderer(new JavaCellRenderer());
ac.setAutoCompleteEnabled(isAutoCompleteEnabled());
ac.setAutoActivationEnabled(isAutoActivationEnabled());
ac.setAutoActivationDelay(getAutoActivationDelay());
ac.setParameterAssistanceEnabled(isParameterAssistanceEnabled());
ac.setParamChoicesRenderer(new JavaParamListCellRenderer());
ac.setShowDescWindow(getShowDescWindow());
ac.install(textArea);
installImpl(textArea, ac);
textArea.setToolTipSupplier(p);
Listener listener = new Listener(textArea);
textArea.putClientProperty(PROPERTY_LISTENER, listener);
JavaParser parser = new JavaParser(textArea);
textArea.putClientProperty(PROPERTY_LANGUAGE_PARSER, parser);
textArea.addParser(parser);
Info info = new Info(textArea, p, parser);
parserToInfoMap.put(parser, info);
installKeyboardShortcuts(textArea);
}
/**
* Installs extra keyboard shortcuts supported by this language support.
*
* @param textArea The text area to install the shortcuts into.
*/
private void installKeyboardShortcuts(RSyntaxTextArea textArea) {
InputMap im = textArea.getInputMap();
ActionMap am = textArea.getActionMap();
int c = textArea.getToolkit().getMenuShortcutKeyMask();
int shift = InputEvent.SHIFT_MASK;
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, c|shift), "GoToType");
am.put("GoToType", new GoToMemberAction(JavaOutlineTree.class));
}
/**
* {@inheritDoc}
*/
public void uninstall(RSyntaxTextArea textArea) {
uninstallImpl(textArea);
JavaParser parser = getParser(textArea);
Info info = (Info)parserToInfoMap.remove(parser);
if (info!=null) { // Should always be true
parser.removePropertyChangeListener(
JavaParser.PROPERTY_COMPILATION_UNIT, info);
}
textArea.removeParser(parser);
textArea.putClientProperty(PROPERTY_LANGUAGE_PARSER, null);
Object listener = textArea.getClientProperty(PROPERTY_LISTENER);
if (listener instanceof Listener) { // Should always be true
((Listener)listener).uninstall();
textArea.putClientProperty(PROPERTY_LISTENER, null);
}
uninstallKeyboardShortcuts(textArea);
}
/**
* Uninstalls any keyboard shortcuts specific to this language support.
*
* @param textArea The text area to uninstall the actions from.
*/
private void uninstallKeyboardShortcuts(RSyntaxTextArea textArea) {
InputMap im = textArea.getInputMap();
ActionMap am = textArea.getActionMap();
int c = textArea.getToolkit().getMenuShortcutKeyMask();
int shift = InputEvent.SHIFT_MASK;
im.remove(KeyStroke.getKeyStroke(KeyEvent.VK_O, c|shift));
am.remove("GoToType");
}
/**
* Information about an import statement to add and where it should be
* added. This is used internally when a class completion is inserted,
* and it needs an import statement added to the source.
*/
private static class ImportToAddInfo {
public int offs;
public String text;
public ImportToAddInfo(int offset, String text) {
this.offs = offset;
this.text = text;
}
}
/**
* Manages information about the parsing/auto-completion for a single text
* area. Unlike many simpler language supports,
* JavaLanguageSupport cannot share any information amongst
* instances of RSyntaxTextArea.
*/
private static class Info implements PropertyChangeListener {
public JavaCompletionProvider provider;
//public JavaParser parser;
public Info(RSyntaxTextArea textArea, JavaCompletionProvider provider,
JavaParser parser) {
this.provider = provider;
//this.parser = parser;
parser.addPropertyChangeListener(
JavaParser.PROPERTY_COMPILATION_UNIT, this);
}
/**
* Called when a text area is re-parsed.
*
* @param e The event.
*/
public void propertyChange(PropertyChangeEvent e) {
String name = e.getPropertyName();
if (JavaParser.PROPERTY_COMPILATION_UNIT.equals(name)) {
CompilationUnit cu = (CompilationUnit)e.getNewValue();
// structureTree.update(file, cu);
// updateTable();
provider.setCompilationUnit(cu);
}
}
}
/**
* A hack of AutoCompletion that forces the JavaParser
* to re-parse the document when the user presses ctrl+space.
*/
private class JavaAutoCompletion extends AutoCompletion {
private RSyntaxTextArea textArea;
private String replacementTextPrefix;
public JavaAutoCompletion(JavaCompletionProvider provider,
RSyntaxTextArea textArea) {
super(provider);
this.textArea = textArea;
}
private String getCurrentLineText() {
int caretPosition = textArea.getCaretPosition();
Element root = textArea.getDocument().getDefaultRootElement();
int line= root.getElementIndex(caretPosition);
Element elem = root.getElement(line);
int endOffset = elem.getEndOffset();
int lineStart = elem.getStartOffset();
String text = "";
try {
text = textArea.getText(lineStart, endOffset-lineStart).trim();
} catch (BadLocationException e) {
e.printStackTrace();
}
return text;
}
/**
* Overridden to allow for prepending to the replacement text. This
* allows us to insert fully qualified class names. instead of
* unqualified ones, if necessary (i.e. if the user tries to
* auto-complete javax.swing.text.Document
, but they've
* explicitly imported org.w3c.dom.Document
- we need to
* insert the fully qualified name in that case).
*/
protected String getReplacementText(Completion c, Document doc,
int start, int len) {
String text = super.getReplacementText(c, doc, start, len);
if (replacementTextPrefix!=null) {
text = replacementTextPrefix + text;
replacementTextPrefix = null;
}
return text;
}
/**
* Determines whether the class name being completed has been imported,
* and if it hasn't, returns the import statement that should be added
* for it. Alternatively, if the class hasn't been imported, but a
* class with the same (unqualified) name HAS been imported, this
* method sets things up so the fully-qualified version of this class's
* name is inserted.
*
* Thanks to Guilherme Joao Frantz and Jonatas Schuler for helping
* with the patch!
*
* @param c The completion being inserted.
* @return Whether an import was added.
*/
private ImportToAddInfo getShouldAddImport(ClassCompletion cc) {
String text = getCurrentLineText();
// Make sure we're not currently typing an import statement.
if (!text.startsWith("import ")) {
JavaCompletionProvider provider = (JavaCompletionProvider)
getCompletionProvider();
CompilationUnit cu = provider.getCompilationUnit();
int offset = 0;
boolean alreadyImported = false;
// Try to bail early, if possible.
if (cu==null) { // Can never happen, right?
return null;
}
if ("java.lang".equals(cc.getPackageName())) {
// Package java.lang is "imported" by default.
return null;
}
String className = cc.getClassName(false);
String fqClassName = cc.getClassName(true);
// If the completion is in the same package as the source we're
// editing (or both are in the default package), bail.
int lastClassNameDot = fqClassName.lastIndexOf('.');
boolean ccInPackage = lastClassNameDot>-1;
Package pkg = cu.getPackage();
if (ccInPackage && pkg!=null) {
String ccPkg = fqClassName.substring(0, lastClassNameDot);
String pkgName = pkg.getName();
if (ccPkg.equals(pkgName)) {
return null;
}
}
else if (!ccInPackage && pkg==null) {
return null;
}
// Loop through all import statements.
for (Iterator i=cu.getImportIterator(); i.hasNext(); ) {
ImportDeclaration id = (ImportDeclaration)i.next();
offset = id.getNameEndOffset() + 1;
// Pulling in static methods, etc. from a class - skip
if (id.isStatic()) {
continue;
}
// Importing all classes in the package...
else if (id.isWildcard()) {
// NOTE: Class may be in default package...
if (lastClassNameDot>-1) {
String imported = id.getName();
int dot = imported.lastIndexOf('.');
String importedPkg = imported.substring(0, dot);
String classPkg = fqClassName.substring(0, lastClassNameDot);
if (importedPkg.equals(classPkg)) {
alreadyImported = true;
break;
}
}
}
// Importing a single class from a package...
else {
String fullyImportedClassName = id.getName();
int dot = fullyImportedClassName.lastIndexOf('.');
String importedClassName = fullyImportedClassName.
substring(dot + 1);
// If they explicitly imported a class with the
// same name, but it's in a different package, then
// the user is required to fully-qualify the class
// in their code (if unqualified, it would be
// assumed to be of the type of the qualified
// class).
if (className.equals(importedClassName)) {
offset = -1; // Means "must fully qualify"
if (fqClassName.equals(fullyImportedClassName)) {
alreadyImported = true;
}
break;
}
}
}
// If the class wasn't imported, we'll need to add an
// import statement!
if (!alreadyImported) {
StringBuffer importToAdd = new StringBuffer();
// If there are no previous imports, add the import
// statement after the package line (if any).
if (offset == 0) {
if (pkg!=null) {
offset = pkg.getNameEndOffset() + 1;
// Keep an empty line between package and imports.
importToAdd.append('\n');
}
}
// We read through all imports, but didn't find our class.
// Add a new import statement after the last one.
if (offset > -1) {
//System.out.println(classCompletion.getAlreadyEntered(textArea));
if (offset>0) {
importToAdd.append("\nimport ").append(fqClassName).append(';');
}
else {
importToAdd.append("import ").append(fqClassName).append(";\n");
}
// TODO: Determine whether the imports are alphabetical,
// and if so, add the new one alphabetically.
return new ImportToAddInfo(offset, importToAdd.toString());
}
// Otherwise, either the class was imported, or a class
// with the same name was explicitly imported.
else {
// Another class with the same name was imported.
// We must insert the fully-qualified class name
// so the compiler resolves the correct class.
int dot = fqClassName.lastIndexOf('.');
if (dot>-1) {
String pkgName = fqClassName.substring(0, dot+1);
replacementTextPrefix = pkgName;
}
}
}
}
return null;
}
/**
* Overridden to handle special cases, because sometimes Java code
* completions will edit more in the source file than just the text
* at the current caret position.
*/
protected void insertCompletion(Completion c) {
ImportToAddInfo importInfo = null;
// We special-case class completions because they may add import
// statements to the top of our source file. We don't add the
// (possible) new import statement until after the completion is
// inserted; that way, when we treat it as an atomic undo/redo,
// when the user undoes the completion, the caret stays in the
// code instead of jumping to the import.
if (c instanceof ClassCompletion) {
importInfo = getShouldAddImport((ClassCompletion)c);
if (importInfo!=null) {
textArea.beginAtomicEdit();
}
}
try {
super.insertCompletion(c);
if (importInfo!=null) {
textArea.insert(importInfo.text, importInfo.offs);
}
} finally {
// Be safe and always pair beginAtomicEdit() and endAtomicEdit()
textArea.endAtomicEdit();
}
}
protected int refreshPopupWindow() {
// Force the parser to re-parse
JavaParser parser = getParser(textArea);
RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
String style = textArea.getSyntaxEditingStyle();
parser.parse(doc, style);
return super.refreshPopupWindow();
}
}
/**
* Listens for various events in a text area editing Java (in particular,
* caret events, so we can track the "active" code block).
*/
private class Listener implements CaretListener, ActionListener {
private RSyntaxTextArea textArea;
private Timer t;
public Listener(RSyntaxTextArea textArea) {
this.textArea = textArea;
textArea.addCaretListener(this);
t = new Timer(650, this);
t.setRepeats(false);
}
public void actionPerformed(ActionEvent e) {
JavaParser parser = getParser(textArea);
if (parser==null) {
return; // Shouldn't happen
}
CompilationUnit cu = parser.getCompilationUnit();
// Highlight the line range of the Java method being edited in the
// gutter.
if (cu != null) { // Should always be true
int dot = textArea.getCaretPosition();
Point p = cu.getEnclosingMethodRange(dot);
if (p != null) {
try {
int startLine = textArea.getLineOfOffset(p.x);
int endLine = textArea.getLineOfOffset(p.y);
textArea.setActiveLineRange(startLine, endLine);
} catch (BadLocationException ble) {
ble.printStackTrace();
}
}
else {
textArea.setActiveLineRange(-1, -1);
}
}
}
public void caretUpdate(CaretEvent e) {
t.restart();
}
/**
* Should be called whenever Java language support is removed from a
* text area.
*/
public void uninstall() {
textArea.removeCaretListener(this);
}
}
}