package edu.princeton.swing.text; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import java.util.HashSet; import java.util.Vector; import java.util.Iterator; import edu.princeton.toy.*; import edu.princeton.toy.lang.*; /** * HighlightedDocument is an abstract representation of a document which supports syntax * highlighting. This document can then be displayed with a THighlightedTextArea. * * @author Brian Tsang * @version 7.1 */ public abstract class HighlightedDocument { /** * The allowed character with the greatest integer encoding. */ public static final char MAX_ALLOWED_CHARACTER = (char)126; /** * An array of all characters allowed in a HighlightedDocument. */ public static final boolean CHARACTER_ALLOWED[] = new boolean[MAX_ALLOWED_CHARACTER + 1]; private HashSet positions; private Vector textListeners; private Vector undoableEditListeners; protected char chars[]; protected byte charStyles[]; protected int charCount; protected int lineOffsets[]; protected int lineCount; protected int maxLineLength; private Thread writer; private int readerCount; private int positionCtr; private UndoableEdit undoableEdit; /** * Initialize the ALLOWED_CHARACTERS array. */ static { CHARACTER_ALLOWED['\n'] = true; for (int ctr = 32; ctr <= 126; ctr++) CHARACTER_ALLOWED[ctr] = true; } /** * Constructs a HighlightedDocument. */ public HighlightedDocument() { positions = new HashSet(); textListeners = new Vector(); undoableEditListeners = new Vector(); chars = new char[100000]; charStyles = new byte[100000]; charCount = 0; lineOffsets = new int[10000]; lineCount = 0; maxLineLength = 0; writer = null; readerCount = 0; positionCtr = 0; } /** * Returns the number of distinct styles used by this type of document. This must always * return the same number. * * @return The number of distinct styles used by this type of document. */ public abstract int getStyleCount(); /** * Returns the number of spaces to replace all tabs with. * * @return The number of spaces to replace all tabs with. */ public abstract int getTabSize(); /** * Adds a listener to monitor changes in the text of the document. * * @param listener The listener to add to this document. A null value will cause a * NullPointerException. */ public void addTextListener(TextListener listener) { if (listener == null) throw new NullPointerException(); try { writeLock(); textListeners.add(listener); } finally { writeUnlock(); } } /** * Removes a listener from this document. Nothing will happen if no match was found. * * @param listener The listener to remove from this document. A null value will cause a * NullPointerException. */ public void removeTextListener(TextListener listener) { if (listener == null) throw new NullPointerException(); try { writeLock(); textListeners.remove(listener); } finally { writeUnlock(); } } /** * Adds a listener to monitor undoable changes in the document. Duplicates will be ignored. * * @param listener The listener to add to this document. A null value will cause a * NullPointerException. */ public void addUndoableEditListener(UndoableEditListener listener) { if (listener == null) throw new NullPointerException(); try { writeLock(); undoableEditListeners.add(listener); } finally { writeUnlock(); } } /** * Removes a listener from this document. Nothing will happen if no match was found. * * @param listener The listener to remove from this document. A null value will cause a * NullPointerException. */ public void removeUndoableEditListener(UndoableEditListener listener) { if (listener == null) throw new NullPointerException(); try { writeLock(); undoableEditListeners.remove(listener); } finally { writeUnlock(); } } /** * Creates a position at a given offset which will move with the text. * * @param offset The offset at which the position will start. * @return The position object which will move with the text. */ public Position createPosition(int offset) { Position answer = null; if (offset < 0) throw new IllegalArgumentException(); try { writeLock(); if (offset > charCount) offset = charCount; answer = new Position(positionCtr, offset); positions.add(answer); positionCtr++; } finally { writeUnlock(); } return answer; } /** * Frees a position from future updates. * * @param position The position object to be detached. */ public void freePosition(Position position) { if (position == null) throw new NullPointerException(); try { writeLock(); positions.remove(position); } finally { writeUnlock(); } } /** * Creates a position triplet. * * @return The position triplet. */ public PositionTriplet createPositionTriplet() { PositionTriplet answer = null; try { writeLock(); answer = new PositionTriplet( new Position(positionCtr , 0), new Position(positionCtr + 1, 0), new Position(positionCtr + 2, 0) ); positions.add(answer.selectionDot); positions.add(answer.selectionMark); positions.add(answer.caretPosition); positionCtr += 3; } finally { writeUnlock(); } return answer; } /** * Frees a position triplet from future updates. * * @param positionTriplet The positionTriplet object to be detached. */ public void freePositionTriplet(PositionTriplet positionTriplet) { if (positionTriplet == null) throw new NullPointerException(); try { writeLock(); positions.remove(positionTriplet.selectionDot); positions.remove(positionTriplet.selectionMark); positions.remove(positionTriplet.caretPosition); } finally { writeUnlock(); } } /** * Returns an integer array containing a backup image of the position HashSet. */ protected int[] getPositionOffsets() { try { readLock(); Iterator iterator = positions.iterator(); int positionOffsets[] = new int[positionCtr]; while (iterator.hasNext()) { Position position = (Position)iterator.next(); positionOffsets[position.id] = position.offset; } return positionOffsets; } finally { readUnlock(); } } /** * Removes a segment of text specified by its offset and length. A TextEvent will be dispatched * to the listeners. * * @param offset The index of the start of the segment to remove. * @param length The length of the segment to remove. * @param mergable Wheter or not the UndoableEdit generated can be merged with appropriately * similar mergable UndoableEdits. */ public void remove(int offset, int length, boolean mergable) { replace(offset, length, "", mergable, true, null, false); } /** * Inserts a string at the specified index. A TextEvent and an UndoableEditEvent will be * dispatched to the listeners. * * @param offset The index at which to insert the string. * @param string The string to insert. * @param mergable Wheter or not the UndoableEdit generated can be merged with appropriately * similar mergable UndoableEdits. */ public void insertString(int offset, String string, boolean mergable) { replace(offset, 0, string, mergable, true, null, false); } /** * Replaces the text in a given range with the specified string. A TextEvent and an * UndoableEditEvent will be dispatched to the listeners. * * @param offset The offset of the range about to be replaced. * @param length The length of the range about to be replaced. * @param string The string with which to replace the range. * @param mergable Wheter or not the UndoableEdit generated can be merged with appropriately * similar mergable UndoableEdits. */ public void replace(int offset, int length, String string, boolean mergable) { replace(offset, length, string, mergable, true, null, false); } /** * Sets the text to the given string. A TextEvent will be dispatched to the listeners. * * @param string The string replace the old text with. */ public void setText(String string) { replace(0, Integer.MAX_VALUE, string, false, true, new int[0], true); } protected void utilReplace(int offset, int length, StringBuffer buffer, int newLength, int positionOffsets[], boolean disablePositionUpdate) { // Replace the characters char oldChars[] = chars; int difference = newLength - length; // Expand the array if necessary if (charCount + difference > chars.length) { chars = new char[2 * (charCount + difference)]; charStyles = new byte[2 * (charCount + difference)]; for (int ctr = 0; ctr < offset; ctr++) chars[ctr] = oldChars[ctr]; } // Shift the characters after the replace range, if necessary if (difference != 0 || chars != oldChars) { if (difference > 0) { int stopIndex = offset + length; for (int ctr = charCount - 1; ctr >= stopIndex; ctr--) chars[ctr + difference] = oldChars[ctr]; } else { for (int ctr = offset + length; ctr < charCount; ctr++) chars[ctr + difference] = oldChars[ctr]; } } // Copy the new characters from the buffer onto the character array buffer.getChars(0, newLength, chars, offset); charCount += difference; // Update the line structure lineOffsets[0] = (int)0; lineCount = 1; for (int ctr = 0; ctr < charCount; ctr++) { if (chars[ctr] == '\n') { if (lineCount == lineOffsets.length) { int oldLineOffsets[] = lineOffsets; lineOffsets = new int[lineCount * 2]; for (int ctr2 = 0; ctr2 < lineCount; ctr2++) lineOffsets[ctr2] = oldLineOffsets[ctr2]; } lineOffsets[lineCount] = (int)(ctr + 1); lineCount++; } } maxLineLength = charCount - lineOffsets[lineCount - 1]; for (int ctr = 0; ctr < lineCount - 1; ctr++) { int lineLength = lineOffsets[ctr + 1] - lineOffsets[ctr] - 1; if (lineLength > maxLineLength) maxLineLength = lineLength; } // Update the styles assignStyles(); if (!positions.isEmpty()) { // Restore the positions if necessary if (positionOffsets != null) { Iterator iterator = positions.iterator(); while (iterator.hasNext()) { Position position = (Position)iterator.next(); if (position.id < positionOffsets.length) { // If the offset is known, restore it position.offset = positionOffsets[position.id]; } else { // Otherwise, set it to 0 position.offset = 0; } } } // Update the positions if (!disablePositionUpdate) { Iterator iterator = positions.iterator(); while (iterator.hasNext()) { Position position = (Position)iterator.next(); int positionOffset = position.offset; if (positionOffset >= offset + length) { // The position was to the right of the range, shift it by the // difference position.offset = positionOffset + difference; } else if (positionOffset >= offset) { // The position was in the range, move it to the end of the replacement // text position.offset = offset + newLength; } else { // Do nothing } } } // Restoring position offsets and disabling the position update are dangerous // actions. We simply have to double check the work. if (positionOffsets != null || disablePositionUpdate) { Iterator iterator = positions.iterator(); while (iterator.hasNext()) { Position position = (Position)iterator.next(); int positionOffset = position.offset; if (positionOffset >= charCount) { // The position was too large position.offset = charCount; } else if (positionOffset < 0) { // The position was too small position.offset = 0; } else { // Do nothing } } } } } private String replaceString(String oldStr, String newStr, int index, int count) { StringBuilder sb = new StringBuilder(); char[] charArray = oldStr.toCharArray(); sb.append(charArray , 0, index).append(newStr); sb.append(charArray, index + count, charArray.length - (index + count)); return sb.toString(); } /** * Replaces the text in a given range with the specified string. And moves the positions around * (if a positions array is specified). A TextEvent and an UndoableEditEvent will be * dispatched to the listeners. * * @param offset The offset of the range about to be replaced. * @param length The length of the range about to be replaced. * @param string The string with which to replace the range. * @param mergable Wheter or not the UndoableEdit generated can be merged with appropriately * similar mergable UndoableEdits. * @param positionOffsets An id-indexed array of position offsets to restore (this will be done * before the position adjustment step). Leave as null if positions should be moved around * normally. * @param disablePositionUpdate Wheter or not to disable position update. */ protected void replace(int offset, int length, String string, boolean mergable, boolean generateUndoEvent, int positionOffsets[], boolean disablePositionUpdate) { if (offset < 0 || length < 0) throw new IllegalArgumentException(); if (string == null) throw new NullPointerException(); // remove non-standard characters StringBuffer buffer = filterString(string); int newLength = buffer.length(); if (length == 0 && newLength == 0 && positions == null) return; try { writeLock(); // if insertion offset is after last character in the document if (offset > charCount) offset = charCount; // if the replacement range exceeds the document end if (offset + length > charCount) length = charCount - offset; if (length == 0 && newLength == 0 && positions == null) return; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // (1) take a snapshot for the Undo String charsStr = new String(chars, 0, charCount); StringBuffer textBefore = new StringBuffer(charsStr); // (2) Insert the string without intervention utilReplace(offset, length, buffer, newLength, positionOffsets, disablePositionUpdate); // (3) Important computations int lineStart = offset - offsetToCoordinate(offset).x;; int lineEnd = coordinateToOffset(Integer.MAX_VALUE, offsetToCoordinate(offset + string.length()).y); String newLines = getText(lineStart, lineEnd); // (4) Parse every line in the new lines and insert pseudocode where needed String oldLines = new String(newLines); int index = 0; int oldIndex = 0; boolean changed = false; while (index < newLines.length()) { // read a line StringBuilder line = new StringBuilder(); while (index < newLines.length() && newLines.charAt(index) != '\n') { line.append(newLines.charAt(index)); index++; } // Check if this line needs pseudocode int lineLength = line.length(); TWord command; String pseudoCodeString = ""; boolean needsPseudo = true; if (lineLength < 8) { // line too short to have an instruction needsPseudo = false; } else if (!isCommand(line.substring(0, 8))) { // no instruction needsPseudo = false; } // Update the line only if it needs a pseudocode if (needsPseudo) { String commentedLine = TWord.commentLine(line.toString()); if (!commentedLine.equals(line.toString())) { index += commentedLine.length() - line.length(); newLines = replaceString(newLines, commentedLine, oldIndex, line.length()); changed = true; } } // bypass the \n index++; oldIndex = index; } if (changed) { StringBuffer bf = new StringBuffer(newLines); utilReplace(lineStart, oldLines.length(), bf, bf.length(), positionOffsets, disablePositionUpdate); } // Dispatch the events if (!textListeners.isEmpty()) { TextEvent e = new TextEvent(this, TextEvent.TEXT_VALUE_CHANGED); Object array[] = textListeners.toArray(); for (int ctr = 0; ctr < array.length; ctr++) ((TextListener)array[ctr]).textValueChanged(e); } if (generateUndoEvent && !undoableEditListeners.isEmpty()) { // take a snapshot of the current text document charsStr = new String(chars, 0, charCount); StringBuffer textAfter = new StringBuffer(charsStr); // create an undo command javax.swing.undo.UndoableEdit undoableEdit = null; undoableEdit = new UndoableEdit(0, textBefore, textAfter, mergable); UndoableEditEvent e = new UndoableEditEvent(this, undoableEdit); Object array[] = undoableEditListeners.toArray(); for (int ctr = 0; ctr < array.length; ctr++) ((UndoableEditListener)array[ctr]).undoableEditHappened(e); } } finally { writeUnlock(); } } /** * check if the given string is a TOY instruction */ private boolean isCommand(String str) { if (str.length() > 8) return false; for (int i = 0; i < str.length(); i++) { if ((i < 2 || i > 3) && (!TWord.isHexDigit(str.charAt(i)))) return false; else if (i == 2 && str.charAt(i) != ':') return false; else if (i == 3 && str.charAt(i) != ' ') return false; } return true; } /** * Updates the charStyles array based on chars, charCount, lineOffsets, and lineCount. The * implementation should assume that a write lock will have been obtained by the caller of this * function. */ protected abstract void assignStyles(); /** * Filters out all "non-standard" ASCII characters from the string. * * @param string The string to be filtered. A null value will result in a NullPointerException. * @return A StringBuffer containing the original string, without any "non-standard" ASCII * characters. */ public StringBuffer filterString(String string) { int length = string.length(); StringBuffer buffer = new StringBuffer(2 * length); // Traslate mac/windows newline styles and extended ASCII characters for (int ctr = 0; ctr < length; ctr++) { char character = string.charAt(ctr); switch (character) { // Translate Mac extended characters case (char)212: // standard ' case (char)213: // backward ' buffer.append('\''); break; case (char)210: // opening " case (char)211: // closing " buffer.append('\"'); break; // Translate Windows extended characters case (char)8216: // standard ' case (char)8217: // backward ' buffer.append('\''); break; case (char)8220: // opening " case (char)8221: // closing " buffer.append('\"'); break; // Convert carriage-returns to UNIX-style newlines case '\r': if (ctr + 1 < length && string.charAt(ctr + 1) == '\n') { // Windows uses "\r\n" buffer.append('\n'); ctr++; } else { // Mac uses "\r" buffer.append('\n'); } break; // Convert tabs into getTabSize() spaces case '\t': { int tabSize = getTabSize(); for (int ctr2 = 0; ctr2 < tabSize; ctr2++) buffer.append(' '); } break; // Only allow the printable ASCII characters as well as the space and newline default: // Only accept newlines, or printable ASCII if (character <= MAX_ALLOWED_CHARACTER && CHARACTER_ALLOWED[character]) { buffer.append(character); } } } return buffer; } /** * Returns the text of the document. * * @return The text of the document. */ public String getText() { return getText(0, Integer.MAX_VALUE); } /** * Returns part of the text of the document. * * @param start The offset at which to begin. * @param end The offset at which to end. * @return The text of the document. */ public String getText(int start, int end) { String answer; if (start < 0 || start > end) throw new IllegalArgumentException(); if (start == end) return ""; try { readLock(); if (start > charCount) start = charCount; if (end > charCount) end = charCount; answer = new String(chars, start, end - start); } finally { readUnlock(); } return answer; } /** * Returns the number of characters in the document. * * @return The number of characters in the document. */ public int getLength() { int answer; try { readLock(); answer = charCount; } finally { readUnlock(); } return answer; } /** * Returns the number of lines in the document. * * @return The number of lines in the document. */ public int getLineCount() { int answer; try { readLock(); answer = lineCount; } finally { readUnlock(); } return answer; } /** * Returns the length of the longest line. * * @return The length of the longest line. */ public int getMaxLineLength() { int answer; try { readLock(); answer = maxLineLength; } finally { readUnlock(); } return answer; } /** * Returns the bounds of the word containing the character at the given offset. The following * are the rules used to get the word bounds: *