/**
* Copyright (c) 2001-2014 Mathew A. Nelson and Robocode contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://robocode.sourceforge.net/license/epl-v10.html
*/
package net.sf.robocode.ui.editor;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import net.sf.robocode.ui.editor.theme.EditorThemePropertiesManager;
import net.sf.robocode.ui.editor.theme.EditorThemePropertyChangeAdapter;
import net.sf.robocode.ui.editor.theme.IEditorThemeProperties;
import net.sf.robocode.util.StringUtil;
// FIXME: Column in status bar does not take tab size into account
// TODO: Make it configurable to extend the Java keywords.
// TODO: Highlight methods from Robocode API?
// TODO: Highlight numbers?
// TODO: Method names and method invocations in bold?
// TODO: Trim trailing white-spaces from all lines?
/**
* Represents a styled Java document used for syntax high-lightning.
*
* @author Flemming N. Larsen (original)
*/
@SuppressWarnings("serial")
public class JavaDocument extends StyledDocument {
/** The text pane this document is used with necessary for setting the caret position when auto indenting */
private final EditorPane textPane;
/** Flag defining if the contained text is being loaded or replaced externally */
private boolean isReplacingText;
// Indentation //
/** Flag defining if auto indentation is enabled */
private boolean isAutoIndentEnabled = true;
/** Flag defining if spaces must be used for indentation; otherwise tabulation characters are being used */
private boolean useSpacesForIndentation = false;
/** Tab size (column width) */
private int tabSize = 4; // Default is every 4th column
/** Java language quote delimiter characters represented in a string */
private static final String QUOTE_DELIMITERS = "\"'";
/** Java keywords */
private static final Set<String> KEYWORDS = new HashSet<String>(
Arrays.asList(
new String[] {
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
"default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
"implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
"protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
"throw", "throws", "transient", "try", "void", "volatile", "while" }));
/** Predefined Java literals */
private static final Set<String> PREDEFINED_LITERALS = new HashSet<String>(
Arrays.asList(new String[] { "false", "true", "null" }));
/** Normal text attribute set */
private final SimpleAttributeSet normalAttrSet = new SimpleAttributeSet();
/** Quoted text attribute set */
private final SimpleAttributeSet quoteAttrSet = new SimpleAttributeSet();
/** Keyword attribute set */
private final SimpleAttributeSet keywordAttrSet = new SimpleAttributeSet();
/** Predefined literal attribute set */
private final SimpleAttributeSet literalAttrSet = new SimpleAttributeSet();
/** Annotation attribute set */
private final SimpleAttributeSet annotationAttrSet = new SimpleAttributeSet();
/** Comment attribute set */
private SimpleAttributeSet commentAttrSet = new SimpleAttributeSet();
/** String buffer holding only space characters for fast replacement of tabulator characters */
private String spaceBuffer;
/** Old start offset for syntax highlighting */
private int lastSyntaxHighlightStartOffset;
/** Old end offset for syntax highlighting */
private int lastSyntaxHighlightEndOffset;
private int autoIndentationCaretPos = -1;
private boolean updateSyntaxHighlightingEDTidle = true;
/**
* Constructor that creates a Java document.
*
* @param textPane
* is the text pane that this Java documents must apply to.
*/
public JavaDocument(EditorPane textPane) {
super();
this.textPane = textPane;
// Setup text colors and styles
setTextColorsAndStyles(null);
// Setup document listener in order to update caret position and update syntax highlighting
addDocumentListener(new JavaDocumentListener());
// Setup editor properties change listener
EditorThemePropertiesManager.addListener(new EditorThemePropertyChangeAdapter() {
@Override
public void onNormalTextColorChanged(Color newColor) {
setNormalTextColor(newColor);
updateSyntaxHighlighting(true);
}
@Override
public void onNormalTextStyleChanged(FontStyle newStyle) {
setNormalTextStyle(newStyle);
updateSyntaxHighlighting(true);
}
@Override
public void onQuotedTextColorChanged(Color newColor) {
setQuotedTextColor(newColor);
updateSyntaxHighlighting(true);
}
@Override
public void onQuotedTextStyleChanged(FontStyle newStyle) {
setQuotedTextStyle(newStyle);
updateSyntaxHighlighting(true);
}
@Override
public void onKeywordTextColorChanged(Color newColor) {
setKeywordTextColor(newColor);
updateSyntaxHighlighting(true);
}
@Override
public void onKeywordTextStyleChanged(FontStyle newStyle) {
setKeywordTextStyle(newStyle);
updateSyntaxHighlighting(true);
}
@Override
public void onLiteralTextColorChanged(Color newColor) {
setLiteralTextColor(newColor);
updateSyntaxHighlighting(true);
}
@Override
public void onLiteralTextStyleChanged(FontStyle newStyle) {
setLiteralTextStyle(newStyle);
updateSyntaxHighlighting(true);
}
@Override
public void onAnnotationTextColorChanged(Color newColor) {
setAnnotationTextColor(newColor);
updateSyntaxHighlighting(true);
}
@Override
public void onAnnotationTextStyleChanged(FontStyle newStyle) {
setAnnotationTextStyle(newStyle);
updateSyntaxHighlighting(true);
}
@Override
public void onCommentTextColorChanged(Color newColor) {
setCommentTextColor(newColor);
updateSyntaxHighlighting(true);
}
@Override
public void onCommentTextStyleChanged(FontStyle newStyle) {
setCommentTextStyle(newStyle);
updateSyntaxHighlighting(true);
}
});
// Setup change listener and focus listener on the viewport of the text pane
JViewport viewport = textPane.getViewport();
viewport.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
updateSyntaxHighlighting(false);
}
});
viewport.addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {
updateSyntaxHighlighting(false);
}
public void focusLost(FocusEvent e) {}
});
}
/**
* Checks if auto indentation is enabled.
*
* @return true if auto indentation is enabled; false otherwise.
*/
public boolean isAutoIndentEnabled() {
return isReplacingText ? false : isAutoIndentEnabled;
}
/**
* Enable or disable auto indentation.
*
* @param enable is set to true, if auto indentation must be enabled; false is auto indentation must be disabled.
*/
public void setAutoIndentEnabled(boolean enable) {
isAutoIndentEnabled = enable;
}
/**
* Checks if the contained text is currently being replaced externally.
*
* @return true if the text is being replaced; false otherwise.
*/
public boolean isReplacingText() {
return isReplacingText;
}
/**
* Sets the flag if the contained text is currently being replaced externally or not.
*
* @param isReplacingText is true if the text is currently being replaced; false otherwise.
*/
public void setReplacingText(boolean isReplacingText) {
this.isReplacingText = isReplacingText;
}
@Override
public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
// Process auto indentation on inserted string
final Indentation indent = applyAutoIndentation(offset, str);
// Replace indentation tabulation characters with spaces if spaces must be used instead of tabulation characters
str = replaceTabulatorCharacters(getEndOffset(getElementFromOffset(offset)), indent.text);
// Set the new caret position on the text pane if the caret position has been set
autoIndentationCaretPos = indent.caretPos;
// Insert the modified string using the original method for inserting string into this document
super.insertString(offset, str, a);
}
/**
* Sets the tabulation column size.
*
* @param tabSize
* is the new tabulation column size, which must be >= 1.
*/
public void setTabSize(int tabSize) {
this.tabSize = tabSize;
}
private void setTextColorsAndStyles(IEditorThemeProperties themeProps) {
if (themeProps == null) {
themeProps = EditorThemePropertiesManager.getCurrentEditorThemeProperties();
}
setNormalTextColor(themeProps.getNormalTextColor());
setNormalTextStyle(themeProps.getNormalTextStyle());
setQuotedTextColor(themeProps.getQuotedTextColor());
setQuotedTextStyle(themeProps.getQuotedTextStyle());
setKeywordTextColor(themeProps.getKeywordTextColor());
setKeywordTextStyle(themeProps.getKeywordTextStyle());
setLiteralTextColor(themeProps.getLiteralTextColor());
setLiteralTextStyle(themeProps.getLiteralTextStyle());
setAnnotationTextColor(themeProps.getAnnotationTextColor());
setAnnotationTextStyle(themeProps.getAnnotationTextStyle());
setCommentTextColor(themeProps.getCommentTextColor());
setCommentTextStyle(themeProps.getCommentTextStyle());
}
private void setNormalTextColor(Color newColor) {
StyleConstants.setForeground(normalAttrSet, newColor);
}
private void setNormalTextStyle(FontStyle newStyle) {
StyleConstants.setBold(normalAttrSet, newStyle.isBold());
StyleConstants.setItalic(normalAttrSet, newStyle.isItalic());
}
private void setQuotedTextColor(Color newColor) {
StyleConstants.setForeground(quoteAttrSet, newColor);
}
private void setQuotedTextStyle(FontStyle newStyle) {
StyleConstants.setBold(quoteAttrSet, newStyle.isBold());
StyleConstants.setItalic(quoteAttrSet, newStyle.isItalic());
}
private void setKeywordTextColor(Color newColor) {
StyleConstants.setForeground(keywordAttrSet, newColor);
}
private void setKeywordTextStyle(FontStyle newStyle) {
StyleConstants.setBold(keywordAttrSet, newStyle.isBold());
StyleConstants.setItalic(keywordAttrSet, newStyle.isItalic());
}
private void setLiteralTextColor(Color newColor) {
StyleConstants.setForeground(literalAttrSet, newColor);
}
private void setLiteralTextStyle(FontStyle newStyle) {
StyleConstants.setBold(literalAttrSet, newStyle.isBold());
StyleConstants.setItalic(literalAttrSet, newStyle.isItalic());
}
private void setAnnotationTextColor(Color newColor) {
StyleConstants.setForeground(annotationAttrSet, newColor);
}
private void setAnnotationTextStyle(FontStyle newStyle) {
StyleConstants.setBold(annotationAttrSet, newStyle.isBold());
StyleConstants.setItalic(annotationAttrSet, newStyle.isItalic());
}
private void setCommentTextColor(Color newColor) {
StyleConstants.setForeground(commentAttrSet, newColor);
}
private void setCommentTextStyle(FontStyle newStyle) {
StyleConstants.setBold(commentAttrSet, newStyle.isBold());
StyleConstants.setItalic(commentAttrSet, newStyle.isItalic());
}
/**
* Applies indentation for an inserted string at a given offset if auto indentation is enabled.
*
* @param offset
* is the offset of the inserted string.
* @param str
* is the inserted string.
* @return an Indentation container with indentation details.
* @throws BadLocationException
*/
private Indentation applyAutoIndentation(int offset, String str) throws BadLocationException {
// Prepare indentation container
Indentation indentation = new Indentation();
indentation.caretPos = -1; // Meaning that caret position is not changed
indentation.text = str;
// Apply auto indentation if it is enabled, and the new line character has been entered
if (isAutoIndentEnabled() && str.equals("\n")) {
// Save the current indentation for later, as it might change from here on
int currentIndentation = getIndentationLengthFromOffset(offset);
// Read the line content from the offset
String line = getLineFromOffset(offset);
// Calculate the end offset of the line
int lineEndOffset = getElementFromOffset(offset).getStartOffset() + line.length();
// Continue if the line has content and the offset is after the line end offset
if (line.length() > 0 && offset >= lineEndOffset) {
// Check/compose and return a body start indentation a '{' is found
if (composeBodyStartIndentation(offset, line, indentation)) {
return indentation;
}
// Check/compose and return a multiline comment start indentation if a '/*' is found
if (composeMultilineCommentStartIndentation(offset, line, indentation)) {
return indentation;
}
// Check/compose and return a continued multiline comment if a '*' is found inside a multiline comment
if (composeMultilineCommentContinuedIndentation(offset, line, indentation)) {
return indentation;
}
// Check/compose and return a multiline comment end indentation if a '*/' is found
if (composeMultilineCommentEndIndentation(offset, line, indentation)) {
return indentation;
}
// Extend the current indentation text based on the current indentation
// Note: Will replace tabulator characters with spaces etc. if this must be done.
indentation.text += getStartIndentation(currentIndentation); // FIXME
}
}
return indentation;
}
/**
* Returns a string where tabulator characters in the given string is replaced with spaces, but only if these must
* be replaced with spaces by configuration.
*
* @param startIndex
* is the start index of the string in a line used for determine the current tabulator column.
* @param str
* is the string containing tabulator characters that might need to be replaced with spaces.
* @return a string where tabulator characters might have been replaced with spaces.
*/
private String replaceTabulatorCharacters(int startIndex, String str) {
// Check if tabulator characters needs to be replaced with space characters
if (useSpacesForIndentation) {
// Prepare string buffer containing replaced text
StringBuilder sb = new StringBuilder();
int tabIndex; // Index of current tabulator character
// Run loop as long as we find a tabulator character from the start index
while ((tabIndex = str.indexOf('\t', startIndex)) >= 0) {
// Put the current text (non tabulator characters) into the string buffer
sb.append(str.substring(startIndex, tabIndex));
// Calculate the number of spaces that remain before the next tabulator column
int numSpaces = tabSize - (sb.length() % tabSize);
// Run loop while the follower character is a tabulator character
while (++tabIndex < str.length() && str.charAt(tabIndex) == '\t') {
// Increment the number of spaces that to use to replace tabulator characters with the tabulator
// column size.
numSpaces += tabSize;
}
// Append the calculated number of space characters to replace the tabulator characters
sb.append(getSpaces(numSpaces));
// The new start index is the current tabulator index
startIndex = tabIndex;
}
// Append the text that remain to the string buffer from the current start index
sb.append(str.substring(startIndex));
// Set the result to the build string from the string buffer
str = sb.toString();
}
// Return the resulting string
return str;
}
/**
* Returns a string containing a specific number of spaces only.
*
* @param count
* is the number of spaces the the string should contain.
* @return a string containing only the given number of space characters.
*/
private String getSpaces(int count) {
if (count == 0) {
return "";
}
// Determine the current buffer size
int bufferSize = (spaceBuffer == null) ? 0 : spaceBuffer.length();
// Check if we need to reallocate the buffer to accommodate the given number of spaces
if (count > bufferSize) {
// Determine the new buffer size, which is set to twice the number of spaces, but minimum 100 characters
bufferSize = Math.max(2 * count, 100);
// Create a new string containing spaces with the new buffer size
char[] chars = new char[bufferSize];
Arrays.fill(chars, ' ');
spaceBuffer = new String(chars);
}
// Return a string containing the given number of spaces
return spaceBuffer.substring(0, count);
}
/**
* Composes a body start indentation block starting with a '{' and ending with a '}' and update the caret position.
*
* @param offset
* is the offset of the inserted string.
* @param line
* is the line, where the indentation block must be appended to.
* @param indentation
* is containing the current indentation date.
* @return true if a body start indentation block should be inserted after the given line; false otherwise.
* @throws BadLocationException
*/
private boolean composeBodyStartIndentation(int offset, String line, Indentation indentation)
throws BadLocationException {
// Check if the given line ends with a body start character, i.e. '{'
if (line.endsWith("{")) {
// We only start a new body indentation if the number of body start characters in the first part of the
// text up to specified offset lesser than the number of body end characters in the last part of the text
String textFirstHalf = getText(0, offset);
String textLastHalf = getText(offset, getLength() - offset);
if (StringUtil.countChar(textLastHalf, '}') >= StringUtil.countChar(textFirstHalf, '{')) {
return false;
}
// Calculated current start indentation length from the given offset
int startIndentLen = getIndentationLengthFromOffset(offset);
// Calculate the start indentation string
String startIndent = getStartIndentation(startIndentLen);
// Prapare buffer for containing the indentation block
StringBuilder sb = new StringBuilder("\n");
// Append new indented line to the buffer, that is indented based on the start indentation
sb.append(getStartIndentation(startIndentLen + tabSize)).append('\n');
// Update the caret position to be placed in the end of the new indented line
indentation.caretPos = offset + sb.toString().length() - 1;
// Append the body end character on a new line
sb.append(startIndent).append('}');
// Set the indentation block text to the string containing from the buffer
indentation.text = sb.toString();
// Indentation block was created
return true;
}
// Indentation block was not created
return false;
}
/**
* Composes a multiline comment start indentation block.
*
* @param offset
* is the offset of the inserted string.
* @param line
* is the line, where the indentation block must be appended to.
* @param indentation
* is containing the current indentation date.
* @return true if a multiline comment start indentation block should be inserted after the given line; false
* otherwise.
* @throws BadLocationException
*/
private boolean composeMultilineCommentStartIndentation(int offset, String line, Indentation indentation)
throws BadLocationException {
// Check if the given line contains a multiline start string, i.e. '/*'
if (line.trim().startsWith("/*")) {
// Calculated current start indentation length from the given offset
int startIndentLen = getIndentationLengthFromOffset(offset);
// Calculate the start indentation string
String startIndent = getStartIndentation(startIndentLen);
// Prepare buffer for containing the indentation block
StringBuilder sb = new StringBuilder("\n");
// Append new indented multiline comment line to the buffer, that is indented based on the start indentation
sb.append(startIndent).append(" * \n");
// Update the caret position to be placed in the end of the new indented line
indentation.caretPos = offset + sb.toString().length() - 1;
// Append the multiline comment end character on a new line
sb.append(startIndent).append(" */");
// Set the indentation block text to the string containing from the buffer
indentation.text = sb.toString();
// Indentation block was created
return true;
}
// Indentation block was not created
return false;
}
/**
* Composes a continued multiline comment indentation block.
*
* @param offset
* is the offset of the inserted string.
* @param line
* is the line, where the indentation block must be appended to.
* @param indentation
* is containing the current indentation date.
* @return true if a continued multiline comment indentation block should be inserted after the given line; false
* otherwise.
* @throws BadLocationException
*/
private boolean composeMultilineCommentContinuedIndentation(int offset, String line, Indentation indentation)
throws BadLocationException {
// Check if the given line contains a '*' and is located inside of a multiline comment
if (line.trim().startsWith("*") && isInMultilineComment(offset)) {
// Calculated current start indentation length from the given offset
int startIndentLen = getIndentationLengthFromOffset(offset);
// Calculate the start indentation string
String startIndent = getStartIndentation(startIndentLen);
// Prepare buffer for containing the indentation block
StringBuilder sb = new StringBuilder("\n");
// Append new indented multiline comment line to the buffer, that is indented based on the start indentation
sb.append(startIndent).append("* ");
// Update the caret position to be placed in the end of the new indented line
indentation.caretPos = offset + sb.toString().length();
// Set the indentation block text to the string containing from the buffer
indentation.text = sb.toString();
// Indentation block was created
return true;
}
// Indentation block was not created
return false;
}
/**
* Composes a multiline end start indentation block.
*
* @param offset
* is the offset of the inserted string.
* @param line
* is the line, where the indentation block must be appended to.
* @param indentation
* is containing the current indentation date.
* @return true if a multiline comment end indentation block should be inserted after the given line; false
* otherwise.
* @throws BadLocationException
*/
private boolean composeMultilineCommentEndIndentation(int offset, String line, Indentation indentation)
throws BadLocationException {
// Check if the given line contains a multiline end string, i.e. '*/'
if (line.trim().endsWith("*/")) {
// Get the line index from the current offset
int lineIndex = getElementIndex(offset);
// Run loop as long as the line index is still positive (till the start of the document text)
while (lineIndex >= 0) {
// Check if the current line line is not in a multiline comment
int lineOffset = getElement(lineIndex).getStartOffset();
if (!isInMultilineComment(lineOffset)) {
// Append indentation that matches the start offset of the current line
indentation.text += getStartIndentation(getIndentationLengthFromOffset(lineOffset));
break;
}
// Move to previous line
lineIndex--;
}
// Indentation block was created
return true;
}
// Indentation block was not created
return false;
}
/**
* Returns line from the specified offset where trailing white spaces are removed. Note: The white spaces in the
* beginning of the line is not removed as these serve to determine start indentation for inserted lines following
* this line.
*
* @param offset
* is the offset to retrieve the line from.
* @return the line at the given offset where trailing white spaces have been trimmed.
* @throws BadLocationException
*/
private String getLineFromOffset(int offset) throws BadLocationException {
// Get the start and end index of the line
Element element = getElementFromOffset(offset);
int start = element.getStartOffset();
int end = getEndOffset(element);
if (end == start) {
return "";
}
// Get the line content
String origLine = getText(start, end - start);
// Trim trailing white spaces
String trimmedLine = origLine.replaceAll("\\s*$", "");
// Return the trimmed line, but only if it did not contain only white spaces as we need the white spaces for
// indentation for lines inserted after this line.
return trimmedLine.length() > 0 ? trimmedLine : origLine;
}
/**
* Calculates and returns the indentation length (number of space characters) of the line at the given offset.
*
* @param offset
* is the document offset of the line.
* @return the calculated indentation length of the line at the given offset.
* @throws BadLocationException
*/
private int getIndentationLengthFromOffset(int offset) throws BadLocationException {
// Gets the start offset of the line at the given offset
int startOffset = getElementFromOffset(offset).getStartOffset();
// Run loop while the next character is a space of tabulator character.
int length = 0;
for (;;) {
// Get the next character
char ch = getText(startOffset++, 1).charAt(0);
if (ch == ' ') {
// Increment the length by 1 if the character is a space
length++;
} else if (ch == '\t') {
// Increment the length by the number of spaces the remain in order to reach the next tabulator column,
// if the character is a tabulator character.
length = tabSize * (length / tabSize + 1);
} else {
// Otherwise, stop looping
break;
}
}
// Return the calculated indentation length
return length;
}
/**
* Returns a start indentation string of the given length.
*
* @param length
* is the length of the indentation measured in number of spaces.
* @return a string to apply to the start of a line in order to indent the line.
*/
private String getStartIndentation(int length) {
if (length == 0) {
return "";
}
// Prepare string buffer for containing the indentation string
StringBuilder sb = new StringBuilder();
// Check if tabulator characters must be used for indentation
if (!useSpacesForIndentation) {
// Append as many tabulator characters to the indentation string that fits into the given length
for (int i = length / tabSize; i > 0; i--) {
sb.append('\t');
}
// Set the length to the spaces remaining to reach the input length
length %= tabSize;
}
// Apply spaces to the string buffer (might be the spaces that remain to fit the given length)
sb.append(getSpaces(length));
// Return the indentation string contained in the string buffer
return sb.toString();
}
/**
* Updates the syntax highlighting on the document using the EDT.
*/
private void updateSyntaxHighlighting(final boolean force) {
// Only invoke the EDT, if this operation is not already initiated
if (updateSyntaxHighlightingEDTidle) {
updateSyntaxHighlightingEDTidle = false;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
// Apply syntax highlighting from the current offset
performSyntaxHighlighting(force);
updateSyntaxHighlightingEDTidle = true;
} catch (BadLocationException e) {
e.printStackTrace();
}
}
});
}
}
/**
* Perform syntax highlighting on the document. This implementation only performs syntax highlighting on the current
* visible text in the view port of the text pane, and detects if the text has been changed before performing the
* syntax highlighting.
*
* @throws BadLocationException
*/
private void performSyntaxHighlighting(boolean force) throws BadLocationException {
// Return if there is nothing to highlight
if (getLength() == 0 && !force) {
return;
}
// Get the start and end offset of the visible text
JViewport viewport = textPane.getViewport();
Point startPoint = viewport.getViewPosition();
Dimension size = viewport.getExtentSize();
Point endPoint = new Point(startPoint.x + size.width, startPoint.y + size.height);
int startOffset = textPane.viewToModel(startPoint);
int endOffset = textPane.viewToModel(endPoint);
// Return if the current start and end offset is equal to the last ones
if (!force && startOffset == lastSyntaxHighlightStartOffset && endOffset == lastSyntaxHighlightEndOffset) {
return;
}
lastSyntaxHighlightStartOffset = startOffset;
lastSyntaxHighlightEndOffset = endOffset;
setCharacterAttributes(startOffset, endOffset - startOffset, normalAttrSet, true);
// Get start and end line
int startLine = getElementIndex(startOffset);
int endLine = getElementIndex(endOffset);
// Process each changed line one by one from the start line to the end line
for (int line = startLine; line <= endLine; line++) {
processChangedLine(line);
}
}
/**
* Process changed line by applying syntax highlighting on the entire line at the given index.
*
* @param lineIndex
* is the index of the line.
* @throws BadLocationException
*/
private void processChangedLine(int lineIndex) throws BadLocationException {
// Process the syntax tokens on the given line
Element element = getElement(lineIndex);
processLineTokens(element.getStartOffset(), getEndOffset(element));
}
/**
* Process the syntax tokens contained in a line.
*
* @param startOffset
* is the document start offset of the line to process.
* @param endOffset
* is the document end offset of the line to process.
* @throws BadLocationException
*/
private void processLineTokens(int startOffset, int endOffset) throws BadLocationException {
// Calculate the length of the line based on the given start and end offset
int len = endOffset - startOffset;
// Process tokens one by one
String textFragment = getText(startOffset, len);
int index = 0;
while (index < len) {
index += processToken(textFragment.substring(index), startOffset + index);
}
}
/**
* Process the syntax token contained in a text fragment.
*
* @param textFragment
* is the text fragment.
* @param startOffset
* is the start offset of the text fragment to process.
* @return the number of processed characters.
* @throws BadLocationException
*/
private int processToken(final String textFragment, final int startOffset) throws BadLocationException {
// Process quote token if the first character in the text fragment is a quote delimiter
if (isQuoteDelimiter(textFragment.charAt(0))) {
return processQuoteToken(textFragment, startOffset);
}
int len;
// Check if the token is a single line comment
// Note: Single line comment has higher precedence than a multiline comment.
if (textFragment.startsWith("//")) {
len = textFragment.length();
setCharacterAttributes(startOffset, len, commentAttrSet, true);
return len;
}
// Check if the token is a multiline comment
if (textFragment.startsWith("/*")) {
return processStartMultilineCommentToken(textFragment, startOffset);
}
// Check if the token is in the middle of a multiline comment
if (isInMultilineComment(startOffset)) {
// Check if the token contains the end mark of the multiline comment
int endCommentIndex = textFragment.indexOf("*/");
if (endCommentIndex >= 0) {
len = endCommentIndex + 2; // Limit length to the end mark
} else {
len = textFragment.length(); // Use the whole token
}
setCharacterAttributes(startOffset, len, commentAttrSet, true);
return len;
}
// Skip delimiter characters
len = 1;
while (len < textFragment.length()) {
char ch = textFragment.charAt(len);
if (!Character.isLetter(ch)) {
break;
}
len++;
}
// Limit token to current length, e.g. if it contained delimiter characters
String token = textFragment.substring(0, len);
// Check if the token is a keyword or an annotation
if (startOffset > 0 && !Character.isLetter(getText(startOffset - 1, 1).charAt(0)) || startOffset == 0) {
if (isKeyword(token)) {
setCharacterAttributes(startOffset, len, keywordAttrSet, true);
} else if (isPredefinedLiteral(token)) {
setCharacterAttributes(startOffset, len, literalAttrSet, true);
} else if (isAnnotation(token)) {
setCharacterAttributes(startOffset, len, annotationAttrSet, true);
} else {
len = 1;
}
} else {
len = 1;
}
// Return the number of processed characters
return len;
}
/**
* Process a quote token.
*
* @param textFragment
* is the text fragment.
* @param startOffset
* is the start offset of the text fragment to process.
* @return the number of processed characters.
* @throws BadLocationException
*/
private int processQuoteToken(final String textFragment, final int startOffset) {
char quoteDelimiter = textFragment.charAt(0);
// Find end quote if it exists. Ignore escaped quotes
int indexQuote = 1;
int quoteEndIndex = -1;
for (;;) {
indexQuote = textFragment.indexOf(quoteDelimiter, indexQuote);
if (indexQuote <= 0 || indexQuote > textFragment.length()) {
break;
}
if (textFragment.charAt(indexQuote - 1) != '\\') {
quoteEndIndex = indexQuote;
break;
}
indexQuote++;
}
// Set length of token, if end quote was found
int len = quoteEndIndex >= 0 ? quoteEndIndex + 1 : textFragment.length();
// Set quote attribute set
setCharacterAttributes(startOffset, len, quoteAttrSet, true);
return len;
}
/**
* Process a multiline comment token.
*
* @param textFragment
* is the text fragment.
* @param startOffset
* is the start offset of the text fragment to process.
* @return the number of processed characters.
* @throws BadLocationException
*/
private int processStartMultilineCommentToken(final String textFragment, final int startOffset) {
int endIndex = textFragment.indexOf("*/", 1);
int len = endIndex >= 0 ? endIndex + 2 : textFragment.length();
setCharacterAttributes(startOffset, len, commentAttrSet, true);
return len;
}
/**
* Checks if the current text offset is within a multiline comment.
*
* @param offset
* is the text offset.
* @return true if the given offset is within a multiline comment; false otherwise.
* @throws BadLocationException
*/
private boolean isInMultilineComment(final int offset) throws BadLocationException {
return isInMultilineComment(getElementIndex(offset), offset);
}
/**
* Checks if the current text offset is within a multiline comment.
*
* @oaram lineIndex is the element index of a line.
* @param offset
* is the text offset.
* @return true if the given line element index and offset is within a multiline comment; false otherwise.
* @throws BadLocationException
*/
private boolean isInMultilineComment(final int lineIndex, final int offset) throws BadLocationException {
int startOffset = getElement(lineIndex).getStartOffset();
String lineText = getLineFromOffset(startOffset);
int len = Math.min((offset - startOffset), lineText.length());
lineText = lineText.substring(0, len);
int commentStart = lineText.lastIndexOf("/*");
int commentEnd = lineText.lastIndexOf("*/");
if (commentStart >= 0) {
if (commentEnd >= 0) {
if (commentEnd > commentStart) {
return (len < commentEnd);
}
} else {
return true;
}
}
if (commentEnd >= 0) {
return false;
}
int prevLine = lineIndex - 1;
return (prevLine >= 0) ? isInMultilineComment(prevLine, offset) : false;
}
/**
* Checks if a character is a quote delimiter character.
*
* @param ch
* is the character.
* @return true if the given character is a quote delimiter character; false otherwise.
*/
private static boolean isQuoteDelimiter(char ch) {
return QUOTE_DELIMITERS.indexOf(ch) >= 0;
}
/**
* Checks if a token is a keyword.
*
* @param token
* is the token.
* @return true if the given token is a keyword; false otherwise.
*/
private static boolean isKeyword(String token) {
return KEYWORDS.contains(token);
}
/**
* Checks if a token is a predefined literal.
*
* @param token
* is the token.
* @return true if the given token is a predefined literal; false otherwise.
*/
private static boolean isPredefinedLiteral(String token) {
return PREDEFINED_LITERALS.contains(token);
}
/**
* Checks if a token is an annotation.
*
* @param token
* is the token.
* @return true if the given token is an annotation; false otherwise.
*/
private static boolean isAnnotation(String token) {
return token.matches("@\\w.*");
}
/**
* Class containing data for indentation.
*/
private class Indentation {
// Is the new caret position after the indentation has been made. -1 means no change.
int caretPos;
// Is the indentation text
String text;
}
/**
* This document listener is used for updating the caret position and update syntax highlighting when the contents
* of the document is changed.
*/
private class JavaDocumentListener implements DocumentListener {
// Caret position updater
final CaretPositionUpdater caretPositionUpdater = new CaretPositionUpdater();
public void insertUpdate(final DocumentEvent e) {
int newCaretPosition;
// Check if the caret position has been changed by auto indentation
if (autoIndentationCaretPos >= 0) {
// Set the new caret position to the one set for auto indentation
newCaretPosition = autoIndentationCaretPos;
// Signal that the caret position for auto indentation has been set
autoIndentationCaretPos = -1;
} else {
// Set the new caret position to the end of the inserted text
newCaretPosition = e.getOffset() + e.getLength();
}
// Update the caret position to the new position
caretPositionUpdater.updateCaretPosition(newCaretPosition);
// Apply syntax highlighting from the current offset
updateSyntaxHighlighting(false);
}
public void removeUpdate(final DocumentEvent e) {
// Set the caret position where the text was removed.
caretPositionUpdater.updateCaretPosition(e.getOffset());
// Apply syntax highlighting from the current offset
updateSyntaxHighlighting(false);
}
public void changedUpdate(DocumentEvent e) {}
private final class CaretPositionUpdater {
/**
* Updates the caret position via the EDT.
*
* @param newCaretPosition is the new caret position.
*/
public void updateCaretPosition(int newCaretPosition) {
// Set the caret position, and take care that it is not out of range
textPane.setCaretPosition(Math.min(newCaretPosition, getLength()));
}
}
}
}