org.syntax.jedit.tokenmarker.TokenMarker Maven / Gradle / Ivy
/*
* soapUI, copyright (C) 2004-2011 smartbear.com
*
* soapUI is free software; you can redistribute it and/or modify it under the
* terms of version 2.1 of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* soapUI 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 at gnu.org.
*/
package org.syntax.jedit.tokenmarker;
import javax.swing.text.Segment;
/**
* A token marker that splits lines of text into tokens. Each token carries a
* length field and an indentification tag that can be mapped to a color for
* painting that token.
*
*
* For performance reasons, the linked list of tokens is reused after each line
* is tokenized. Therefore, the return value of markTokens
should
* only be used for immediate painting. Notably, it cannot be cached.
*
* @author Slava Pestov
* @version $Id$
*
* @see org.syntax.jedit.Token
*/
public abstract class TokenMarker
{
/**
* A wrapper for the lower-level markTokensImpl
method that is
* called to split a line up into tokens.
*
* @param line
* The line
* @param lineIndex
* The line number
*/
public Token markTokens( Segment line, int lineIndex )
{
if( lineIndex >= length )
{
throw new IllegalArgumentException( "Tokenizing invalid line: " + lineIndex );
}
lastToken = null;
LineInfo info = lineInfo[lineIndex];
LineInfo prev;
if( lineIndex == 0 )
prev = null;
else
prev = lineInfo[lineIndex - 1];
byte oldToken = info.token;
byte token = markTokensImpl( prev == null ? Token.NULL : prev.token, line, lineIndex );
info.token = token;
/*
* This is a foul hack. It stops nextLineRequested from being cleared if
* the same line is marked twice.
*
* Why is this necessary? It's all JEditTextArea's fault. When something
* is inserted into the text, firing a document event, the insertUpdate()
* method shifts the caret (if necessary) by the amount inserted.
*
* All caret movement is handled by the select() method, which eventually
* pipes the new position to scrollTo() and calls repaint().
*
* Note that at this point in time, the new line hasn't yet been painted;
* the caret is moved first.
*
* scrollTo() calls offsetToX(), which tokenizes the line unless it is
* being called on the last line painted (in which case it uses the text
* area's painter cached token list). What scrollTo() does next is
* irrelevant.
*
* After scrollTo() has done it's job, repaint() is called, and eventually
* we end up in paintLine(), whose job is to paint the changed line. It,
* too, calls markTokens().
*
* The problem was that if the line started a multiline token, the first
* markTokens() (done in offsetToX()) would set nextLineRequested (because
* the line end token had changed) but the second would clear it (because
* the line was the same that time) and therefore paintLine() would never
* know that it needed to repaint subsequent lines.
*
* This bug took me ages to track down, that's why I wrote all the
* relevant info down so that others wouldn't duplicate it.
*/
if( !( lastLine == lineIndex && nextLineRequested ) )
nextLineRequested = ( oldToken != token );
lastLine = lineIndex;
addToken( 0, Token.END );
return firstToken;
}
/**
* An abstract method that splits a line up into tokens. It should parse the
* line, and call addToken()
to add syntax tokens to the token
* list. Then, it should return the initial token type for the next line.
*
*
* For example if the current line contains the start of a multiline comment
* that doesn't end on that line, this method should return the comment token
* type so that it continues on the next line.
*
* @param token
* The initial token type for this line
* @param line
* The line to be tokenized
* @param lineIndex
* The index of the line in the document, starting at 0
* @return The initial token type for the next line
*/
protected abstract byte markTokensImpl( byte token, Segment line, int lineIndex );
/**
* Returns if the token marker supports tokens that span multiple lines. If
* this is true, the object using this token marker is required to pass all
* lines in the document to the markTokens()
method (in turn).
*
*
* The default implementation returns true; it should be overridden to return
* false on simpler token markers for increased speed.
*/
public boolean supportsMultilineTokens()
{
return true;
}
/**
* Informs the token marker that lines have been inserted into the document.
* This inserts a gap in the lineInfo
array.
*
* @param index
* The first line number
* @param lines
* The number of lines
*/
public void insertLines( int index, int lines )
{
if( lines <= 0 )
return;
length += lines;
ensureCapacity( length );
int len = index + lines;
System.arraycopy( lineInfo, index, lineInfo, len, lineInfo.length - len );
for( int i = index + lines - 1; i >= index; i-- )
{
lineInfo[i] = new LineInfo();
}
}
/**
* Informs the token marker that line have been deleted from the document.
* This removes the lines in question from the lineInfo
array.
*
* @param index
* The first line number
* @param lines
* The number of lines
*/
public void deleteLines( int index, int lines )
{
if( lines <= 0 )
return;
int len = index + lines;
length -= lines;
System.arraycopy( lineInfo, len, lineInfo, index, lineInfo.length - len );
}
/**
* Returns the number of lines in this token marker.
*/
public int getLineCount()
{
return length;
}
/**
* Returns true if the next line should be repainted. This will return true
* after a line has been tokenized that starts a multiline token that
* continues onto the next line.
*/
public boolean isNextLineRequested()
{
return nextLineRequested;
}
// protected members
/**
* The first token in the list. This should be used as the return value from
* markTokens()
.
*/
protected Token firstToken;
/**
* The last token in the list. New tokens are added here. This should be set
* to null before a new line is to be tokenized.
*/
protected Token lastToken;
/**
* An array for storing information about lines. It is enlarged and shrunk
* automatically by the insertLines()
and
* deleteLines()
methods.
*/
protected LineInfo[] lineInfo;
/**
* The number of lines in the model being tokenized. This can be less than
* the length of the lineInfo
array.
*/
protected int length;
/**
* The last tokenized line.
*/
protected int lastLine;
/**
* True if the next line should be painted.
*/
protected boolean nextLineRequested;
/**
* Creates a new TokenMarker
. This DOES NOT create a lineInfo
* array; an initial call to insertLines()
does that.
*/
protected TokenMarker()
{
lastLine = -1;
}
/**
* Ensures that the lineInfo
array can contain the specified
* index. This enlarges it if necessary. No action is taken if the array is
* large enough already.
*
*
* It should be unnecessary to call this under normal circumstances;
* insertLine()
should take care of enlarging the line info
* array automatically.
*
* @param index
* The array index
*/
protected void ensureCapacity( int index )
{
if( lineInfo == null )
lineInfo = new LineInfo[index + 1];
else if( lineInfo.length <= index )
{
LineInfo[] lineInfoN = new LineInfo[( index + 1 ) * 2];
System.arraycopy( lineInfo, 0, lineInfoN, 0, lineInfo.length );
lineInfo = lineInfoN;
}
}
/**
* Adds a token to the token list.
*
* @param length
* The length of the token
* @param id
* The id of the token
*/
protected void addToken( int length, byte id )
{
if( id >= Token.INTERNAL_FIRST && id <= Token.INTERNAL_LAST )
throw new InternalError( "Invalid id: " + id );
if( length == 0 && id != Token.END )
return;
if( firstToken == null )
{
firstToken = new Token( length, id );
lastToken = firstToken;
}
else if( lastToken == null )
{
lastToken = firstToken;
firstToken.length = length;
firstToken.id = id;
}
else if( lastToken.next == null )
{
lastToken.next = new Token( length, id );
lastToken = lastToken.next;
}
else
{
lastToken = lastToken.next;
lastToken.length = length;
lastToken.id = id;
}
}
/**
* Inner class for storing information about tokenized lines.
*/
public class LineInfo
{
/**
* Creates a new LineInfo object with token = Token.NULL and obj = null.
*/
public LineInfo()
{
}
/**
* Creates a new LineInfo object with the specified parameters.
*/
public LineInfo( byte token, Object obj )
{
this.token = token;
this.obj = obj;
}
/**
* The id of the last token of the line.
*/
public byte token;
/**
* This is for use by the token marker implementations themselves. It can
* be used to store anything that is an object and that needs to exist on
* a per-line basis.
*/
public Object obj;
}
}