/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package edu.mit.csail.sdg.alloy4; import java.awt.Color; import java.awt.Font; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultEditorKit; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.TabSet; import javax.swing.text.TabStop; import static edu.mit.csail.sdg.alloy4.OurConsole.style; /** Graphical syntax-highlighting StyledDocument. * * <p><b>Thread Safety:</b> Can be called only by the AWT event thread */ class OurSyntaxDocument extends DefaultStyledDocument { /** This ensures the class can be serialized reliably. */ private static final long serialVersionUID = 0; /** The "comment mode" at the start of each line (0 = no comment) (1 = block comment) (2 = javadoc comment) (-1 = unknown) */ private final List<Integer> comments = new ArrayList<Integer>(); /** Whether syntax highlighting is currently enabled or not. */ private boolean enabled = true; /** The current font name is. */ private String font = "Monospaced"; /** The current font size. */ private int fontSize = 14; /** The current tab size. */ private int tabSize = 4; /** The list of font+color styles (eg. regular text, symbols, keywords, comments, etc). */ private final List<MutableAttributeSet> all = new ArrayList<MutableAttributeSet>(); /** The character style for regular text. */ private final MutableAttributeSet styleNormal = style(font, fontSize, false, Color.BLACK, 0); { all.add(styleNormal); } /** The character style for symbols. */ private final MutableAttributeSet styleSymbol = style(font, fontSize, true, Color.BLACK, 0); { all.add(styleSymbol); } /** The character style for integer constants. */ private final MutableAttributeSet styleNumber = style(font, fontSize, true, new Color(0xA80A0A), 0); { all.add(styleNumber); } /** The character style for keywords. */ private final MutableAttributeSet styleKeyword = style(font, fontSize, true, new Color(0x1E1EA8), 0); { all.add(styleKeyword); } /** The character style for string literals. */ private final MutableAttributeSet styleString = style(font, fontSize, false, new Color(0xA80AA8), 0); { all.add(styleString); } /** The character style for up-to-end-of-line-style comment. */ private final MutableAttributeSet styleComment = style(font, fontSize, false, new Color(0x0A940A), 0); { all.add(styleComment); } /** The character style for non-javadoc-style block comment. */ private final MutableAttributeSet styleBlock = style(font, fontSize, false, new Color(0x0A940A), 0); { all.add(styleBlock); } /** The character style for javadoc-style block comment. */ private final MutableAttributeSet styleJavadoc = style(font, fontSize, true, new Color(0x0A940A), 0); { all.add(styleJavadoc); } /** The paragraph style for indentation. */ private final MutableAttributeSet tabset = new SimpleAttributeSet(); /** This stores the currently recognized set of reserved keywords. */ private static final String[] keywords = new String[] {"abstract", "all", "and", "as", "assert", "but", "check", "disj", "disjoint", "else", "enum", "exactly", "exh", "exhaustive", "expect", "extends", "fact", "for", "fun", "iden", "iff", "implies", "in", "Int", "int", "let", "lone", "module", "no", "none", "not", "one", "open", "or", "part", "partition", "pred", "private", "run", "seq", "set", "sig", "some", "String", "sum", "this", "univ" }; /** Returns true if array[start .. start+len-1] matches one of the reserved keyword. */ private static final boolean do_keyword(String array, int start, int len) { if (len >= 2 && len <= 10) for(int i = keywords.length - 1; i >= 0; i--) { String str = keywords[i]; if (str.length()==len) for(int j=0; ;j++) if (j==len) return true; else if (str.charAt(j) != array.charAt(start+j)) break; } return false; } /** Returns true if "c" can be in the start or middle or end of an identifier. */ private static final boolean do_iden(char c) { return (c>='A' && c<='Z') || (c>='a' && c<='z') || c=='$' || (c>='0' && c<='9') || c=='_' || c=='\'' || c=='\"'; } /** Constructor. */ public OurSyntaxDocument(String fontName, int fontSize) { putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n"); tabSize++; do_setFont(fontName, fontSize, tabSize - 1); // assigns the given font, and also forces recomputation of the tab size } /** Enables or disables syntax highlighting. */ public final void do_enableSyntax (boolean flag) { if (enabled == flag) return; else { enabled = flag; comments.clear(); } if (flag) do_reapplyAll(); else setCharacterAttributes(0, getLength(), styleNormal, false); } /** Return the number of lines represented by the current text (where partial line counts as a line). * <p> For example: count("")==1, count("x")==1, count("x\n")==2, and count("x\ny")==2 */ public final int do_getLineCount() { String txt = toString(); for(int n=txt.length(), ans=1, i=0; ; i++) if (i>=n) return ans; else if (txt.charAt(i)=='\n') ans++; } /** Return the starting offset of the given line (If "line" argument is too large, it will return the last line's starting offset) * <p> For example: given "ab\ncd\n", start(0)==0, start(1)==3, start(2...)==6. Same thing when given "ab\ncd\ne". */ public final int do_getLineStartOffset(int line) { String txt = toString(); for(int n=txt.length(), ans=0, i=0, y=0; ; i++) if (i>=n || y>=line) return ans; else if (txt.charAt(i)=='\n') {ans=i+1; y++;} } /** Return the line number that the offset is in (If "offset" argument is too large, it will just return do_getLineCount()-1). * <p> For example: given "ab\ncd\n", offset(0..2)==0, offset(3..5)==1, offset(6..)==2. Same thing when given "ab\ncd\ne". */ public final int do_getLineOfOffset(int offset) { String txt = toString(); for(int n=txt.length(), ans=0, i=0; ; i++) if (i>=n || i>=offset) return ans; else if (txt.charAt(i)=='\n') ans++; } /** This method is called by Swing to insert a String into this document. * We intentionally ignore "attr" and instead use our own coloring. */ @Override public void insertString(int offset, String string, AttributeSet attr) throws BadLocationException { if (string.indexOf('\r')>=0) string = Util.convertLineBreak(string); // we don't want '\r' if (!enabled) { super.insertString(offset, string, styleNormal); return; } int startLine = do_getLineOfOffset(offset); for(int i = 0; i < string.length(); i++) { // For each inserted '\n' we need to shift the values in "comments" array down if (string.charAt(i)=='\n') { if (startLine < comments.size()-1) comments.add(startLine+1, -1); } } super.insertString(offset, string, styleNormal); try { do_update(startLine); } catch(Exception ex) { comments.clear(); } } /** This method is called by Swing to delete text from this document. */ @Override public void remove(int offset, int length) throws BadLocationException { if (!enabled) { super.remove(offset, length); return; } int i = 0, startLine = do_getLineOfOffset(offset); for(String oldText = toString(); i<length; i++) { // For each deleted '\n' we need to shift the values in "comments" array up if (oldText.charAt(offset+i)=='\n') if (startLine < comments.size()-1) comments.remove(startLine+1); } super.remove(offset, length); try { do_update(startLine); } catch(Exception ex) { comments.clear(); } } /** This method is called by Swing to replace text in this document. */ @Override public void replace(int offset, int length, String string, AttributeSet attrs) throws BadLocationException { if (length > 0) this.remove(offset, length); if (string != null && string.length() > 0) this.insertString(offset, string, styleNormal); } /** Reapply styles assuming the given line has just been modified */ private final void do_update(int line) throws BadLocationException { String content = toString(); int lineCount = do_getLineCount(); while(line>0 && (line>=comments.size() || comments.get(line)<0)) line--; // "-1" in comments array are always contiguous int comment = do_reapply(line==0 ? 0 : comments.get(line), content, line); for (line++; line < lineCount; line++) { // update each subsequent line until it already starts with its expected comment mode if (line < comments.size() && comments.get(line) == comment) break; else comment = do_reapply(comment, content, line); } } /** Re-color the given line assuming it starts with a given comment mode, then return the comment mode for start of next line. */ private final int do_reapply(int comment, final String txt, final int line) { while (line >= comments.size()) comments.add(-1); // enlarge array if needed comments.set(line, comment); // record the fact that this line starts with the given comment mode for(int n = txt.length(), i = do_getLineStartOffset(line); i < n;) { final int oldi = i; final char c = txt.charAt(i); if (c=='\n') break; if (comment==0 && c=='/' && i<n-3 && txt.charAt(i+1)=='*' && txt.charAt(i+2)=='*' && txt.charAt(i+3)!='/') comment = 2; if (comment==0 && c=='/' && i==n-3 && txt.charAt(i+1)=='*' && txt.charAt(i+2)=='*') comment = 2; if (comment==0 && c=='/' && i<n-1 && txt.charAt(i+1)=='*') { comment = 1; i = i + 2; } if (comment>0) { AttributeSet style = (comment==1 ? styleBlock : styleJavadoc); while(i<n && txt.charAt(i)!='\n' && (txt.charAt(i)!='*' || i+1==n || txt.charAt(i+1)!='/')) i = i + 1; if (i<n-1 && txt.charAt(i)=='*' && txt.charAt(i+1)=='/') { i = i + 2; comment = 0; } setCharacterAttributes(oldi, i-oldi, style, false); } else if ((c=='/' || c=='-') && i<n-1 && txt.charAt(i+1)==c) { i = txt.indexOf('\n', i); setCharacterAttributes(oldi, i<0 ? (n-oldi) : (i-oldi), styleComment, false); break; } else if (c=='\"') { for(i++; i<n; i++) { if (txt.charAt(i)=='\n') break; if (txt.charAt(i)=='\"') {i++; break;} if (txt.charAt(i)=='\\' && i+1<n && txt.charAt(i+1)!='\n') i++; } setCharacterAttributes(oldi, i-oldi, styleString, false); } else if (do_iden(c)) { for(i++; i<n && do_iden(txt.charAt(i)); i++) { } AttributeSet style = (c>='0' && c<='9') ? styleNumber : (do_keyword(txt, oldi, i-oldi) ? styleKeyword : styleNormal); setCharacterAttributes(oldi, i-oldi, style, false); } else { for(i++; i<n && !do_iden(txt.charAt(i)) && txt.charAt(i)!='\n' && txt.charAt(i)!='-' && txt.charAt(i)!='/'; i++) { } setCharacterAttributes(oldi, i-oldi, styleSymbol, false); } } return comment; } /** Reapply the appropriate style to the entire document. */ private final void do_reapplyAll() { setCharacterAttributes(0, getLength(), styleNormal, true); comments.clear(); String content = toString(); for(int comment = 0, i = 0, n = do_getLineCount(); i < n; i++) comment = do_reapply(comment, content, i); } /** Changes the font and tabsize for the document. */ public final void do_setFont(String fontName, int fontSize, int tabSize) { if (tabSize < 1) tabSize = 1; else if (tabSize > 100) tabSize = 100; if (fontName.equals(this.font) && fontSize == this.fontSize && tabSize == this.tabSize) return; this.font = fontName; this.fontSize = fontSize; this.tabSize = tabSize; for(MutableAttributeSet s: all) { StyleConstants.setFontFamily(s, fontName); StyleConstants.setFontSize(s, fontSize); } do_reapplyAll(); BufferedImage im = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB); // this is used to derive the tab width int gap = tabSize * im.createGraphics().getFontMetrics(new Font(fontName, Font.PLAIN, fontSize)).charWidth('X'); TabStop[] pos = new TabStop[100]; for(int i=0; i<100; i++) { pos[i] = new TabStop(i*gap + gap); } StyleConstants.setTabSet(tabset, new TabSet(pos)); setParagraphAttributes(0, getLength(), tabset, false); } /** Overriden to return the full text of the document. * @return the entire text */ @Override public String toString() { try { return getText(0, getLength()); } catch(BadLocationException ex) { return ""; } } }