Package net.sf.robocode.ui.editor

Source Code of net.sf.robocode.ui.editor.JavaDocument$JavaDocumentListener$CaretPositionUpdater

/**
* 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()));
      }
    }
  }
}
TOP

Related Classes of net.sf.robocode.ui.editor.JavaDocument$JavaDocumentListener$CaretPositionUpdater

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.