RSyntaxTextArea.
*
* @author Robert Futrell
* @version 0.9
*/
class ParserManager implements DocumentListener, ActionListener, HyperlinkListener, PropertyChangeListener {
private RSyntaxTextArea textArea;
private ListParserNotices that compare equally via
* equals(). Real-world example: The Perl compiler will return 2+
* identical error messages if the same error is committed in a single line more
* than once.
*/
private Listtrue, debug messages will be
* printed to stdout to help diagnose parsing issues.
*/
private static final String PROPERTY_DEBUG_PARSING = "rsta.debugParsing";
/**
* Whether to print debug messages while running parsers.
*/
private static final boolean DEBUG_PARSING;
/**
* The default delay between the last key press and when the document is parsed,
* in milliseconds.
*/
private static final int DEFAULT_DELAY_MS = 1250;
/**
* Constructor.
*
* @param textArea The text area whose document the parser will be parsing.
*/
ParserManager(RSyntaxTextArea textArea) {
this(DEFAULT_DELAY_MS, textArea);
}
/**
* Constructor.
*
* @param delay The delay between the last key press and when the document is
* parsed.
* @param textArea The text area whose document the parser will be parsing.
*/
ParserManager(int delay, RSyntaxTextArea textArea) {
this.textArea = textArea;
textArea.getDocument().addDocumentListener(this);
textArea.addPropertyChangeListener("document", this);
parsers = new ArrayListnull, nothing happens.
* @see #getParser(int)
* @see #removeParser(Parser)
*/
public void addParser(Parser parser) {
if (parser != null && !parsers.contains(parser)) {
if (running) {
timer.stop();
}
parsers.add(parser);
if (parsers.size() == 1) {
// Okay to call more than once.
ToolTipManager.sharedInstance().registerComponent(textArea);
}
if (running) {
timer.restart();
}
}
}
/**
* Adds highlights for a list of parser notices. Any current notices from the
* same Parser, in the same parsed range, are removed.
*
* @param res The result of a parsing.
* @see #clearParserNoticeHighlights()
*/
private void addParserNoticeHighlights(ParseResult res) {
// Parsers are supposed to return at least empty ParseResults, but
// we'll be defensive here.
if (res == null) {
return;
}
if (DEBUG_PARSING) {
System.out.println("[DEBUG]: Adding parser notices from " + res.getParser());
}
if (noticeHighlightPairs == null) {
noticeHighlightPairs = new ArrayList
*
* This method can be useful when a Parser can be configured as to
* what notices it returns. For example, if a Java language parser can be
* configured to set whether no serialVersionUID is a warning, error, or
* ignored, this method can be called after changing the expected notice type to
* have the document re-parsed.
*
* @param parser The index of the Parser to re-run.
* @see #getParser(int)
*/
public void forceReparsing(int parser) {
Parser p = getParser(parser);
RSyntaxDocument doc = (RSyntaxDocument) textArea.getDocument();
String style = textArea.getSyntaxEditingStyle();
doc.readLock();
try {
if (p.isEnabled()) {
ParseResult res = p.parse(doc, style);
addParserNoticeHighlights(res);
} else {
clearParserNoticeHighlights(p);
}
textArea.fireParserNoticesChange();
} finally {
doc.readUnlock();
}
}
/**
* Returns the delay between the last "concurrent" edit and when the document is
* re-parsed.
*
* @return The delay, in milliseconds.
* @see #setDelay(int)
*/
public int getDelay() {
return timer.getDelay();
}
/**
* Returns the specified parser.
*
* @param index The index of the parser.
* @return The parser.
* @see #getParserCount()
* @see #addParser(Parser)
* @see #removeParser(Parser)
*/
public Parser getParser(int index) {
return parsers.get(index);
}
/**
* Returns the number of registered parsers.
*
* @return The number of registered parsers.
*/
public int getParserCount() {
return parsers.size();
}
/**
* Returns a list of the current parser notices for this text area. This method
* (like most Swing methods) should only be called on the EDT.
*
* @return The list of notices. This will be an empty list if there are none.
*/
public ListviewToModel() returns the closest model position,
* and the position doesn't necessarily contain the point passed in as
* an argument, this method checks whether the point is indeed contained in the
* view rectangle for the specified offset.
*
* @param notice The parser notice.
* @param p The point possibly contained in the view range of the parser
* notice.
* @return Whether the parser notice actually contains the specified point in
* the view.
*/
private boolean noticeContainsPointInView(ParserNotice notice, Point p) {
try {
int start, end;
if (notice.getKnowsOffsetAndLength()) {
start = notice.getOffset();
end = start + notice.getLength() - 1;
} else {
Document doc = textArea.getDocument();
Element root = doc.getDefaultRootElement();
int line = notice.getLine();
// Defend against possible bad user-defined notices.
if (line < 0) {
return false;
}
Element elem = root.getElement(line);
start = elem.getStartOffset();
end = elem.getEndOffset() - 1;
}
Rectangle r1 = textArea.modelToView(start);
Rectangle r2 = textArea.modelToView(end);
if (r1.y != r2.y) {
// If the notice spans multiple lines, give them the benefit
// of the doubt. This is only "wrong" if the user is in empty
// space "to the right" of the error marker when it ends at the
// end of a line anyway.
return true;
}
r1.y--; // Be a tiny bit lenient.
r1.height += 2; // Ditto
return p.x >= r1.x && p.x < (r2.x + r2.width) && p.y >= r1.y && p.y < (r1.y + r1.height);
} catch (BadLocationException ble) { // Never occurs
// Give them the benefit of the doubt, should 99% of the time be
// true anyway
return true;
}
}
/**
* Called when a property we're interested in changes.
*
* @param e The property change event.
*/
@Override
public void propertyChange(PropertyChangeEvent e) {
String name = e.getPropertyName();
if ("document".equals(name)) {
// The document switched out from under us
RDocument old = (RDocument) e.getOldValue();
if (old != null) {
old.removeDocumentListener(this);
}
RDocument newDoc = (RDocument) e.getNewValue();
if (newDoc != null) {
newDoc.addDocumentListener(this);
}
}
}
/**
* Removes a parser.
*
* @param parser The parser to remove.
* @return Whether the parser was found.
* @see #addParser(Parser)
* @see #getParser(int)
*/
public boolean removeParser(Parser parser) {
removeParserNotices(parser);
boolean removed = parsers.remove(parser);
if (removed) {
textArea.fireParserNoticesChange();
}
return removed;
}
/**
* Removes all parser notices (and clears highlights in the editor) from a
* particular parser.
*
* @param parser The parser.
*/
private void removeParserNotices(Parser parser) {
if (noticeHighlightPairs != null) {
RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter) textArea.getHighlighter();
for (Iterator0.
* @see #getDelay()
*/
public void setDelay(int millis) {
if (running) {
timer.stop();
}
timer.setInitialDelay(millis);
timer.setDelay(millis);
if (running) {
timer.start();
}
}
/**
* Returns whether a parser notice should be removed, based on a parse result.
*
* @param notice The notice in question.
* @param res The result.
* @return Whether the notice should be removed.
*/
private boolean shouldRemoveNotice(ParserNotice notice, ParseResult res) {
if (DEBUG_PARSING) {
System.out.println(
"[DEBUG]: ... ... shouldRemoveNotice " + notice + ": " + (notice.getParser() == res.getParser()));
}
// NOTE: We must currently remove all notices for the parser. Parser
// implementors are required to parse the entire document each parsing
// request, as RSTA is not yet sophisticated enough to determine the
// minimum range of text to parse (and ParserNotices' locations aren't
// updated when the Document is mutated, which would be a requirement
// for this as well).
// return same_parser && (in_reparsed_range || in_deleted_end_of_doc)
return notice.getParser() == res.getParser();
}
/**
* Stops parsing the document.
*
* @see #restartParsing()
*/
public void stopParsing() {
timer.stop();
running = false;
}
/**
* Mapping of a parser notice to its highlight in the editor.
*/
private static class NoticeHighlightPair {
private ParserNotice notice;
private HighlightInfo highlight;
NoticeHighlightPair(ParserNotice notice, HighlightInfo highlight) {
this.notice = notice;
this.highlight = highlight;
}
}
static {
boolean debugParsing = false;
try {
debugParsing = Boolean.getBoolean(PROPERTY_DEBUG_PARSING);
} catch (AccessControlException ace) {
// Likely an applet's security manager.
debugParsing = false; // FindBugs
}
DEBUG_PARSING = debugParsing;
}
}