Package org.fife.ui.rsyntaxtextarea

Source Code of org.fife.ui.rsyntaxtextarea.TokenImpl

/*
* 02/21/2004
*
* Token.java - A token used in syntax highlighting.
*
* 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.Font;
import java.awt.FontMetrics;
import java.awt.Rectangle;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.Utilities;


/**
* The default implementation of {@link Token}.<p>
*
* <b>Note:</b> The instances of <code>Token</code> returned by
* {@link RSyntaxDocument}s are pooled and should always be treated as
* immutable.  They should not be cast to <code>TokenImpl</code> and modified.
* Modifying tokens you did not create yourself can and will result in
* rendering issues and/or runtime exceptions. You have been warned!
*
* @author Robert Futrell
* @version 0.3
*/
public class TokenImpl implements Token {

  /**
   * The text this token represents.  This is implemented as a segment so we
   * can point directly to the text in the document without having to make a
   * copy of it.
   */
  public char[] text;
  public int textOffset;
  public int textCount;
 
  /**
   * The offset into the document at which this token resides.
   */
  private int offset;

  /**
   * The type of token this is; for example, {@link #FUNCTION}.
   */
  private int type;

  /**
   * Whether this token is a hyperlink.
   */
  private boolean hyperlink;

  /**
   * The next token in this linked list.
   */
  private Token nextToken;

  /**
   * The language this token is in, <code>&gt;= 0</code>.
   */
  private int languageIndex;


  /**
   * Creates a "null token."  The token itself is not null; rather, it
   * signifies that it is the last token in a linked list of tokens and
   * that it is not part of a "multi-line token."
   */
  public TokenImpl() {
    this.text = null;
    this.textOffset = -1;
    this.textCount = -1;
    this.setType(NULL);
    setOffset(-1);
    hyperlink = false;
    nextToken = null;
  }


  /**
   * Constructor.
   *
   * @param line The segment from which to get the token.
   * @param beg The first character's position in <code>line</code>.
   * @param end The last character's position in <code>line</code>.
   * @param startOffset The offset into the document at which this
   *        token begins.
   * @param type A token type listed as "generic" above.
   */
  public TokenImpl(Segment line, int beg, int end, int startOffset, int type){
    this(line.array, beg,end, startOffset, type);
  }


  /**
   * Constructor.
   *
   * @param line The segment from which to get the token.
   * @param beg The first character's position in <code>line</code>.
   * @param end The last character's position in <code>line</code>.
   * @param startOffset The offset into the document at which this
   *        token begins.
   * @param type A token type listed as "generic" above.
   */
  public TokenImpl(char[] line, int beg, int end, int startOffset, int type) {
    this();
    set(line, beg,end, startOffset, type);
  }


  /**
   * Creates this token as a copy of the passed-in token.
   *
   * @param t2 The token from which to make a copy.
   */
  public TokenImpl(Token t2) {
    this();
    copyFrom(t2);
  }


  public StringBuilder appendHTMLRepresentation(StringBuilder sb,
                      RSyntaxTextArea textArea,
                      boolean fontFamily) {
    return appendHTMLRepresentation(sb, textArea, fontFamily, false);
  }


  public StringBuilder appendHTMLRepresentation(StringBuilder sb,
                RSyntaxTextArea textArea, boolean fontFamily,
                boolean tabsToSpaces) {

    SyntaxScheme colorScheme = textArea.getSyntaxScheme();
    Style scheme = colorScheme.getStyle(getType());
    Font font = textArea.getFontForTokenType(getType());//scheme.font;

    if (font.isBold()) sb.append("<b>");
    if (font.isItalic()) sb.append("<em>");
    if (scheme.underline || isHyperlink()) sb.append("<u>");

    sb.append("<font");
    if (fontFamily) {
      sb.append(" face=\"").append(font.getFamily()).append("\"");
    }
    sb.append(" color=\"").
      append(getHTMLFormatForColor(scheme.foreground)).
      append("\">");

    // NOTE: Don't use getLexeme().trim() because whitespace tokens will
    // be turned into NOTHING.
    appendHtmlLexeme(textArea, sb, tabsToSpaces);

    sb.append("</font>");
    if (scheme.underline || isHyperlink()) sb.append("</u>");
    if (font.isItalic()) sb.append("</em>");
    if (font.isBold()) sb.append("</b>");

    return sb;

  }


  /**
   * Appends an HTML version of the lexeme of this token (i.e. no style
   * HTML, but replacing chars such as <code>\t</code>, <code>&lt;</code>
   * and <code>&gt;</code> with their escapes).
   *
   * @param textArea The text area.
   * @param sb The buffer to append to.
   * @param tabsToSpaces Whether to convert tabs into spaces.
   * @return The same buffer.
   */
  private final StringBuilder appendHtmlLexeme(RSyntaxTextArea textArea,
                StringBuilder sb, boolean tabsToSpaces) {

    boolean lastWasSpace = false;
    int i = textOffset;
    int lastI = i;
    String tabStr = null;

    while (i<textOffset+textCount) {
      char ch = text[i];
      switch (ch) {
        case ' ':
          sb.append(text, lastI, i-lastI);
          lastI = i+1;
          sb.append(lastWasSpace ? "&nbsp;" : " ");
          lastWasSpace = true;
          break;
        case '\t':
          sb.append(text, lastI, i-lastI);
          lastI = i+1;
          if (tabsToSpaces && tabStr==null) {
            tabStr = "";
            for (int j=0; j<textArea.getTabSize(); j++) {
              tabStr += "&nbsp;";
            }
          }
          sb.append(tabsToSpaces ? tabStr : "&#09;");
          lastWasSpace = false;
          break;
        case '<':
          sb.append(text, lastI, i-lastI);
          lastI = i+1;
          sb.append("&lt;");
          lastWasSpace = false;
          break;
        case '>':
          sb.append(text, lastI, i-lastI);
          lastI = i+1;
          sb.append("&gt;");
          lastWasSpace = false;
          break;
        default:
          lastWasSpace = false;
          break;
      }
      i++;
    }
    if (lastI<textOffset+textCount) {
      sb.append(text, lastI, textOffset+textCount-lastI);
    }
    return sb;
  }


  public char charAt(int index) {
    return text[textOffset + index];
  }


  public boolean containsPosition(int pos) {
    return pos>=getOffset() && pos<getOffset()+textCount;
  }


  /**
   * Makes one token point to the same text segment, and have the same value
   * as another token.
   *
   * @param t2 The token from which to copy.
   */
  public void copyFrom(Token t2) {
    text = t2.getTextArray();
    textOffset = t2.getTextOffset();
    textCount = t2.length();
    setOffset(t2.getOffset());
    setType(t2.getType());
    hyperlink = t2.isHyperlink();
    languageIndex = t2.getLanguageIndex();
    nextToken = t2.getNextToken();
  }


  public int documentToToken(int pos) {
    return pos + (textOffset-getOffset());
  }


  public boolean endsWith(char[] ch) {
    if (ch==null || ch.length>textCount) {
      return false;
    }
    final int start = textOffset + textCount - ch.length;
    for (int i=0; i<ch.length; i++) {
      if (text[start+i]!=ch[i]) {
        return false;
      }
    }
    return true;
  }


  @Override
  public boolean equals(Object obj) {

    if (obj==this) {
      return true;
    }
    if (!(obj instanceof Token)) {
      return false;
    }

    Token t2 = (Token)obj;
    return offset==t2.getOffset() &&
        type==t2.getType() &&
        languageIndex==t2.getLanguageIndex() &&
        hyperlink==t2.isHyperlink() &&
        ((getLexeme()==null && t2.getLexeme()==null) ||
          (getLexeme()!=null && getLexeme().equals(t2.getLexeme())));

  }


  public int getEndOffset() {
    return offset + textCount;
  }


  /**
   * Returns a <code>String</code> of the form "#xxxxxx" good for use
   * in HTML, representing the given color.
   *
   * @param color The color to get a string for.
   * @return The HTML form of the color.  If <code>color</code> is
   *         <code>null</code>, <code>#000000</code> is returned.
   */
  private static final String getHTMLFormatForColor(Color color) {
    if (color==null) {
      return "black";
    }
    String hexRed = Integer.toHexString(color.getRed());
    if (hexRed.length()==1)
      hexRed = "0" + hexRed;
    String hexGreen = Integer.toHexString(color.getGreen());
    if (hexGreen.length()==1)
      hexGreen = "0" + hexGreen;
    String hexBlue = Integer.toHexString(color.getBlue());
    if (hexBlue.length()==1)
      hexBlue = "0" + hexBlue;
    return "#" + hexRed + hexGreen + hexBlue;
  }


  public String getHTMLRepresentation(RSyntaxTextArea textArea) {
    StringBuilder buf = new StringBuilder();
    appendHTMLRepresentation(buf, textArea, true);
    return buf.toString();
  }


  public int getLanguageIndex() {
    return languageIndex;
  }


  public Token getLastNonCommentNonWhitespaceToken() {

    Token last = null;

    for (Token t=this; t!=null && t.isPaintable(); t=t.getNextToken()) {
      switch (t.getType()) {
        case COMMENT_DOCUMENTATION:
        case COMMENT_EOL:
        case COMMENT_MULTILINE:
        case COMMENT_KEYWORD:
        case COMMENT_MARKUP:
        case WHITESPACE:
          break;
        default:
          last = t;
          break;
      }
    }

    return last;

  }


  public Token getLastPaintableToken() {
    Token t = this;
    while (t.isPaintable()) {
      Token next = t.getNextToken();
      if (next==null || !next.isPaintable()) {
        return t;
      }
      t = next;
    }
    return null;
  }


  public String getLexeme() {
    return text==null ? null : new String(text, textOffset, textCount);
  }


  public int getListOffset(RSyntaxTextArea textArea, TabExpander e,
      float x0, float x) {

    // If the coordinate in question is before this line's start, quit.
    if (x0 >= x)
      return getOffset();

    float currX = x0; // x-coordinate of current char.
    float nextX = x0; // x-coordinate of next char.
    float stableX = x0; // Cached ending x-coord. of last tab or token.
    TokenImpl token = this;
    int last = getOffset();
    FontMetrics fm = null;

    while (token != null && token.isPaintable()) {

      fm = textArea.getFontMetricsForTokenType(token.getType());
      char[] text = token.text;
      int start = token.textOffset;
      int end = start + token.textCount;

      for (int i = start; i < end; i++) {
        currX = nextX;
        if (text[i] == '\t') {
          nextX = e.nextTabStop(nextX, 0);
          stableX = nextX; // Cache ending x-coord. of tab.
          start = i + 1; // Do charsWidth() from next char.
        }
        else {
          nextX = stableX + fm.charsWidth(text, start, i - start + 1);
        }
        if (x >= currX && x < nextX) {
          if ((x - currX) < (nextX - x)) {
            return last + i - token.textOffset;
          }
          return last + i + 1 - token.textOffset;
        }
      }

      stableX = nextX; // Cache ending x-coordinate of token.
      last += token.textCount;
      token = (TokenImpl)token.getNextToken();

    }

    // If we didn't find anything, return the end position of the text.
    return last;

  }


  public Token getNextToken() {
    return nextToken;
  }


  public int getOffset() {
    return offset;
  }


  public int getOffsetBeforeX(RSyntaxTextArea textArea, TabExpander e,
              float startX, float endBeforeX) {

    FontMetrics fm = textArea.getFontMetricsForTokenType(getType());
    int i = textOffset;
    int stop = i + textCount;
    float x = startX;

    while (i<stop) {
      if (text[i]=='\t')
        x = e.nextTabStop(x, 0);
      else
        x += fm.charWidth(text[i]);
      if (x>endBeforeX) {
        // If not even the first character fits into the space, go
        // ahead and say the first char does fit so we don't go into
        // an infinite loop.
        int intoToken = Math.max(i-textOffset, 1);
        return getOffset() + intoToken;
      }
      i++;
    }

    // If we got here, the whole token fit in (endBeforeX-startX) pixels.
    return getOffset() + textCount - 1;

  }


  public char[] getTextArray() {
    return text;
  }


  public int getTextOffset() {
    return textOffset;
  }


  public int getType() {
    return type;
  }


  public float getWidth(RSyntaxTextArea textArea, TabExpander e, float x0) {
    return getWidthUpTo(textCount, textArea, e, x0);
  }


  public float getWidthUpTo(int numChars, RSyntaxTextArea textArea,
      TabExpander e, float x0) {
    float width = x0;
    FontMetrics fm = textArea.getFontMetricsForTokenType(getType());
    if (fm != null) {
      int w;
      int currentStart = textOffset;
      int endBefore = textOffset + numChars;
      for (int i = currentStart; i < endBefore; i++) {
        if (text[i] == '\t') {
          // Since TokenMaker implementations usually group all
          // adjacent whitespace into a single token, there
          // aren't usually any characters to compute a width
          // for here, so we check before calling.
          w = i - currentStart;
          if (w > 0)
            width += fm.charsWidth(text, currentStart, w);
          currentStart = i + 1;
          width = e.nextTabStop(width, 0);
        }
      }
      // Most (non-whitespace) tokens will have characters at this
      // point to get the widths for, so we don't check for w>0 (mini-
      // optimization).
      w = endBefore - currentStart;
      width += fm.charsWidth(text, currentStart, w);
    }
    return width - x0;
  }


  @Override
  public int hashCode() {
    return offset + (getLexeme()==null ? 0 : getLexeme().hashCode());
  }


  public boolean is(char[] lexeme) {
    if (textCount==lexeme.length) {
      for (int i=0; i<textCount; i++) {
        if (text[textOffset+i]!=lexeme[i]) {
          return false;
        }
      }
      return true;
    }
    return false;
  }


  public boolean is(int type, char[] lexeme) {
    if (this.getType()==type && textCount==lexeme.length) {
      for (int i=0; i<textCount; i++) {
        if (text[textOffset+i]!=lexeme[i]) {
          return false;
        }
      }
      return true;
    }
    return false;
  }


  public boolean is(int type, String lexeme) {
    return this.getType()==type && textCount==lexeme.length() &&
        lexeme.equals(getLexeme());
  }


  public boolean isComment() {
    return getType()>=Token.COMMENT_EOL && getType()<=Token.COMMENT_MARKUP;
  }


  public boolean isCommentOrWhitespace() {
    return isComment() || isWhitespace();
  }


  public boolean isHyperlink() {
    return hyperlink;
  }


  public boolean isIdentifier() {
    return getType()==IDENTIFIER;
  }


  public boolean isLeftCurly() {
    return getType()==SEPARATOR && isSingleChar('{');
  }


  public boolean isRightCurly() {
    return getType()==SEPARATOR && isSingleChar('}');
  }


  public boolean isPaintable() {
    return getType()>Token.NULL;
  }


  public boolean isSingleChar(char ch) {
    return textCount==1 && text[textOffset]==ch;
  }


  public boolean isSingleChar(int type, char ch) {
    return this.getType()==type && isSingleChar(ch);
  }


  public boolean isWhitespace() {
    return getType()==WHITESPACE;
  }


  public int length() {
    return textCount;
  }


  public Rectangle listOffsetToView(RSyntaxTextArea textArea, TabExpander e,
      int pos, int x0, Rectangle rect) {

    int stableX = x0; // Cached ending x-coord. of last tab or token.
    TokenImpl token = this;
    FontMetrics fm = null;
    Segment s = new Segment();

    while (token != null && token.isPaintable()) {

      fm = textArea.getFontMetricsForTokenType(token.getType());
      if (fm == null) {
        return rect; // Don't return null as things'll error.
      }
      char[] text = token.text;
      int start = token.textOffset;
      int end = start + token.textCount;

      // If this token contains the position for which to get the
      // bounding box...
      if (token.containsPosition(pos)) {

        s.array = token.text;
        s.offset = token.textOffset;
        s.count = pos - token.getOffset();

        // Must use this (actually fm.charWidth()), and not
        // fm.charsWidth() for returned value to match up with where
        // text is actually painted on OS X!
        int w = Utilities.getTabbedTextWidth(s, fm, stableX, e,
            token.getOffset());
        rect.x = stableX + w;
        end = token.documentToToken(pos);

        if (text[end] == '\t') {
          rect.width = fm.charWidth(' ');
        }
        else {
          rect.width = fm.charWidth(text[end]);
        }

        return rect;

      }

      // If this token does not contain the position for which to get
      // the bounding box...
      else {
        s.array = token.text;
        s.offset = token.textOffset;
        s.count = token.textCount;
        stableX += Utilities.getTabbedTextWidth(s, fm, stableX, e,
            token.getOffset());
      }

      token = (TokenImpl)token.getNextToken();

    }

    // If we didn't find anything, we're at the end of the line. Return
    // a width of 1 (so selection highlights don't extend way past line's
    // text). A ConfigurableCaret will know to paint itself with a larger
    // width.
    rect.x = stableX;
    rect.width = 1;
    return rect;

  }


  /**
   * Makes this token start at the specified offset into the document.<p>
   *
   * <b>Note:</b> You should not modify <code>Token</code> instances you
   * did not create yourself (e.g., came from an
   * <code>RSyntaxDocument</code>).  If you do, rendering issues and/or
   * runtime exceptions will likely occur.  You have been warned!
   *
   * @param pos The offset into the document this token should start at.
   *        Note that this token must already contain this position; if
   *        it doesn't, an exception is thrown.
   * @throws IllegalArgumentException If pos is not already contained by
   *         this token.
   * @see #moveOffset(int)
   */
  public void makeStartAt(int pos) {
    if (pos<getOffset() || pos>=(getOffset()+textCount)) {
      throw new IllegalArgumentException("pos " + pos +
        " is not in range " + getOffset() + "-" + (getOffset()+textCount-1));
    }
    int shift = pos - getOffset();
    setOffset(pos);
    textOffset += shift;
    textCount -= shift;
  }


  /**
   * Moves the starting offset of this token.<p>
   *
   * <b>Note:</b> You should not modify <code>Token</code> instances you
   * did not create yourself (e.g., came from an
   * <code>RSyntaxDocument</code>).  If you do, rendering issues and/or
   * runtime exceptions will likely occur.  You have been warned!
   *
   * @param amt The amount to move the starting offset.  This should be
   *        between <code>0</code> and <code>textCount</code>, inclusive.
   * @throws IllegalArgumentException If <code>amt</code> is an invalid value.
   * @see #makeStartAt(int)
   */
  public void moveOffset(int amt) {
    if (amt<0 || amt>textCount) {
      throw new IllegalArgumentException("amt " + amt +
          " is not in range 0-" + textCount);
    }
    setOffset(getOffset() + amt);
    textOffset += amt;
    textCount -= amt;
  }


  /**
   * Sets the value of this token to a particular segment of a document.
   * The "next token" value is cleared.
   *
   * @param line The segment from which to get the token.
   * @param beg The first character's position in <code>line</code>.
   * @param end The last character's position in <code>line</code>.
   * @param offset The offset into the document at which this token begins.
   * @param type A token type listed as "generic" above.
   */
  public void set(final char[] line, final int beg, final int end,
              final int offset, final int type) {
    this.text = line;
    this.textOffset = beg;
    this.textCount = end - beg + 1;
    this.setType(type);
    this.setOffset(offset);
    nextToken = null;
  }


  /**
   * Sets whether this token is a hyperlink.
   *
   * @param hyperlink Whether this token is a hyperlink.
   * @see #isHyperlink()
   */
  public void setHyperlink(boolean hyperlink) {
    this.hyperlink = hyperlink;
  }


  /**
   * Sets the language index for this token.  If this value is positive, it
   * denotes a specific "secondary" language this token represents (such as
   * JavaScript code or CSS embedded in an HTML file).  If this value is
   * <code>0</code>, this token is in the "main" language being edited.
   * Negative values are invalid and treated as <code>0</code>.
   *
   * @param languageIndex The new language index.  A value of
   *        <code>0</code> denotes the "main" language, any positive value
   *        denotes a specific secondary language.  Negative values will
   *        be treated as <code>0</code>.
   * @see #getLanguageIndex()
   */
  public void setLanguageIndex(int languageIndex) {
    this.languageIndex = languageIndex;
  }


  /**
   * Sets the "next token" pointer of this token to point to the specified
   * token.
   *
   * @param nextToken The new next token.
   * @see #getNextToken()
   */
  public void setNextToken(Token nextToken) {
    this.nextToken = nextToken;
  }


  /**
   * Sets the offset into the document at which this token resides.
   *
   * @param offset The new offset into the document.
   * @see #getOffset()
   */
  public void setOffset(int offset) {
    this.offset = offset;
  }


  /**
   * {@inheritDoc}
   */
  public void setType(int type) {
    this.type = type;
  }


  /**
   * {@inheritDoc}
   */
  public boolean startsWith(char[] chars) {
    if (chars.length<=textCount){
      for (int i=0; i<chars.length; i++) {
        if (text[textOffset+i]!=chars[i]) {
          return false;
        }
      }
      return true;
    }
    return false;
  }


  /**
   * {@inheritDoc}
   */
  public int tokenToDocument(int pos) {
    return pos + (getOffset()-textOffset);
  }


  /**
   * Returns this token as a <code>String</code>, which is useful for
   * debugging.
   *
   * @return A string describing this token.
   */
  @Override
  public String toString() {
    return "[Token: " +
      (getType()==Token.NULL ? "<null token>" :
        "text: '" +
          (text==null ? "<null>" : getLexeme() + "'; " +
             "offset: " + getOffset() + "; type: " + getType() + "; " +
           "isPaintable: " + isPaintable() +
           "; nextToken==null: " + (nextToken==null))) +
       "]";
  }


}
TOP

Related Classes of org.fife.ui.rsyntaxtextarea.TokenImpl

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.