/*
* Copyright 2009-2016 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 org.codehaus.groovy.eclipse.editor;
import java.util.ArrayList;
import java.util.List;
import org.codehaus.groovy.eclipse.GroovyPlugin;
import org.codehaus.groovy.eclipse.core.preferences.PreferenceConstants;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.internal.ui.text.AbstractJavaScanner;
import org.eclipse.jdt.internal.ui.text.CombinedWordRule;
import org.eclipse.jdt.internal.ui.text.CombinedWordRule.WordMatcher;
import org.eclipse.jdt.internal.ui.text.JavaWhitespaceDetector;
import org.eclipse.jdt.internal.ui.text.JavaWordDetector;
import org.eclipse.jdt.ui.text.IColorManager;
import org.eclipse.jface.text.rules.ICharacterScanner;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.IWhitespaceDetector;
import org.eclipse.jface.text.rules.IWordDetector;
import org.eclipse.jface.text.rules.SingleLineRule;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.jface.text.rules.WhitespaceRule;
/**
* A code scanner for Groovy files.
*
* Much of this class has been adapted from {@link JavaCodeScanner}
*
* @author Andrew Eisenberg
* @created Dec 31, 2010
*/
public class GroovyTagScanner extends AbstractJavaScanner {
/**
* Rule to detect java operators.
*
* @since 3.0
*/
protected class OperatorRule implements IRule {
/** Java operators */
private final char[] JAVA_OPERATORS = { ';', '(', ')', '{', '}', '.', '=', '/', '\\', '+', '-', '*', '[', ']', '<', '>', ':', '?', '!', ',', '|', '&', '^', '%', '~' };
/** Token to return for this rule */
private final IToken fToken;
/**
* Creates a new operator rule.
*
* @param token Token to use for this rule
*/
public OperatorRule(IToken token) {
fToken = token;
}
/**
* Is this character an operator character?
*
* @param character Character to determine whether it is an operator
* character
* @return <code>true</code> iff the character is an operator,
* <code>false</code> otherwise.
*/
public boolean isOperator(char character) {
for (int index = 0; index < JAVA_OPERATORS.length; index++) {
if (JAVA_OPERATORS[index] == character)
return true;
}
return false;
}
/*
* @see
* org.eclipse.jface.text.rules.IRule#evaluate(org.eclipse.jface.text
* .rules.ICharacterScanner)
*/
public IToken evaluate(ICharacterScanner scanner) {
int character = scanner.read();
if (isOperator((char) character)) {
do {
character = scanner.read();
} while (isOperator((char) character));
scanner.unread();
return fToken;
} else {
scanner.unread();
return Token.UNDEFINED;
}
}
}
/**
* Rule to detect java brackets.
*
* @since 3.3
*/
private static final class BracketRule implements IRule {
/** Java brackets */
private final char[] JAVA_BRACKETS = { '(', ')', '{', '}', '[', ']' };
/** Token to return for this rule */
private final IToken fToken;
/**
* Creates a new bracket rule.
*
* @param token Token to use for this rule
*/
public BracketRule(IToken token) {
fToken = token;
}
/**
* Is this character a bracket character?
*
* @param character Character to determine whether it is a bracket
* character
* @return <code>true</code> iff the character is a bracket,
* <code>false</code> otherwise.
*/
public boolean isBracket(char character) {
for (int index = 0; index < JAVA_BRACKETS.length; index++) {
if (JAVA_BRACKETS[index] == character)
return true;
}
return false;
}
/*
* @see
* org.eclipse.jface.text.rules.IRule#evaluate(org.eclipse.jface.text
* .rules.ICharacterScanner)
*/
public IToken evaluate(ICharacterScanner scanner) {
int character = scanner.read();
if (isBracket((char) character)) {
do {
character = scanner.read();
} while (isBracket((char) character));
scanner.unread();
return fToken;
} else {
scanner.unread();
return Token.UNDEFINED;
}
}
}
/**
* An annotation rule matches the '@' symbol, any following whitespace and
* a following java identifier or the <code>interface</code> keyword.
*
* It does not match if there is a comment between the '@' symbol and
* the identifier. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=82452
*
* @since 3.1
*/
private static class AnnotationRule implements IRule {
/**
* A resettable scanner supports marking a position in a scanner and
* unreading back to the marked position.
*/
private static final class ResettableScanner implements ICharacterScanner {
private final ICharacterScanner fDelegate;
private int fReadCount;
/**
* Creates a new resettable scanner that will forward calls
* to <code>scanner</code>, but store a marked position.
*
* @param scanner the delegate scanner
*/
public ResettableScanner(final ICharacterScanner scanner) {
Assert.isNotNull(scanner);
fDelegate = scanner;
mark();
}
/*
* @see org.eclipse.jface.text.rules.ICharacterScanner#getColumn()
*/
public int getColumn() {
return fDelegate.getColumn();
}
/*
* @see
* org.eclipse.jface.text.rules.ICharacterScanner#getLegalLineDelimiters
* ()
*/
public char[][] getLegalLineDelimiters() {
return fDelegate.getLegalLineDelimiters();
}
/*
* @see org.eclipse.jface.text.rules.ICharacterScanner#read()
*/
public int read() {
int ch = fDelegate.read();
if (ch != ICharacterScanner.EOF)
fReadCount++;
return ch;
}
/*
* @see org.eclipse.jface.text.rules.ICharacterScanner#unread()
*/
public void unread() {
if (fReadCount > 0)
fReadCount--;
fDelegate.unread();
}
/**
* Marks an offset in the scanned content.
*/
public void mark() {
fReadCount = 0;
}
/**
* Resets the scanner to the marked position.
*/
public void reset() {
while (fReadCount > 0)
unread();
while (fReadCount < 0)
read();
}
}
/**
* Do not mark '@' followed by newline; even if it's legal it's uncommon.
* Doing sp could be confusing when an incomplete annotation is present
* and the modifier of a method or field is highlighted as an annotation.
*/
private final IWhitespaceDetector fWhitespaceDetector = new IWhitespaceDetector() {
public boolean isWhitespace(char ch) {
return Character.isWhitespace(ch) && ch != '\n' && ch != '\r';
}
};
private final IWordDetector fWordDetector = new JavaWordDetector();
private final IToken fInterfaceToken;
private final IToken fAnnotationToken;
/**
* Creates a new rule.
*
* @param interfaceToken the token to return if
* <code>'@\s*interface'</code> is matched
* @param annotationToken the token to return if <code>'@\s*\w+'</code>
* is matched, but not <code>'@\s*interface'</code>
*/
public AnnotationRule(IToken interfaceToken, Token annotationToken) {
fInterfaceToken = interfaceToken;
fAnnotationToken = annotationToken;
}
/*
* @see
* org.eclipse.jface.text.rules.IRule#evaluate(org.eclipse.jface.text
* .rules.ICharacterScanner)
*/
public IToken evaluate(ICharacterScanner scanner) {
ResettableScanner resettable = new ResettableScanner(scanner);
if (resettable.read() == '@')
if (skipWhitespace(resettable))
return readAnnotation(resettable);
resettable.reset();
return Token.UNDEFINED;
}
private IToken readAnnotation(ResettableScanner scanner) {
StringBuffer buffer = new StringBuffer();
if (!readIdentifier(scanner, buffer)) {
scanner.reset();
return Token.UNDEFINED;
}
if ("interface".equals(buffer.toString())) //$NON-NLS-1$
return fInterfaceToken;
while (readSegment(new ResettableScanner(scanner))) {
// do nothing
}
return fAnnotationToken;
}
private boolean readSegment(ResettableScanner scanner) {
scanner.mark();
if (skipWhitespace(scanner) && skipDot(scanner) && skipWhitespace(scanner) && readIdentifier(scanner, null))
return true;
scanner.reset();
return false;
}
private boolean skipDot(ICharacterScanner scanner) {
int ch = scanner.read();
if (ch == '.')
return true;
scanner.unread();
return false;
}
private boolean readIdentifier(ICharacterScanner scanner, StringBuffer buffer) {
int ch = scanner.read();
boolean read = false;
while (fWordDetector.isWordPart((char) ch)) {
if (buffer != null)
buffer.append((char) ch);
ch = scanner.read();
read = true;
}
if (ch != ICharacterScanner.EOF)
scanner.unread();
return read;
}
private boolean skipWhitespace(ICharacterScanner scanner) {
while (fWhitespaceDetector.isWhitespace((char) scanner.read())) {
// do nothing
}
scanner.unread();
return true;
}
}
private static final String[] types = {
"boolean",
"byte",
"char",
"class",
"double",
"float",
"int",
"interface",
"long",
"short",
"void"
};
private static final String[] keywords = {
"abstract",
"assert",
"break",
"case",
"catch",
"const",
"continue",
"default",
"do",
"else",
"enum",
"extends",
"false",
"final",
"finally",
"for",
"goto",
"if",
"implements",
"import",
"instanceof",
"interface",
"native",
"new",
"null",
"package",
"private",
"protected",
"public",
//"return", use the special return keyword now so returns can be highlighted differently
"static",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"true",
"try",
"void",
"volatile",
"while",
};
private static final String[] groovyKeywords = {
"as",
"in",
"def",
"trait",
};
private static final String returnKeyword = "return";
private static final String[] fgTokenProperties = {
PreferenceConstants.GROOVY_EDITOR_DEFAULT_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_GJDK_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_JAVAKEYWORDS_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_GROOVYKEYWORDS_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_JAVATYPES_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_ANNOTATION_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_BRACKET_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_OPERATOR_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_RETURN_COLOR,
PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_STRINGS_COLOR,
};
private final List<IRule> initialAdditionalRules;
private final List<IRule> additionalRules;
private final List<String> additionalGroovyKeywords;
private final List<String> additionalGJDKWords;
/**
* @deprecated
*/
@Deprecated
public GroovyTagScanner(IColorManager manager) {
this(manager, null, null, null);
}
/**
* @param manager the color manager
* @param additionalRules Additional scanner rules for sub-types to add new kinds of partitioning
* @param additionalGroovyKeywords Additional keywords for sub-types to add new kinds of syntax highlighting
* @deprecated use the syntaxHighlightingExtender extension point instead. This gets all of the additional keyword
* highlighting into editors of files in a project with a particular nature.
*/
@Deprecated
public GroovyTagScanner(IColorManager manager, List<IRule> initialAdditionalRules, List<IRule> additionalRules, List<String> additionalGroovyKeywords) {
this(manager, initialAdditionalRules, additionalRules, additionalGroovyKeywords, null);
}
/**
* @param manager the color manager
* @param additionalRules Additional scanner rules for sub-types to add new kinds of partitioning
* @param additionalGroovyKeywords Additional keywords for sub-types to add new kinds of groovy keyword syntax highlighting
* @param additionalGJDKKeywords Additional keywords for sub-types to add new kinds of gjdk syntax highlightin
*/
public GroovyTagScanner(IColorManager manager, List<IRule> initialAdditionalRules, List<IRule> additionalRules, List<String> additionalGroovyKeywords, List<String> additionalGJDKKeywords) {
super(manager, GroovyPlugin.getDefault().getPreferenceStore());
this.initialAdditionalRules = initialAdditionalRules;
this.additionalRules = additionalRules;
this.additionalGroovyKeywords = additionalGroovyKeywords;
this.additionalGJDKWords = additionalGJDKKeywords;
initialize();
}
@Override
protected String[] getTokenProperties() {
return fgTokenProperties;
}
@Override
protected List<IRule> createRules() {
List<IRule> rules = new ArrayList<IRule>();
// initial additional rules
if (initialAdditionalRules != null) {
rules.addAll(initialAdditionalRules);
}
// Add rule for character constants.
Token token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_STRINGS_COLOR);
rules.add(new SingleLineRule("'", "'", token, '\\')); //$NON-NLS-2$ //$NON-NLS-1$
// Add generic whitespace rule.
rules.add(new WhitespaceRule(new JavaWhitespaceDetector()));
// Add JLS3 rule for /@\s*interface/
AnnotationRule atInterfaceRule = new AnnotationRule(
getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_ANNOTATION_COLOR),
getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_ANNOTATION_COLOR));
rules.add(atInterfaceRule);
// combined rule for all keywords
JavaWordDetector wordDetector = new JavaWordDetector();
token = getToken(PreferenceConstants.GROOVY_EDITOR_DEFAULT_COLOR);
CombinedWordRule combinedWordRule = new CombinedWordRule(wordDetector, token);
// Java keywords
WordMatcher javaKeywordsMatcher = new WordMatcher();
token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_JAVAKEYWORDS_COLOR);
for (int i = 0; i < keywords.length; i++) {
javaKeywordsMatcher.addWord(keywords[i], token);
}
combinedWordRule.addWordMatcher(javaKeywordsMatcher);
// Java types
WordMatcher javaTypesMatcher = new WordMatcher();
token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_JAVATYPES_COLOR);
for (int i = 0; i < types.length; i++) {
javaTypesMatcher.addWord(types[i], token);
}
combinedWordRule.addWordMatcher(javaTypesMatcher);
// Groovy Keywords, including additional keywords
WordMatcher groovyKeywordsMatcher = new WordMatcher();
token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_GROOVYKEYWORDS_COLOR);
for (int i = 0; i < groovyKeywords.length; i++) {
groovyKeywordsMatcher.addWord(groovyKeywords[i], token);
}
if (additionalGroovyKeywords != null) {
for (String additional : additionalGroovyKeywords) {
groovyKeywordsMatcher.addWord(additional, token);
}
}
combinedWordRule.addWordMatcher(groovyKeywordsMatcher);
// additional GJDK words
WordMatcher gjdkWordsMatcher = new WordMatcher();
token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_GJDK_COLOR);
if (additionalGJDKWords != null) {
for (String additional : additionalGJDKWords) {
gjdkWordsMatcher.addWord(additional, token);
}
}
combinedWordRule.addWordMatcher(gjdkWordsMatcher);
// Add rule for brackets
// this rule must come before the operator rule
token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_BRACKET_COLOR);
rules.add(new BracketRule(token));
// Add rule for operators
token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_OPERATOR_COLOR);
rules.add(new OperatorRule(token));
// Add word rule for keyword 'return'.
CombinedWordRule.WordMatcher returnWordRule = new CombinedWordRule.WordMatcher();
token = getToken(PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_RETURN_COLOR);
returnWordRule.addWord(returnKeyword, token);
combinedWordRule.addWordMatcher(returnWordRule);
rules.add(combinedWordRule);
// additional rules
if (additionalRules != null) {
rules.addAll(additionalRules);
}
setDefaultReturnToken(getToken(PreferenceConstants.GROOVY_EDITOR_DEFAULT_COLOR));
return rules;
}
}