source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/RSyntaxUtilities.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 37.0 KB
Line 
1/*
2 * 08/06/2004
3 *
4 * RSyntaxUtilities.java - Utility methods used by RSyntaxTextArea and its
5 * views.
6 *
7 * This library is distributed under a modified BSD license. See the included
8 * RSyntaxTextArea.License.txt file for details.
9 */
10package org.fife.ui.rsyntaxtextarea;
11
12import java.awt.Color;
13import java.awt.Container;
14import java.awt.Point;
15import java.awt.Rectangle;
16import java.awt.Shape;
17import java.awt.Toolkit;
18import java.util.Map;
19import javax.swing.*;
20import javax.swing.text.BadLocationException;
21import javax.swing.text.Caret;
22import javax.swing.text.Document;
23import javax.swing.text.Element;
24import javax.swing.text.Position;
25import javax.swing.text.Segment;
26import javax.swing.text.TabExpander;
27import javax.swing.text.View;
28
29import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
30import org.fife.ui.rtextarea.Gutter;
31import org.fife.ui.rtextarea.RTextArea;
32import org.fife.ui.rtextarea.RTextScrollPane;
33
34
35/**
36 * Utility methods used by <code>RSyntaxTextArea</code> and its associated
37 * classes.
38 *
39 * @author Robert Futrell
40 * @version 0.2
41 */
42public class RSyntaxUtilities implements SwingConstants {
43
44 //private static final int DIGIT_MASK = 1;
45 private static final int LETTER_MASK = 2;
46 //private static final int WHITESPACE_MASK = 4;
47 //private static final int UPPER_CASE_MASK = 8;
48 private static final int HEX_CHARACTER_MASK = 16;
49 private static final int LETTER_OR_DIGIT_MASK = 32;
50 private static final int BRACKET_MASK = 64;
51 private static final int JAVA_OPERATOR_MASK = 128;
52
53 /**
54 * A lookup table used to quickly decide if a 16-bit Java char is a
55 * US-ASCII letter (A-Z or a-z), a digit, a whitespace char (either space
56 * (0x0020) or tab (0x0009)), etc. This method should be faster
57 * than <code>Character.isLetter</code>, <code>Character.isDigit</code>,
58 * and <code>Character.isWhitespace</code> because we know we are dealing
59 * with ASCII chars and so don't have to worry about code planes, etc.
60 */
61 private static final int[] dataTable = {
62 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, // 0-15
63 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
64 4, 128, 0, 0, 0, 128, 128, 0, 64, 64, 128, 128, 0, 128, 0, 128, // 32-47
65 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 128, 0, 128, 128, 128, 128, // 48-63
66 0, 58, 58, 58, 58, 58, 58, 42, 42, 42, 42, 42, 42, 42, 42, 42, // 64-79
67 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 64, 0, 64, 128, 0, // 80-95
68 0, 50, 50, 50, 50, 50, 50, 34, 34, 34, 34, 34, 34, 34, 34, 34, // 96-111
69 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 64, 128, 64, 128, 0, // 112-127
70 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143
71 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-
72 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-
73 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-
74 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-
75 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-
76 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-
77 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255.
78 };
79
80 /**
81 * Used in bracket matching methods.
82 */
83 private static Segment charSegment = new Segment();
84
85 /**
86 * Used internally.
87 */
88 private static final char[] JS_KEYWORD_RETURN = { 'r', 'e', 't', 'u', 'r', 'n' };
89
90
91 /**
92 * Returns a string with characters that are special to HTML (such as
93 * <code>&lt;</code>, <code>&gt;</code> and <code>&amp;</code>) replaced
94 * by their HTML escape sequences.
95 *
96 * @param s The input string.
97 * @param newlineReplacement What to replace newline characters with.
98 * If this is <code>null</code>, they are simply removed.
99 * @param inPreBlock Whether this HTML will be in within <code>pre</code>
100 * tags. If this is <code>true</code>, spaces will be kept as-is;
101 * otherwise, they will be converted to "<code>&nbsp;</code>".
102 * @return The escaped version of <code>s</code>.
103 */
104 public static final String escapeForHtml(String s,
105 String newlineReplacement, boolean inPreBlock) {
106
107 if (s==null) {
108 return null;
109 }
110 if (newlineReplacement==null) {
111 newlineReplacement = "";
112 }
113 final String tabString = " ";
114 boolean lastWasSpace = false;
115
116 // TODO: When updating to 1.5, replace with StringBuilder, and change
117 // loop to use new append(str, offs,len) method.
118 StringBuffer sb = new StringBuffer();
119
120 for (int i=0; i<s.length(); i++) {
121 char ch = s.charAt(i);
122 switch (ch) {
123 case ' ':
124 if (inPreBlock || !lastWasSpace) {
125 sb.append(' ');
126 }
127 else {
128 sb.append("&nbsp;");
129 }
130 lastWasSpace = true;
131 break;
132 case '\n':
133 sb.append(newlineReplacement);
134 lastWasSpace = false;
135 break;
136 case '&':
137 sb.append("&amp;");
138 lastWasSpace = false;
139 break;
140 case '\t':
141 sb.append(tabString);
142 lastWasSpace = false;
143 break;
144 case '<':
145 sb.append("&lt;");
146 lastWasSpace = false;
147 break;
148 case '>':
149 sb.append("&gt;");
150 lastWasSpace = false;
151 break;
152 default:
153 sb.append(ch);
154 lastWasSpace = false;
155 break;
156 }
157 }
158
159 return sb.toString();
160
161 }
162
163
164 /**
165 * Returns the rendering hints for text that will most accurately reflect
166 * those of the native windowing system.
167 *
168 * @return The rendering hints, or <code>null</code> if they cannot be
169 * determined.
170 */
171 public static Map getDesktopAntiAliasHints() {
172 return (Map)Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints");
173 }
174
175
176 /**
177 * Returns the color to use for the line underneath a folded region line.
178 *
179 * @param textArea The text area.
180 * @return The color to use.
181 */
182 public static Color getFoldedLineBottomColor(RSyntaxTextArea textArea) {
183 Color color = Color.gray;
184 Gutter gutter = RSyntaxUtilities.getGutter(textArea);
185 if (gutter!=null) {
186 color = gutter.getFoldIndicatorForeground();
187 }
188 return color;
189 }
190
191
192 /**
193 * Returns the gutter component of the scroll pane containing a text
194 * area, if any.
195 *
196 * @param textArea The text area.
197 * @return The gutter, or <code>null</code> if the text area is not in
198 * an {@link RTextScrollPane}.
199 * @see RTextScrollPane#getGutter()
200 */
201 public static Gutter getGutter(RTextArea textArea) {
202 Gutter gutter = null;
203 Container parent = textArea.getParent();
204 if (parent instanceof JViewport) {
205 parent = parent.getParent();
206 if (parent instanceof RTextScrollPane) {
207 RTextScrollPane sp = (RTextScrollPane)parent;
208 gutter = sp.getGutter(); // Should always be non-null
209 }
210 }
211 return gutter;
212 }
213
214
215 /**
216 * Returns the leading whitespace of a string.
217 *
218 * @param text The String to check.
219 * @return The leading whitespace.
220 */
221 public static String getLeadingWhitespace(String text) {
222 int count = 0;
223 int len = text.length();
224 while (count<len && RSyntaxUtilities.isWhitespace(text.charAt(count))) {
225 count++;
226 }
227 return text.substring(0, count);
228 }
229
230
231 private static final Element getLineElem(Document d, int offs) {
232 Element map = d.getDefaultRootElement();
233 int index = map.getElementIndex(offs);
234 Element elem = map.getElement(index);
235 if ((offs>=elem.getStartOffset()) && (offs<elem.getEndOffset())) {
236 return elem;
237 }
238 return null;
239 }
240
241
242 /**
243 * Returns the bounding box (in the current view) of a specified position
244 * in the model. This method is designed for line-wrapped views to use,
245 * as it allows you to specify a "starting position" in the line, from
246 * which the x-value is assumed to be zero. The idea is that you specify
247 * the first character in a physical line as <code>p0</code>, as this is
248 * the character where the x-pixel value is 0.
249 *
250 * @param textArea The text area containing the text.
251 * @param s A segment in which to load the line. This is passed in so we
252 * don't have to reallocate a new <code>Segment</code> for each
253 * call.
254 * @param p0 The starting position in the physical line in the document.
255 * @param p1 The position for which to get the bounding box in the view.
256 * @param e How to expand tabs.
257 * @param rect The rectangle whose x- and width-values are changed to
258 * represent the bounding box of <code>p1</code>. This is reused
259 * to keep from needlessly reallocating Rectangles.
260 * @param x0 The x-coordinate (pixel) marking the left-hand border of the
261 * text. This is useful if the text area has a border, for example.
262 * @return The bounding box in the view of the character <code>p1</code>.
263 * @throws BadLocationException If <code>p0</code> or <code>p1</code> is
264 * not a valid location in the specified text area's document.
265 * @throws IllegalArgumentException If <code>p0</code> and <code>p1</code>
266 * are not on the same line.
267 */
268 public static Rectangle getLineWidthUpTo(RSyntaxTextArea textArea,
269 Segment s, int p0, int p1,
270 TabExpander e, Rectangle rect,
271 int x0)
272 throws BadLocationException {
273
274 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
275
276 // Ensure p0 and p1 are valid document positions.
277 if (p0<0)
278 throw new BadLocationException("Invalid document position", p0);
279 else if (p1>doc.getLength())
280 throw new BadLocationException("Invalid document position", p1);
281
282 // Ensure p0 and p1 are in the same line, and get the start/end
283 // offsets for that line.
284 Element map = doc.getDefaultRootElement();
285 int lineNum = map.getElementIndex(p0);
286 // We do ">1" because p1 might be the first position on the next line
287 // or the last position on the previous one.
288 // if (lineNum!=map.getElementIndex(p1))
289 if (Math.abs(lineNum-map.getElementIndex(p1))>1)
290 throw new IllegalArgumentException("p0 and p1 are not on the " +
291 "same line (" + p0 + ", " + p1 + ").");
292
293 // Get the token list.
294 Token t = doc.getTokenListForLine(lineNum);
295
296 // Modify the token list 't' to begin at p0 (but still have correct
297 // token types, etc.), and get the x-location (in pixels) of the
298 // beginning of this new token list.
299 makeTokenListStartAt(t, p0, e, textArea, 0);
300
301 rect = t.listOffsetToView(textArea, e, p1, x0, rect);
302 return rect;
303
304 }
305
306
307 /**
308 * Returns the location of the bracket paired with the one at the current
309 * caret position.
310 *
311 * @param textArea The text area.
312 * @return The location of the matching bracket in the document, or
313 * <code>-1</code> if there isn't a matching bracket (or the caret
314 * isn't on a bracket).
315 */
316 public static int getMatchingBracketPosition(RSyntaxTextArea textArea) {
317
318 try {
319
320 // Actually position just BEFORE caret.
321 int caretPosition = textArea.getCaretPosition() - 1;
322 if (caretPosition>-1) {
323
324 // Some variables that will be used later.
325 Token token;
326 Element map;
327 int curLine;
328 Element line;
329 int start, end;
330 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
331 char bracket = doc.charAt(caretPosition);
332
333 // First, see if the previous char was a bracket
334 // ('{', '}', '(', ')', '[', ']').
335 // If it was, then make sure this bracket isn't sitting in
336 // the middle of a comment or string. If it isn't, then
337 // initialize some stuff so we can continue on.
338 char bracketMatch;
339 boolean goForward;
340 switch (bracket) {
341
342 case '{':
343 case '(':
344 case '[':
345
346 // Ensure this bracket isn't in a comment.
347 map = doc.getDefaultRootElement();
348 curLine = map.getElementIndex(caretPosition);
349 line = map.getElement(curLine);
350 start = line.getStartOffset();
351 end = line.getEndOffset();
352 token = doc.getTokenListForLine(curLine);
353 token = RSyntaxUtilities.getTokenAtOffset(token, caretPosition);
354 // All brackets are always returned as "separators."
355 if (token.type!=Token.SEPARATOR) {
356 return -1;
357 }
358 bracketMatch = bracket=='{' ? '}' : (bracket=='(' ? ')' : ']');
359 goForward = true;
360 break;
361
362 case '}':
363 case ')':
364 case ']':
365
366 // Ensure this bracket isn't in a comment.
367 map = doc.getDefaultRootElement();
368 curLine = map.getElementIndex(caretPosition);
369 line = map.getElement(curLine);
370 start = line.getStartOffset();
371 end = line.getEndOffset();
372 token = doc.getTokenListForLine(curLine);
373 token = RSyntaxUtilities.getTokenAtOffset(token, caretPosition);
374 // All brackets are always returned as "separators."
375 if (token.type!=Token.SEPARATOR) {
376 return -1;
377 }
378 bracketMatch = bracket=='}' ? '{' : (bracket==')' ? '(' : '[');
379 goForward = false;
380 break;
381
382 default:
383 return -1;
384
385 }
386
387 if (goForward) {
388
389 int lastLine = map.getElementCount();
390
391 // Start just after the found bracket since we're sure
392 // we're not in a comment.
393 start = caretPosition + 1;
394 int numEmbedded = 0;
395 boolean haveTokenList = false;
396
397 while (true) {
398
399 doc.getText(start,end-start, charSegment);
400 int segOffset = charSegment.offset;
401
402 for (int i=segOffset; i<segOffset+charSegment.count; i++) {
403
404 char ch = charSegment.array[i];
405
406 if (ch==bracket) {
407 if (haveTokenList==false) {
408 token = doc.getTokenListForLine(curLine);
409 haveTokenList = true;
410 }
411 int offset = start + (i-segOffset);
412 token = RSyntaxUtilities.getTokenAtOffset(token, offset);
413 if (token.type==Token.SEPARATOR)
414 numEmbedded++;
415 }
416
417 else if (ch==bracketMatch) {
418 if (haveTokenList==false) {
419 token = doc.getTokenListForLine(curLine);
420 haveTokenList = true;
421 }
422 int offset = start + (i-segOffset);
423 token = RSyntaxUtilities.getTokenAtOffset(token, offset);
424 if (token.type==Token.SEPARATOR) {
425 if (numEmbedded==0) {
426 if (textArea.isCodeFoldingEnabled() &&
427 textArea.getFoldManager().isLineHidden(curLine)) {
428 return -1; // Match hidden in a fold
429 }
430 return offset;
431 }
432 numEmbedded--;
433 }
434 }
435
436 } // End of for (int i=segOffset; i<segOffset+charSegment.count; i++).
437
438 // Bail out if we've gone through all lines and
439 // haven't found the match.
440 if (++curLine==lastLine)
441 return -1;
442
443 // Otherwise, go through the next line.
444 haveTokenList = false;
445 line = map.getElement(curLine);
446 start = line.getStartOffset();
447 end = line.getEndOffset();
448
449 } // End of while (true).
450
451 } // End of if (goForward).
452
453
454 // Otherwise, we're going backward through the file
455 // (since we found '}', ')' or ']').
456 else { // goForward==false
457
458 // End just before the found bracket since we're sure
459 // we're not in a comment.
460 end = caretPosition;// - 1;
461 int numEmbedded = 0;
462 boolean haveTokenList = false;
463 Token t2;
464
465 while (true) {
466
467 doc.getText(start,end-start, charSegment);
468 int segOffset = charSegment.offset;
469 int iStart = segOffset + charSegment.count - 1;
470
471 for (int i=iStart; i>=segOffset; i--) {
472
473 char ch = charSegment.array[i];
474
475 if (ch==bracket) {
476 if (haveTokenList==false) {
477 token = doc.getTokenListForLine(curLine);
478 haveTokenList = true;
479 }
480 int offset = start + (i-segOffset);
481 t2 = RSyntaxUtilities.getTokenAtOffset(token, offset);
482 if (t2.type==Token.SEPARATOR)
483 numEmbedded++;
484 }
485
486 else if (ch==bracketMatch) {
487 if (haveTokenList==false) {
488 token = doc.getTokenListForLine(curLine);
489 haveTokenList = true;
490 }
491 int offset = start + (i-segOffset);
492 t2 = RSyntaxUtilities.getTokenAtOffset(token, offset);
493 if (t2.type==Token.SEPARATOR) {
494 if (numEmbedded==0)
495 return offset;
496 numEmbedded--;
497 }
498 }
499
500 }
501
502 // Bail out if we've gone through all lines and
503 // haven't found the match.
504 if (--curLine==-1)
505 return -1;
506
507 // Otherwise, get ready for going through the
508 // next line.
509 haveTokenList = false;
510 line = map.getElement(curLine);
511 start = line.getStartOffset();
512 end = line.getEndOffset();
513
514 } // End of while (true).
515
516 } // End of else.
517
518 } // End of if (caretPosition>-1).
519
520 } catch (BadLocationException ble) {
521 // Shouldn't ever happen.
522 ble.printStackTrace();
523 }
524
525 // Something went wrong...
526 return -1;
527
528 }
529
530
531 /**
532 * Provides a way to determine the next visually represented model
533 * location at which one might place a caret.
534 * Some views may not be visible,
535 * they might not be in the same order found in the model, or they just
536 * might not allow access to some of the locations in the model.<p>
537 *
538 * NOTE: You should only call this method if the passed-in
539 * <code>javax.swing.text.View</code> is an instance of
540 * {@link TokenOrientedView} and <code>javax.swing.text.TabExpander</code>;
541 * otherwise, a <code>ClassCastException</code> could be thrown.
542 *
543 * @param pos the position to convert >= 0
544 * @param a the allocated region in which to render
545 * @param direction the direction from the current position that can
546 * be thought of as the arrow keys typically found on a keyboard.
547 * This will be one of the following values:
548 * <ul>
549 * <li>SwingConstants.WEST
550 * <li>SwingConstants.EAST
551 * <li>SwingConstants.NORTH
552 * <li>SwingConstants.SOUTH
553 * </ul>
554 * @return the location within the model that best represents the next
555 * location visual position
556 * @exception BadLocationException
557 * @exception IllegalArgumentException if <code>direction</code>
558 * doesn't have one of the legal values above
559 */
560 public static int getNextVisualPositionFrom(int pos, Position.Bias b,
561 Shape a, int direction,
562 Position.Bias[] biasRet, View view)
563 throws BadLocationException {
564
565 RSyntaxTextArea target = (RSyntaxTextArea)view.getContainer();
566 biasRet[0] = Position.Bias.Forward;
567
568 // Do we want the "next position" above, below, to the left or right?
569 switch (direction) {
570
571 case NORTH:
572 case SOUTH:
573 if (pos == -1) {
574 pos = (direction == NORTH) ?
575 Math.max(0, view.getEndOffset() - 1) :
576 view.getStartOffset();
577 break;
578 }
579 Caret c = (target != null) ? target.getCaret() : null;
580 // YECK! Ideally, the x location from the magic caret
581 // position would be passed in.
582 Point mcp;
583 if (c != null)
584 mcp = c.getMagicCaretPosition();
585 else
586 mcp = null;
587 int x;
588 if (mcp == null) {
589 Rectangle loc = target.modelToView(pos);
590 x = (loc == null) ? 0 : loc.x;
591 }
592 else {
593 x = mcp.x;
594 }
595 if (direction == NORTH)
596 pos = getPositionAbove(target,pos,x,(TabExpander)view);
597 else
598 pos = getPositionBelow(target,pos,x,(TabExpander)view);
599 break;
600
601 case WEST:
602 if(pos == -1) {
603 pos = Math.max(0, view.getEndOffset() - 1);
604 }
605 else {
606 pos = Math.max(0, pos - 1);
607 if (target.isCodeFoldingEnabled()) {
608 int last = target.getLineOfOffset(pos+1);
609 int current = target.getLineOfOffset(pos);
610 if (last!=current) { // If moving up a line...
611 FoldManager fm = target.getFoldManager();
612 if (fm.isLineHidden(current)) {
613 while (--current>0 && fm.isLineHidden(current));
614 pos = target.getLineEndOffset(current) - 1;
615 }
616 }
617 }
618 }
619 break;
620
621 case EAST:
622 if(pos == -1) {
623 pos = view.getStartOffset();
624 }
625 else {
626 pos = Math.min(pos + 1, view.getDocument().getLength());
627 if (target.isCodeFoldingEnabled()) {
628 int last = target.getLineOfOffset(pos-1);
629 int current = target.getLineOfOffset(pos);
630 if (last!=current) { // If moving down a line...
631 FoldManager fm = target.getFoldManager();
632 if (fm.isLineHidden(current)) {
633 int lineCount = target.getLineCount();
634 while (++current<lineCount && fm.isLineHidden(current));
635 pos = current==lineCount ?
636 target.getLineEndOffset(last)-1 : // Was the last visible line
637 target.getLineStartOffset(current);
638 }
639 }
640 }
641 }
642 break;
643
644 default:
645 throw new IllegalArgumentException(
646 "Bad direction: " + direction);
647 }
648
649 return pos;
650
651 }
652
653
654 /**
655 * Determines the position in the model that is closest to the given
656 * view location in the row above. The component given must have a
657 * size to compute the result. If the component doesn't have a size
658 * a value of -1 will be returned.
659 *
660 * @param c the editor
661 * @param offs the offset in the document >= 0
662 * @param x the X coordinate >= 0
663 * @return the position >= 0 if the request can be computed, otherwise
664 * a value of -1 will be returned.
665 * @exception BadLocationException if the offset is out of range
666 */
667 public static final int getPositionAbove(RSyntaxTextArea c, int offs,
668 float x, TabExpander e) throws BadLocationException {
669
670 TokenOrientedView tov = (TokenOrientedView)e;
671 Token token = tov.getTokenListForPhysicalLineAbove(offs);
672 if (token==null)
673 return -1;
674
675 // A line containing only Token.NULL is an empty line.
676 else if (token.type==Token.NULL) {
677 int line = c.getLineOfOffset(offs); // Sure to be >0 ??
678 return c.getLineStartOffset(line-1);
679 }
680
681 else {
682 return token.getListOffset(c, e, 0, x);
683 }
684
685 }
686
687
688 /**
689 * Determines the position in the model that is closest to the given
690 * view location in the row below. The component given must have a
691 * size to compute the result. If the component doesn't have a size
692 * a value of -1 will be returned.
693 *
694 * @param c the editor
695 * @param offs the offset in the document >= 0
696 * @param x the X coordinate >= 0
697 * @return the position >= 0 if the request can be computed, otherwise
698 * a value of -1 will be returned.
699 * @exception BadLocationException if the offset is out of range
700 */
701 public static final int getPositionBelow(RSyntaxTextArea c, int offs,
702 float x, TabExpander e) throws BadLocationException {
703
704 TokenOrientedView tov = (TokenOrientedView)e;
705 Token token = tov.getTokenListForPhysicalLineBelow(offs);
706 if (token==null)
707 return -1;
708
709 // A line containing only Token.NULL is an empty line.
710 else if (token.type==Token.NULL) {
711 int line = c.getLineOfOffset(offs); // Sure to be > c.getLineCount()-1 ??
712// return c.getLineStartOffset(line+1);
713FoldManager fm = c.getFoldManager();
714line = fm.getVisibleLineBelow(line);
715return c.getLineStartOffset(line);
716 }
717
718 else {
719 return token.getListOffset(c, e, 0, x);
720 }
721
722 }
723
724
725 /**
726 * Returns the token at the specified index, or <code>null</code> if
727 * the given offset isn't in this token list's range.<br>
728 * Note that this method does NOT check to see if <code>tokenList</code>
729 * is null; callers should check for themselves.
730 *
731 * @param tokenList The list of tokens in which to search.
732 * @param offset The offset at which to get the token.
733 * @return The token at <code>offset</code>, or <code>null</code> if
734 * none of the tokens are at that offset.
735 */
736 public static final Token getTokenAtOffset(Token tokenList, int offset) {
737 for (Token t=tokenList; t!=null; t=t.getNextToken()) {
738 if (t.containsPosition(offset))
739 return t;
740 }
741 return null;
742 }
743
744
745 /**
746 * Returns the end of the word at the given offset.
747 *
748 * @param textArea The text area.
749 * @param offs The offset into the text area's content.
750 * @return The end offset of the word.
751 * @throws BadLocationException If <code>offs</code> is invalid.
752 * @see #getWordStart(RSyntaxTextArea, int)
753 */
754 public static int getWordEnd(RSyntaxTextArea textArea, int offs)
755 throws BadLocationException {
756
757 Document doc = textArea.getDocument();
758 int endOffs = textArea.getLineEndOffsetOfCurrentLine();
759 int lineEnd = Math.min(endOffs, doc.getLength());
760 if (offs == lineEnd) { // End of the line.
761 return offs;
762 }
763
764 String s = doc.getText(offs, lineEnd-offs-1);
765 if (s!=null && s.length()>0) { // Should always be true
766 int i = 0;
767 int count = s.length();
768 char ch = s.charAt(i);
769 if (Character.isWhitespace(ch)) {
770 while (i<count && Character.isWhitespace(s.charAt(i++)));
771 }
772 else if (Character.isLetterOrDigit(ch)) {
773 while (i<count && Character.isLetterOrDigit(s.charAt(i++)));
774 }
775 else {
776 i = 2;
777 }
778 offs += i - 1;
779 }
780
781 return offs;
782
783 }
784
785 /**
786 * Returns the start of the word at the given offset.
787 *
788 * @param textArea The text area.
789 * @param offs The offset into the text area's content.
790 * @return The start offset of the word.
791 * @throws BadLocationException If <code>offs</code> is invalid.
792 * @see #getWordEnd(RSyntaxTextArea, int)
793 */
794 public static int getWordStart(RSyntaxTextArea textArea, int offs)
795 throws BadLocationException {
796
797 Document doc = textArea.getDocument();
798 Element line = getLineElem(doc, offs);
799 if (line == null) {
800 throw new BadLocationException("No word at " + offs, offs);
801 }
802
803 int lineStart = line.getStartOffset();
804 if (offs==lineStart) { // Start of the line.
805 return offs;
806 }
807
808 int endOffs = Math.min(offs+1, doc.getLength());
809 String s = doc.getText(lineStart, endOffs-lineStart);
810 if(s != null && s.length() > 0) {
811 int i = s.length() - 1;
812 char ch = s.charAt(i);
813 if (Character.isWhitespace(ch)) {
814 while (i>0 && Character.isWhitespace(s.charAt(i-1))) {
815 i--;
816 }
817 offs = lineStart + i;
818 }
819 else if (Character.isLetterOrDigit(ch)) {
820 while (i>0 && Character.isLetterOrDigit(s.charAt(i-1))) {
821 i--;
822 }
823 offs = lineStart + i;
824 }
825
826 }
827
828 return offs;
829
830 }
831
832
833 /**
834 * Determines the width of the given token list taking tabs
835 * into consideration. This is implemented in a 1.1 style coordinate
836 * system where ints are used and 72dpi is assumed.<p>
837 *
838 * This method also assumes that the passed-in token list begins at
839 * x-pixel <code>0</code> in the view (for tab purposes).
840 *
841 * @param tokenList The tokenList list representing the text.
842 * @param textArea The text area in which this token list resides.
843 * @param e The tab expander. This value cannot be <code>null</code>.
844 * @return The width of the token list, in pixels.
845 */
846 public static final float getTokenListWidth(Token tokenList,
847 RSyntaxTextArea textArea,
848 TabExpander e) {
849 return getTokenListWidth(tokenList, textArea, e, 0);
850 }
851
852
853 /**
854 * Determines the width of the given token list taking tabs
855 * into consideration. This is implemented in a 1.1 style coordinate
856 * system where ints are used and 72dpi is assumed.<p>
857 *
858 * @param tokenList The token list list representing the text.
859 * @param textArea The text area in which this token list resides.
860 * @param e The tab expander. This value cannot be <code>null</code>.
861 * @param x0 The x-pixel coordinate of the start of the token list.
862 * @return The width of the token list, in pixels.
863 * @see #getTokenListWidthUpTo
864 */
865 public static final float getTokenListWidth(final Token tokenList,
866 RSyntaxTextArea textArea,
867 TabExpander e, float x0) {
868 float width = x0;
869 for (Token t=tokenList; t!=null&&t.isPaintable(); t=t.getNextToken()) {
870 width += t.getWidth(textArea, e, width);
871 }
872 return width - x0;
873 }
874
875
876 /**
877 * Determines the width of the given token list taking tabs into
878 * consideration and only up to the given index in the document
879 * (exclusive).
880 *
881 * @param tokenList The token list representing the text.
882 * @param textArea The text area in which this token list resides.
883 * @param e The tab expander. This value cannot be <code>null</code>.
884 * @param x0 The x-pixel coordinate of the start of the token list.
885 * @param upTo The document position at which you want to stop,
886 * exclusive. If this position is before the starting position
887 * of the token list, a width of <code>0</code> will be
888 * returned; similarly, if this position comes after the entire
889 * token list, the width of the entire token list is returned.
890 * @return The width of the token list, in pixels, up to, but not
891 * including, the character at position <code>upTo</code>.
892 * @see #getTokenListWidth
893 */
894 public static final float getTokenListWidthUpTo(final Token tokenList,
895 RSyntaxTextArea textArea, TabExpander e,
896 float x0, int upTo) {
897 float width = 0;
898 for (Token t=tokenList; t!=null&&t.isPaintable(); t=t.getNextToken()) {
899 if (t.containsPosition(upTo)) {
900 return width + t.getWidthUpTo(upTo-t.offset, textArea, e,
901 x0+width);
902 }
903 width += t.getWidth(textArea, e, x0+width);
904 }
905 return width;
906 }
907
908
909 /**
910 * Returns whether or not this character is a "bracket" to be matched by
911 * such programming languages as C, C++, and Java.
912 *
913 * @param ch The character to check.
914 * @return Whether or not the character is a "bracket" - one of '(', ')',
915 * '[', ']', '{', and '}'.
916 */
917 public static final boolean isBracket(char ch) {
918 // We need the first condition as it might be that ch>255, and thus
919 // not in our table. '}' is the highest-valued char in the bracket
920 // set.
921 return ch<='}' && (dataTable[ch]&BRACKET_MASK)>0;
922 }
923
924
925 /**
926 * Returns whether or not a character is a digit (0-9).
927 *
928 * @param ch The character to check.
929 * @return Whether or not the character is a digit.
930 */
931 public static final boolean isDigit(char ch) {
932 // We do it this way as we'd need to do two conditions anyway (first
933 // to check that ch<255 so it can index into our table, then whether
934 // that table position has the digit mask).
935 return ch>='0' && ch<='9';
936 }
937
938
939 /**
940 * Returns whether or not this character is a hex character. This method
941 * accepts both upper- and lower-case letters a-f.
942 *
943 * @param ch The character to check.
944 * @return Whether or not the character is a hex character 0-9, a-f, or
945 * A-F.
946 */
947 public static final boolean isHexCharacter(char ch) {
948 // We need the first condition as it could be that ch>255 (and thus
949 // not a valid index into our table). 'f' is the highest-valued
950 // char that is a valid hex character.
951 return (ch<='f') && (dataTable[ch]&HEX_CHARACTER_MASK)>0;
952 }
953
954
955 /**
956 * Returns whether a character is a Java operator. Note that C and C++
957 * operators are the same as Java operators.
958 *
959 * @param ch The character to check.
960 * @return Whether or not the character is a Java operator.
961 */
962 public static final boolean isJavaOperator(char ch) {
963 // We need the first condition as it could be that ch>255 (and thus
964 // not a valid index into our table). '~' is the highest-valued
965 // char that is a valid Java operator.
966 return (ch<='~') && (dataTable[ch]&JAVA_OPERATOR_MASK)>0;
967 }
968
969
970 /**
971 * Returns whether a character is a US-ASCII letter (A-Z or a-z).
972 *
973 * @param ch The character to check.
974 * @return Whether or not the character is a US-ASCII letter.
975 */
976 public static final boolean isLetter(char ch) {
977 // We need the first condition as it could be that ch>255 (and thus
978 // not a valid index into our table).
979 return (ch<='z') && (dataTable[ch]&LETTER_MASK)>0;
980 }
981
982
983 /**
984 * Returns whether or not a character is a US-ASCII letter or a digit.
985 *
986 * @param ch The character to check.
987 * @return Whether or not the character is a US-ASCII letter or a digit.
988 */
989 public static final boolean isLetterOrDigit(char ch) {
990 // We need the first condition as it could be that ch>255 (and thus
991 // not a valid index into our table).
992 return (ch<='z') && (dataTable[ch]&LETTER_OR_DIGIT_MASK)>0;
993 }
994
995
996 /**
997 * Returns whether or not a character is a whitespace character (either
998 * a space ' ' or tab '\t'). This checks for the Unicode character values
999 * 0x0020 and 0x0009.
1000 *
1001 * @param ch The character to check.
1002 * @return Whether or not the character is a whitespace character.
1003 */
1004 public static final boolean isWhitespace(char ch) {
1005 // We do it this way as we'd need to do two conditions anyway (first
1006 // to check that ch<255 so it can index into our table, then whether
1007 // that table position has the whitespace mask).
1008 return ch==' ' || ch=='\t';
1009 }
1010
1011
1012 /**
1013 * Modifies the passed-in token list to start at the specified offset.
1014 * For example, if the token list covered positions 20-60 in the document
1015 * (inclusive) like so:
1016 * <pre>
1017 * [token1] -> [token2] -> [token3] -> [token4]
1018 * 20 30 31 40 41 50 51 60
1019 * </pre>
1020 * and you used this method to make the token list start at position 44,
1021 * then the token list would be modified to be the following:
1022 * <pre>
1023 * [part-of-old-token3] -> [token4]
1024 * 44 50 51 60
1025 * </pre>
1026 * Tokens that come before the specified position are forever lost, and
1027 * the token containing that position is made to begin at that position if
1028 * necessary. All token types remain the same as they were originally.<p>
1029 *
1030 * This method can be useful if you are only interested in part of a token
1031 * list (i.e., the line it represents), but you don't want to modify the
1032 * token list yourself.
1033 *
1034 * @param tokenList The list to make start at the specified position.
1035 * This parameter is modified.
1036 * @param pos The position at which the new token list is to start. If
1037 * this position is not in the passed-in token list,
1038 * returned token list will either be <code>null</code> or the
1039 * unpaintable token(s) at the end of the passed-in token list.
1040 * @param e How to expand tabs.
1041 * @param textArea The text area from which the token list came.
1042 * @param x0 The initial x-pixel position of the old token list.
1043 * @return The width, in pixels, of the part of the token list "removed
1044 * from the front." This way, you know the x-offset of the "new"
1045 * token list.
1046 */
1047 public static float makeTokenListStartAt(Token tokenList, int pos,
1048 TabExpander e,
1049 final RSyntaxTextArea textArea,
1050 float x0) {
1051
1052 Token t = tokenList;
1053
1054 // Loop through the token list until you find the one that contains
1055 // pos. Remember the cumulative width of all of these tokens.
1056 while (t!=null && t.isPaintable() && !t.containsPosition(pos)) {
1057 x0 += t.getWidth(textArea, e, x0);
1058 t = t.getNextToken();
1059 }
1060
1061 // Make the token that contains pos start at pos.
1062 if (t!=null && t.isPaintable() && t.offset!=pos) {
1063 // Number of chars between p0 and token start.
1064 int difference = pos - t.offset;
1065 x0 += t.getWidthUpTo(t.textCount-difference+1, textArea, e, x0);
1066 t.makeStartAt(pos);
1067 }
1068
1069 // Make the passed-in token list point to the proper place.
1070 // t can be null, for example, if line ends with unended MLC.
1071 if (t!=null && t.isPaintable())
1072 tokenList.copyFrom(t);
1073 else
1074 tokenList = null;
1075 t = null;
1076
1077 // Return the x-offset (in pixels) of the newly-modified t.
1078 return x0;
1079
1080 }
1081
1082
1083 /**
1084 * Returns whether a regular expression token can follow the specified
1085 * token in JavaScript.
1086 *
1087 * @param t The token to check, which may be <code>null</code>.
1088 * @return Whether a regular expression token may follow this one in
1089 * JavaScript.
1090 */
1091 public static boolean regexCanFollowInJavaScript(Token t) {
1092 char ch;
1093 // We basically try to mimic Eclipse's JS editor's behavior here.
1094 return t==null ||
1095 //t.isOperator() ||
1096 (t.textCount==1 && (
1097 (ch=t.text[t.textOffset])=='=' ||
1098 ch=='(' ||
1099 ch==',' ||
1100 ch=='?' ||
1101 ch==':' ||
1102 ch=='[' ||
1103 ch=='!' ||
1104 ch=='&'
1105 )) ||
1106 /* Operators "==", "===", "!=", "!==" */
1107 (t.type==Token.OPERATOR &&
1108 t.text[t.textOffset+t.textCount-1]=='=') ||
1109 t.is(Token.RESERVED_WORD, JS_KEYWORD_RETURN);
1110 }
1111
1112
1113 /**
1114 * If the character is an upper-case US-ASCII letter, it returns the
1115 * lower-case version of that letter; otherwise, it just returns the
1116 * character.
1117 *
1118 * @param ch The character to lower-case (if it is a US-ASCII upper-case
1119 * character).
1120 * @return The lower-case version of the character.
1121 */
1122 public static final char toLowerCase(char ch) {
1123 // We can logical OR with 32 because A-Z are 65-90 in the ASCII table
1124 // and none of them have the 6th bit (32) set, and a-z are 97-122 in
1125 // the ASCII table, which is 32 over from A-Z.
1126 // We do it this way as we'd need to do two conditions anyway (first
1127 // to check that ch<255 so it can index into our table, then whether
1128 // that table position has the upper-case mask).
1129 if (ch>='A' && ch<='Z')
1130 return (char)(ch | 0x20);
1131 return ch;
1132 }
1133
1134
1135}
Note: See TracBrowser for help on using the repository browser.