/* * 10/16/2004 * * RSyntaxDocument.java - A document capable of syntax highlighting, used by * RSyntaxTextArea. * Copyright (C) 2004 Robert Futrell * robert_futrell at users.sourceforge.net * http://fifesoft.com/rsyntaxtextarea * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. */ package org.fife.ui.rsyntaxtextarea; import java.awt.event.ActionEvent; import java.io.IOException; import java.io.ObjectInputStream; import javax.swing.Action; import javax.swing.event.*; import javax.swing.text.*; import org.fife.ui.rsyntaxtextarea.modes.AbstractMarkupTokenMaker; import org.fife.util.DynamicIntArray; /** * The document used by {@link org.fife.ui.rsyntaxtextarea.RSyntaxTextArea}. This document is like * <code>javax.swing.text.PlainDocument</code> except that it also keeps track of syntax highlighting in the document. * It has a "style" attribute associated with it that determines how syntax highlighting is done (i.e., what language is * being highlighted). * <p> * * Instances of <code>RSyntaxTextArea</code> will only accept instances of <code>RSyntaxDocument</code>, since it is * this document that keeps track of syntax highlighting. All others will cause an exception to be thrown. * <p> * * To change the language being syntax highlighted at any time, you merely have to call {@link #setSyntaxStyle}. Other * than that, this document can be treated like any other save one caveat: all <code>DocumentEvent</code>s of type * <code>CHANGE</code> use their offset and length values to represent the first and last lines, respectively, that have * had their syntax coloring change. This is really a hack to increase the speed of the painting code and should really * be corrected, but oh well. * * @author Robert Futrell * @version 0.1 */ public class RSyntaxDocument extends PlainDocument implements SyntaxConstants { /** * Creates a {@link TokenMaker} appropriate for a given programming language. */ private transient TokenMakerFactory tokenMakerFactory; /** * Splits text into tokens for the current programming language. */ private TokenMaker tokenMaker; /** * Array of values representing the "last token type" on each line. This is used in cases such as multiline * comments: if the previous line ended with an (unclosed) multiline comment, we can use this knowledge and start * the current line's syntax highlighting in multiline comment state. */ protected DynamicIntArray lastTokensOnLines; private transient Segment s; /** * Constructs a plain text document. A default root element is created, and the tab size set to 5. * * @param syntaxStyle * The syntax highlighting scheme to use. */ public RSyntaxDocument(String syntaxStyle) { this(null, syntaxStyle); } /** * Constructs a plain text document. A default root element is created, and the tab size set to 5. * * @param tmf * The <code>TokenMakerFactory</code> for this document. If this is <code>null</code>, a default factory * is used. * @param syntaxStyle * The syntax highlighting scheme to use. */ public RSyntaxDocument(TokenMakerFactory tmf, String syntaxStyle) { super(new RGapContent()); putProperty(tabSizeAttribute, new Integer(5)); lastTokensOnLines = new DynamicIntArray(400); lastTokensOnLines.add(Token.NULL); // Initial (empty) line. s = new Segment(); setTokenMakerFactory(tmf); setSyntaxStyle(syntaxStyle); } /** * Returns the character in the document at the specified offset. * * @param offset * The offset of the character. * @return The character. * @throws BadLocationException * If the offset is invalid. */ public char charAt(int offset) throws BadLocationException { return ((RGapContent) getContent()).charAt(offset); } /** * Alerts all listeners to this document of an insertion. This is overridden so we can update our syntax * highlighting stuff. * <p> * The syntax highlighting stuff has to be here instead of in <code>insertUpdate</code> because * <code>insertUpdate</code> is not called by the undo/redo actions, but this method is. * * @param e * The change. */ protected void fireInsertUpdate(DocumentEvent e) { /* * Now that the text is actually inserted into the content and element structure, we can update our token * elements and "last tokens on lines" structure. */ Element lineMap = getDefaultRootElement(); DocumentEvent.ElementChange change = e.getChange(lineMap); Element[] added = change == null ? null : change.getChildrenAdded(); int numLines = lineMap.getElementCount(); int line = lineMap.getElementIndex(e.getOffset()); int previousLine = line - 1; int previousTokenType = (previousLine > -1 ? lastTokensOnLines.get(previousLine) : Token.NULL); // If entire lines were added... if (added != null && added.length > 0) { Element[] removed = change.getChildrenRemoved(); int numRemoved = removed != null ? removed.length : 0; int endBefore = line + added.length - numRemoved; // System.err.println("... adding lines: " + line + " - " + (endBefore-1)); // System.err.println("... ... added: " + added.length + ", removed:" + numRemoved); for (int i = line; i < endBefore; i++) { setSharedSegment(i); // Loads line i's text into s. int tokenType = tokenMaker.getLastTokenTypeOnLine(s, previousTokenType); lastTokensOnLines.add(i, tokenType); // System.err.println("--------- lastTokensOnLines.size() == " + lastTokensOnLines.getSize()); previousTokenType = tokenType; } // End of for (int i=line; i<endBefore; i++). // Update last tokens for lines below until they stop changing. updateLastTokensBelow(endBefore, numLines, previousTokenType); } // End of if (added!=null && added.length>0). // Otherwise, text was inserted on a single line... else { // Update last tokens for lines below until they stop changing. updateLastTokensBelow(line, numLines, previousTokenType); } // End of else. // Let all listeners know about the insertion. super.fireInsertUpdate(e); } /** * This method is called AFTER the content has been inserted into the document and the element structure has been * updated. * <p> * The syntax-highlighting updates need to be done here (as opposed to an override of <code>postRemoveUpdate</code>) * as this method is called in response to undo/redo events, whereas <code>postRemoveUpdate</code> is not. * <p> * Now that the text is actually inserted into the content and element structure, we can update our token elements * and "last tokens on lines" structure. * * @param chng * The change that occurred. * @see #removeUpdate */ protected void fireRemoveUpdate(DocumentEvent chng) { Element lineMap = getDefaultRootElement(); int numLines = lineMap.getElementCount(); DocumentEvent.ElementChange change = chng.getChange(lineMap); Element[] removed = change == null ? null : change.getChildrenRemoved(); // If entire lines were removed... if (removed != null && removed.length > 0) { int line = change.getIndex(); // First line entirely removed. int previousLine = line - 1; // Line before that. int previousTokenType = (previousLine > -1 ? lastTokensOnLines.get(previousLine) : Token.NULL); Element[] added = change.getChildrenAdded(); int numAdded = added == null ? 0 : added.length; // Remove the cached last-token values for the removed lines. int endBefore = line + removed.length - numAdded; // System.err.println("... removing lines: " + line + " - " + (endBefore-1)); // System.err.println("... added: " + numAdded + ", removed: " + removed.length); lastTokensOnLines.removeRange(line, endBefore); // Removing values for lines [line-(endBefore-1)]. // System.err.println("--------- lastTokensOnLines.size() == " + lastTokensOnLines.getSize()); // Update last tokens for lines below until they've stopped changing. updateLastTokensBelow(line, numLines, previousTokenType); } // End of if (removed!=null && removed.size()>0). // Otherwise, text was removed from just one line... else { int line = lineMap.getElementIndex(chng.getOffset()); if (line >= lastTokensOnLines.getSize()) return; // If we're editing the last line in a document... int previousLine = line - 1; int previousTokenType = (previousLine > -1 ? lastTokensOnLines.get(previousLine) : Token.NULL); // System.err.println("previousTokenType for line : " + previousLine + " is " + previousTokenType); // Update last tokens for lines below until they've stopped changing. updateLastTokensBelow(line, numLines, previousTokenType); } // Let all of our listeners know about the removal. super.fireRemoveUpdate(chng); } /** * Returns whether closing markup tags should be automatically completed. This method only returns <code>true</code> * if {@link #getLanguageIsMarkup()} also returns <code>true</code>. * * @return Whether markup closing tags should be automatically completed. * @see #getLanguageIsMarkup() */ public boolean getCompleteMarkupCloseTags() { // TODO: Remove terrible dependency on AbstractMarkupTokenMaker return getLanguageIsMarkup() && ((AbstractMarkupTokenMaker) tokenMaker).getCompleteCloseTags(); } /** * Returns whether the current programming language uses curly braces ('<tt>{</tt>' and '<tt>}</tt>') to denote code * blocks. * * @return Whether curly braces denote code blocks. */ public boolean getCurlyBracesDenoteCodeBlocks() { return tokenMaker.getCurlyBracesDenoteCodeBlocks(); } /** * Returns whether the current language is a markup language, such as HTML, XML or PHP. * * @return Whether the current language is a markup language. */ public boolean getLanguageIsMarkup() { return tokenMaker.isMarkupLanguage(); } /** * Returns the token type of the last token on the given line. * * @param line * The line to inspect. * @return The token type of the last token on the specified line. If the line is invalid, an exception is thrown. */ public int getLastTokenTypeOnLine(int line) { return lastTokensOnLines.get(line); } /** * Returns the text to place at the beginning and end of a line to "comment" it in the current programming language. * * @return The start and end strings to add to a line to "comment" it out. A <code>null</code> value for either * means there is no string to add for that part. A value of <code>null</code> for the array means this * language does not support commenting/uncommenting lines. */ public String[] getLineCommentStartAndEnd() { return tokenMaker.getLineCommentStartAndEnd(); } /** * Returns whether tokens of the specified type should have "mark occurrences" enabled for the current programming * language. * * @param type * The token type. * @return Whether tokens of this type should have "mark occurrences" enabled. */ boolean getMarkOccurrencesOfTokenType(int type) { return tokenMaker.getMarkOccurrencesOfTokenType(type); } /** * This method returns whether auto indentation should be done if Enter is pressed at the end of the specified line. * * @param line * The line to check. * @return Whether an extra indentation should be done. */ public boolean getShouldIndentNextLine(int line) { Token t = getTokenListForLine(line); t = t.getLastNonCommentNonWhitespaceToken(); return tokenMaker.getShouldIndentNextLineAfter(t); } /** * Returns a token list for the specified segment of text representing the specified line number. This method is * basically a wrapper for <code>tokenMaker.getTokenList</code> that takes into account the last token on the * previous line to assure token accuracy. * * @param line * The line number of <code>text</code> in the document, >= 0. * @return A token list representing the specified line. */ public final Token getTokenListForLine(int line) { Element map = getDefaultRootElement(); Element elem = map.getElement(line); int startOffset = elem.getStartOffset(); // int endOffset = (line==map.getElementCount()-1 ? elem.getEndOffset() - 1: // elem.getEndOffset() - 1); int endOffset = elem.getEndOffset() - 1; // Why always "-1"? try { getText(startOffset, endOffset - startOffset, s); } catch (BadLocationException ble) { ble.printStackTrace(); return null; } int initialTokenType = line == 0 ? Token.NULL : getLastTokenTypeOnLine(line - 1); return tokenMaker.getTokenList(s, initialTokenType, startOffset); } boolean insertBreakSpecialHandling(ActionEvent e) { Action a = tokenMaker.getInsertBreakAction(); if (a != null) { a.actionPerformed(e); return true; } return false; } /** * Deserializes a document. * * @param s * The stream to read from. * @throws ClassNotFoundException * @throws IOException */ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { s.defaultReadObject(); this.s = new Segment(); } /** * Makes our private <code>Segment s</code> point to the text in our document referenced by the specified element. * Note that <code>line</code> MUST be a valid line number in the document. * * @param line * The line number you want to get. */ private final void setSharedSegment(int line) { Element map = getDefaultRootElement(); // int numLines = map.getElementCount(); Element element = map.getElement(line); if (element == null) throw new InternalError("Invalid line number: " + line); int startOffset = element.getStartOffset(); // int endOffset = (line==numLines-1 ? // element.getEndOffset()-1 : element.getEndOffset() - 1); int endOffset = element.getEndOffset() - 1; // Why always "-1"? try { getText(startOffset, endOffset - startOffset, s); } catch (BadLocationException ble) { throw new InternalError("Text range not in document: " + startOffset + "-" + endOffset); } } /** * Sets the syntax style being used for syntax highlighting in this document. What styles are supported by a * document is determined by its {@link TokenMakerFactory}. By default, all <code>RSyntaxDocument</code>s support * all languages built into <code>RSyntaxTextArea</code>. * * @param styleKey * The new style to use, such as {@link SyntaxConstants#SYNTAX_STYLE_JAVA}. If this style is not known or * supported by this document, then {@link SyntaxConstants#SYNTAX_STYLE_NONE} is used. */ public void setSyntaxStyle(String styleKey) { tokenMaker = tokenMakerFactory.getTokenMaker(styleKey); updateSyntaxHighlightingInformation(); } /** * Sets the syntax style being used for syntax highlighting in this document. You should call this method if you've * created a custom token maker for a language not normally supported by <code>RSyntaxTextArea</code>. * * @param tokenMaker * The new token maker to use. */ public void setSyntaxStyle(TokenMaker tokenMaker) { this.tokenMaker = tokenMaker; updateSyntaxHighlightingInformation(); } /** * Sets the token maker factory used by this document. * * @param tmf * The <code>TokenMakerFactory</code> for this document. If this is <code>null</code>, a default factory * is used. */ public void setTokenMakerFactory(TokenMakerFactory tmf) { tokenMakerFactory = tmf != null ? tmf : TokenMakerFactory.getDefaultInstance(); } /** * Sets whether whitespace is visible. This property is actually setting whether the tokens generated from this * document "paint" something when they represent whitespace. * * @param visible * Whether whitespace should be visible. */ public void setWhitespaceVisible(boolean visible, RSyntaxTextArea textArea) { tokenMaker.setWhitespaceVisible(visible, textArea); } /** * Loops through the last-tokens-on-lines array from a specified point onward, updating last-token values until they * stop changing. This should be called when lines are updated/inserted/removed, as doing so may cause lines below * to change color. * * @param line * The first line to check for a change in last-token value. * @param numLines * The number of lines in the document. * @param previousTokenType * The last-token value of the line just before <code>line</code>. * @return The last line that needs repainting. */ private int updateLastTokensBelow(int line, int numLines, int previousTokenType) { int firstLine = line; // Loop through all lines past our starting point. Update even the last // line's info, even though there aren't any lines after it that depend // on it changing for them to be changed, as its state may be used // elsewhere in the library. int end = numLines; // System.err.println("--- end==" + end + " (numLines==" + numLines + ")"); while (line < end) { setSharedSegment(line); // Sets s's text to that of line 'line' in the document. int oldTokenType = lastTokensOnLines.get(line); int newTokenType = tokenMaker.getLastTokenTypeOnLine(s, previousTokenType); // System.err.println("---------------- line " + line + "; oldTokenType==" + oldTokenType + // ", newTokenType==" + newTokenType + ", s=='" + s + "'"); // If this line's end-token value didn't change, stop here. Note // that we're saying this line needs repainting; this is because // the beginning of this line did indeed change color, but the // end didn't. if (oldTokenType == newTokenType) { // System.err.println("... ... ... repainting lines " + firstLine + "-" + line); fireChangedUpdate(new DefaultDocumentEvent(firstLine, line, DocumentEvent.EventType.CHANGE)); return line; } // If the line's end-token value did change, update it and // keep going. // NOTE: "setUnsafe" is okay here as the bounds checking was // already done in lastTokensOnLines.get(line) above. lastTokensOnLines.setUnsafe(line, newTokenType); previousTokenType = newTokenType; line++; } // End of while (line<numLines). // If any lines had their token types changed, fire a changed update // for them. The view will repaint the area covered by the lines. // FIXME: We currently cheat and send the line range that needs to be // repainted as the "offset and length" of the change, since this is // what the view needs. We really should send the actual offset and // length. if (line > firstLine) { // System.err.println("... ... ... repainting lines " + firstLine + "-" + line); fireChangedUpdate(new DefaultDocumentEvent(firstLine, line, DocumentEvent.EventType.CHANGE)); } return line; } /** * Updates internal state information; e.g. the "last tokens on lines" data. After this, a changed update is fired * to let listeners know that the document's structure has changed. * <p> * * This is called internally whenever the syntax style changes. */ protected void updateSyntaxHighlightingInformation() { // Reinitialize the "last token on each line" array. Note that since // the actual text in the document isn't changing, the number of lines // is the same. Element map = getDefaultRootElement(); int numLines = map.getElementCount(); int lastTokenType = Token.NULL; for (int i = 0; i < numLines; i++) { setSharedSegment(i); lastTokenType = tokenMaker.getLastTokenTypeOnLine(s, lastTokenType); lastTokensOnLines.set(i, lastTokenType); } // Let everybody know that syntax styles have (probably) changed. fireChangedUpdate(new DefaultDocumentEvent( 0, numLines - 1, DocumentEvent.EventType.CHANGE)); } /** * Document content that provides access to individual characters. * * @author Robert Futrell * @version 1.0 */ private static class RGapContent extends GapContent { public RGapContent() { } public char charAt(int offset) throws BadLocationException { if (offset < 0 || offset >= length()) { throw new BadLocationException("Invalid offset", offset); } int g0 = getGapStart(); char[] array = (char[]) getArray(); if (offset < g0) { // below gap return array[offset]; } return array[getGapEnd() + offset - g0]; // above gap } } }