/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 ro.nextreports.designer.ui.sqleditor.syntax;
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 javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
import javax.swing.text.Segment;
import javax.swing.undo.UndoManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @author Decebal Suiu
*/
public class SyntaxDocument extends PlainDocument {
private static final Log LOG = LogFactory.getLog(SyntaxDocument.class);
protected Lexer lexer;
protected List<Token> tokens;
protected UndoManager undo = new CompoundUndoManager();
public SyntaxDocument(Lexer lexer) {
super();
putProperty(PlainDocument.tabSizeAttribute, 4); // outside ?!
this.lexer = lexer;
// Listen for undo and redo events
addUndoableEditListener(new UndoableEditListener() {
public void undoableEditHappened(UndoableEditEvent event) {
if (event.getEdit().isSignificant()) {
undo.addEdit(event.getEdit());
}
}
});
}
@Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
super.insertString(offs, str, a);
parse();
}
@Override
public void remove(int offs, int len) throws BadLocationException {
super.remove(offs, len);
parse();
}
@Override
public void replace(int offset, int length, String text, AttributeSet attrs)
throws BadLocationException {
super.replace(offset, length, text, attrs);
parse();
}
/**
* 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 token = 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.start + t.length))) {
token = t;
}
} else {
token = tokens.get(ndx);
}
return token;
}
/**
* 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();
}
}
/**
* This will discard all undoable edits
*/
public void clearUndos() {
undo.discardAllEdits();
}
/**
* Return an iterator of tokens between p0 and p1.
*
* @param start
* @param end
* @return
*/
protected Iterator<Token> getTokens(int start, int end) {
return new TokenIterator(start, end);
}
/**
* 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> tokens = new ArrayList<Token>(getLength() / 10);
long time = System.nanoTime();
int length = getLength();
try {
Segment segment = new Segment();
getText(0, getLength(), segment);
CharArrayReader reader = new CharArrayReader(segment.array, segment.offset, segment.count);
lexer.yyreset(reader);
Token token;
while ((token = lexer.yylex()) != null) {
tokens.add(token);
}
} catch (BadLocationException e) {
LOG.error(e.getMessage(), e);
} catch (IOException e) {
// This will not be thrown from the Lexer
LOG.error(e.getMessage(), e);
} finally {
// Benchmarks:
// Parsed 574038 chars in 81 ms, giving 74584 tokens
// System.out.printf("Parsed %d in %d ms, giving %d tokens",
// len, (System.nanoTime() - ts) / 1000000, toks.size());
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Parsed %d in %d ms, giving %d tokens",
length, (System.nanoTime() - time) / 1000000, tokens.size()));
}
this.tokens = tokens;
}
}
/**
* This class is used to iterate over tokens between two positions.
*/
class TokenIterator implements Iterator<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.start + t.length <= start) {
ndx++;
}
}
}
}
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;
}
public Token next() {
return tokens.get(ndx++);
}
public void remove() {
throw new UnsupportedOperationException("Not supported yet.");
}
}
}