/* * 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, c.getMargin().left, 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, c.getMargin().left, 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; } }