/*
* 08/06/2004
*
* RSyntaxUtilities.java - Utility methods used by RSyntaxTextArea and its
* views.
*
* 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.Container;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
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;
import org.fife.ui.rsyntaxtextarea.TokenUtils.TokenSubList;
import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
import org.fife.ui.rtextarea.Gutter;
import org.fife.ui.rtextarea.RTextArea;
import org.fife.ui.rtextarea.RTextScrollPane;
/**
* Utility methods used by <code>RSyntaxTextArea</code> and its associated
* classes.
*
* @author Robert Futrell
* @version 0.2
*/
public class RSyntaxUtilities implements SwingConstants {
/**
* Integer constant representing a Windows-variant OS.
*/
public static final int OS_WINDOWS = 1;
/**
* Integer constant representing Mac OS X.
*/
public static final int OS_MAC_OSX = 2;
/**
* Integer constant representing Linux.
*/
public static final int OS_LINUX = 4;
/**
* Integer constant representing an "unknown" OS. 99.99% of the
* time, this means some UNIX variant (AIX, SunOS, etc.).
*/
public static final int OS_OTHER = 8;
/**
* Used for the color of hyperlinks when a LookAndFeel uses light text
* against a dark background.
*/
private static final Color LIGHT_HYPERLINK_FG = new Color(0xd8ffff);
private static final int OS = getOSImpl();
//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.
};
/**
* Used in bracket matching methods.
*/
private static Segment charSegment = new Segment();
/**
* Used in token list manipulation methods.
*/
private static final TokenImpl tempToken = new TokenImpl();
/**
* Used internally.
*/
private static final char[] JS_KEYWORD_RETURN = { 'r', 'e', 't', 'u', 'r', 'n' };
private static final char[] JS_AND = { '&', '&' };
private static final char[] JS_OR = { '|', '|' };
/**
* Used internally.
*/
private static final String BRACKETS = "{([})]";
/**
* Returns a string with characters that are special to HTML (such as
* <code><</code>, <code>></code> and <code>&</code>) replaced
* by their HTML escape sequences.
*
* @param s The input string.
* @param newlineReplacement What to replace newline characters with.
* If this is <code>null</code>, they are simply removed.
* @param inPreBlock Whether this HTML will be in within <code>pre</code>
* tags. If this is <code>true</code>, spaces will be kept as-is;
* otherwise, they will be converted to "<code> </code>".
* @return The escaped version of <code>s</code>.
*/
public static final String escapeForHtml(String s,
String newlineReplacement, boolean inPreBlock) {
if (s==null) {
return null;
}
if (newlineReplacement==null) {
newlineReplacement = "";
}
final String tabString = " ";
boolean lastWasSpace = false;
StringBuilder sb = new StringBuilder();
for (int i=0; i<s.length(); i++) {
char ch = s.charAt(i);
switch (ch) {
case ' ':
if (inPreBlock || !lastWasSpace) {
sb.append(' ');
}
else {
sb.append(" ");
}
lastWasSpace = true;
break;
case '\n':
sb.append(newlineReplacement);
lastWasSpace = false;
break;
case '&':
sb.append("&");
lastWasSpace = false;
break;
case '\t':
sb.append(tabString);
lastWasSpace = false;
break;
case '<':
sb.append("<");
lastWasSpace = false;
break;
case '>':
sb.append(">");
lastWasSpace = false;
break;
default:
sb.append(ch);
lastWasSpace = false;
break;
}
}
return sb.toString();
}
/**
* Returns the rendering hints for text that will most accurately reflect
* those of the native windowing system.
*
* @return The rendering hints, or <code>null</code> if they cannot be
* determined.
*/
public static Map<?,?> getDesktopAntiAliasHints() {
return (Map<?,?>)Toolkit.getDefaultToolkit().
getDesktopProperty("awt.font.desktophints");
}
/**
* Returns the color to use for the line underneath a folded region line.
*
* @param textArea The text area.
* @return The color to use.
*/
public static Color getFoldedLineBottomColor(RSyntaxTextArea textArea) {
Color color = Color.gray;
Gutter gutter = RSyntaxUtilities.getGutter(textArea);
if (gutter!=null) {
color = gutter.getFoldIndicatorForeground();
}
return color;
}
/**
* Returns the gutter component of the scroll pane containing a text
* area, if any.
*
* @param textArea The text area.
* @return The gutter, or <code>null</code> if the text area is not in
* an {@link RTextScrollPane}.
* @see RTextScrollPane#getGutter()
*/
public static Gutter getGutter(RTextArea textArea) {
Gutter gutter = null;
Container parent = textArea.getParent();
if (parent instanceof JViewport) {
parent = parent.getParent();
if (parent instanceof RTextScrollPane) {
RTextScrollPane sp = (RTextScrollPane)parent;
gutter = sp.getGutter(); // Should always be non-null
}
}
return gutter;
}
/**
* Returns the color to use for hyperlink-style components. This method
* will return <code>Color.blue</code> unless it appears that the current
* LookAndFeel uses light text on a dark background, in which case a
* brighter alternative is returned.
*
* @return The color to use for hyperlinks.
* @see #isLightForeground(Color)
*/
public static final Color getHyperlinkForeground() {
// This property is defined by all standard LaFs, even Nimbus (!),
// but you never know what crazy LaFs there are...
Color fg = UIManager.getColor("Label.foreground");
if (fg==null) {
fg = new JLabel().getForeground();
}
return isLightForeground(fg) ? LIGHT_HYPERLINK_FG : Color.blue;
}
/**
* Returns the leading whitespace of a string.
*
* @param text The String to check.
* @return The leading whitespace.
* @see #getLeadingWhitespace(Document, int)
*/
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);
}
/**
* Returns the leading whitespace of a specific line in a document.
*
* @param doc The document.
* @param offs The offset whose line to get the leading whitespace for.
* @return The leading whitespace.
* @throws BadLocationException If <code>offs</code> is not a valid offset
* in the document.
* @see #getLeadingWhitespace(String)
*/
public static String getLeadingWhitespace(Document doc, int offs)
throws BadLocationException {
Element root = doc.getDefaultRootElement();
int line = root.getElementIndex(offs);
Element elem = root.getElement(line);
int startOffs = elem.getStartOffset();
int endOffs = elem.getEndOffset() - 1;
String text = doc.getText(startOffs, endOffs-startOffs);
return getLeadingWhitespace(text);
}
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.
TokenSubList subList = TokenUtils.getSubTokenList(t, p0, e, textArea,
0, tempToken);
t = subList.tokenList;
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.
* @param input A point to use as the return value. If this is
* <code>null</code>, a new object is created and returned.
* @return A point representing the matched bracket info. The "x" field
* is the offset of the bracket at the caret position (either just
* before or just after the caret), and the "y" field is the offset
* of the matched bracket. Both "x" and "y" will be
* <code>-1</code> if there isn't a matching bracket (or the caret
* isn't on a bracket).
*/
public static Point getMatchingBracketPosition(RSyntaxTextArea textArea,
Point input) {
if (input==null) {
input = new Point();
}
input.setLocation(-1, -1);
try {
// Actually position just BEFORE caret.
int caretPosition = textArea.getCaretPosition() - 1;
RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
char bracket = 0;
// If the caret was at offset 0, we can't check "to its left."
if (caretPosition>=0) {
bracket = doc.charAt(caretPosition);
}
// Try to match a bracket "to the right" of the caret if one
// was not found on the left.
int index = BRACKETS.indexOf(bracket);
if (index==-1 && caretPosition<doc.getLength()-1) {
bracket = doc.charAt(++caretPosition);
}
// First, see if the char was a bracket (one of "{[()]}").
if (index==-1) {
index = BRACKETS.indexOf(bracket);
if (index==-1) {
return input;
}
}
// 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;
Element map = doc.getDefaultRootElement();
int curLine = map.getElementIndex(caretPosition);
Element line = map.getElement(curLine);
int start = line.getStartOffset();
int end = line.getEndOffset();
Token token = doc.getTokenListForLine(curLine);
token = RSyntaxUtilities.getTokenAtOffset(token, caretPosition);
// All brackets are always returned as "separators."
if (token.getType()!=Token.SEPARATOR) {
return input;
}
int languageIndex = token.getLanguageIndex();
if (index<3) { // One of "{[("
goForward = true;
bracketMatch = BRACKETS.charAt(index + 3);
}
else { // One of ")]}"
goForward = false;
bracketMatch = BRACKETS.charAt(index - 3);
}
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.getType()==Token.SEPARATOR &&
token.getLanguageIndex()==languageIndex)
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.getType()==Token.SEPARATOR &&
token.getLanguageIndex()==languageIndex) {
if (numEmbedded==0) {
if (textArea.isCodeFoldingEnabled() &&
textArea.getFoldManager().isLineHidden(curLine)) {
return input; // Match hidden in a fold
}
input.setLocation(caretPosition, offset);
return input;
}
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 input;
// 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.getType()==Token.SEPARATOR &&
token.getLanguageIndex()==languageIndex)
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.getType()==Token.SEPARATOR &&
token.getLanguageIndex()==languageIndex) {
if (numEmbedded==0) {
input.setLocation(caretPosition, offset);
return input;
}
numEmbedded--;
}
}
}
// Bail out if we've gone through all lines and
// haven't found the match.
if (--curLine==-1) {
return input;
}
// 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.
} catch (BadLocationException ble) {
// Shouldn't ever happen.
ble.printStackTrace();
}
// Something went wrong...
return input;
}
/**
* Returns the next non-whitespace, non-comment token in a text area.
*
* @param t The next token in this line's token list.
* @param textArea The text area.
* @param line The current line index (the line index of <code>t</code>).
* @return The next non-whitespace, non-comment token, or <code>null</code>
* if there isn't one.
* @see #getPreviousImportantToken(RSyntaxDocument, int)
* @see #getPreviousImportantTokenFromOffs(RSyntaxDocument, int)
*/
public static final Token getNextImportantToken(Token t,
RSyntaxTextArea textArea, int line) {
while (t!=null && t.isPaintable() && t.isCommentOrWhitespace()) {
t = t.getNextToken();
}
if ((t==null || !t.isPaintable()) && line<textArea.getLineCount()-1) {
t = textArea.getTokenListForLine(++line);
return getNextImportantToken(t, textArea, line);
}
return t;
}
/**
* 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 {
RSyntaxTextArea target = (RSyntaxTextArea)view.getContainer();
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;
}
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:
int endOffs = view.getEndOffset();
if(pos == -1) {
pos = Math.max(0, endOffs - 1);
}
else {
pos = Math.max(0, pos - 1);
if (target.isCodeFoldingEnabled()) {
int last = pos==endOffs-1 ? target.getLineCount()-1 :
target.getLineOfOffset(pos+1);
int current = target.getLineOfOffset(pos);
if (last!=current) { // If moving up a line...
FoldManager fm = target.getFoldManager();
if (fm.isLineHidden(current)) {
while (--current>0 && fm.isLineHidden(current));
pos = target.getLineEndOffset(current) - 1;
}
}
}
}
break;
case EAST:
if(pos == -1) {
pos = view.getStartOffset();
}
else {
pos = Math.min(pos + 1, view.getDocument().getLength());
if (target.isCodeFoldingEnabled()) {
int last = pos==0 ? 0 : target.getLineOfOffset(pos-1);
int current = target.getLineOfOffset(pos);
if (last!=current) { // If moving down a line...
FoldManager fm = target.getFoldManager();
if (fm.isLineHidden(current)) {
int lineCount = target.getLineCount();
while (++current<lineCount && fm.isLineHidden(current));
pos = current==lineCount ?
target.getLineEndOffset(last)-1 : // Was the last visible line
target.getLineStartOffset(current);
}
}
}
}
break;
default:
throw new IllegalArgumentException(
"Bad direction: " + direction);
}
return pos;
}
/**
* Returns an integer constant representing the OS. This can be handy for
* special case situations such as Mac OS-X (special application
* registration) or Windows (allow mixed case, etc.).
*
* @return An integer constant representing the OS.
*/
public static final int getOS() {
return OS;
}
/**
* Returns an integer constant representing the OS. This can be handy for
* special case situations such as Mac OS-X (special application
* registration) or Windows (allow mixed case, etc.).
*
* @return An integer constant representing the OS.
*/
private static final int getOSImpl() {
int os = OS_OTHER;
String osName = System.getProperty("os.name");
if (osName!=null) { // Should always be true.
osName = osName.toLowerCase();
if (osName.indexOf("windows") > -1)
os = OS_WINDOWS;
else if (osName.indexOf("mac os x") > -1)
os = OS_MAC_OSX;
else if (osName.indexOf("linux") > -1)
os = OS_LINUX;
else
os = OS_OTHER;
}
return os;
}
/**
* Returns the flags necessary to create a {@link Pattern}.
*
* @param matchCase Whether the pattern should be case sensitive.
* @param others Any other flags. This may be <code>0</code>.
* @return The flags.
*/
public static final int getPatternFlags(boolean matchCase, int others) {
if (!matchCase) {
others |= Pattern.CASE_INSENSITIVE|Pattern.UNICODE_CASE;
}
return others;
}
/**
* 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.getType()==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.getType()==Token.NULL) {
int line = c.getLineOfOffset(offs); // Sure to be > c.getLineCount()-1 ??
// return c.getLineStartOffset(line+1);
FoldManager fm = c.getFoldManager();
line = fm.getVisibleLineBelow(line);
return c.getLineStartOffset(line);
}
else {
return token.getListOffset(c, e, 0, x);
}
}
/**
* Returns the last non-whitespace, non-comment token, starting with the
* specified line.
*
* @param doc The document.
* @param line The line at which to start looking.
* @return The last non-whitespace, non-comment token, or <code>null</code>
* if there isn't one.
* @see #getNextImportantToken(Token, RSyntaxTextArea, int)
* @see #getPreviousImportantTokenFromOffs(RSyntaxDocument, int)
*/
public static final Token getPreviousImportantToken(RSyntaxDocument doc,
int line) {
if (line<0) {
return null;
}
Token t = doc.getTokenListForLine(line);
if (t!=null) {
t = t.getLastNonCommentNonWhitespaceToken();
if (t!=null) {
return t;
}
}
return getPreviousImportantToken(doc, line-1);
}
/**
* Returns the last non-whitespace, non-comment token, before the
* specified offset.
*
* @param doc The document.
* @param offs The ending offset for the search.
* @return The last non-whitespace, non-comment token, or <code>null</code>
* if there isn't one.
* @see #getPreviousImportantToken(RSyntaxDocument, int)
* @see #getNextImportantToken(Token, RSyntaxTextArea, int)
*/
public static final Token getPreviousImportantTokenFromOffs(
RSyntaxDocument doc, int offs) {
Element root = doc.getDefaultRootElement();
int line = root.getElementIndex(offs);
Token t = doc.getTokenListForLine(line);
// Check line containing offs
Token target = null;
while (t!=null && t.isPaintable() && !t.containsPosition(offs)) {
if (!t.isCommentOrWhitespace()) {
target = t;
}
t = t.getNextToken();
}
// Check previous line(s)
if (target==null) {
target = RSyntaxUtilities.getPreviousImportantToken(doc, line-1);
}
return target;
}
/**
* Returns the token at the specified offset.
*
* @param textArea The text area.
* @param offset The offset of the token.
* @return The token, or <code>null</code> if the offset is not valid.
* @see #getTokenAtOffset(RSyntaxDocument, int)
* @see #getTokenAtOffset(Token, int)
*/
public static final Token getTokenAtOffset(RSyntaxTextArea textArea,
int offset) {
RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
return RSyntaxUtilities.getTokenAtOffset(doc, offset);
}
/**
* Returns the token at the specified offset.
*
* @param doc The document.
* @param offset The offset of the token.
* @return The token, or <code>null</code> if the offset is not valid.
* @see #getTokenAtOffset(RSyntaxTextArea, int)
* @see #getTokenAtOffset(Token, int)
*/
public static final Token getTokenAtOffset(RSyntaxDocument doc,
int offset) {
Element root = doc.getDefaultRootElement();
int lineIndex = root.getElementIndex(offset);
Token t = doc.getTokenListForLine(lineIndex);
return RSyntaxUtilities.getTokenAtOffset(t, offset);
}
/**
* 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.
* @see #getTokenAtOffset(RSyntaxTextArea, int)
* @see #getTokenAtOffset(RSyntaxDocument, int)
*/
public static final Token getTokenAtOffset(Token tokenList, int offset) {
for (Token t=tokenList; t!=null && t.isPaintable(); 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.getOffset(), 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 the specified color is "light" to use as a foreground.
* Colors that return <code>true</code> indicate that the current Look and
* Feel probably uses light text colors on a dark background.
*
* @param fg The foreground color.
* @return Whether it is a "light" foreground color.
* @see #getHyperlinkForeground()
*/
public static final boolean isLightForeground(Color fg) {
return fg.getRed()>0xa0 && fg.getGreen()>0xa0 && fg.getBlue()>0xa0;
}
/**
* Returns whether the specified token is a single non-word char (e.g. not
* in <code>[A-Za-z]</code>. This is a HACK to work around the fact that
* many standard token makers return things like semicolons and periods as
* {@link Token#IDENTIFIER}s just to make the syntax highlighting coloring
* look a little better.
*
* @param t The token to check. This cannot be <code>null</code>.
* @return Whether the token is a single non-word char.
*/
public static final boolean isNonWordChar(Token t) {
return t.length()==1 && !RSyntaxUtilities.isLetter(t.charAt(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';
}
/**
* Returns whether a regular expression token can follow the specified
* token in JavaScript.
*
* @param t The token to check, which may be <code>null</code>.
* @return Whether a regular expression token may follow this one in
* JavaScript.
*/
public static boolean regexCanFollowInJavaScript(Token t) {
char ch;
// We basically try to mimic Eclipse's JS editor's behavior here.
return t==null ||
//t.isOperator() ||
(t.length()==1 && (
(ch=t.charAt(0))=='=' ||
ch=='(' ||
ch==',' ||
ch=='?' ||
ch==':' ||
ch=='[' ||
ch=='!' ||
ch=='&'
)) ||
/* Operators "==", "===", "!=", "!==", "&&", "||" */
(t.getType()==Token.OPERATOR &&
(t.charAt(t.length()-1)=='=' ||
t.is(JS_AND) || t.is(JS_OR))) ||
t.is(Token.RESERVED_WORD_2, JS_KEYWORD_RETURN);
}
/**
* Selects a range of text in a text component. If the new selection is
* outside of the previous viewable rectangle, then the view is centered
* around the new selection.
*
* @param textArea The text component whose selection is to be centered.
* @param range The range to select.
*/
public static final void selectAndPossiblyCenter(JTextArea textArea,
DocumentRange range, boolean select) {
int start = range.getStartOffset();
int end = range.getEndOffset();
boolean foldsExpanded = false;
if (textArea instanceof RSyntaxTextArea) {
RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
FoldManager fm = rsta.getFoldManager();
if (fm.isCodeFoldingSupportedAndEnabled()) {
foldsExpanded = fm.ensureOffsetNotInClosedFold(start);
foldsExpanded |= fm.ensureOffsetNotInClosedFold(end);
}
}
if (select) {
textArea.setSelectionStart(start);
textArea.setSelectionEnd(end);
}
Rectangle r = null;
try {
r = textArea.modelToView(start);
if (r==null) { // Not yet visible; i.e. JUnit tests
return;
}
if (end!=start) {
r = r.union(textArea.modelToView(end));
}
} catch (BadLocationException ble) { // Never happens
ble.printStackTrace();
if (select) {
textArea.setSelectionStart(start);
textArea.setSelectionEnd(end);
}
return;
}
Rectangle visible = textArea.getVisibleRect();
// If the new selection is already in the view, don't scroll,
// as that is visually jarring.
if (!foldsExpanded && visible.contains(r)) {
if (select) {
textArea.setSelectionStart(start);
textArea.setSelectionEnd(end);
}
return;
}
visible.x = r.x - (visible.width - r.width) / 2;
visible.y = r.y - (visible.height - r.height) / 2;
Rectangle bounds = textArea.getBounds();
Insets i = textArea.getInsets();
bounds.x = i.left;
bounds.y = i.top;
bounds.width -= i.left + i.right;
bounds.height -= i.top + i.bottom;
if (visible.x < bounds.x) {
visible.x = bounds.x;
}
if (visible.x + visible.width > bounds.x + bounds.width) {
visible.x = bounds.x + bounds.width - visible.width;
}
if (visible.y < bounds.y) {
visible.y = bounds.y;
}
if (visible.y + visible.height > bounds.y + bounds.height) {
visible.y = bounds.y + bounds.height - visible.height;
}
textArea.scrollRectToVisible(visible);
}
/**
* 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;
}
/**
* Creates a regular expression pattern that matches a "wildcard" pattern.
*
* @param wildcard The wildcard pattern.
* @param matchCase Whether the pattern should be case sensitive.
* @param escapeStartChar Whether to escape a starting <code>'^'</code>
* character.
* @return The pattern.
*/
public static Pattern wildcardToPattern(String wildcard, boolean matchCase,
boolean escapeStartChar) {
int flags = RSyntaxUtilities.getPatternFlags(matchCase, 0);
StringBuilder sb = new StringBuilder();
for (int i=0; i<wildcard.length(); i++) {
char ch = wildcard.charAt(i);
switch (ch) {
case '*':
sb.append(".*");
break;
case '?':
sb.append('.');
break;
case '^':
if (i>0 || escapeStartChar) {
sb.append('\\');
}
sb.append('^');
break;
case '\\':
case '.': case '|':
case '+': case '-':
case '$':
case '[': case ']':
case '{': case '}':
case '(': case ')':
sb.append('\\').append(ch);
break;
default:
sb.append(ch);
break;
}
}
Pattern p = null;
try {
p = Pattern.compile(sb.toString(), flags);
} catch (PatternSyntaxException pse) {
pse.printStackTrace();
p = Pattern.compile(".+");
}
return p;
}
}