/*
* 08/06/2004
*
* RSyntaxUtilities.java - Utility methods used by RSyntaxTextArea and its
* views.
* Copyright (C) 2004 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.View;
/**
* Utility methods used by <code>RSyntaxTextArea</code> and its associated classes.
*
* @author Robert Futrell
* @version 0.2
*/
public class RSyntaxUtilities implements SwingConstants {
// private static final int DIGIT_MASK = 1;
private static final int LETTER_MASK = 2;
// private static final int WHITESPACE_MASK = 4;
// private static final int UPPER_CASE_MASK = 8;
private static final int HEX_CHARACTER_MASK = 16;
private static final int LETTER_OR_DIGIT_MASK = 32;
private static final int BRACKET_MASK = 64;
private static final int JAVA_OPERATOR_MASK = 128;
/**
* A lookup table used to quickly decide if a 16-bit Java char is a US-ASCII letter (A-Z or a-z), a digit, a
* whitespace char (either space (0x0020) or tab (0x0009)), etc. This method should be faster than
* <code>Character.isLetter</code>, <code>Character.isDigit</code>, and <code>Character.isWhitespace</code> because
* we know we are dealing with ASCII chars and so don't have to worry about code planes, etc.
*/
private static final int[] dataTable = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, // 0-15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
4, 128, 0, 0, 0, 128, 128, 0, 64, 64, 128, 128, 0, 128, 0, 128, // 32-47
49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 128, 0, 128, 128, 128, 128, // 48-63
0, 58, 58, 58, 58, 58, 58, 42, 42, 42, 42, 42, 42, 42, 42, 42, // 64-79
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 64, 0, 64, 128, 0, // 80-95
0, 50, 50, 50, 50, 50, 50, 34, 34, 34, 34, 34, 34, 34, 34, 34, // 96-111
34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 64, 128, 64, 128, 0, // 112-127
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255.
};
/**
* Returns the leading whitespace of a string.
*
* @param text
* The String to check.
* @return The leading whitespace.
*/
public static String getLeadingWhitespace(String text) {
int count = 0;
int len = text.length();
while (count < len && RSyntaxUtilities.isWhitespace(text.charAt(count))) {
count++;
}
return text.substring(0, count);
}
private static final Element getLineElem(Document d, int offs) {
Element map = d.getDefaultRootElement();
int index = map.getElementIndex(offs);
Element elem = map.getElement(index);
if ((offs >= elem.getStartOffset()) && (offs < elem.getEndOffset())) {
return elem;
}
return null;
}
/**
* Returns the bounding box (in the current view) of a specified position in the model. This method is designed for
* line-wrapped views to use, as it allows you to specify a "starting position" in the line, from which the x-value
* is assumed to be zero. The idea is that you specify the first character in a physical line as <code>p0</code>, as
* this is the character where the x-pixel value is 0.
*
* @param textArea
* The text area containing the text.
* @param s
* A segment in which to load the line. This is passed in so we don't have to reallocate a new
* <code>Segment</code> for each call.
* @param p0
* The starting position in the physical line in the document.
* @param p1
* The position for which to get the bounding box in the view.
* @param e
* How to expand tabs.
* @param rect
* The rectangle whose x- and width-values are changed to represent the bounding box of <code>p1</code>.
* This is reused to keep from needlessly reallocating Rectangles.
* @param x0
* The x-coordinate (pixel) marking the left-hand border of the text. This is useful if the text area has
* a border, for example.
* @return The bounding box in the view of the character <code>p1</code>.
* @throws BadLocationException
* If <code>p0</code> or <code>p1</code> is not a valid location in the specified text area's document.
* @throws IllegalArgumentException
* If <code>p0</code> and <code>p1</code> are not on the same line.
*/
public static Rectangle getLineWidthUpTo(RSyntaxTextArea textArea,
Segment s, int p0, int p1,
TabExpander e, Rectangle rect,
int x0)
throws BadLocationException {
RSyntaxDocument doc = (RSyntaxDocument) textArea.getDocument();
// Ensure p0 and p1 are valid document positions.
if (p0 < 0)
throw new BadLocationException("Invalid document position", p0);
else if (p1 > doc.getLength())
throw new BadLocationException("Invalid document position", p1);
// Ensure p0 and p1 are in the same line, and get the start/end
// offsets for that line.
Element map = doc.getDefaultRootElement();
int lineNum = map.getElementIndex(p0);
// We do ">1" because p1 might be the first position on the next line
// or the last position on the previous one.
// if (lineNum!=map.getElementIndex(p1))
if (Math.abs(lineNum - map.getElementIndex(p1)) > 1)
throw new IllegalArgumentException("p0 and p1 are not on the " +
"same line (" + p0 + ", " + p1 + ").");
// Get the token list.
Token t = doc.getTokenListForLine(lineNum);
// Modify the token list 't' to begin at p0 (but still have correct
// token types, etc.), and get the x-location (in pixels) of the
// beginning of this new token list.
makeTokenListStartAt(t, p0, e, textArea, 0);
rect = t.listOffsetToView(textArea, e, p1, x0, rect);
return rect;
}
/**
* Returns the location of the bracket paired with the one at the current caret position.
*
* @param textArea
* The text area.
* @return The location of the matching bracket in the document, or <code>-1</code> if there isn't a matching
* bracket (or the caret isn't on a bracket).
*/
private static Segment charSegment = new Segment();
public static int getMatchingBracketPosition(RSyntaxTextArea textArea) {
try {
// Actually position just BEFORE caret.
int caretPosition = textArea.getCaretPosition() - 1;
if (caretPosition > -1) {
// Some variables that will be used later.
Token token;
Element map;
int curLine;
Element line;
int start, end;
RSyntaxDocument doc = (RSyntaxDocument) textArea.getDocument();
char bracket = doc.charAt(caretPosition);
// First, see if the previous char was a bracket
// ('{', '}', '(', ')', '[', ']').
// If it was, then make sure this bracket isn't sitting in
// the middle of a comment or string. If it isn't, then
// initialize some stuff so we can continue on.
char bracketMatch;
boolean goForward;
switch (bracket) {
case '{':
case '(':
case '[':
// Ensure this bracket isn't in a comment.
map = doc.getDefaultRootElement();
curLine = map.getElementIndex(caretPosition);
line = map.getElement(curLine);
start = line.getStartOffset();
end = line.getEndOffset();
token = doc.getTokenListForLine(curLine);
token = RSyntaxUtilities.getTokenAtOffset(token, caretPosition);
// All brackets are always returned as "separators."
if (token.type != Token.SEPARATOR) {
return -1;
}
bracketMatch = bracket == '{' ? '}' : (bracket == '(' ? ')' : ']');
goForward = true;
break;
case '}':
case ')':
case ']':
// Ensure this bracket isn't in a comment.
map = doc.getDefaultRootElement();
curLine = map.getElementIndex(caretPosition);
line = map.getElement(curLine);
start = line.getStartOffset();
end = line.getEndOffset();
token = doc.getTokenListForLine(curLine);
token = RSyntaxUtilities.getTokenAtOffset(token, caretPosition);
// All brackets are always returned as "separators."
if (token.type != Token.SEPARATOR) {
return -1;
}
bracketMatch = bracket == '}' ? '{' : (bracket == ')' ? '(' : '[');
goForward = false;
break;
default:
return -1;
}
if (goForward) {
int lastLine = map.getElementCount();
// Start just after the found bracket since we're sure
// we're not in a comment.
start = caretPosition + 1;
int numEmbedded = 0;
boolean haveTokenList = false;
while (true) {
doc.getText(start, end - start, charSegment);
int segOffset = charSegment.offset;
for (int i = segOffset; i < segOffset + charSegment.count; i++) {
char ch = charSegment.array[i];
if (ch == bracket) {
if (haveTokenList == false) {
token = doc.getTokenListForLine(curLine);
haveTokenList = true;
}
int offset = start + (i - segOffset);
token = RSyntaxUtilities.getTokenAtOffset(token, offset);
if (token.type == Token.SEPARATOR)
numEmbedded++;
}
else if (ch == bracketMatch) {
if (haveTokenList == false) {
token = doc.getTokenListForLine(curLine);
haveTokenList = true;
}
int offset = start + (i - segOffset);
token = RSyntaxUtilities.getTokenAtOffset(token, offset);
if (token.type == Token.SEPARATOR) {
if (numEmbedded == 0)
return offset;
numEmbedded--;
}
}
} // End of for (int i=segOffset; i<segOffset+charSegment.count; i++).
// Bail out if we've gone through all lines and
// haven't found the match.
if (++curLine == lastLine)
return -1;
// Otherwise, go through the next line.
haveTokenList = false;
line = map.getElement(curLine);
start = line.getStartOffset();
end = line.getEndOffset();
} // End of while (true).
} // End of if (goForward).
// Otherwise, we're going backward through the file
// (since we found '}', ')' or ']').
else { // goForward==false
// End just before the found bracket since we're sure
// we're not in a comment.
end = caretPosition;// - 1;
int numEmbedded = 0;
boolean haveTokenList = false;
Token t2;
while (true) {
doc.getText(start, end - start, charSegment);
int segOffset = charSegment.offset;
int iStart = segOffset + charSegment.count - 1;
for (int i = iStart; i >= segOffset; i--) {
char ch = charSegment.array[i];
if (ch == bracket) {
if (haveTokenList == false) {
token = doc.getTokenListForLine(curLine);
haveTokenList = true;
}
int offset = start + (i - segOffset);
t2 = RSyntaxUtilities.getTokenAtOffset(token, offset);
if (t2.type == Token.SEPARATOR)
numEmbedded++;
}
else if (ch == bracketMatch) {
if (haveTokenList == false) {
token = doc.getTokenListForLine(curLine);
haveTokenList = true;
}
int offset = start + (i - segOffset);
t2 = RSyntaxUtilities.getTokenAtOffset(token, offset);
if (t2.type == Token.SEPARATOR) {
if (numEmbedded == 0)
return offset;
numEmbedded--;
}
}
} // End of for (int i=segOffset; i<segOffset+charSegment.count; i++).
// Bail out if we've gone through all lines and
// haven't found the match.
if (--curLine == -1)
return -1;
// Otherwise, get ready for going through the
// next line.
haveTokenList = false;
line = map.getElement(curLine);
start = line.getStartOffset();
end = line.getEndOffset();
} // End of while (true).
} // End of else.
} // End of if (caretPosition>-1).
} catch (BadLocationException ble) {
// Shouldn't ever happen.
ble.printStackTrace();
}
// Something went wrong...
return -1;
}
/**
* Provides a way to determine the next visually represented model location at which one might place a caret. Some
* views may not be visible, they might not be in the same order found in the model, or they just might not allow
* access to some of the locations in the model.
* <p>
*
* NOTE: You should only call this method if the passed-in <code>javax.swing.text.View</code> is an instance of
* {@link TokenOrientedView} and <code>javax.swing.text.TabExpander</code>; otherwise, a
* <code>ClassCastException</code> could be thrown.
*
* @param pos
* the position to convert >= 0
* @param a
* the allocated region in which to render
* @param direction
* the direction from the current position that can be thought of as the arrow keys typically found on a
* keyboard. This will be one of the following values:
* <ul>
* <li>SwingConstants.WEST <li>SwingConstants.EAST <li>SwingConstants.NORTH <li>SwingConstants.SOUTH
* </ul>
* @return the location within the model that best represents the next location visual position
* @exception BadLocationException
* @exception IllegalArgumentException
* if <code>direction</code> doesn't have one of the legal values above
*/
public static int getNextVisualPositionFrom(int pos, Position.Bias b,
Shape a, int direction,
Position.Bias[] biasRet, View view)
throws BadLocationException {
biasRet[0] = Position.Bias.Forward;
// Do we want the "next position" above, below, to the left or right?
switch (direction) {
case NORTH:
case SOUTH:
if (pos == -1) {
pos = (direction == NORTH) ?
Math.max(0, view.getEndOffset() - 1) :
view.getStartOffset();
break;
}
RSyntaxTextArea target = (RSyntaxTextArea) view.
getContainer();
Caret c = (target != null) ? target.getCaret() : null;
// YECK! Ideally, the x location from the magic caret
// position would be passed in.
Point mcp;
if (c != null)
mcp = c.getMagicCaretPosition();
else
mcp = null;
int x;
if (mcp == null) {
Rectangle loc = target.modelToView(pos);
x = (loc == null) ? 0 : loc.x;
}
else {
x = mcp.x;
}
if (direction == NORTH)
pos = getPositionAbove(target, pos, x, (TabExpander) view);
else
pos = getPositionBelow(target, pos, x, (TabExpander) view);
break;
case WEST:
if (pos == -1)
pos = Math.max(0, view.getEndOffset() - 1);
else
pos = Math.max(0, pos - 1);
break;
case EAST:
if (pos == -1)
pos = view.getStartOffset();
else
pos = Math.min(pos + 1, view.getDocument().
getLength());
break;
default:
throw new IllegalArgumentException(
"Bad direction: " + direction);
}
return pos;
}
/**
* Determines the position in the model that is closest to the given view location in the row above. The component
* given must have a size to compute the result. If the component doesn't have a size a value of -1 will be
* returned.
*
* @param c
* the editor
* @param offs
* the offset in the document >= 0
* @param x
* the X coordinate >= 0
* @return the position >= 0 if the request can be computed, otherwise a value of -1 will be returned.
* @exception BadLocationException
* if the offset is out of range
*/
public static final int getPositionAbove(RSyntaxTextArea c, int offs,
float x, TabExpander e) throws BadLocationException {
TokenOrientedView tov = (TokenOrientedView) e;
Token token = tov.getTokenListForPhysicalLineAbove(offs);
if (token == null)
return -1;
// A line containing only Token.NULL is an empty line.
else if (token.type == Token.NULL) {
int line = c.getLineOfOffset(offs); // Sure to be >0 ??
return c.getLineStartOffset(line - 1);
}
else {
return token.getListOffset(c, e, 0, x);
}
}
/**
* Determines the position in the model that is closest to the given view location in the row below. The component
* given must have a size to compute the result. If the component doesn't have a size a value of -1 will be
* returned.
*
* @param c
* the editor
* @param offs
* the offset in the document >= 0
* @param x
* the X coordinate >= 0
* @return the position >= 0 if the request can be computed, otherwise a value of -1 will be returned.
* @exception BadLocationException
* if the offset is out of range
*/
public static final int getPositionBelow(RSyntaxTextArea c, int offs,
float x, TabExpander e) throws BadLocationException {
TokenOrientedView tov = (TokenOrientedView) e;
Token token = tov.getTokenListForPhysicalLineBelow(offs);
if (token == null)
return -1;
// A line containing only Token.NULL is an empty line.
else if (token.type == Token.NULL) {
int line = c.getLineOfOffset(offs); // Sure to be > c.getLineCount()-1 ??
return c.getLineStartOffset(line + 1);
}
else {
return token.getListOffset(c, e, 0, x);
}
}
/**
* Returns the token at the specified index, or <code>null</code> if the given offset isn't in this token list's
* range.<br>
* Note that this method does NOT check to see if <code>tokenList</code> is null; callers should check for
* themselves.
*
* @param tokenList
* The list of tokens in which to search.
* @param offset
* The offset at which to get the token.
* @return The token at <code>offset</code>, or <code>null</code> if none of the tokens are at that offset.
*/
public static final Token getTokenAtOffset(Token tokenList, int offset) {
for (Token t = tokenList; t != null; t = t.getNextToken()) {
if (t.containsPosition(offset))
return t;
}
return null;
}
/**
* Returns the end of the word at the given offset.
*
* @param textArea
* The text area.
* @param offs
* The offset into the text area's content.
* @return The end offset of the word.
* @throws BadLocationException
* If <code>offs</code> is invalid.
* @see #getWordStart(RSyntaxTextArea, int)
*/
public static int getWordEnd(RSyntaxTextArea textArea, int offs)
throws BadLocationException {
Document doc = textArea.getDocument();
int endOffs = textArea.getLineEndOffsetOfCurrentLine();
int lineEnd = Math.min(endOffs, doc.getLength());
if (offs == lineEnd) { // End of the line.
return offs;
}
String s = doc.getText(offs, lineEnd - offs - 1);
if (s != null && s.length() > 0) { // Should always be true
int i = 0;
int count = s.length();
char ch = s.charAt(i);
if (Character.isWhitespace(ch)) {
while (i < count && Character.isWhitespace(s.charAt(i++)))
;
}
else if (Character.isLetterOrDigit(ch)) {
while (i < count && Character.isLetterOrDigit(s.charAt(i++)))
;
}
else {
i = 2;
}
offs += i - 1;
}
return offs;
}
/**
* Returns the start of the word at the given offset.
*
* @param textArea
* The text area.
* @param offs
* The offset into the text area's content.
* @return The start offset of the word.
* @throws BadLocationException
* If <code>offs</code> is invalid.
* @see #getWordEnd(RSyntaxTextArea, int)
*/
public static int getWordStart(RSyntaxTextArea textArea, int offs)
throws BadLocationException {
Document doc = textArea.getDocument();
Element line = getLineElem(doc, offs);
if (line == null) {
throw new BadLocationException("No word at " + offs, offs);
}
int lineStart = line.getStartOffset();
if (offs == lineStart) { // Start of the line.
return offs;
}
int endOffs = Math.min(offs + 1, doc.getLength());
String s = doc.getText(lineStart, endOffs - lineStart);
if (s != null && s.length() > 0) {
int i = s.length() - 1;
char ch = s.charAt(i);
if (Character.isWhitespace(ch)) {
while (i > 0 && Character.isWhitespace(s.charAt(i - 1))) {
i--;
}
offs = lineStart + i;
}
else if (Character.isLetterOrDigit(ch)) {
while (i > 0 && Character.isLetterOrDigit(s.charAt(i - 1))) {
i--;
}
offs = lineStart + i;
}
}
return offs;
}
/**
* Determines the width of the given token list taking tabs into consideration. This is implemented in a 1.1 style
* coordinate system where ints are used and 72dpi is assumed.
* <p>
*
* This method also assumes that the passed-in token list begins at x-pixel <code>0</code> in the view (for tab
* purposes).
*
* @param tokenList
* The tokenList list representing the text.
* @param textArea
* The text area in which this token list resides.
* @param e
* The tab expander. This value cannot be <code>null</code>.
* @return The width of the token list, in pixels.
*/
public static final float getTokenListWidth(Token tokenList,
RSyntaxTextArea textArea,
TabExpander e) {
return getTokenListWidth(tokenList, textArea, e, 0);
}
/**
* Determines the width of the given token list taking tabs into consideration. This is implemented in a 1.1 style
* coordinate system where ints are used and 72dpi is assumed.
* <p>
*
* @param tokenList
* The token list list representing the text.
* @param textArea
* The text area in which this token list resides.
* @param e
* The tab expander. This value cannot be <code>null</code>.
* @param x0
* The x-pixel coordinate of the start of the token list.
* @return The width of the token list, in pixels.
* @see #getTokenListWidthUpTo
*/
public static final float getTokenListWidth(final Token tokenList,
RSyntaxTextArea textArea,
TabExpander e, float x0) {
float width = x0;
for (Token t = tokenList; t != null && t.isPaintable(); t = t.getNextToken()) {
width += t.getWidth(textArea, e, width);
}
return width - x0;
}
/**
* Determines the width of the given token list taking tabs into consideration and only up to the given index in the
* document (exclusive).
*
* @param tokenList
* The token list representing the text.
* @param textArea
* The text area in which this token list resides.
* @param e
* The tab expander. This value cannot be <code>null</code>.
* @param x0
* The x-pixel coordinate of the start of the token list.
* @param upTo
* The document position at which you want to stop, exclusive. If this position is before the starting
* position of the token list, a width of <code>0</code> will be returned; similarly, if this position
* comes after the entire token list, the width of the entire token list is returned.
* @return The width of the token list, in pixels, up to, but not including, the character at position
* <code>upTo</code>.
* @see #getTokenListWidth
*/
public static final float getTokenListWidthUpTo(final Token tokenList,
RSyntaxTextArea textArea, TabExpander e,
float x0, int upTo) {
float width = 0;
for (Token t = tokenList; t != null && t.isPaintable(); t = t.getNextToken()) {
if (t.containsPosition(upTo)) {
return width + t.getWidthUpTo(upTo - t.offset, textArea, e,
x0 + width);
}
width += t.getWidth(textArea, e, x0 + width);
}
return width;
}
/**
* Returns whether or not this character is a "bracket" to be matched by such programming languages as C, C++, and
* Java.
*
* @param ch
* The character to check.
* @return Whether or not the character is a "bracket" - one of '(', ')', '[', ']', '{', and '}'.
*/
public static final boolean isBracket(char ch) {
// We need the first condition as it might be that ch>255, and thus
// not in our table. '}' is the highest-valued char in the bracket
// set.
return ch <= '}' && (dataTable[ch] & BRACKET_MASK) > 0;
}
/**
* Returns whether or not a character is a digit (0-9).
*
* @param ch
* The character to check.
* @return Whether or not the character is a digit.
*/
public static final boolean isDigit(char ch) {
// We do it this way as we'd need to do two conditions anyway (first
// to check that ch<255 so it can index into our table, then whether
// that table position has the digit mask).
return ch >= '0' && ch <= '9';
}
/**
* Returns whether or not this character is a hex character. This method accepts both upper- and lower-case letters
* a-f.
*
* @param ch
* The character to check.
* @return Whether or not the character is a hex character 0-9, a-f, or A-F.
*/
public static final boolean isHexCharacter(char ch) {
// We need the first condition as it could be that ch>255 (and thus
// not a valid index into our table). 'f' is the highest-valued
// char that is a valid hex character.
return (ch <= 'f') && (dataTable[ch] & HEX_CHARACTER_MASK) > 0;
}
/**
* Returns whether a character is a Java operator. Note that C and C++ operators are the same as Java operators.
*
* @param ch
* The character to check.
* @return Whether or not the character is a Java operator.
*/
public static final boolean isJavaOperator(char ch) {
// We need the first condition as it could be that ch>255 (and thus
// not a valid index into our table). '~' is the highest-valued
// char that is a valid Java operator.
return (ch <= '~') && (dataTable[ch] & JAVA_OPERATOR_MASK) > 0;
}
/**
* Returns whether a character is a US-ASCII letter (A-Z or a-z).
*
* @param ch
* The character to check.
* @return Whether or not the character is a US-ASCII letter.
*/
public static final boolean isLetter(char ch) {
// We need the first condition as it could be that ch>255 (and thus
// not a valid index into our table).
return (ch <= 'z') && (dataTable[ch] & LETTER_MASK) > 0;
}
/**
* Returns whether or not a character is a US-ASCII letter or a digit.
*
* @param ch
* The character to check.
* @return Whether or not the character is a US-ASCII letter or a digit.
*/
public static final boolean isLetterOrDigit(char ch) {
// We need the first condition as it could be that ch>255 (and thus
// not a valid index into our table).
return (ch <= 'z') && (dataTable[ch] & LETTER_OR_DIGIT_MASK) > 0;
}
/**
* Returns whether or not a character is a whitespace character (either a space ' ' or tab '\t'). This checks for
* the Unicode character values 0x0020 and 0x0009.
*
* @param ch
* The character to check.
* @return Whether or not the character is a whitespace character.
*/
public static final boolean isWhitespace(char ch) {
// We do it this way as we'd need to do two conditions anyway (first
// to check that ch<255 so it can index into our table, then whether
// that table position has the whitespace mask).
return ch == ' ' || ch == '\t';
}
/**
* Modifies the passed-in token list to start at the specified offset. For example, if the token list covered
* positions 20-60 in the document (inclusive) like so:
*
* <pre>
* [token1] -> [token2] -> [token3] -> [token4]
* 20 30 31 40 41 50 51 60
* </pre>
*
* and you used this method to make the token list start at position 44, then the token list would be modified to be
* the following:
*
* <pre>
* [part-of-old-token3] -> [token4]
* 44 50 51 60
* </pre>
*
* Tokens that come before the specified position are forever lost, and the token containing that position is made
* to begin at that position if necessary. All token types remain the same as they were originally.
* <p>
*
* This method can be useful if you are only interested in part of a token list (i.e., the line it represents), but
* you don't want to modify the token list yourself.
*
* @param tokenList
* The list to make start at the specified position. This parameter is modified.
* @param pos
* The position at which the new token list is to start. If this position is not in the passed-in token
* list, returned token list will either be <code>null</code> or the unpaintable token(s) at the end of
* the passed-in token list.
* @param e
* How to expand tabs.
* @param textArea
* The text area from which the token list came.
* @param x0
* The initial x-pixel position of the old token list.
* @return The width, in pixels, of the part of the token list "removed from the
* front." This way, you know the x-offset of the "new" token list.
*/
public static float makeTokenListStartAt(Token tokenList, int pos,
TabExpander e,
final RSyntaxTextArea textArea,
float x0) {
Token t = tokenList;
// Loop through the token list until you find the one that contains
// pos. Remember the cumulative width of all of these tokens.
while (t != null && t.isPaintable() && !t.containsPosition(pos)) {
x0 += t.getWidth(textArea, e, x0);
t = t.getNextToken();
}
// Make the token that contains pos start at pos.
if (t != null && t.isPaintable() && t.offset != pos) {
// Number of chars between p0 and token start.
int difference = pos - t.offset;
x0 += t.getWidthUpTo(t.textCount - difference + 1, textArea, e, x0);
t.makeStartAt(pos);
}
// Make the passed-in token list point to the proper place.
// t can be null, for example, if line ends with unended MLC.
if (t != null && t.isPaintable())
tokenList.copyFrom(t);
else
tokenList = null;
t = null;
// Return the x-offset (in pixels) of the newly-modified t.
return x0;
}
/**
* If the character is an upper-case US-ASCII letter, it returns the lower-case version of that letter; otherwise,
* it just returns the character.
*
* @param ch
* The character to lower-case (if it is a US-ASCII upper-case character).
* @return The lower-case version of the character.
*/
public static final char toLowerCase(char ch) {
// We can logical OR with 32 because A-Z are 65-90 in the ASCII table
// and none of them have the 6th bit (32) set, and a-z are 97-122 in
// the ASCII table, which is 32 over from A-Z.
// We do it this way as we'd need to do two conditions anyway (first
// to check that ch<255 so it can index into our table, then whether
// that table position has the upper-case mask).
if (ch >= 'A' && ch <= 'Z')
return (char) (ch | 0x20);
return ch;
}
}