/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* CALDocument.java
* Creation date: (1/18/01 4:04:26 PM)
* By: Luke Evans
*/
package org.openquark.gems.client.caleditor;
import java.awt.Point;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.event.DocumentEvent;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import org.openquark.cal.compiler.CALMultiplexedLexer;
/**
* CALDocument is a document representing CAL source.
* Creation date: (1/18/01 4:04:26 PM)
* @author Luke Evans
*/
public class CALDocument extends javax.swing.text.PlainDocument {
private static final long serialVersionUID = -6481758112928802591L;
/**
* Keys to be used in AttributeSet's holding a value of Token.
*/
// Key to indicate a complex comment. i.e. the comment starts/ends
// part way through the line, or the line contains multiple comments.
// The value stored will be an object of type CCommentInfo.
static final Object CCommentAttribute = new AttributeKey();
// Key for attribute to indicate a line that is all comment. The stored
// value will be the same object as the key.
static final Object CommentAttribute = new AttributeKey();
static final Object TestCommentAttribute = new AttributeKey();
static class AttributeKey {
private AttributeKey() {
}
@Override
public String toString() {
return "comment";
}
}
static class CCommentInfo {
// Store a collection of Point objects.
// Each point will contain the start/end offset
// of a comment.
public Point[] comments;
public CCommentInfo(Point[] comments) {
this.comments = comments;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < comments.length; ++i) {
sb.append(comments[i] + " ");
}
return sb.toString();
}
}
/**
* The Scanner class is a wrapper around our standard CALLexer.
* This will be used to scan over portions of the text in the viewport
* (on a paint) when we need to ascertain what the lexemes are in order
* to decide on the style in which to paint them.
*
* The wrapper is necessary currently, as we have to recreate the lexer
* everytime as there appears to be no easy way to keep resetting the
* input stream. We can make this much ligherweight in future by NOT
* constructing a new CALLexer every time we're called upon to consider a
* new portion of the document. If this can happen, then we should make
* Scanner a subclass of CALScanner for cleanliness too.
*
* We also implement some local utility methods here. There's a nasty one
* amongst them, namely getLexerPos which converts between the CALLexer's
* token positions (given in line/column pairs) and character offsets from
* the position at which we started the lexer. This is a rather inefficient
* conversion as it requires fetching all the text buffer form the beginning
* of the lexer position to the end, and then counting all the characters
* up to the line/column position held in a token. This can only be achieved
* by stepping through a character at a time.
*
* getLexerPos is used by getStartOffset and getEndOffset to return
* text buffer offsets for a given token.
*
* Creation date: (1/23/2001 10:09:41 PM)
* @author Luke Evans
*/
public class Scanner {
// The CALMultiplexedLexer we will use. Currently containership rather than inheritance
// as we may need to recreate the lexer
private CALMultiplexedLexer lexer;
int p0;
Scanner() {
// TODOEL - the lexer is constructed with a null value for the compiler.
// This means that we will miss any compiler messages.
//super(new DocumentInputStream(0, getLength()));
lexer = new CALMultiplexedLexer(null, new DocumentReader(0, getLength()), null);
//scanComments = true;
}
/**
* Sets the range of the scanner. This should be called
* to reinitialize the scanner to the desired range of
* coverage.
*/
public void setRange(int p0, int p1) {
// TODOEL - the lexer is constructed with a null value for the compiler.
// This means that we will miss any compiler messages.
//useInputStream(new DocumentInputStream(p0, p1));
lexer = new CALMultiplexedLexer(null, new DocumentReader(p0, p1), null);
this.p0 = p0;
}
/**
* Get a buffer position based on a line/column tuple.
* We have to have this routine as we can't currently see an absolute buffer position
* from the lexer, but instead only line/col positions.
* Creation date: (1/22/01 5:57:55 PM)
* @param line int the line number
* @param column int the column number
* @return int the buffer position
*/
public int getLexerPos(int line, int column) {
// We start from p0 and count the number of characters in each line, then
// add the column number
int offset = 0;
String buffer = null;
try {
// Retrieve all the characters, from where the lexer's start point is, to
// the end of the document, in order to do the character counting
buffer = CALDocument.this.getText(p0, CALDocument.this.getLength() - p0);
} catch (javax.swing.text.BadLocationException e) {
IllegalStateException ise = new IllegalStateException();
ise.initCause(e);
throw ise;
}
int pos = -1;
final int bufSize = buffer.length();
// Line counts are 1 based!
for (; line > 1; line--) {
// Add all the characters in this line to offset
while (++pos < bufSize) {
offset++;
if (buffer.charAt(pos) == '\n') {
break;
}
}
}
//note the commented out line below doesn't work because of tabs!
//offset += column - 1;
final int tabSize = lexer.getTabSize();
//column counts are 1 based.
int currentColumnPos = 1;
while (++pos < bufSize && currentColumnPos < column) {
offset++;
if (buffer.charAt(pos) != '\t') {
currentColumnPos++;
} else {
//tabs can consume from 1 to tabSize columns (a tab character moves the column to the next tab stop)
currentColumnPos = (((currentColumnPos-1)/tabSize) + 1) * tabSize + 1;
}
}
// Return the full offset
return offset;
}
/**
* This fetches the starting location of the given
* token in the document.
*/
public final int getStartOffset(antlr.Token tok) {
// No offset if no token yet!
int begOffs = 0;
if (tok != null) {
begOffs = getLexerPos(tok.getLine(), tok.getColumn());
}
return p0 + begOffs;
}
/**
* This fetches the starting location of the current
* token in the document.
*/
public final int getStartOffset() {
antlr.Token tok = lexer.getTokenObject();
// Use the more general routine
return getStartOffset(tok);
}
/**
* This fetches the ending location of the current
* token in the document.
*/
public final int getEndOffset() {
antlr.Token tok = lexer.getTokenObject();
// No length if no token!
int tokLength = 0;
if (tok != null) {
String tokText = tok.getText();
// Some tokens (like EOF have null images)
if (tokText != null) {
// Get the length of this token
tokLength = tokText.length();
}
}
return getStartOffset(tok) + tokLength;
}
/**
* Scan over one token, indicate if successful
* Creation date: (1/22/01 1:55:15 PM)
* @return boolean true if not at end of document, false otherwise (and on error)
*/
public boolean scan() {
try {
if (lexer.nextToken().getType() != org.openquark.cal.compiler.CALTokenTypes.EOF) {
return true;
} else {
return false;
}
} catch (antlr.TokenStreamException e) {
// We failed to lex the input for some reason.
// No big deal, but we're not likely to be able to parse the
// code in this state, and may want to indicate this in the
// future to the owner
return false;
}
}
/**
* Return the CALToken for the given lexer position
* Creation date: (1/22/01 2:01:47 PM)
* @return CALToken the token including formatting directives
*/
public antlr.Token getToken() {
// Return the token from the lexer
return lexer.getTokenObject();
}
}
/**
* Class to provide Reader functionality from a portion of a
* Document.
*/
class DocumentReader extends java.io.Reader {
javax.swing.text.Segment segment;
int p1; // end position
int pos; // pos in document
int index; // index into array of the segment
public DocumentReader(int p0, int p1) {
this.segment = new javax.swing.text.Segment();
this.p1 = Math.min(getLength(), p1);
pos = p0;
try {
loadSegment(1024);
} catch (java.io.IOException ioe) {
throw new Error("unexpected: " + ioe);
}
}
/**
* {@inheritDoc}
*/
@Override
public int read() throws java.io.IOException {
int endIndex = index;
if (endIndex >= segment.offset + segment.count) {
if (pos >= p1) {
// no more data
return -1;
}
loadSegment(1024);
}
return segment.array[index++];
}
/**
* {@inheritDoc}
*/
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
int endIndex = index + len - 1;
final int availableLength;
if (endIndex >= segment.offset + segment.count) {
if (pos >= p1) {
// no more data
return -1;
}
int nCharsBuffered = loadSegment(1024 + len);
availableLength = Math.min(len, nCharsBuffered);
} else {
availableLength = len;
}
System.arraycopy(segment, index, cbuf, off, availableLength);
index += availableLength;
return availableLength;
}
int loadSegment(int nCharsToBuffer) throws java.io.IOException {
try {
nCharsToBuffer = Math.min(nCharsToBuffer, p1 - pos);
getText(pos, nCharsToBuffer, segment);
pos += nCharsToBuffer;
index = segment.offset;
return nCharsToBuffer;
} catch (javax.swing.text.BadLocationException e) {
throw new java.io.IOException("Bad location");
}
}
@Override
public void close() throws IOException {
// We do not need to close anything.
}
}
/**
* CALDocument constructor comment.
*/
public CALDocument() {
// Construct with a GapContent storage object
super(new javax.swing.text.GapContent(1024));
// Set the default tab size for CALDocuments to 4
putProperty("tabSize", Integer.valueOf(4));
}
/**
* CALDocument constructor comment.
* @param c javax.swing.text.AbstractDocument.Content
*/
protected CALDocument(javax.swing.text.AbstractDocument.Content c) {
super(c);
}
/**
* Create a lexical analyzer for this document.
*/
public Scanner createScanner() {
return new Scanner();
}
/**
* Fetch a reasonable location to start scanning
* given the desired start location. This allows
* for adjustments needed to accomodate multiline
* comments.
*/
public int getScannerStart(int p) {
javax.swing.text.Element elem = getDefaultRootElement();
int lineNum = elem.getElementIndex(p);
javax.swing.text.Element line = elem.getElement(lineNum);
javax.swing.text.AttributeSet a = line.getAttributes();
while (a.isDefined(CommentAttribute) && lineNum > 0) {
lineNum -= 1;
line = elem.getElement(lineNum);
a = line.getAttributes();
}
return line.getStartOffset();
}
/**
* Updates document structure as a result of text insertion. This
* will happen within a write lock. The superclass behavior of
* updating the line map is executed followed by marking any comment
* areas that should backtracked before scanning.
*
* @param chng the change event
* @param attr the set of attributes
*/
@Override
protected void insertUpdate(DefaultDocumentEvent chng, javax.swing.text.AttributeSet attr) {
//System.out.println ("before insertUpdate: " );
//checkAttributes ();
super.insertUpdate(chng, attr);
//System.out.println ("after insertUpdate: " );
//checkAttributes ();
int lastChangedLine = updateCommentMarks(chng);
//System.out.println ("lastChangedLine = " + lastChangedLine);
changeCommentLines(chng, lastChangedLine);
}
/**
* Updates any document structure as a result of text removal.
* This will happen within a write lock. The superclass behavior of
* updating the line map is executed followed by placing a lexical
* update command on the analyzer queue. Note: this is called before
* the text is actually removed.
*
* @param chng the change event
*/
@Override
protected void removeUpdate(DefaultDocumentEvent chng) {
super.removeUpdate(chng);
}
/**
* Updates any document structure as a result of text removal. This
* method is called after the text has been removed from the Content.
* This will happen within a write lock. If a subclass
* of this class reimplements this method, it should delegate to the
* superclass as well.
*
* @param chng a description of the change
*/
@Override
protected void postRemoveUpdate(AbstractDocument.DefaultDocumentEvent chng) {
super.postRemoveUpdate(chng);
//checkAttributes ();
int lastChangedLine = updateCommentMarks(chng);
//System.out.println ("lastChangedLine = " + lastChangedLine);
changeCommentLines(chng, lastChangedLine);
}
protected void changeCommentLines(AbstractDocument.DefaultDocumentEvent chng, int lastChangedLine) {
int offset = chng.getOffset();
Element root = getDefaultRootElement();
int lineIndex = root.getElementIndex(offset);
for (int i = lineIndex + 1; i <= lastChangedLine; ++i) {
Element line = root.getElement(i);
int lineStart = line.getStartOffset();
AbstractDocument.DefaultDocumentEvent otherChange = new AbstractDocument.DefaultDocumentEvent(lineStart, 0, DocumentEvent.EventType.REMOVE);
fireRemoveUpdate(otherChange);
}
}
/**
* Marks lines as comments.
* Assumes that each sub-element under the root element
* corresponds to a line of text.
*
* @param chng DocumentEvent
* @return int Indicates the last line that has a change due to the commenting.
*/
protected int updateCommentMarks(DocumentEvent chng) {
//System.out.println ("update comment marks: " );
//checkAttributes ();
// update comment marks
javax.swing.text.Element root = getDefaultRootElement();
int offset = chng.getOffset();
int endOffset = chng.getLength() + offset;
int lineIndex = root.getElementIndex(offset);
int endLineIndex = root.getElementIndex(endOffset);
int lastChangedLine = lineIndex;
int signalledLine = lineIndex;
if (endLineIndex < signalledLine + 1) {
endLineIndex = signalledLine + 1;
}
//System.out.println ("offset = " + offset + ", length = " + chng.getLength () + ", endOffset = " + endOffset);
//System.out.println ("lineIndex = " + lineIndex + ", endLineIndex = " + endLineIndex + ", nLines = " + root.getElementCount());
boolean inComment = false;
lineIndex -= 2;
if (lineIndex < 0) {
lineIndex = 0;
}
if (lineIndex > 0) {
Element prevLine = root.getElement(lineIndex - 1);
javax.swing.text.MutableAttributeSet a = (javax.swing.text.MutableAttributeSet) prevLine.getAttributes();
if (a != null && a.isDefined(CommentAttribute)) {
inComment = true;
}
}
boolean changeToLine = true;
for (int i = lineIndex;(changeToLine || i <= endLineIndex) && i < root.getElementCount(); ++i) {
//System.out.println ("commenting line " + i);
changeToLine = false;
javax.swing.text.Element elem = root.getElement(i);
int p0 = elem.getStartOffset();
int p1 = elem.getEndOffset();
String s;
try {
s = getText(p0, p1 - p0);
} catch (javax.swing.text.BadLocationException bl) {
s = null;
}
//System.out.println("elem: " + p0 + " - " + p1 + ", " + s);
// Clear any existing comment info for the line.
javax.swing.text.MutableAttributeSet a = (javax.swing.text.MutableAttributeSet) elem.getAttributes();
boolean prevOpenComment = a.isDefined(CommentAttribute);
CCommentInfo prevInfo = (CCommentInfo) a.getAttribute(CCommentAttribute);
a.removeAttribute(CommentAttribute);
a.removeAttribute(CCommentAttribute);
a.addAttribute(TestCommentAttribute, TestCommentAttribute);
// Get a set of points indicating comment ranges in the line.
Point[] points = scanLineForComments(s, inComment);
if (points != null) {
// The values in the points returned are relative to the beginning of the line of
// text. We need to update them so that the values represent offsets relative to
// the beginning of the document.
inComment = false;
if (prevInfo == null || prevInfo.comments.length != points.length) {
changeToLine = true;
}
for (int j = 0; j < points.length; ++j) {
Point p = points[j];
if (p.y == -1) {
p.y = p1 - p0;
inComment = true;
// Attribute the line to indicate that there is still an 'open' comment
// at the end of the line.
a.addAttribute(CommentAttribute, CommentAttribute);
if (inComment != prevOpenComment) {
changeToLine = true;
}
}
if (!changeToLine && (p.x != prevInfo.comments[j].x || p.y != prevInfo.comments[j].y)) {
changeToLine = true;
}
}
a.addAttribute(CCommentAttribute, new CCommentInfo(points));
} else {
if (prevInfo != null || inComment != prevOpenComment) {
changeToLine = true;
}
// No start/end of comments found in line.
if (inComment) {
// If we are already in a comment then the whole line
// is a comment.
points = new Point[1];
points[0] = new Point(p0, p1);
a.addAttribute(CCommentAttribute, new CCommentInfo(points));
a.addAttribute(CommentAttribute, CommentAttribute);
}
}
if (changeToLine && i > lastChangedLine) {
lastChangedLine = i;
}
}
//System.out.println ("after update comment marks: " );
//checkAttributes ();
return lastChangedLine;
}
/**
* Scans a line of text building up information on which portions of the line
* are comments.
*
* @param line A string containing the content of the line.
* @param inComment A boolean flag indicating that this line is a continuation of an existing comment.
*/
Point[] scanLineForComments(String line, boolean inComment) {
//System.out.println ("scanLineForComments (String line = " + line + ", boolean inComment = " + inComment + ")");
// The comment info for a line consists of a set of Point objects indicating the
// start and end of a comment range in the line. An end value of -1 means 'to the end of the line'.
List<Point> vPoints = new ArrayList<Point>();
// First find the range of any /* */ comments.
int index = (inComment) ? 0 : line.indexOf("/*");
while (index >= 0) {
// Actually want to move back to include any leading
// whitespace.
while (index - 1 >= 0 && Character.isWhitespace(line.charAt(index - 1))) {
index--;
}
//System.out.println ("cc start at " + index);
Point p = new Point(index, -1);
vPoints.add(p);
// Look for end of comment.
int endIndex = -1;
if (index == 0 && inComment) {
endIndex = line.indexOf("*/", index);
} else {
endIndex = line.indexOf("*/", index + 2);
}
if (endIndex != -1) {
p.y = endIndex + 2;
//System.out.println ("cc end at " + endIndex);
index = line.indexOf("/*", endIndex);
} else {
index = -1;
}
}
// Now find any locations where a single line comment start is in the line.
int lastFound = -1;
List<Point> lcPoints = new ArrayList<Point>();
while ((index = line.indexOf("//", lastFound)) >= 0) {
lastFound = index + 2;
// Actually want to move back to include any leading
// whitespace.
while (index - 1 >= 0 && Character.isWhitespace(line.charAt(index - 1))) {
index--;
}
lcPoints.add(new Point(index, line.length()));
//System.out.println ("lc start at " + index);
}
// Now we want to remove any // comments that start inside a /* */ comment.
// Then we remove any /* */ comments that come after a //.
if (!vPoints.isEmpty() && !lcPoints.isEmpty()) {
for (int i = lcPoints.size() - 1; i >= 0; --i) {
Point lcPoint = lcPoints.get(i);
// See if this is inside any /* */ ranges.
for (int j = 0, nVPoints = vPoints.size(); j < nVPoints; ++j) {
Point vPoint = vPoints.get(j);
if (lcPoint.x > vPoint.x && lcPoint.x < vPoint.y) {
lcPoints.remove(i);
break;
}
}
}
// If there are any // comment points left we only need to worry about the first one.
if (!lcPoints.isEmpty()) {
Point lcPoint = lcPoints.get(0);
// Now if there is a valid // comment point we want to remove any /* */
// comment ranges that come after it.
for (int i = vPoints.size() - 1; i >= 0; --i) {
Point p = vPoints.get(i);
if (lcPoint.x < p.x) {
vPoints.remove(i);
}
}
vPoints.add(lcPoint);
}
} else {
if (!lcPoints.isEmpty()) {
vPoints.add(lcPoints.get(0));
}
}
if (!vPoints.isEmpty()) {
return vPoints.toArray(new Point[vPoints.size()]);
}
return null;
}
/**
* Gets the line element which contains the given offset.
* @param offset int
* @return Element
*/
protected Element getLineElementForOffset(int offset) {
Element root = getDefaultRootElement();
int lineIndex = root.getElementIndex(offset);
return root.getElement(lineIndex);
}
public void checkAttributes() {
javax.swing.text.Element root = getDefaultRootElement();
for (int i = 0; i < root.getElementCount(); ++i) {
javax.swing.text.Element elem = root.getElement(i);
javax.swing.text.MutableAttributeSet a = (javax.swing.text.MutableAttributeSet) elem.getAttributes();
boolean c = a.isDefined(CommentAttribute);
boolean cc = a.isDefined(CCommentAttribute);
boolean ct = a.isDefined(TestCommentAttribute);
System.out.println(" line " + i + ": c = " + c + ", cc = " + cc + ", ct = " + ct);
}
}
/**
* Removes some content from the document.
* Removing content causes a write lock to be held while the
* actual changes are taking place. Observers are notified
* of the change on the thread that called this method.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/products/jfc/swingdoc-archive/threads.html">Threads
* and Swing</A> for more information.
*
* @param offs the starting offset >= 0
* @param len the number of characters to remove >= 0
* @exception BadLocationException the given remove position is not a valid
* position within the document
* @see javax.swing.text.Document#remove
*/
@Override
public void remove(int offs, int len) throws BadLocationException {
super.remove(offs, len);
}
/**
* Inserts some content into the document.
* Inserting content causes a write lock to be held while the
* actual changes are taking place, followed by notification
* to the observers on the thread that grabbed the write lock.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/products/jfc/swingdoc-archive/threads.html">Threads
* and Swing</A> for more information.
*
* @param offs the starting offset >= 0
* @param str the string to insert; does nothing with null/empty strings
* @param a the attributes for the inserted content
* @exception BadLocationException the given insert position is not a valid
* position within the document
* @see javax.swing.text.Document#insertString
*/
@Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
super.insertString(offs, str, a);
}
}