Package org.fife.ui.rsyntaxtextarea

Source Code of org.fife.ui.rsyntaxtextarea.ParserManager$NoticeHighlightPair

/*
* 09/26/2005
*
* ParserManager.java - Manages the parsing of an RSyntaxTextArea's document,
* if necessary.
*
* This library is distributed under a modified BSD license.  See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.rsyntaxtextarea;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.Timer;
import javax.swing.ToolTipManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Position;

import org.fife.ui.rsyntaxtextarea.focusabletip.FocusableTip;
import org.fife.ui.rsyntaxtextarea.parser.ParseResult;
import org.fife.ui.rsyntaxtextarea.parser.Parser;
import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
import org.fife.ui.rsyntaxtextarea.parser.ToolTipInfo;
import org.fife.ui.rtextarea.RTextAreaHighlighter.HighlightInfo;



/**
* Manages running a parser object for an <code>RSyntaxTextArea</code>.
*
* @author Robert Futrell
* @version 0.9
*/
class ParserManager implements DocumentListener, ActionListener,
                HyperlinkListener {

  private RSyntaxTextArea textArea;
  private List<Parser> parsers;
  private Timer timer;
  private boolean running;
  private Parser parserForTip;
  private Position firstOffsetModded;
  private Position lastOffsetModded;

  /**
   * Mapping of notices to their highlights in the editor.  Can't use a Map
   * since parsers could return two <code>ParserNotice</code>s that compare
   * equally via <code>equals()</code>.  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 List<NoticeHighlightPair> noticeHighlightPairs;

  /**
   * Painter used to underline errors.
   */
  private SquiggleUnderlineHighlightPainter parserErrorHighlightPainter =
            new SquiggleUnderlineHighlightPainter(Color.RED);

  /**
   * If this system property is set to <code>true</code>, 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.
   */
  public 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.
   */
  public ParserManager(int delay, RSyntaxTextArea textArea) {
    this.textArea = textArea;
    textArea.getDocument().addDocumentListener(this);
    parsers = new ArrayList<Parser>(1); // Usually small
    timer = new Timer(delay, this);
    timer.setRepeats(false);
    running = true;
  }


  /**
   * Called when the timer fires (e.g. it's time to parse the document).
   *
   * @param e The event.
   */
  public void actionPerformed(ActionEvent e) {

    // Sanity check - should have >1 parser if event is fired.
    int parserCount = getParserCount();
    if (parserCount==0) {
      return;
    }

    long begin = 0;
    if (DEBUG_PARSING) {
      begin = System.currentTimeMillis();
    }

    RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();

    Element root = doc.getDefaultRootElement();
    int firstLine = firstOffsetModded==null ? 0 : root.getElementIndex(firstOffsetModded.getOffset());
    int lastLine = lastOffsetModded==null ? root.getElementCount()-1 : root.getElementIndex(lastOffsetModded.getOffset());
    firstOffsetModded = lastOffsetModded = null;
    if (DEBUG_PARSING) {
      System.out.println("[DEBUG]: Minimum lines to parse: " + firstLine + "-" + lastLine);
    }

    String style = textArea.getSyntaxEditingStyle();
    doc.readLock();
    try {
      for (int i=0; i<parserCount; i++) {
        Parser parser = getParser(i);
        if (parser.isEnabled()) {
          ParseResult res = parser.parse(doc, style);
          addParserNoticeHighlights(res);
        }
        else {
          clearParserNoticeHighlights(parser);
        }
      }
      textArea.fireParserNoticesChange();
    } finally {
      doc.readUnlock();
    }

    if (DEBUG_PARSING) {
      float time = (System.currentTimeMillis()-begin)/1000f;
      System.out.println("Total parsing time: " + time + " seconds");
    }

  }


  /**
   * Adds a parser for the text area.
   *
   * @param parser The new parser.  If this is <code>null</code>, 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<NoticeHighlightPair>();
    }

    removeParserNotices(res);

    List<ParserNotice> notices = res.getNotices();
    if (notices.size()>0) { // Guaranteed non-null

      RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
                          textArea.getHighlighter();

      for (ParserNotice notice : notices) {
        if (DEBUG_PARSING) {
          System.out.println("[DEBUG]: ... adding: " + notice);
        }
        try {
          HighlightInfo highlight = null;
          if (notice.getShowInEditor()) {
            highlight = h.addParserHighlight(notice,
                      parserErrorHighlightPainter);
          }
          noticeHighlightPairs.add(new NoticeHighlightPair(notice, highlight));
        } catch (BadLocationException ble) { // Never happens
          ble.printStackTrace();
        }
      }

    }

    if (DEBUG_PARSING) {
      System.out.println("[DEBUG]: Done adding parser notices from " +
                res.getParser());
    }

  }


  /**
   * Called when the document is modified.
   *
   * @param e The document event.
   */
  public void changedUpdate(DocumentEvent e) {
  }


  private void clearParserNoticeHighlights() {
    RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
                      textArea.getHighlighter();
    if (h!=null) {
      h.clearParserHighlights();
    }
    if (noticeHighlightPairs!=null) {
      noticeHighlightPairs.clear();
    }
  }


  /**
   * Removes all parser notice highlights for a specific parser.
   *
   * @param parser The parser whose highlights to remove.
   */
  private void clearParserNoticeHighlights(Parser parser) {
    RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
                      textArea.getHighlighter();
    if (h!=null) {
      h.clearParserHighlights(parser);
    }
    if (noticeHighlightPairs!=null) {
      for (Iterator<NoticeHighlightPair> i=noticeHighlightPairs.iterator(); i.hasNext(); ) {
        NoticeHighlightPair pair = i.next();
        if (pair.notice.getParser()==parser) {
          i.remove();
        }
      }
    }
  }


  /**
   * Removes all parsers and any highlights they have created.
   *
   * @see #addParser(Parser)
   */
  public void clearParsers() {
    timer.stop();
    clearParserNoticeHighlights();
    parsers.clear();
    textArea.fireParserNoticesChange();
  }


  /**
   * Forces the given {@link Parser} to re-parse the content of this text
   * area.<p>
   *
   * This method can be useful when a <code>Parser</code> 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 <code>Parser</code> 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 List<ParserNotice> getParserNotices() {
    List<ParserNotice> notices = new ArrayList<ParserNotice>();
    if (noticeHighlightPairs!=null) {
      for (NoticeHighlightPair pair : noticeHighlightPairs) {
        notices.add(pair.notice);
      }
    }
    return notices;
  }


  /**
   * Returns the tool tip to display for a mouse event at the given
   * location.  This method is overridden to give a registered parser a
   * chance to display a tool tip (such as an error description when the
   * mouse is over an error highlight).
   *
   * @param e The mouse event.
   * @return The tool tip to display, and possibly a hyperlink event handler.
   */
  public ToolTipInfo getToolTipText(MouseEvent e) {

    String tip = null;
    HyperlinkListener listener = null;
    parserForTip = null;
    Point p = e.getPoint();

//    try {
      int pos = textArea.viewToModel(p);
      /*
      Highlighter.Highlight[] highlights = textArea.getHighlighter().
                        getHighlights();
      for (int i=0; i<highlights.length; i++) {
        Highlighter.Highlight h = highlights[i];
        //if (h instanceof ParserNoticeHighlight) {
        //  ParserNoticeHighlight pnh = (ParserNoticeHighlight)h;
          int start = h.getStartOffset();
          int end = h.getEndOffset();
          if (start<=pos && end>=pos) {
            //return pnh.getMessage();
            return textArea.getText(start, end-start);
          }
        //}
      }
      */
      if (noticeHighlightPairs!=null) {
        for (NoticeHighlightPair pair : noticeHighlightPairs) {
          ParserNotice notice = pair.notice;
          if (noticeContainsPosition(notice, pos) &&
              noticeContainsPointInView(notice, p)) {
            tip = notice.getToolTipText();
            parserForTip = notice.getParser();
            if (parserForTip instanceof HyperlinkListener) {
              listener = (HyperlinkListener)parserForTip;
            }
            break;
          }
        }
      }
//    } catch (BadLocationException ble) {
//      ble.printStackTrace();  // Should never happen.
//    }

    URL imageBase = parserForTip==null ? null : parserForTip.getImageBase();
    return new ToolTipInfo(tip, listener, imageBase);

  }


  /**
   * Called when the document is modified.
   *
   * @param e The document event.
   */
  public void handleDocumentEvent(DocumentEvent e) {
    if (running && parsers.size()>0) {
      timer.restart();
    }
  }


  /**
   * Called when the user clicks a hyperlink in a {@link FocusableTip}.
   *
   * @param e The event.
   */
  public void hyperlinkUpdate(HyperlinkEvent e) {
    if (parserForTip!=null && parserForTip.getHyperlinkListener()!=null) {
      parserForTip.getHyperlinkListener().linkClicked(textArea, e);
    }
  }


  /**
   * Called when the document is modified.
   *
   * @param e The document event.
   */
  public void insertUpdate(DocumentEvent e) {

    // Keep track of the first and last offset modified.  Some parsers are
    // smart and will only re-parse this section of the file.
    try {
      int offs = e.getOffset();
      if (firstOffsetModded==null || offs<firstOffsetModded.getOffset()) {
        firstOffsetModded = e.getDocument().createPosition(offs);
      }
      offs = e.getOffset() + e.getLength();
      if (lastOffsetModded==null || offs>lastOffsetModded.getOffset()) {
        lastOffsetModded = e.getDocument().createPosition(offs);
      }
    } catch (BadLocationException ble) {
      ble.printStackTrace(); // Shouldn't happen
    }

    handleDocumentEvent(e);

  }


  /**
   * Returns whether a parser notice contains the specified offset.
   *
   * @param notice The notice.
   * @param offs The offset.
   * @return Whether the notice contains the offset.
   */
  private final boolean noticeContainsPosition(ParserNotice notice, int offs){
    if (notice.getKnowsOffsetAndLength()) {
      return notice.containsPosition(offs);
    }
    Document doc = textArea.getDocument();
    Element root = doc.getDefaultRootElement();
    int line = notice.getLine();
    if (line<0) { // Defensive against possible bad user-defined notices.
      return false;
    }
    Element elem = root.getElement(line);
    return offs>=elem.getStartOffset() && offs<elem.getEndOffset();
  }


  /**
   * Since <code>viewToModel()</code> returns the <em>closest</em> model
   * position, and the position doesn't <em>necessarily</em> 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 final 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;
    }

  }


  /**
   * 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 (Iterator<NoticeHighlightPair> i=noticeHighlightPairs.iterator(); i.hasNext(); ) {
        NoticeHighlightPair pair = i.next();
        if (pair.notice.getParser()==parser && pair.highlight!=null) {
          h.removeParserHighlight(pair.highlight);
          i.remove();
        }
      }
    }
  }


  /**
   * Removes any currently stored notices (and the corresponding highlights
   * from the editor) from the same Parser, and in the given line range,
   * as in the results.
   *
   * @param res The results.
   */
  private void removeParserNotices(ParseResult res) {
    if (noticeHighlightPairs!=null) {
      RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
                        textArea.getHighlighter();
      for (Iterator<NoticeHighlightPair> i=noticeHighlightPairs.iterator(); i.hasNext(); ) {
        NoticeHighlightPair pair = i.next();
        boolean removed = false;
        if (shouldRemoveNotice(pair.notice, res)) {
          if (pair.highlight!=null) {
            h.removeParserHighlight(pair.highlight);
          }
          i.remove();
          removed = true;
        }
        if (DEBUG_PARSING) {
          String text = removed ? "[DEBUG]: ... notice removed: " :
                      "[DEBUG]: ... notice not removed: ";
          System.out.println(text + pair.notice);
        }
      }

    }

  }


  /**
   * Called when the document is modified.
   *
   * @param e The document event.
   */
  public void removeUpdate(DocumentEvent e) {

    // Keep track of the first and last offset modified.  Some parsers are
    // smart and will only re-parse this section of the file.  Note that
    // for removals, only the line at the removal start needs to be
    // re-parsed.
    try {
      int offs = e.getOffset();
      if (firstOffsetModded==null || offs<firstOffsetModded.getOffset()) {
        firstOffsetModded = e.getDocument().createPosition(offs);
      }
      if (lastOffsetModded==null || offs>lastOffsetModded.getOffset()) {
        lastOffsetModded = e.getDocument().createPosition(offs);
      }
    } catch (BadLocationException ble) { // Never happens
      ble.printStackTrace();
    }

    handleDocumentEvent(e);

  }


  /**
   * Restarts parsing the document.
   *
   * @see #stopParsing()
   */
  public void restartParsing() {
    timer.restart();
    running = true;
  }


  /**
   * Sets the delay between the last "concurrent" edit and when the document
   * is re-parsed.
   *
   * @param millis The new delay, in milliseconds.  This must be greater
   *        than <code>0</code>.
   * @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 final 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 {

    public ParserNotice notice;
    public HighlightInfo highlight;

    public 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;
  }


}
TOP

Related Classes of org.fife.ui.rsyntaxtextarea.ParserManager$NoticeHighlightPair

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.