/*
Original Version: Copyright (C) 2010 www.squidy-lib.de
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Modifications: Copyright (C) 2011-2012 John-Paul Verkamp
License: source-license.txt
If this code is used independently, copy the license here.
*/
package wombat.gui.text;
import javax.swing.SwingUtilities;
import javax.swing.text.*;
import wombat.util.Options;
import java.util.HashMap;
import java.util.Map;
/**
* Scheme document.
* Original source: http://www.squidy-lib.de/
* (heavily modified)
*/
public class SchemeDocument extends DefaultStyledDocument {
private static final long serialVersionUID = 8684954217591619402L;
// Can we replace lambdas?
public boolean AllowLambdaMode = true;
// For use in formatting.
DefaultStyledDocument doc;
Element rootElement;
boolean multiLineComment;
// Possible attributes.
static Map<String, MutableAttributeSet> attributes = new HashMap<String, MutableAttributeSet>();;
// For bracket matching (used to determine indentation).
int currentBracketStart = -1;
int currentBracketEnd = -1;
int nextBracketStart = -1;
int nextBracketEnd = -1;
// Reload all of the settings.
public static void reload() {
attributes.clear();
for (String key : "default keyword comment string bracket invalid-bracket".split(" "))
{
attributes.put(key, new SimpleAttributeSet());
StyleConstants.setFontSize(attributes.get(key), Options.FontSize);
}
for (String key : Options.Colors.keySet())
if (attributes.containsKey(key) && Options.Colors.containsKey(key))
StyleConstants.setForeground(attributes.get(key), Options.Colors.get(key));
}
/**
* Create a new Scheme document.
*/
public SchemeDocument() {
// Create the basic document.
doc = this;
rootElement = doc.getDefaultRootElement();
putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n");
}
/*
* Override to apply syntax highlighting after the document has been updated
*/
public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
super.insertString(offset, str, a);
processChangedLines(offset, str.length());
}
/*
* Override to apply syntax highlighting after the document has been updated
*/
public void remove(int offset, int length) throws BadLocationException {
super.remove(offset, length);
processChangedLines(offset, 0);
}
/*
* Determine how many lines have been changed,
* then apply highlighting to each line
*/
public void processChangedLines(int offset, int length) throws BadLocationException {
String content = doc.getText(0, doc.getLength());
// The lines affected by the latest document update
int startLine = rootElement.getElementIndex(offset);
int endLine = rootElement.getElementIndex(offset + length);
// Make sure all comment lines prior to the start line are commented
// and determine if the start line is still in a multi line comment
setMultiLineComment(commentLinesBefore(content, startLine));
// Do the actual highlighting
for (int i = startLine; i <= endLine; i++)
applyHighlighting(content, i);
// Resolve highlighting to the next end multi line delimiter
if (isMultiLineComment())
commentLinesAfter(content, endLine);
else
highlightLinesAfter(content, endLine);
}
/*
* Highlight lines when a multi line comment is still 'open'
* (ie. matching end delimiter has not yet been encountered)
*/
private boolean commentLinesBefore(String content, int line) {
int offset = rootElement.getElement(line).getStartOffset();
// If there isn't a start of a comment before the text, we're not in a multiline comment.
int startDelimiter = lastIndexOf(content, "#|", offset - 2);
if (startDelimiter < 0)
return false;
// Same thing for the end.
int endDelimiter = indexOf(content, "|#", startDelimiter);
if (endDelimiter < offset & endDelimiter != -1)
return false;
// We're in the middle, so the lines are a comment.
if (attributes.containsKey("comment"))
doc.setCharacterAttributes(startDelimiter, offset - startDelimiter + 1, attributes.get("comment"), true);
return true;
}
/*
* Highlight comment lines to matching end delimiter
*/
private void commentLinesAfter(String content, int line) {
int offset = rootElement.getElement(line).getEndOffset();
// End of comment not found, nothing to do
int endDelimiter = indexOf(content, "|#", offset);
if (endDelimiter < 0)
return;
// Matching start/end of comment found, comment the lines
int startDelimiter = lastIndexOf(content, "#|", endDelimiter);
if (startDelimiter < 0 || startDelimiter <= offset) {
if (attributes.containsKey("comment"))
doc.setCharacterAttributes(offset, endDelimiter - offset + 1, attributes.get("comment"), true);
}
}
/*
* Highlight lines to start or end delimiter
*/
private void highlightLinesAfter(String content, int line)
throws BadLocationException {
int offset = rootElement.getElement(line).getEndOffset();
// Start/End delimiter not found, nothing to do
int startDelimiter = indexOf(content, "#|", offset);
int endDelimiter = indexOf(content, "|#", offset);
if (startDelimiter < 0)
startDelimiter = content.length();
if (endDelimiter < 0)
endDelimiter = content.length();
int delimiter = Math.min(startDelimiter, endDelimiter);
if (delimiter < offset)
return;
// Start/End delimiter found, reapply highlighting
int endLine = rootElement.getElementIndex(delimiter);
for (int i = line + 1; i < endLine; i++) {
Element branch = rootElement.getElement(i);
Element leaf = doc.getCharacterElement(branch.getStartOffset());
AttributeSet as = leaf.getAttributes();
if (attributes.containsKey("comment"))
if (as.isEqual(attributes.get("comment")))
applyHighlighting(content, i);
}
}
/*
* Parse the line to determine the appropriate highlighting
*/
private void applyHighlighting(String content, int line) throws BadLocationException {
int startOffset = rootElement.getElement(line).getStartOffset();
int endOffset = rootElement.getElement(line).getEndOffset() - 1;
int lineLength = endOffset - startOffset;
int contentLength = content.length();
if (endOffset >= contentLength)
endOffset = contentLength - 1;
// check for multi line comments
// (always set the comment attribute for the entire line)
if (endingMultiLineComment(content, startOffset, endOffset)
|| isMultiLineComment()
|| startingMultiLineComment(content, startOffset, endOffset)) {
if (attributes.containsKey("comment"))
doc.setCharacterAttributes(startOffset, endOffset - startOffset + 1, attributes.get("comment"), true);
return;
}
// set default attributes for the line
if (attributes.containsKey("default"))
doc.setCharacterAttributes(startOffset, lineLength, attributes.get("default"), true);
// check for single line comment
int index = content.indexOf(';', startOffset);
if ((index > -1) && (index < endOffset)) {
if (attributes.containsKey("comment"))
doc.setCharacterAttributes(index, endOffset - index + 1, attributes.get("comment"), true);
endOffset = index - 1;
}
// check for tokens
checkForTokens(content, startOffset, endOffset);
}
/*
* Does this line contain the start delimiter
*/
private boolean startingMultiLineComment(String content, int startOffset, int endOffset)
throws BadLocationException {
int index = indexOf(content, "#|", startOffset);
if ((index < 0) || (index > endOffset))
return false;
else {
setMultiLineComment(true);
return true;
}
}
/*
* Does this line contain the end delimiter
*/
private boolean endingMultiLineComment(String content, int startOffset, int endOffset) throws BadLocationException {
int index = indexOf(content, "|#", startOffset);
if ((index < 0) || (index > endOffset))
return false;
else {
setMultiLineComment(false);
return true;
}
}
/*
* We have found a start delimiter
* and are still searching for the end delimiter
*/
private boolean isMultiLineComment() {
return multiLineComment;
}
private void setMultiLineComment(boolean value) {
multiLineComment = value;
}
/*
* Parse the line for tokens to highlight
*/
private void checkForTokens(String content, int startOffset, int endOffset) {
while (startOffset <= endOffset) {
// skip the delimiters to find the start of a new token
while (isDelimiter(content.substring(startOffset, startOffset + 1))) {
if (startOffset < endOffset)
startOffset++;
else
return;
}
// Extract and process the entire token
if (content.charAt(startOffset) == '"')
startOffset = getQuoteToken(content, startOffset, endOffset);
else
startOffset = getOtherToken(content, startOffset, endOffset);
}
}
/*
*
*/
private int getQuoteToken(String content, int startOffset, int endOffset) {
String quoteDelimiter = content.substring(startOffset, startOffset + 1);
String escapeString = "\\\"";
int index;
int endOfQuote = startOffset;
// skip over the escape quotes in this quote
index = content.indexOf(escapeString, endOfQuote + 1);
while ((index > -1) && (index < endOffset)) {
endOfQuote = index + 1;
index = content.indexOf(escapeString, endOfQuote);
}
// now find the matching delimiter
index = content.indexOf(quoteDelimiter, endOfQuote + 1);
if ((index < 0) || (index > endOffset))
endOfQuote = endOffset;
else
endOfQuote = index;
if (attributes.containsKey("string"))
doc.setCharacterAttributes(startOffset, endOfQuote - startOffset + 1, attributes.get("string"), true);
return endOfQuote + 1;
}
/*
*
*/
private int getOtherToken(String content, int startOffset, int endOffset) {
int endOfToken = startOffset + 1;
while (endOfToken <= endOffset) {
if (isDelimiter(content.substring(endOfToken, endOfToken + 1)))
break;
endOfToken++;
}
String token = content.substring(startOffset, endOfToken);
// When we see a lambda, remember later to fix it for lambda mode.
final SchemeDocument me = this;
// Lambda / Greek mode may be disabled
if (AllowLambdaMode) {
// Check for full Greek mode
if (!Options.LambdaMode) {
for (final String[] pair : Options.GreekModeCharacters) {
if ((Options.GreekMode && pair[1].equals(token)) || (!Options.GreekMode && pair[0].equals(token))) {
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
final String from = Options.GreekMode ? pair[1] : pair[0];
final String to = Options.GreekMode ? pair[0] : pair[1];
int i = -1;
try {
while ((i = me.getText(0, me.getLength()).indexOf(from)) != -1) {
me.remove(i, from.length());
me.insertString(i, to, attributes.get("keyword"));
}
} catch (BadLocationException e) {
e.printStackTrace();
}
}
});
}
}
}
// If not Greek, try to fall back on Lambda mode
if (!Options.GreekMode) {
if ((Options.LambdaMode && "lambda".equals(token)) || (!Options.LambdaMode && "\u03BB".equals(token))) {
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
final String from = Options.LambdaMode ? "lambda" : "\u03BB";
final String to = Options.LambdaMode ? "\u03BB" : "lambda";
int i = -1;
try {
while ((i = me.getText(0, me.getLength()).indexOf(from)) != -1) {
me.remove(i, from.length());
me.insertString(i, to, attributes.get("keyword"));
}
} catch (BadLocationException e) {
e.printStackTrace();
}
}
});
}
}
}
if (Options.Keywords.containsKey(token)) {
if (attributes.containsKey("keyword"))
doc.setCharacterAttributes(startOffset, endOfToken - startOffset, attributes.get("keyword"), true);
}
return endOfToken + 1;
}
/*
* Assume the needle will the found at the start/end of the line
*/
private int indexOf(String content, String needle, int offset) {
int index;
while ((index = content.indexOf(needle, offset)) != -1) {
String text = getLine(content, index).trim();
if (text.startsWith(needle) || text.endsWith(needle))
break;
else
offset = index + 1;
}
return index;
}
/*
* Assume the needle will the found at the start/end of the line
*/
private int lastIndexOf(String content, String needle, int offset) {
int index;
while ((index = content.lastIndexOf(needle, offset)) != -1) {
String text = getLine(content, index).trim();
if (text.startsWith(needle) || text.endsWith(needle))
break;
else
offset = index - 1;
}
return index;
}
private String getLine(String content, int offset) {
int line = rootElement.getElementIndex(offset);
Element lineElement = rootElement.getElement(line);
int start = lineElement.getStartOffset();
int end = lineElement.getEndOffset();
return content.substring(start, end - 1);
}
/*
* Override for other languages
*/
protected boolean isDelimiter(String character) {
return (Character.isWhitespace(character.charAt(0)) || "()[]#|".indexOf(character) != -1);
}
}