/* * 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.io.CharArrayReader; import java.io.IOException; 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.event.UndoableEditEvent; import javax.swing.event.UndoableEditListener; import javax.swing.text.BadLocationException; import javax.swing.text.Element; import javax.swing.text.PlainDocument; import javax.swing.text.Segment; import javax.swing.undo.UndoManager; /** * 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; UndoManager undo = new CompoundUndoManager(); public SyntaxDocument(Lexer lexer) { super(); putProperty(PlainDocument.tabSizeAttribute, 4); this.lexer = lexer; // Listen for undo and redo events addUndoableEditListener(new UndoableEditListener() { @Override public void undoableEditHappened(UndoableEditEvent evt) { if (evt.getEdit().isSignificant()) { undo.addEdit(evt.getEdit()); } } }); } /** * 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); CharArrayReader reader = new CharArrayReader(seg.array, seg.offset, seg.count); lexer.yyreset(reader); Token token; while ((token = lexer.yylex()) != null) { toks.add(token); } } catch (BadLocationException ex) { log.log(Level.SEVERE, null, ex); } catch (IOException ex) { // This will not be thrown from the Lexer 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); } @Override protected void fireUndoableEditUpdate(UndoableEditEvent e) { parse(); super.fireUndoableEditUpdate(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(); } 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; } /** * 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 * @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(); } } public boolean canUndo(){ return undo.canUndo(); } public boolean canRedo(){ return undo.canRedo(); } /** * Find the location of the given String in the document. returns -1 * if the search string is not found starting at position <code>start</code> * @param search The String to search for * @param start The beginning index of search * @return * @deprecated use {@link getMatcher} instead */ @Deprecated public int getIndexOf(String search, int start) { int flag = Pattern.LITERAL; Pattern pattern = Pattern.compile(search, flag); return getIndexOf(pattern, start); } /** * Find the next position that matches <code>pattern</code> in the document. * returns -1 if the pattern is not found. * @param pattern the regex pattern to find * @param start The beginning index of search * @return * @deprecated use {@link getMatcher} instead */ @Deprecated public int getIndexOf(Pattern pattern, int start) { int ndx = -1; if (pattern == null || getLength() == 0) { return -1; } try { Segment segment = new Segment(); getText(start, getLength() - start, segment); Matcher m = pattern.matcher(segment); if (m.find()) { // remember that the index is relative to the document, so // always add the start position to it ndx = m.start() + start; } } catch (BadLocationException ex) { log.log(Level.SEVERE, null, ex); } return ndx; } /** * 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; } try { 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') { return seg.subSequence(0, seg.length() - 1).toString(); } 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.COMMENT != t.type && TokenType.COMMENT2 != t.type) { 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(); } // our logger instance... private static final Logger log = Logger.getLogger(SyntaxDocument.class.getName()); }