/* * codjo.net * * Common Apache License 2.0 */ package net.codjo.dataprocess.gui.util.editor; import java.awt.Color; import java.util.HashMap; import java.util.Map; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultEditorKit; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.Element; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; /** * */ public class SyntaxDocument extends DefaultStyledDocument { public static final String STYLE_NORMAL = "@" + System.currentTimeMillis() + "_STYLE_NORMAL"; public static final String STYLE_COMMENT = "@" + System.currentTimeMillis() + "_STYLE_COMMENT"; public static final String STYLE_QUOTE = "@" + System.currentTimeMillis() + "_STYLE_QUOTE"; public static final String STYLE_KEYWORD = "@" + System.currentTimeMillis() + "_STYLE_KEYWORD"; private DefaultStyledDocument doc; private Element rootElement; private boolean multiLineComment; private Map<String, SimpleAttributeSet> styles; private Map<String, SimpleAttributeSet> keywordsToStyles; public SyntaxDocument() { doc = this; rootElement = doc.getDefaultRootElement(); putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n"); keywordsToStyles = new HashMap<String, SimpleAttributeSet>(); styles = new HashMap<String, SimpleAttributeSet>(); addNewStyle(STYLE_NORMAL, Color.black, null, false, false, false); addNewStyle(STYLE_COMMENT, new Color(0, 120, 0), null, false, true, false); addNewStyle(STYLE_QUOTE, new Color(140, 0, 0), null, false, false, false); addNewStyle(STYLE_KEYWORD, new Color(0, 0, 140), null, true, false, false); } public void addKeyWord(String keyWord) { addKeyWord(keyWord, STYLE_NORMAL); } public void addKeyWord(String keyWord, String styleName) { if (!styles.keySet().contains(styleName)) { throw new IllegalArgumentException("Style '" + styleName + "' unkown !"); } keywordsToStyles.put(keyWord, styles.get(styleName)); } public void addNewStyle(String name, SimpleAttributeSet attributeSet) { styles.put(name, attributeSet); } public void addNewStyle(String name, Color foreGroundColor, Color backGroundColor, boolean bold, boolean italic, boolean underline) { SimpleAttributeSet attributeSet = new SimpleAttributeSet(); if (backGroundColor != null) { StyleConstants.setBackground(attributeSet, backGroundColor); } StyleConstants.setForeground(attributeSet, foreGroundColor); StyleConstants.setBold(attributeSet, bold); StyleConstants.setItalic(attributeSet, italic); StyleConstants.setUnderline(attributeSet, underline); styles.put(name, attributeSet); } /* * Override to apply syntax highlighting after the document has been updated */ @Override public void insertString(int offset, String str, AttributeSet attributeSet) throws BadLocationException { super.insertString(offset, str, attributeSet); processChangedLines(offset, str.length()); } /* * Override to apply syntax highlighting after the document has been updated */ @Override public void remove(int offset, int length) throws BadLocationException { super.remove(offset, length); processChangedLines(offset, 0); } public void replaceWord(WordElement currendWordElement, String replacedBy) throws BadLocationException { remove(currendWordElement.getStartOffset(), currendWordElement.getFullWord().length()); insertString(currendWordElement.getStartOffset(), replacedBy, new SimpleAttributeSet()); } public WordElement getCurrentWordElement(int carretPosition) throws BadLocationException { //change le carret position en index de caracter int startWordOffset = carretPosition > 0 ? carretPosition - 1 : carretPosition; int endWordOffset = startWordOffset; //recup�re les donn�es de la ligne int element = rootElement.getElementIndex(startWordOffset); int startLineOffset = rootElement.getElement(element).getStartOffset(); int endLineOffset = rootElement.getElement(element).getEndOffset(); if (endLineOffset > getLength()) { endLineOffset = getLength(); } //remonte le mot while (startWordOffset > startLineOffset && !isDelimiter(getText(startWordOffset - 1, 1))) { startWordOffset--; } //avance dans le mot while (endWordOffset <= endLineOffset && !isDelimiter(getText(endWordOffset + 1, 1))) { endWordOffset++; } return new WordElement(getText(startWordOffset, endWordOffset - startWordOffset + 1), startWordOffset, carretPosition - startWordOffset); } /* * Determine how many lines have been changed, * then apply highlighting to each line */ private 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 startOffset line are commented // and determine if the startOffset 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(); // Start of comment not found, nothing to do int startDelimiter = lastIndexOf(content, getStartDelimiter(), offset - 2); if (startDelimiter < 0) { return false; } // Matching startOffset/end of comment found, nothing to do int endDelimiter = indexOf(content, getEndDelimiter(), startDelimiter); if (endDelimiter < offset & endDelimiter != -1) { return false; } // End of comment not found, highlight the lines doc.setCharacterAttributes(startDelimiter, offset - startDelimiter + 1, styles.get(STYLE_COMMENT), false); 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, getEndDelimiter(), offset); if (endDelimiter < 0) { return; } // Matching startOffset/end of comment found, comment the lines int startDelimiter = lastIndexOf(content, getStartDelimiter(), endDelimiter); if (startDelimiter < 0 || startDelimiter <= offset) { doc.setCharacterAttributes(offset, endDelimiter - offset + 1, styles.get(STYLE_COMMENT), false); } } /* * Highlight lines to startOffset 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, getStartDelimiter(), offset); int endDelimiter = indexOf(content, getEndDelimiter(), 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 (as.isEqual(styles.get(STYLE_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)) { doc.setCharacterAttributes(startOffset, endOffset - startOffset + 1, styles.get(STYLE_COMMENT), false); return; } // set normal attributes for the line doc.setCharacterAttributes(startOffset, lineLength, styles.get(STYLE_NORMAL), true); // check for single line comment int index = content.indexOf(getSingleLineDelimiter(), startOffset); if ((index > -1) && (index < endOffset)) { doc.setCharacterAttributes(index, endOffset - index + 1, styles.get(STYLE_COMMENT), false); endOffset = index - 1; } // check for tokens checkForTokens(content, startOffset, endOffset); } /* * Does this line contain the startOffset delimiter */ private boolean startingMultiLineComment(String content, int startOffset, int endOffset) throws BadLocationException { int index = indexOf(content, getStartDelimiter(), 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, getEndDelimiter(), startOffset); if ((index < 0) || (index > endOffset)) { return false; } else { setMultiLineComment(false); return true; } } /* * We have found a startOffset 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 startOffset of a new token while (isDelimiter(content.substring(startOffset, startOffset + 1))) { if (startOffset < endOffset) { startOffset++; } else { return; } } // Extract and process the entire token if (isQuoteDelimiter(content.substring(startOffset, startOffset + 1))) { 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 = getEscapeString(quoteDelimiter); 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; } doc.setCharacterAttributes(startOffset, endOfQuote - startOffset + 1, styles.get(STYLE_QUOTE), false); 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); if (isKeyword(token)) { doc.setCharacterAttributes(startOffset, endOfToken - startOffset, keywordsToStyles.get(token), false); } return endOfToken + 1; } /* * Assume the needle will the found at the startOffset/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 startOffset/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) { String operands = ",;:{}()[]+-/%<=>!&|^~*"; return Character.isWhitespace(character.charAt(0)) || operands.contains(character); } /* * Override for other languages */ protected boolean isQuoteDelimiter(String character) { String quoteDelimiters = "\"'"; return quoteDelimiters.contains(character); } /* * Override for other languages */ protected boolean isKeyword(String token) { return keywordsToStyles.keySet().contains(token); } /* * Override for other languages */ protected String getStartDelimiter() { return "/*"; } /* * Override for other languages */ protected String getEndDelimiter() { return "*/"; } /* * Override for other languages */ protected String getSingleLineDelimiter() { return "//"; } /* * Override for other languages */ protected String getEscapeString(String quoteDelimiter) { return "\\" + quoteDelimiter; } public static class WordElement { private String fullWord; private int startOffset; private final int cutPoint; public WordElement(String fullWord, int start, int cutPoint) { this.fullWord = fullWord; this.startOffset = start; this.cutPoint = cutPoint; } public String getFullWord() { return fullWord; } public String getPrefix() { return fullWord.substring(0, cutPoint); } public String getSuffix() { return fullWord.substring(cutPoint); } public int getStartOffset() { return startOffset; } } }