/*
* 12/21/2008
*
* DefaultCompletionProvider.java - A basic completion provider implementation.
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.autocomplete;
import java.awt.Point;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.Segment;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;
/**
* A basic completion provider implementation. This provider has no
* understanding of language semantics. It simply checks the text entered up
* to the caret position for a match against known completions. This is all
* that is needed in the majority of cases.
*
* @author Robert Futrell
* @version 1.0
*/
public class DefaultCompletionProvider extends AbstractCompletionProvider {
protected Segment seg;
/**
* Used to speed up {@link #getCompletionsAt(JTextComponent, Point)}.
*/
private String lastCompletionsAtText;
/**
* Used to speed up {@link #getCompletionsAt(JTextComponent, Point)},
* since this may be called multiple times in succession (this is usually
* called by <tt>JTextComponent.getToolTipText()</tt>, and if the user
* wiggles the mouse while a tool tip is displayed, this method gets
* repeatedly called. It can be costly so we try to speed it up a tad).
*/
private List<Completion> lastParameterizedCompletionsAt;
/**
* Constructor. The returned provider will not be aware of any completions.
*
* @see #addCompletion(Completion)
*/
public DefaultCompletionProvider() {
init();
}
/**
* Creates a completion provider that provides completion for a simple
* list of words.
*
* @param words The words to offer as completion suggestions. If this is
* <code>null</code>, no completions will be known.
* @see BasicCompletion
*/
public DefaultCompletionProvider(String[] words) {
init();
addWordCompletions(words);
}
/**
* Returns the text just before the current caret position that could be
* the start of something auto-completable.<p>
*
* This method returns all characters before the caret that are matched
* by {@link #isValidChar(char)}.
*
* {@inheritDoc}
*/
public String getAlreadyEnteredText(JTextComponent comp) {
Document doc = comp.getDocument();
int dot = comp.getCaretPosition();
Element root = doc.getDefaultRootElement();
int index = root.getElementIndex(dot);
Element elem = root.getElement(index);
int start = elem.getStartOffset();
int len = dot-start;
try {
doc.getText(start, len, seg);
} catch (BadLocationException ble) {
ble.printStackTrace();
return EMPTY_STRING;
}
int segEnd = seg.offset + len;
start = segEnd - 1;
while (start>=seg.offset && isValidChar(seg.array[start])) {
start--;
}
start++;
len = segEnd - start;
return len==0 ? EMPTY_STRING : new String(seg.array, start, len);
}
/**
* {@inheritDoc}
*/
public List<Completion> getCompletionsAt(JTextComponent tc, Point p) {
int offset = tc.viewToModel(p);
if (offset<0 || offset>=tc.getDocument().getLength()) {
lastCompletionsAtText = null;
return lastParameterizedCompletionsAt = null;
}
Segment s = new Segment();
Document doc = tc.getDocument();
Element root = doc.getDefaultRootElement();
int line = root.getElementIndex(offset);
Element elem = root.getElement(line);
int start = elem.getStartOffset();
int end = elem.getEndOffset() - 1;
try {
doc.getText(start, end-start, s);
// Get the valid chars before the specified offset.
int startOffs = s.offset + (offset-start) - 1;
while (startOffs>=s.offset && isValidChar(s.array[startOffs])) {
startOffs--;
}
// Get the valid chars at and after the specified offset.
int endOffs = s.offset + (offset-start);
while (endOffs<s.offset+s.count && isValidChar(s.array[endOffs])) {
endOffs++;
}
int len = endOffs - startOffs - 1;
if (len<=0) {
return lastParameterizedCompletionsAt = null;
}
String text = new String(s.array, startOffs+1, len);
if (text.equals(lastCompletionsAtText)) {
return lastParameterizedCompletionsAt;
}
// Get a list of all Completions matching the text.
List<Completion> list = getCompletionByInputText(text);
lastCompletionsAtText = text;
return lastParameterizedCompletionsAt = list;
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
lastCompletionsAtText = null;
return lastParameterizedCompletionsAt = null;
}
/**
* {@inheritDoc}
*/
public List<ParameterizedCompletion> getParameterizedCompletions(
JTextComponent tc) {
List<ParameterizedCompletion> list = null;
// If this provider doesn't support parameterized completions,
// bail out now.
char paramListStart = getParameterListStart();
if (paramListStart==0) {
return list; // null
}
int dot = tc.getCaretPosition();
Segment s = new Segment();
Document doc = tc.getDocument();
Element root = doc.getDefaultRootElement();
int line = root.getElementIndex(dot);
Element elem = root.getElement(line);
int offs = elem.getStartOffset();
int len = dot - offs - 1/*paramListStart.length()*/;
if (len<=0) { // Not enough chars on line for a method.
return list; // null
}
try {
doc.getText(offs, len, s);
// Get the identifier preceding the '(', ignoring any whitespace
// between them.
offs = s.offset + len - 1;
while (offs>=s.offset && Character.isWhitespace(s.array[offs])) {
offs--;
}
int end = offs;
while (offs>=s.offset && isValidChar(s.array[offs])) {
offs--;
}
String text = new String(s.array, offs+1, end-offs);
// Get a list of all Completions matching the text, but then
// narrow it down to just the ParameterizedCompletions.
List<Completion> l = getCompletionByInputText(text);
if (l!=null && !l.isEmpty()) {
for (int i=0; i<l.size(); i++) {
Object o = l.get(i);
if (o instanceof ParameterizedCompletion) {
if (list==null) {
list = new ArrayList<ParameterizedCompletion>(1);
}
list.add((ParameterizedCompletion)o);
}
}
}
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
return list;
}
/**
* Initializes this completion provider.
*/
protected void init() {
completions = new ArrayList<Completion>();
seg = new Segment();
}
/**
* Returns whether the specified character is valid in an auto-completion.
* The default implementation is equivalent to
* "<code>Character.isLetterOrDigit(ch) || ch=='_'</code>". Subclasses
* can override this method to change what characters are matched.
*
* @param ch The character.
* @return Whether the character is valid.
*/
protected boolean isValidChar(char ch) {
return Character.isLetterOrDigit(ch) || ch=='_';
}
/**
* Loads completions from an XML file. The XML should validate against
* <code>CompletionXml.dtd</code>.
*
* @param file An XML file to load from.
* @throws IOException If an IO error occurs.
*/
public void loadFromXML(File file) throws IOException {
BufferedInputStream bin = new BufferedInputStream(
new FileInputStream(file));
try {
loadFromXML(bin);
} finally {
bin.close();
}
}
/**
* Loads completions from an XML input stream. The XML should validate
* against <code>CompletionXml.dtd</code>.
*
* @param in The input stream to read from.
* @throws IOException If an IO error occurs.
*/
public void loadFromXML(InputStream in) throws IOException {
loadFromXML(in, null);
}
/**
* Loads completions from an XML input stream. The XML should validate
* against <code>CompletionXml.dtd</code>.
*
* @param in The input stream to read from.
* @param cl The class loader to use when loading any extra classes defined
* in the XML, such as custom {@link FunctionCompletion}s. This
* may be <code>null</code> if the default is to be used, or if no
* custom completions are defined in the XML.
* @throws IOException If an IO error occurs.
*/
public void loadFromXML(InputStream in, ClassLoader cl) throws IOException {
//long start = System.currentTimeMillis();
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(true);
CompletionXMLParser handler = new CompletionXMLParser(this, cl);
BufferedInputStream bin = new BufferedInputStream(in);
try {
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(bin, handler);
List<Completion> completions = handler.getCompletions();
addCompletions(completions);
char startChar = handler.getParamStartChar();
if (startChar!=0) {
char endChar = handler.getParamEndChar();
String sep = handler.getParamSeparator();
if (endChar!=0 && sep!=null && sep.length()>0) { // Sanity
setParameterizedCompletionParams(startChar, sep, endChar);
}
}
} catch (SAXException se) {
throw new IOException(se.toString());
} catch (ParserConfigurationException pce) {
throw new IOException(pce.toString());
} finally {
//long time = System.currentTimeMillis() - start;
//System.out.println("XML loaded in: " + time + "ms");
bin.close();
}
}
/**
* Loads completions from an XML file. The XML should validate against
* <code>CompletionXml.dtd</code>.
*
* @param resource A resource the current ClassLoader can get to.
* @throws IOException If an IO error occurs.
*/
public void loadFromXML(String resource) throws IOException {
ClassLoader cl = getClass().getClassLoader();
InputStream in = cl.getResourceAsStream(resource);
if (in==null) {
File file = new File(resource);
if (file.isFile()) {
in = new FileInputStream(file);
}
else {
throw new IOException("No such resource: " + resource);
}
}
BufferedInputStream bin = new BufferedInputStream(in);
try {
loadFromXML(bin);
} finally {
bin.close();
}
}
}