package net.sourceforge.pmd.eclipse.ui.editors; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import net.sourceforge.pmd.util.StringUtil; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Display; /** * * @author Brian Remedios */ public class StyleExtractor { private SyntaxData syntaxData; private List<int[]> commentOffsets; private static final Color COMMENT_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GREEN); private static final Color REFERENCED_VAR_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GREEN); private static final Color UNREFERENCED_VAR_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_YELLOW); private static final Color COMMENT_BACKGROUND = Display.getCurrent().getSystemColor(SWT.COLOR_WHITE); private static final Color PUNCTUATION_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_BLACK); private static final Color KEYWORD_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_MAGENTA); private static final Color STRING_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_BLUE); public StyleExtractor(SyntaxData theSyntaxData) { syntaxData = theSyntaxData; commentOffsets = new LinkedList<int[]>(); } public void syntax(SyntaxData theSyntax) { syntaxData = theSyntax; } /** * Refreshes the offsets for all multiline comments in the parent StyledText. * The parent StyledText should call this whenever its text is modified. Note * that this code doesn't ignore comment markers inside strings. * * @param text * the text from the StyledText */ public void refreshMultilineComments(String text) { // Clear any stored offsets commentOffsets.clear(); if (syntaxData != null) { // Go through all the instances of COMMENT_START for (int pos = text.indexOf(syntaxData.getMultiLineCommentStart()); pos > -1; pos = text.indexOf(syntaxData.getMultiLineCommentStart(), pos)) { // offsets[0] holds the COMMENT_START offset // and COMMENT_END holds the ending offset int[] offsets = new int[2]; offsets[0] = pos; // Find the corresponding end comment. pos = text.indexOf(syntaxData.getMultiLineCommentEnd(), pos); // If no corresponding end comment, use the end of the text offsets[1] = pos == -1 ? text.length() - 1 : pos + syntaxData.getMultiLineCommentEnd().length() - 1; pos = offsets[1]; // Add the offsets to the collection commentOffsets.add(offsets); } } } /** * Checks to see if the specified section of text begins inside a multiline * comment. Returns the index of the closing comment, or the end of the line * if the whole line is inside the comment. Returns -1 if the line doesn't * begin inside a comment. * * @param start * the starting offset of the text * @param length * the length of the text * @return int */ private int getBeginsInsideComment(int start, int length) { // Assume section doesn't being inside a comment int index = -1; // Go through the multiline comment ranges for (int i = 0, n = commentOffsets.size(); i < n; i++) { int[] offsets = commentOffsets.get(i); // If starting offset is past range, quit if (offsets[0] > start + length) break; // Check to see if section begins inside a comment if (offsets[0] <= start && offsets[1] >= start) { // It does; determine if the closing comment marker is inside this section index = offsets[1] > start + length ? start + length : offsets[1] + syntaxData.getMultiLineCommentEnd().length() - 1; } } return index; } private boolean isDefinedVariable(String text) { return StringUtil.isNotEmpty(text); } private boolean atMultiLineCommentStart(String text, int position) { return text.indexOf(syntaxData.getMultiLineCommentStart(), position) == position; } private boolean atStringStart(String text, int position) { return text.indexOf(syntaxData.stringStart, position) == position; } private boolean atVarnameReference(String text, int position) { if (syntaxData.varnameReference == null) return false; return text.indexOf(syntaxData.varnameReference, position) == position; } private boolean atSingleLineComment(String text, int position) { if (syntaxData.getComment() == null) return false; return text.indexOf(syntaxData.getComment(), position) == position; } private int getKeywordEnd(String lineText, int start) { int length = lineText.length(); StringBuilder buf = new StringBuilder(length); int i = start; // Call any consecutive letters a word for (; i < length && Character.isLetter(lineText.charAt(i)); i++) { buf.append(lineText.charAt(i)); } return syntaxData.isKeyword(buf.toString()) ? i : 0-i; } /** * Chop up the text into individual lines starting from offset and * then determine the required styles for each. Ensures the offset * is properly accounted for in each. * * @param text * @param offset * @param length * @return */ public List<StyleRange> stylesFor(String text, int offset, int length, String lineSeparator) { if (syntaxData == null) return Collections.emptyList(); String content = text.substring(offset, offset + length); String[] lines = content.split(lineSeparator); List<StyleRange> styles = new ArrayList<StyleRange>(); int separatorLength = lineSeparator.length(); int currentOffset = offset; for (String line : lines) { int lineLength = line.length(); List<StyleRange> lineStyles = lineStylesFor(line, 0, lineLength); for (StyleRange sr : lineStyles) sr.start += currentOffset; styles.addAll(lineStyles); currentOffset += (lineLength + separatorLength); } return styles; } public List<StyleRange> lineStylesFor(String lineText, int lineOffset, int length) { List<StyleRange> styles = new ArrayList<StyleRange>(); int start = 0; // Check if line begins inside a multiline comment int mlIndex = getBeginsInsideComment(lineOffset, lineText.length()); if (mlIndex > -1) { // Line begins inside multiline comment; create the range styles.add(new StyleRange(lineOffset, mlIndex - lineOffset, COMMENT_COLOR, COMMENT_BACKGROUND)); start = mlIndex; } // Do punctuation, single-line comments, and keywords while (start < length) { // Check for multiline comments that begin inside this line if (atMultiLineCommentStart(lineText, start)) { // Determine where comment ends int endComment = lineText.indexOf(syntaxData.getMultiLineCommentEnd(), start); // If comment doesn't end on this line, extend range to end of line if (endComment == -1) endComment = length; else endComment += syntaxData.getMultiLineCommentEnd().length(); styles.add(new StyleRange(lineOffset + start, endComment - start, COMMENT_COLOR, COMMENT_BACKGROUND)); start = endComment; } else if (atStringStart(lineText, start)) { // Determine where comment ends int endString = lineText.indexOf(syntaxData.stringEnd, start+1); // If string doesn't end on this line, extend range to end of line if (endString == -1) endString = length; else endString += syntaxData.stringEnd.length(); styles.add(new StyleRange(lineOffset + start, endString - start, STRING_COLOR, COMMENT_BACKGROUND)); start = endString; } else if (atSingleLineComment(lineText, start)) { // Check for single line comments styles.add(new StyleRange(lineOffset + start, length - start, COMMENT_COLOR, COMMENT_BACKGROUND)); start = length; } else if (atVarnameReference(lineText, start)) { // Check for variable references StringBuilder buf = new StringBuilder(); int i = start + syntaxData.getVarnameReference().length(); // Call any consecutive letters a word for (; i < length && Character.isLetter(lineText.charAt(i)); i++) { buf.append(lineText.charAt(i)); } // See if the word is a variable if (isDefinedVariable(buf.toString())) { // It's a keyword; create the StyleRange styles.add(new StyleRange(lineOffset + start, i - start, REFERENCED_VAR_COLOR, null, SWT.BOLD)); } // Move the marker to the last char (the one that wasn't a letter) // so it can be retested in the next iteration through the loop start = i; } // Check for punctuation else if (syntaxData.isPunctuation(lineText.charAt(start))) { // Add range for punctuation styles.add(new StyleRange(lineOffset + start, 1, PUNCTUATION_COLOR, null)); ++start; } else if (Character.isLetter(lineText.charAt(start))) { int kwEnd = getKeywordEnd(lineText, start); // See if the word is a keyword if (kwEnd > start) { // Its a keyword; create the StyleRange styles.add(new StyleRange(lineOffset + start, kwEnd - start, KEYWORD_COLOR, null)); } // Move the marker to the last char (the one that wasn't a letter) // so it can be retested in the next iteration through the loop start = Math.abs(kwEnd); } else ++start; // It's nothing we're interested in; advance the marker } return styles; } }