/*
* 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>>= 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><</code>
* and <code>></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 ? " " : " ");
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 += " ";
}
}
sb.append(tabsToSpaces ? tabStr : "	");
lastWasSpace = false;
break;
case '<':
sb.append(text, lastI, i-lastI);
lastI = i+1;
sb.append("<");
lastWasSpace = false;
break;
case '>':
sb.append(text, lastI, i-lastI);
lastI = i+1;
sb.append(">");
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))) +
"]";
}
}