source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/RSyntaxDocument.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: 21.7 KB
Line 
1/*
2 * 10/16/2004
3 *
4 * RSyntaxDocument.java - A document capable of syntax highlighting, used by
5 * RSyntaxTextArea.
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.event.ActionEvent;
13import java.io.IOException;
14import java.io.ObjectInputStream;
15import java.io.ObjectOutputStream;
16import javax.swing.Action;
17import javax.swing.event.*;
18import javax.swing.text.*;
19
20import org.fife.ui.rsyntaxtextarea.modes.AbstractMarkupTokenMaker;
21import org.fife.util.DynamicIntArray;
22
23
24/**
25 * The document used by {@link org.fife.ui.rsyntaxtextarea.RSyntaxTextArea}.
26 * This document is like <code>javax.swing.text.PlainDocument</code> except that
27 * it also keeps track of syntax highlighting in the document. It has a "style"
28 * attribute associated with it that determines how syntax highlighting is done
29 * (i.e., what language is being highlighted).<p>
30 *
31 * Instances of <code>RSyntaxTextArea</code> will only accept instances of
32 * <code>RSyntaxDocument</code>, since it is this document that keeps
33 * track of syntax highlighting. All others will cause an exception to be
34 * thrown.<p>
35 *
36 * To change the language being syntax highlighted at any time, you merely have
37 * to call {@link #setSyntaxStyle}. Other than that, this document can be
38 * treated like any other save one caveat: all <code>DocumentEvent</code>s of
39 * type <code>CHANGE</code> use their offset and length values to represent the
40 * first and last lines, respectively, that have had their syntax coloring
41 * change. This is really a hack to increase the speed of the painting code
42 * and should really be corrected, but oh well.
43 *
44 * @author Robert Futrell
45 * @version 0.1
46 */
47public class RSyntaxDocument extends PlainDocument implements SyntaxConstants {
48
49 /**
50 * Creates a {@link TokenMaker} appropriate for a given programming
51 * language.
52 */
53 private transient TokenMakerFactory tokenMakerFactory;
54
55 /**
56 * Splits text into tokens for the current programming language.
57 */
58 private transient TokenMaker tokenMaker;
59
60 /**
61 * The current syntax style. Only cached to keep this class serializable.
62 */
63 private String syntaxStyle;
64
65 /**
66 * Array of values representing the "last token type" on each line. This
67 * is used in cases such as multiline comments: if the previous line
68 * ended with an (unclosed) multiline comment, we can use this knowledge
69 * and start the current line's syntax highlighting in multiline comment
70 * state.
71 */
72 protected transient DynamicIntArray lastTokensOnLines;
73
74 private transient Segment s;
75
76
77 /**
78 * Constructs a plain text document. A default root element is created,
79 * and the tab size set to 5.
80 *
81 * @param syntaxStyle The syntax highlighting scheme to use.
82 */
83 public RSyntaxDocument(String syntaxStyle) {
84 this(null, syntaxStyle);
85 }
86
87
88 /**
89 * Constructs a plain text document. A default root element is created,
90 * and the tab size set to 5.
91 *
92 * @param tmf The <code>TokenMakerFactory</code> for this document. If
93 * this is <code>null</code>, a default factory is used.
94 * @param syntaxStyle The syntax highlighting scheme to use.
95 */
96 public RSyntaxDocument(TokenMakerFactory tmf, String syntaxStyle) {
97 super(new RGapContent());
98 putProperty(tabSizeAttribute, new Integer(5));
99 lastTokensOnLines = new DynamicIntArray(400);
100 lastTokensOnLines.add(Token.NULL); // Initial (empty) line.
101 s = new Segment();
102 setTokenMakerFactory(tmf);
103 setSyntaxStyle(syntaxStyle);
104 }
105
106
107 /**
108 * Returns the character in the document at the specified offset.
109 *
110 * @param offset The offset of the character.
111 * @return The character.
112 * @throws BadLocationException If the offset is invalid.
113 */
114 public char charAt(int offset) throws BadLocationException {
115 return ((RGapContent)getContent()).charAt(offset);
116 }
117
118
119 /**
120 * Alerts all listeners to this document of an insertion. This is
121 * overridden so we can update our syntax highlighting stuff.<p>
122 * The syntax highlighting stuff has to be here instead of in
123 * <code>insertUpdate</code> because <code>insertUpdate</code> is not
124 * called by the undo/redo actions, but this method is.
125 *
126 * @param e The change.
127 */
128 protected void fireInsertUpdate(DocumentEvent e) {
129
130 /*
131 * Now that the text is actually inserted into the content and
132 * element structure, we can update our token elements and "last
133 * tokens on lines" structure.
134 */
135
136 Element lineMap = getDefaultRootElement();
137 DocumentEvent.ElementChange change = e.getChange(lineMap);
138 Element[] added = change==null ? null : change.getChildrenAdded();
139
140 int numLines = lineMap.getElementCount();
141 int line = lineMap.getElementIndex(e.getOffset());
142 int previousLine = line - 1;
143 int previousTokenType = (previousLine>-1 ?
144 lastTokensOnLines.get(previousLine) : Token.NULL);
145
146 // If entire lines were added...
147 if (added!=null && added.length>0) {
148
149 Element[] removed = change.getChildrenRemoved();
150 int numRemoved = removed!=null ? removed.length : 0;
151
152 int endBefore = line + added.length - numRemoved;
153 //System.err.println("... adding lines: " + line + " - " + (endBefore-1));
154 //System.err.println("... ... added: " + added.length + ", removed:" + numRemoved);
155 for (int i=line; i<endBefore; i++) {
156
157 setSharedSegment(i); // Loads line i's text into s.
158
159 int tokenType = tokenMaker.getLastTokenTypeOnLine(s, previousTokenType);
160 lastTokensOnLines.add(i, tokenType);
161 //System.err.println("--------- lastTokensOnLines.size() == " + lastTokensOnLines.getSize());
162
163 previousTokenType = tokenType;
164
165 } // End of for (int i=line; i<endBefore; i++).
166
167 // Update last tokens for lines below until they stop changing.
168 updateLastTokensBelow(endBefore, numLines, previousTokenType);
169
170 } // End of if (added!=null && added.length>0).
171
172 // Otherwise, text was inserted on a single line...
173 else {
174
175 // Update last tokens for lines below until they stop changing.
176 updateLastTokensBelow(line, numLines, previousTokenType);
177
178 } // End of else.
179
180 // Let all listeners know about the insertion.
181 super.fireInsertUpdate(e);
182
183 }
184
185
186 /**
187 * This method is called AFTER the content has been inserted into the
188 * document and the element structure has been updated.<p>
189 * The syntax-highlighting updates need to be done here (as opposed to
190 * an override of <code>postRemoveUpdate</code>) as this method is called
191 * in response to undo/redo events, whereas <code>postRemoveUpdate</code>
192 * is not.<p>
193 * Now that the text is actually inserted into the content and element
194 * structure, we can update our token elements and "last tokens on
195 * lines" structure.
196 *
197 * @param chng The change that occurred.
198 * @see #removeUpdate
199 */
200 protected void fireRemoveUpdate(DocumentEvent chng) {
201
202 Element lineMap = getDefaultRootElement();
203 int numLines = lineMap.getElementCount();
204
205 DocumentEvent.ElementChange change = chng.getChange(lineMap);
206 Element[] removed = change==null ? null : change.getChildrenRemoved();
207
208 // If entire lines were removed...
209 if (removed!=null && removed.length>0) {
210
211 int line = change.getIndex(); // First line entirely removed.
212 int previousLine = line - 1; // Line before that.
213 int previousTokenType = (previousLine>-1 ?
214 lastTokensOnLines.get(previousLine) : Token.NULL);
215
216 Element[] added = change.getChildrenAdded();
217 int numAdded = added==null ? 0 : added.length;
218
219 // Remove the cached last-token values for the removed lines.
220 int endBefore = line + removed.length - numAdded;
221 //System.err.println("... removing lines: " + line + " - " + (endBefore-1));
222 //System.err.println("... added: " + numAdded + ", removed: " + removed.length);
223
224 lastTokensOnLines.removeRange(line, endBefore); // Removing values for lines [line-(endBefore-1)].
225 //System.err.println("--------- lastTokensOnLines.size() == " + lastTokensOnLines.getSize());
226
227 // Update last tokens for lines below until they've stopped changing.
228 updateLastTokensBelow(line, numLines, previousTokenType);
229
230 } // End of if (removed!=null && removed.size()>0).
231
232 // Otherwise, text was removed from just one line...
233 else {
234
235 int line = lineMap.getElementIndex(chng.getOffset());
236 if (line>=lastTokensOnLines.getSize())
237 return; // If we're editing the last line in a document...
238
239 int previousLine = line - 1;
240 int previousTokenType = (previousLine>-1 ?
241 lastTokensOnLines.get(previousLine) : Token.NULL);
242 //System.err.println("previousTokenType for line : " + previousLine + " is " + previousTokenType);
243 // Update last tokens for lines below until they've stopped changing.
244 updateLastTokensBelow(line, numLines, previousTokenType);
245
246 }
247
248 // Let all of our listeners know about the removal.
249 super.fireRemoveUpdate(chng);
250
251 }
252
253
254 /**
255 * Returns whether closing markup tags should be automatically completed.
256 * This method only returns <code>true</code> if
257 * {@link #getLanguageIsMarkup()} also returns <code>true</code>.
258 *
259 * @return Whether markup closing tags should be automatically completed.
260 * @see #getLanguageIsMarkup()
261 */
262 public boolean getCompleteMarkupCloseTags() {
263 // TODO: Remove terrible dependency on AbstractMarkupTokenMaker
264 return getLanguageIsMarkup() &&
265 ((AbstractMarkupTokenMaker)tokenMaker).getCompleteCloseTags();
266 }
267
268
269 /**
270 * Returns whether the current programming language uses curly braces
271 * ('<tt>{</tt>' and '<tt>}</tt>') to denote code blocks.
272 *
273 * @return Whether curly braces denote code blocks.
274 */
275 public boolean getCurlyBracesDenoteCodeBlocks() {
276 return tokenMaker.getCurlyBracesDenoteCodeBlocks();
277 }
278
279
280 /**
281 * Returns whether the current language is a markup language, such as
282 * HTML, XML or PHP.
283 *
284 * @return Whether the current language is a markup language.
285 */
286 public boolean getLanguageIsMarkup() {
287 return tokenMaker.isMarkupLanguage();
288 }
289
290
291 /**
292 * Returns the token type of the last token on the given line.
293 *
294 * @param line The line to inspect.
295 * @return The token type of the last token on the specified line. If
296 * the line is invalid, an exception is thrown.
297 */
298 public int getLastTokenTypeOnLine(int line) {
299 return lastTokensOnLines.get(line);
300 }
301
302
303 /**
304 * Returns the text to place at the beginning and end of a
305 * line to "comment" it in the current programming language.
306 *
307 * @return The start and end strings to add to a line to "comment"
308 * it out. A <code>null</code> value for either means there
309 * is no string to add for that part. A value of
310 * <code>null</code> for the array means this language
311 * does not support commenting/uncommenting lines.
312 */
313 public String[] getLineCommentStartAndEnd() {
314 return tokenMaker.getLineCommentStartAndEnd();
315 }
316
317
318 /**
319 * Returns whether tokens of the specified type should have "mark
320 * occurrences" enabled for the current programming language.
321 *
322 * @param type The token type.
323 * @return Whether tokens of this type should have "mark occurrences"
324 * enabled.
325 */
326 boolean getMarkOccurrencesOfTokenType(int type) {
327 return tokenMaker.getMarkOccurrencesOfTokenType(type);
328 }
329
330
331 /**
332 * This method returns whether auto indentation should be done if Enter
333 * is pressed at the end of the specified line.
334 *
335 * @param line The line to check.
336 * @return Whether an extra indentation should be done.
337 */
338 public boolean getShouldIndentNextLine(int line) {
339 Token t = getTokenListForLine(line);
340 t = t.getLastNonCommentNonWhitespaceToken();
341 return tokenMaker.getShouldIndentNextLineAfter(t);
342 }
343
344
345 /**
346 * Returns a token list for the specified segment of text representing
347 * the specified line number. This method is basically a wrapper for
348 * <code>tokenMaker.getTokenList</code> that takes into account the last
349 * token on the previous line to assure token accuracy.
350 *
351 * @param line The line number of <code>text</code> in the document, >= 0.
352 * @return A token list representing the specified line.
353 */
354 public final Token getTokenListForLine(int line) {
355 Element map = getDefaultRootElement();
356 Element elem = map.getElement(line);
357 int startOffset = elem.getStartOffset();
358 //int endOffset = (line==map.getElementCount()-1 ? elem.getEndOffset() - 1:
359 // elem.getEndOffset() - 1);
360 int endOffset = elem.getEndOffset() - 1; // Why always "-1"?
361 try {
362 getText(startOffset,endOffset-startOffset, s);
363 } catch (BadLocationException ble) {
364 ble.printStackTrace();
365 return null;
366 }
367 int initialTokenType = line==0 ? Token.NULL :
368 getLastTokenTypeOnLine(line-1);
369 return tokenMaker.getTokenList(s, initialTokenType, startOffset);
370 }
371
372
373 boolean insertBreakSpecialHandling(ActionEvent e) {
374 Action a = tokenMaker.getInsertBreakAction();
375 if (a!=null) {
376 a.actionPerformed(e);
377 return true;
378 }
379 return false;
380 }
381
382
383 /**
384 * Returns whether whitespace is visible.
385 *
386 * @return Whether whitespace is visible.
387 * @see #setWhitespaceVisible(boolean)
388 */
389 public boolean isWhitespaceVisible() {
390 return tokenMaker==null ? false : tokenMaker.isWhitespaceVisible();
391 }
392
393
394 /**
395 * Deserializes a document.
396 *
397 * @param in The stream to read from.
398 * @throws ClassNotFoundException
399 * @throws IOException
400 */
401 private void readObject(ObjectInputStream in)
402 throws ClassNotFoundException, IOException {
403
404 in.defaultReadObject();
405
406 // Install default TokenMakerFactory. To support custom TokenMakers,
407 // both JVM's should install default TokenMakerFactories that support
408 // the language they want to use beforehand.
409 setTokenMakerFactory(null);
410
411 // Handle other transient stuff
412 this.s = new Segment();
413 int lineCount = getDefaultRootElement().getElementCount();
414 lastTokensOnLines = new DynamicIntArray(lineCount);
415 setSyntaxStyle(syntaxStyle); // Actually install (transient) TokenMaker
416 setWhitespaceVisible(in.readBoolean()); // Do after setSyntaxStyle()
417
418 }
419
420
421 /**
422 * Makes our private <code>Segment s</code> point to the text in our
423 * document referenced by the specified element. Note that
424 * <code>line</code> MUST be a valid line number in the document.
425 *
426 * @param line The line number you want to get.
427 */
428 private final void setSharedSegment(int line) {
429
430 Element map = getDefaultRootElement();
431 //int numLines = map.getElementCount();
432
433 Element element = map.getElement(line);
434 if (element==null)
435 throw new InternalError("Invalid line number: " + line);
436 int startOffset = element.getStartOffset();
437 //int endOffset = (line==numLines-1 ?
438 // element.getEndOffset()-1 : element.getEndOffset() - 1);
439 int endOffset = element.getEndOffset()-1; // Why always "-1"?
440 try {
441 getText(startOffset, endOffset-startOffset, s);
442 } catch (BadLocationException ble) {
443 throw new InternalError("Text range not in document: " +
444 startOffset + "-" + endOffset);
445 }
446
447 }
448
449
450 /**
451 * Sets the syntax style being used for syntax highlighting in this
452 * document. What styles are supported by a document is determined by its
453 * {@link TokenMakerFactory}. By default, all <code>RSyntaxDocument</code>s
454 * support all languages built into <code>RSyntaxTextArea</code>.
455 *
456 * @param styleKey The new style to use, such as
457 * {@link SyntaxConstants#SYNTAX_STYLE_JAVA}. If this style is not
458 * known or supported by this document, then
459 * {@link SyntaxConstants#SYNTAX_STYLE_NONE} is used.
460 */
461 public void setSyntaxStyle(String styleKey) {
462 boolean wsVisible = isWhitespaceVisible();
463 tokenMaker = tokenMakerFactory.getTokenMaker(styleKey);
464 tokenMaker.setWhitespaceVisible(wsVisible);
465 updateSyntaxHighlightingInformation();
466 this.syntaxStyle = styleKey;
467 }
468
469
470 /**
471 * Sets the syntax style being used for syntax highlighting in this
472 * document. You should call this method if you've created a custom token
473 * maker for a language not normally supported by
474 * <code>RSyntaxTextArea</code>.
475 *
476 * @param tokenMaker The new token maker to use.
477 */
478 public void setSyntaxStyle(TokenMaker tokenMaker) {
479 tokenMaker.setWhitespaceVisible(isWhitespaceVisible());
480 this.tokenMaker = tokenMaker;
481 updateSyntaxHighlightingInformation();
482 }
483
484
485 /**
486 * Sets the token maker factory used by this document.
487 *
488 * @param tmf The <code>TokenMakerFactory</code> for this document. If
489 * this is <code>null</code>, a default factory is used.
490 */
491 public void setTokenMakerFactory(TokenMakerFactory tmf) {
492 tokenMakerFactory = tmf!=null ? tmf :
493 TokenMakerFactory.getDefaultInstance();
494 }
495
496
497 /**
498 * Sets whether whitespace is visible. This property is actually setting
499 * whether the tokens generated from this document "paint" something when
500 * they represent whitespace.
501 *
502 * @param visible Whether whitespace should be visible.
503 * @see #isWhitespaceVisible()
504 */
505 public void setWhitespaceVisible(boolean visible) {
506 tokenMaker.setWhitespaceVisible(visible);
507 }
508
509
510 /**
511 * Loops through the last-tokens-on-lines array from a specified point
512 * onward, updating last-token values until they stop changing. This
513 * should be called when lines are updated/inserted/removed, as doing
514 * so may cause lines below to change color.
515 *
516 * @param line The first line to check for a change in last-token value.
517 * @param numLines The number of lines in the document.
518 * @param previousTokenType The last-token value of the line just before
519 * <code>line</code>.
520 * @return The last line that needs repainting.
521 */
522 private int updateLastTokensBelow(int line, int numLines, int previousTokenType) {
523
524 int firstLine = line;
525
526 // Loop through all lines past our starting point. Update even the last
527 // line's info, even though there aren't any lines after it that depend
528 // on it changing for them to be changed, as its state may be used
529 // elsewhere in the library.
530 int end = numLines;
531 //System.err.println("--- end==" + end + " (numLines==" + numLines + ")");
532 while (line<end) {
533
534 setSharedSegment(line); // Sets s's text to that of line 'line' in the document.
535
536 int oldTokenType = lastTokensOnLines.get(line);
537 int newTokenType = tokenMaker.getLastTokenTypeOnLine(s, previousTokenType);
538 //System.err.println("---------------- line " + line + "; oldTokenType==" + oldTokenType + ", newTokenType==" + newTokenType + ", s=='" + s + "'");
539
540 // If this line's end-token value didn't change, stop here. Note
541 // that we're saying this line needs repainting; this is because
542 // the beginning of this line did indeed change color, but the
543 // end didn't.
544 if (oldTokenType==newTokenType) {
545 //System.err.println("... ... ... repainting lines " + firstLine + "-" + line);
546 fireChangedUpdate(new DefaultDocumentEvent(firstLine, line, DocumentEvent.EventType.CHANGE));
547 return line;
548 }
549
550 // If the line's end-token value did change, update it and
551 // keep going.
552 // NOTE: "setUnsafe" is okay here as the bounds checking was
553 // already done in lastTokensOnLines.get(line) above.
554 lastTokensOnLines.setUnsafe(line, newTokenType);
555 previousTokenType = newTokenType;
556 line++;
557
558 } // End of while (line<numLines).
559
560 // If any lines had their token types changed, fire a changed update
561 // for them. The view will repaint the area covered by the lines.
562 // FIXME: We currently cheat and send the line range that needs to be
563 // repainted as the "offset and length" of the change, since this is
564 // what the view needs. We really should send the actual offset and
565 // length.
566 if (line>firstLine) {
567 //System.err.println("... ... ... repainting lines " + firstLine + "-" + line);
568 fireChangedUpdate(new DefaultDocumentEvent(firstLine, line,
569 DocumentEvent.EventType.CHANGE));
570 }
571
572 return line;
573
574 }
575
576
577 /**
578 * Updates internal state information; e.g. the "last tokens on lines"
579 * data. After this, a changed update is fired to let listeners know that
580 * the document's structure has changed.<p>
581 *
582 * This is called internally whenever the syntax style changes.
583 */
584 protected void updateSyntaxHighlightingInformation() {
585
586 // Reinitialize the "last token on each line" array. Note that since
587 // the actual text in the document isn't changing, the number of lines
588 // is the same.
589 Element map = getDefaultRootElement();
590 int numLines = map.getElementCount();
591 int lastTokenType = Token.NULL;
592 for (int i=0; i<numLines; i++) {
593 setSharedSegment(i);
594 lastTokenType = tokenMaker.getLastTokenTypeOnLine(s, lastTokenType);
595 lastTokensOnLines.set(i, lastTokenType);
596 }
597
598 // Let everybody know that syntax styles have (probably) changed.
599 fireChangedUpdate(new DefaultDocumentEvent(
600 0, numLines-1, DocumentEvent.EventType.CHANGE));
601
602 }
603
604
605 /**
606 * Overridden for custom serialization purposes.
607 *
608 * @param out The stream to write to.
609 * @throws IOException If an IO error occurs.
610 */
611 private void writeObject(ObjectOutputStream out)throws IOException {
612 out.defaultWriteObject();
613 out.writeBoolean(isWhitespaceVisible());
614 }
615
616
617 /**
618 * Document content that provides fast access to individual characters.
619 *
620 * @author Robert Futrell
621 * @version 1.0
622 */
623 private static class RGapContent extends GapContent {
624
625 public RGapContent() {
626 }
627
628 public char charAt(int offset) throws BadLocationException {
629 if (offset<0 || offset>=length()) {
630 throw new BadLocationException("Invalid offset", offset);
631 }
632 int g0 = getGapStart();
633 char[] array = (char[]) getArray();
634 if (offset<g0) { // below gap
635 return array[offset];
636 }
637 return array[getGapEnd() + offset - g0]; // above gap
638 }
639
640 }
641
642
643}
Note: See TracBrowser for help on using the repository browser.