blob: c1501627f6c029e479cb7a631aac3608a23fdf69 [file] [log] [blame]
/*
* Copyright 2003-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package groovy.ui.text;
import javax.swing.text.*;
import java.nio.CharBuffer;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author Evan "Hippy" Slatis
*/
public class StructuredSyntaxDocumentFilter extends DocumentFilter {
public static final String TAB_REPLACEMENT = " ";
private static final MLComparator ML_COMPARATOR = new MLComparator();
/**
* The root of the lexical parsing tree.
*/
protected LexerNode lexer = new LexerNode(true);
// The styled document the filter parses
protected DefaultStyledDocument styledDocument;
// the document buffer and segment
private Segment segment = new Segment();
private CharBuffer buffer;
/**
* The position tree of multi-line comments.
*/
protected TreeSet mlTextRunSet = new TreeSet(ML_COMPARATOR);
// Ensures not adding any regexp with capturing groups
private static void checkRegexp(String regexp) {
String checking = regexp.replaceAll("\\\\\\(", "X").replaceAll("\\(\\?", "X");
int checked = checking.indexOf('(');
if (checked > -1) {
String msg = "Only non-capturing groups allowed:\r\n" +
regexp + "\r\n";
for (int i = 0; i < checked; i++) {
msg += " ";
}
msg += "^";
throw new IllegalArgumentException(msg);
}
}
/**
* Creates a new instance of StructuredSyntaxDocumentFilter
* @param document the styled document to parse
*/
public StructuredSyntaxDocumentFilter(DefaultStyledDocument document) {
this.styledDocument = document;
}
private int calcBeginParse(int offset) {
MultiLineRun mlr = getMultiLineRun(offset);
if (mlr != null) {
// means we're in middle of mlr, so start at beginning of mlr
offset = mlr.start();
}
else {
// otherwise, earliest position in line not part of mlr
offset = styledDocument.getParagraphElement(offset).getStartOffset();
mlr = getMultiLineRun(offset);
offset = mlr == null ? offset : mlr.end() + 1;
}
return offset;
}
private int calcEndParse(int offset) {
MultiLineRun mlr = getMultiLineRun(offset);
if (mlr != null) {
// means we're in middle of mlr, so end is at end of mlr
offset = mlr.end();
}
else {
// otherwise, latest position in line not part of mlr
offset = styledDocument.getParagraphElement(offset).getEndOffset();
mlr = getMultiLineRun(offset);
offset = mlr == null ? offset : mlr.end();
}
return offset;
}
/**
* Create a new LexerNode for adding to root.
*
* @return a new LexerNode
*/
public LexerNode createLexerNode() {
return new LexerNode(false);
}
// given an offset, return the mlr it resides in
private MultiLineRun getMultiLineRun(int offset) {
MultiLineRun ml = null;
if (offset > 0) {
Integer os = new Integer(offset);
SortedSet set = mlTextRunSet.headSet(os);
if (!set.isEmpty()) {
ml = (MultiLineRun)set.last();
ml = ml.end() >= offset ? ml : null;
}
}
return ml;
}
/**
* Get the root node for lexing the document. Children can be added such
* that matching patterns can be further parsed if required.
*
* @return the root lexing node.
*/
public LexerNode getRootNode() {
return lexer;
}
/**
* Insert a string into the document, and then parse it if the parser has been
* set.
*
* @param fb
* @param offset
* @param text
* @param attrs
* @throws BadLocationException
*/
public void insertString(DocumentFilter.FilterBypass fb, int offset,
String text, AttributeSet attrs)
throws BadLocationException {
// remove problem meta characters returns
text = replaceMetaCharacters(text);
fb.insertString(offset, text, attrs);
// start on the string that was inserted
parseDocument(offset, text.length());
}
/**
* Parse the Document to update the character styles given an initial start
* position. Called by the filter after it has updated the text.
*
* @param offset
* @param length
* @throws BadLocationException
*/
protected void parseDocument(int offset, int length) throws BadLocationException {
// intialize the segment with the complete document so the segment doesn't
// have an underlying gap in the buffer
styledDocument.getText(0, styledDocument.getLength(), segment);
buffer = CharBuffer.wrap(segment.array).asReadOnlyBuffer();
// initialize the lexer if necessary
if (!lexer.isInitialized()) {
// prime the parser and reparse whole document
lexer.initialize();
offset = 0;
length = styledDocument.getLength();
}
else {
int end = offset + length;
offset = calcBeginParse(offset);
length = calcEndParse(end) - offset;
// clean the tree by ensuring multi line styles are reset in area
// of parsing
SortedSet set = mlTextRunSet.subSet(new Integer(offset),
new Integer(offset + length));
if (set != null) {
set.clear();
}
}
// parse the document
lexer.parse(buffer, offset, length);
}
/**
* Remove a string from the document, and then parse it if the parser has been
* set.
*
* @param fb
* @param offset
* @param length
* @throws BadLocationException
*/
public void remove(DocumentFilter.FilterBypass fb, int offset, int length)
throws BadLocationException {
// FRICKIN' HACK!!!!! For some reason, deleting a string at offset 0
// does not get done properly, so first replace and remove after parsing
if (offset == 0 && length != fb.getDocument().getLength()) {
fb.replace(0, length, "\n", lexer.defaultStyle);
// start on either side of the removed text
parseDocument(offset, 2);
fb.remove(offset, 1);
}
else {
fb.remove(offset, length);
// start on either side of the removed text
if (offset + 1 < fb.getDocument().getLength()) {
parseDocument(offset, 1);
}
else if (offset - 1 > 0) {
parseDocument(offset - 1, 1);
}
else {
// empty text
mlTextRunSet.clear();
}
}
}
/**
* Replace a string in the document, and then parse it if the parser has been
* set.
*
* @param fb
* @param offset
* @param length
* @param text
* @param attrs
* @throws BadLocationException
*/
public void replace(DocumentFilter.FilterBypass fb, int offset,
int length, String text, AttributeSet attrs)
throws BadLocationException {
// remove problem meta characters returns
text = replaceMetaCharacters(text);
fb.replace(offset, length, text, attrs);
// start on the text that was replaced
parseDocument(offset, text.length());
}
// tabs with spaces (I hate tabs)
private String replaceMetaCharacters(String string) {
// just in case remove carriage returns
string = string.replaceAll("\\t", TAB_REPLACEMENT);
return string;
}
public final class LexerNode {
private Style defaultStyle;
private Map styleMap = new LinkedHashMap();
private Map children = new HashMap();
private Matcher matcher;
private List groupList = new ArrayList();
private boolean initialized;
private CharBuffer lastBuffer;
/*
* Creates a new instance of LexerNode
*/
LexerNode(boolean isParent) {
StyleContext sc = StyleContext.getDefaultStyleContext();
defaultStyle = sc.getStyle(StyleContext.DEFAULT_STYLE);
}
private String buildRegexp(String[] regexps) {
String regexp = "";
for (int i = 0; i < regexps.length; i++) {
regexp += "|" + regexps[i];
}
// ensure leading '|' is removed
return regexp.substring(1);
}
public Style getDefaultStyle() {
return defaultStyle;
}
private void initialize() {
matcher = null;
groupList.clear();
groupList.add(null);
Iterator iter = styleMap.keySet().iterator();
String regexp = "";
while (iter.hasNext()) {
String nextRegexp = (String)iter.next();
regexp += "|(" + nextRegexp + ")";
// have to compile regexp first so that it will match
groupList.add(Pattern.compile(nextRegexp).pattern());
}
if (!regexp.equals("")) {
matcher = Pattern.compile(regexp.substring(1)).matcher("");
iter = children.values().iterator();
while (iter.hasNext()) {
((LexerNode)iter.next()).initialize();
}
}
initialized = true;
}
/**
* @return true if initialised
*/
public boolean isInitialized() {
return initialized;
}
/**
* @param buffer
* @param offset
* @param length
* @throws BadLocationException
*/
public void parse(CharBuffer buffer, int offset, int length)
throws BadLocationException {
// get the index of where we can start to look for an exit:
// i.e. after the end of the length of the segment, when we find
// that text in question already is set properly, we can stop
// parsing
int checkPoint = offset + length;
// reset the matcher and start parsing string
if (lastBuffer != buffer) {
matcher.reset(buffer);
lastBuffer = buffer;
}
// the start and end indices of a match in the Matcher looking
int matchEnd = offset;
Style style = null;
while (matchEnd < checkPoint && matcher.find(offset)) {
// when we get something other than -1, we know which regexp
// matched; the 0 group is the complete expression of the
// matcher, which would always return a hit based on the above
// while condition
int groupNum = 0;
while ((offset = matcher.start(++groupNum)) == -1){
}
// if the matching offset is not the same as the end of the
// previous match, we have extra text not matched, so set to
// the default style of this lexer node
if (offset != matchEnd) {
offset = offset > checkPoint ? checkPoint : offset;
styledDocument.setCharacterAttributes(matchEnd,
offset - matchEnd,
defaultStyle,
true);
if (offset >= checkPoint) {
return;
}
}
// track the end of the matching string
matchEnd = matcher.end(groupNum);
// retrieve the proper style from groupNum of the groupList and
// styleMap, then set the attributes of the matching string
style = (Style)styleMap.get((String)groupList.get(groupNum));
styledDocument.setCharacterAttributes(offset,
matchEnd - offset,
style, true);
// if the match was multiline, which we'll know if they span
// multiple paragraph elements, the mark it (this list was cleaned
// above in parseDocument())
if (styledDocument.getParagraphElement(offset).getStartOffset() !=
styledDocument.getParagraphElement(matchEnd).getStartOffset()) {
// mark a ml run
MultiLineRun mlr = new MultiLineRun(offset, matchEnd);
mlTextRunSet.add(mlr);
}
// parse the child regexps, if any, within a matched block
LexerNode node = (LexerNode)children.get(groupList.get(groupNum));
if (node != null) {
node.parse(buffer, offset, matchEnd - offset);
}
// set the offset to start where we left off
offset = matchEnd;
}
if (matchEnd < checkPoint) {
// if we finished before hitting the end of the checkpoint from
// no mroe matches, then set ensure the text is reset to the
// defaultStyle
styledDocument.setCharacterAttributes(matchEnd,
checkPoint - matchEnd,
defaultStyle,
true);
}
}
/**
*
* @param regexp
* @param node
*/
public void putChild(String regexp, LexerNode node) {
node.defaultStyle = (Style)styleMap.get(regexp);
// have to compile regexp first so that it will match
children.put(Pattern.compile(regexp).pattern(), node);
initialized = false;
}
/**
* @param regexps
* @param node
*/
public void putChild(String[] regexps, LexerNode node) {
putChild(buildRegexp(regexps), node);
}
/**
* @param regexp
* @param style
*/
public void putStyle(String regexp, Style style) {
checkRegexp(regexp);
styleMap.put(regexp, style);
initialized = false;
}
/**
* @param regexps
* @param style
*/
public void putStyle(String regexps[], Style style) {
putStyle(buildRegexp(regexps), style);
}
/**
* @param regexp
*/
public void removeChild(String regexp) {
children.remove(regexp);
}
/**
* @param regexp
*/
public void removeStyle(String regexp) {
styleMap.remove(regexp);
children.remove(regexp);
}
/**
* @param regexps
*/
public void removeStyle(String regexps[]) {
removeStyle(buildRegexp(regexps));
}
public void setDefaultStyle(Style style) {
defaultStyle = style;
}
}
protected class MultiLineRun {
private Position start;
private Position end;
private int delimeterSize;
public MultiLineRun(int start, int end) throws BadLocationException {
this(start, end, 2);
}
public MultiLineRun(int start, int end, int delimeterSize) throws BadLocationException {
if (start > end) {
String msg = "Start offset is after end: ";
throw new BadLocationException(msg, start);
}
if (delimeterSize < 1) {
String msg = "Delimiters be at least size 1: " +
delimeterSize;
throw new IllegalArgumentException(msg);
}
this.start = styledDocument.createPosition(start);
this.end = styledDocument.createPosition(end);
this.delimeterSize = delimeterSize;
}
public int getDelimeterSize() {
return delimeterSize;
}
public int end() {
return end.getOffset();
}
public int length() {
return end.getOffset() - start.getOffset();
}
public int start() {
return start.getOffset();
}
public String toString() {
return start.toString() + " " + end.toString();
}
}
private static class MLComparator implements Comparator {
public int compare(Object obj, Object obj1) {
return valueOf(obj) - valueOf(obj1);
}
private int valueOf(Object obj) {
return obj instanceof Integer ?
((Integer)obj).intValue() :
(obj instanceof MultiLineRun) ?
((MultiLineRun)obj).start() :
((Position)obj).getOffset();
}
}
}