/* * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License * at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jsyntaxpane; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.event.DocumentEvent; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Element; import javax.swing.text.PlainDocument; import javax.swing.text.Segment; /** * A document that supports being highlighted. The document maintains an * internal List of all the Tokens. The Tokens are updated using * a Lexer, passed to it during construction. * * @author Ayman Al-Sairafi */ public class SyntaxDocument extends PlainDocument { Lexer lexer; List<Token> tokens; CompoundUndoMan undo; public SyntaxDocument(Lexer lexer) { super(); putProperty(PlainDocument.tabSizeAttribute, 4); this.lexer = lexer; // Listen for undo and redo events undo = new CompoundUndoMan(this); } /** * Parse the entire document and return list of tokens that do not already * exist in the tokens list. There may be overlaps, and replacements, * which we will cleanup later. * @return list of tokens that do not exist in the tokens field */ private void parse() { // if we have no lexer, then we must have no tokens... if (lexer == null) { tokens = null; return; } List<Token> toks = new ArrayList<Token>(getLength() / 10); long ts = System.nanoTime(); int len = getLength(); try { Segment seg = new Segment(); getText(0, getLength(), seg); lexer.parse(seg, 0, toks); } catch (BadLocationException ex) { log.log(Level.SEVERE, null, ex); } finally { if (log.isLoggable(Level.FINEST)) { log.finest(String.format("Parsed %d in %d ms, giving %d tokens\n", len, (System.nanoTime() - ts) / 1000000, toks.size())); } tokens = toks; } } @Override protected void fireChangedUpdate(DocumentEvent e) { parse(); super.fireChangedUpdate(e); } @Override protected void fireInsertUpdate(DocumentEvent e) { parse(); super.fireInsertUpdate(e); } @Override protected void fireRemoveUpdate(DocumentEvent e) { parse(); super.fireRemoveUpdate(e); } /** * Replace the token with the replacement string * @param token * @param replacement */ public void replaceToken(Token token, String replacement) { try { replace(token.start, token.length, replacement, null); } catch (BadLocationException ex) { log.log(Level.WARNING, "unable to replace token: " + token, ex); } } /** * This class is used to iterate over tokens between two positions * */ class TokenIterator implements ListIterator<Token> { int start; int end; int ndx = 0; @SuppressWarnings("unchecked") private TokenIterator(int start, int end) { this.start = start; this.end = end; if (tokens != null && !tokens.isEmpty()) { Token token = new Token(TokenType.COMMENT, start, end - start); ndx = Collections.binarySearch((List) tokens, token); // we will probably not find the exact token... if (ndx < 0) { // so, start from one before the token where we should be... // -1 to get the location, and another -1 to go back.. ndx = (-ndx - 1 - 1 < 0) ? 0 : (-ndx - 1 - 1); Token t = tokens.get(ndx); // if the prev token does not overlap, then advance one if (t.end() <= start) { ndx++; } } } } @Override public boolean hasNext() { if (tokens == null) { return false; } if (ndx >= tokens.size()) { return false; } Token t = tokens.get(ndx); if (t.start >= end) { return false; } return true; } @Override public Token next() { return tokens.get(ndx++); } @Override public void remove() { throw new UnsupportedOperationException(); } @Override public boolean hasPrevious() { if (tokens == null) { return false; } if (ndx <= 0) { return false; } Token t = tokens.get(ndx); if (t.end() <= start) { return false; } return true; } @Override public Token previous() { return tokens.get(ndx--); } @Override public int nextIndex() { return ndx + 1; } @Override public int previousIndex() { return ndx - 1; } @Override public void set(Token e) { throw new UnsupportedOperationException(); } @Override public void add(Token e) { throw new UnsupportedOperationException(); } } /** * Return an iterator of tokens between p0 and p1. * @param start start position for getting tokens * @param end position for last token * @return Iterator for tokens that overal with range from start to end */ public Iterator<Token> getTokens(int start, int end) { return new TokenIterator(start, end); } /** * Find the token at a given position. May return null if no token is * found (whitespace skipped) or if the position is out of range: * @param pos * @return */ public Token getTokenAt(int pos) { if (tokens == null || tokens.isEmpty() || pos > getLength()) { return null; } Token tok = null; Token tKey = new Token(TokenType.DEFAULT, pos, 1); @SuppressWarnings("unchecked") int ndx = Collections.binarySearch((List) tokens, tKey); if (ndx < 0) { // so, start from one before the token where we should be... // -1 to get the location, and another -1 to go back.. ndx = (-ndx - 1 - 1 < 0) ? 0 : (-ndx - 1 - 1); Token t = tokens.get(ndx); if ((t.start <= pos) && (pos <= t.end())) { tok = t; } } else { tok = tokens.get(ndx); } return tok; } public Token getWordAt(int offs, Pattern p) { Token word = null; try { Element line = getParagraphElement(offs); if (line == null) { return word; } int lineStart = line.getStartOffset(); int lineEnd = Math.min(line.getEndOffset(), getLength()); Segment seg = new Segment(); getText(lineStart, lineEnd - lineStart, seg); if (seg.count > 0) { // we need to get the word using the words pattern p Matcher m = p.matcher(seg); int o = offs - lineStart; while (m.find()) { if (m.start() <= o && o <= m.end()) { word = new Token(TokenType.DEFAULT, m.start() + lineStart, m.end() - m.start()); break; } } } } catch (BadLocationException ex) { Logger.getLogger(SyntaxDocument.class.getName()).log(Level.SEVERE, null, ex); } finally { return word; } } /** * Return the token following the current token, or null * <b>This is an expensive operation, so do not use it to update the gui</b> * @param tok * @return */ public Token getNextToken(Token tok) { int n = tokens.indexOf(tok); if ((n >= 0) && (n < (tokens.size() - 1))) { return tokens.get(n + 1); } else { return null; } } /** * Return the token prior to the given token, or null * <b>This is an expensive operation, so do not use it to update the gui</b> * @param tok * @return */ public Token getPrevToken(Token tok) { int n = tokens.indexOf(tok); if ((n > 0) && (!tokens.isEmpty())) { return tokens.get(n - 1); } else { return null; } } /** * This is used to return the other part of a paired token in the document. * A paired part has token.pairValue <> 0, and the paired token will * have the negative of t.pairValue. * This method properly handles nestings of same pairValues, but overlaps * are not checked. * if The document does not contain a paired token, then null is returned. * @param t * @return the other pair's token, or null if nothing is found. */ public Token getPairFor(Token t) { if (t == null || t.pairValue == 0) { return null; } Token p = null; int ndx = tokens.indexOf(t); // w will be similar to a stack. The openners weght is added to it // and the closers are subtracted from it (closers are already negative) int w = t.pairValue; int direction = (t.pairValue > 0) ? 1 : -1; boolean done = false; int v = Math.abs(t.pairValue); while (!done) { ndx += direction; if (ndx < 0 || ndx >= tokens.size()) { break; } Token current = tokens.get(ndx); if (Math.abs(current.pairValue) == v) { w += current.pairValue; if (w == 0) { p = current; done = true; } } } return p; } /** * Perform an undo action, if possible */ public void doUndo() { if (undo.canUndo()) { undo.undo(); parse(); } } /** * Perform a redo action, if possible. */ public void doRedo() { if (undo.canRedo()) { undo.redo(); parse(); } } /** * Return a matcher that matches the given pattern on the entire document * @param pattern * @return matcher object */ public Matcher getMatcher(Pattern pattern) { return getMatcher(pattern, 0, getLength()); } /** * Return a matcher that matches the given pattern in the part of the * document starting at offset start. Note that the matcher will have * offset starting from <code>start</code> * * @param pattern * @param start * @return matcher that <b>MUST</b> be offset by start to get the proper * location within the document */ public Matcher getMatcher(Pattern pattern, int start) { return getMatcher(pattern, start, getLength() - start); } /** * Return a matcher that matches the given pattern in the part of the * document starting at offset start and ending at start + length. * Note that the matcher will have * offset starting from <code>start</code> * * @param pattern * @param start * @param length * @return matcher that <b>MUST</b> be offset by start to get the proper * location within the document */ public Matcher getMatcher(Pattern pattern, int start, int length) { Matcher matcher = null; if (getLength() == 0) { return null; } if (start >= getLength()) { return null; } try { if (start < 0) { start = 0; } if (start + length > getLength()) { length = getLength() - start; } Segment seg = new Segment(); getText(start, length, seg); matcher = pattern.matcher(seg); } catch (BadLocationException ex) { log.log(Level.SEVERE, "Requested offset: " + ex.offsetRequested(), ex); } return matcher; } /** * This will discard all undoable edits */ public void clearUndos() { undo.discardAllEdits(); } /** * Gets the line at given position. The line returned will NOT include * the line terminator '\n' * @param pos Position (usually from text.getCaretPosition() * @return the STring of text at given position * @throws BadLocationException */ public String getLineAt(int pos) throws BadLocationException { Element e = getParagraphElement(pos); Segment seg = new Segment(); getText(e.getStartOffset(), e.getEndOffset() - e.getStartOffset(), seg); char last = seg.last(); if (last == '\n' || last == '\r') { seg.count--; } return seg.toString(); } /** * Deletes the line at given position * @param pos * @throws javax.swing.text.BadLocationException */ public void removeLineAt(int pos) throws BadLocationException { Element e = getParagraphElement(pos); remove(e.getStartOffset(), getElementLength(e)); } /** * Replace the line at given position with the given string, which can span * multiple lines * @param pos * @param newLines * @throws javax.swing.text.BadLocationException */ public void replaceLineAt(int pos, String newLines) throws BadLocationException { Element e = getParagraphElement(pos); replace(e.getStartOffset(), getElementLength(e), newLines, null); } /** * Helper method to get the length of an element and avoid getting * a too long element at the end of the document * @param e * @return */ private int getElementLength(Element e) { int end = e.getEndOffset(); if (end >= (getLength() - 1)) { end--; } return end - e.getStartOffset(); } /** * Gets the text without the comments. For example for the string * <code>{ // it's a comment</code> this method will return "{ ". * @param aStart start of the text. * @param anEnd end of the text. * @return String for the line without comments (if exists). */ public synchronized String getUncommentedText(int aStart, int anEnd) { readLock(); StringBuilder result = new StringBuilder(); Iterator<Token> iter = getTokens(aStart, anEnd); while (iter.hasNext()) { Token t = iter.next(); if (!TokenType.isComment(t)) { result.append(t.getText(this)); } } readUnlock(); return result.toString(); } /** * Returns the starting position of the line at pos * @param pos * @return starting position of the line */ public int getLineStartOffset(int pos) { return getParagraphElement(pos).getStartOffset(); } /** * Returns the end position of the line at pos. * Does a bounds check to ensure the returned value does not exceed * document length * @param pos * @return */ public int getLineEndOffset(int pos) { int end = 0; end = getParagraphElement(pos).getEndOffset(); if (end >= getLength()) { end = getLength(); } return end; } /** * Return the number of lines in this document * @return */ public int getLineCount() { Element e = getDefaultRootElement(); int cnt = e.getElementCount(); return cnt; } /** * Return the line number at given position. The line numbers are zero based * @param pos * @return */ public int getLineNumberAt(int pos) { int lineNr = getDefaultRootElement().getElementIndex(pos); return lineNr; } @Override public String toString() { return "SyntaxDocument(" + lexer + ", " + ((tokens == null) ? 0 : tokens.size()) + " tokens)@" + hashCode(); } /** * We override this here so that the replace is treated as one operation * by the undomanager * @param offset * @param length * @param text * @param attrs * @throws BadLocationException */ @Override public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException { remove(offset, length); undo.startCombine(); insertString(offset, text, attrs); } /** * Append the given string to the text of this document. * @param str * @return this document */ public SyntaxDocument append(String str) { try { insertString(getLength(), str, null); } catch (BadLocationException ex) { log.log(Level.WARNING, "Error appending str", ex); } return this; } // our logger instance... private static final Logger log = Logger.getLogger(SyntaxDocument.class.getName()); }