diff --git a/app/src/processing/app/syntax/JEditTextArea.java b/app/src/processing/app/syntax/JEditTextArea.java index d2ef145a9..010db63cd 100644 --- a/app/src/processing/app/syntax/JEditTextArea.java +++ b/app/src/processing/app/syntax/JEditTextArea.java @@ -620,7 +620,7 @@ public class JEditTextArea extends JComponent * @param offset The offset, from the start of the line */ public int _offsetToX(int line, int offset) { - TokenMarker tokenMarker = getTokenMarker(); + TokenMarkerState tokenMarker = getTokenMarker(); // Use painter's cached info for speed FontMetrics fm = painter.getFontMetrics(); @@ -682,7 +682,7 @@ public class JEditTextArea extends JComponent * @param x The x co-ordinate */ public int xToOffset(int line, int x) { - TokenMarker tokenMarker = getTokenMarker(); + TokenMarkerState tokenMarker = getTokenMarker(); /* Use painter's cached info for speed */ FontMetrics fm = painter.getFontMetrics(); @@ -855,7 +855,7 @@ public class JEditTextArea extends JComponent * Returns the document's token marker. Equivalent to calling * getDocument().getTokenMarker(). */ - public final TokenMarker getTokenMarker() { + public final TokenMarkerState getTokenMarker() { return document.getTokenMarker(); } @@ -1680,7 +1680,7 @@ public class JEditTextArea extends JComponent int segmentOffset = segment.offset; int segmentCount = segment.count; - TokenMarker tokenMarker = doc.getTokenMarker(); + TokenMarkerState tokenMarker = doc.getTokenMarker(); // If syntax coloring is disabled, do simple translation if (tokenMarker == null) { for (int j = 0; j < segmentCount; j++) { diff --git a/app/src/processing/app/syntax/PdeTextAreaPainter.java b/app/src/processing/app/syntax/PdeTextAreaPainter.java index 290cdfc41..24232f23e 100644 --- a/app/src/processing/app/syntax/PdeTextAreaPainter.java +++ b/app/src/processing/app/syntax/PdeTextAreaPainter.java @@ -113,7 +113,7 @@ public class PdeTextAreaPainter extends TextAreaPainter { * @param x horizontal position */ @Override - protected void paintLine(Graphics gfx, int line, int x, TokenMarker marker) { + protected void paintLine(Graphics gfx, int line, int x, TokenMarkerState marker) { try { // TODO This line is causing NPEs randomly ever since I added the // toggle for Java Mode/Debugger toolbar. [Manindra] @@ -346,4 +346,4 @@ public class PdeTextAreaPainter extends TextAreaPainter { public PdeTextArea getPdeTextArea() { return (PdeTextArea) textArea; } -} \ No newline at end of file +} diff --git a/app/src/processing/app/syntax/SyntaxDocument.java b/app/src/processing/app/syntax/SyntaxDocument.java index 25b50d69a..cead84439 100644 --- a/app/src/processing/app/syntax/SyntaxDocument.java +++ b/app/src/processing/app/syntax/SyntaxDocument.java @@ -27,7 +27,7 @@ public class SyntaxDocument extends PlainDocument * of this document up into tokens. May return null if this * document is not to be colorized. */ - public TokenMarker getTokenMarker() + public TokenMarkerState getTokenMarker() { return tokenMarker; } @@ -40,9 +40,11 @@ public class SyntaxDocument extends PlainDocument */ public void setTokenMarker(TokenMarker tm) { - tokenMarker = tm; - if(tm == null) + if (tm == null) { + tokenMarker = null; return; + } + tokenMarker = tm.createStateInstance(); tokenMarker.insertLines(0,getDefaultRootElement() .getElementCount()); tokenizeLines(); @@ -67,7 +69,7 @@ public class SyntaxDocument extends PlainDocument */ public void tokenizeLines(int start, int len) { - if(tokenMarker == null || !tokenMarker.supportsMultilineTokens()) + if(tokenMarker == null || !tokenMarker.marker.supportsMultilineTokens()) return; Segment lineSegment = new Segment(); @@ -118,7 +120,7 @@ public class SyntaxDocument extends PlainDocument public void addUndoableEdit(UndoableEdit edit) {} // protected members - protected TokenMarker tokenMarker; + protected TokenMarkerState tokenMarker; /** * We overwrite this method to update the token marker diff --git a/app/src/processing/app/syntax/TextAreaPainter.java b/app/src/processing/app/syntax/TextAreaPainter.java index 251e6cb8b..fd7c23da2 100644 --- a/app/src/processing/app/syntax/TextAreaPainter.java +++ b/app/src/processing/app/syntax/TextAreaPainter.java @@ -488,7 +488,7 @@ public class TextAreaPainter extends JComponent implements TabExpander { int lastInvalid = firstLine + (clipRect.y + clipRect.height - 1) / height; try { - TokenMarker tokenMarker = textArea.getDocument().getTokenMarker(); + TokenMarkerState tokenMarker = textArea.getDocument().getTokenMarker(); int x = textArea.getHorizontalOffset(); for (int line = firstInvalid; line <= lastInvalid; line++) { @@ -611,7 +611,7 @@ public class TextAreaPainter extends JComponent implements TabExpander { // Font defaultFont = getFont(); // Color defaultColor = getForeground(); protected void paintLine(Graphics gfx, int line, int x, - TokenMarker tokenMarker) { + TokenMarkerState tokenMarker) { currentLineIndex = line; int y = textArea.lineToY(line); @@ -671,7 +671,7 @@ public class TextAreaPainter extends JComponent implements TabExpander { // int line, Font defaultFont, // Color defaultColor, int x, int y) { protected void paintSyntaxLine(Graphics gfx, int line, int x, int y, - TokenMarker tokenMarker) { + TokenMarkerState tokenMarker) { textArea.getLineText(currentLineIndex, currentLine); currentLineTokens = tokenMarker.markTokens(currentLine, currentLineIndex); diff --git a/app/src/processing/app/syntax/TokenMarker.java b/app/src/processing/app/syntax/TokenMarker.java index 0e414e901..57d76e6b7 100644 --- a/app/src/processing/app/syntax/TokenMarker.java +++ b/app/src/processing/app/syntax/TokenMarker.java @@ -15,7 +15,7 @@ import javax.swing.text.Segment; * A token marker that splits lines of text into tokens. Each token carries * a length field and an indentification tag that can be mapped to a color * for painting that token.

- * + *

* For performance reasons, the linked list of tokens is reused after each * line is tokenized. Therefore, the return value of markTokens * should only be used for immediate painting. Notably, it cannot be @@ -23,321 +23,68 @@ import javax.swing.text.Segment; * * @author Slava Pestov */ -public abstract class TokenMarker -{ - abstract public void addColoring(String keyword, String coloring); +public abstract class TokenMarker { - /** - * A wrapper for the lower-level markTokensImpl method - * that is called to split a line up into tokens. - * @param line The line - * @param lineIndex The line number - */ - public Token markTokens(Segment line, int lineIndex) - { - if(lineIndex >= length) - { - throw new IllegalArgumentException("Tokenizing invalid line: " - + lineIndex); - } + public interface TokenListener { + void addToken(int length, byte id); + } - lastToken = null; + // Only needed during markTokensImpl() call so addToken() can be forwarded + private TokenListener tokenListener = null; - LineInfo info = lineInfo[lineIndex]; - LineInfo prev; - if(lineIndex == 0) - prev = null; - else - prev = lineInfo[lineIndex - 1]; + public final void setTokenListener(TokenListener listener) { + this.tokenListener = listener; + } - byte oldToken = info.token; - byte token = markTokensImpl(prev == null ? - Token.NULL : prev.token,line,lineIndex); + public final TokenMarkerState createStateInstance() { + return new TokenMarkerState(this); + } - info.token = token; + /** + * Creates a new TokenMarker. This DOES NOT create + * a lineInfo array; an initial call to insertLines() + * does that. + */ + protected TokenMarker() { } - /* - * This is a foul hack. It stops nextLineRequested - * from being cleared if the same line is marked twice. - * - * Why is this necessary? It's all JEditTextArea's fault. - * When something is inserted into the text, firing a - * document event, the insertUpdate() method shifts the - * caret (if necessary) by the amount inserted. - * - * All caret movement is handled by the select() method, - * which eventually pipes the new position to scrollTo() - * and calls repaint(). - * - * Note that at this point in time, the new line hasn't - * yet been painted; the caret is moved first. - * - * scrollTo() calls offsetToX(), which tokenizes the line - * unless it is being called on the last line painted - * (in which case it uses the text area's painter cached - * token list). What scrollTo() does next is irrelevant. - * - * After scrollTo() has done it's job, repaint() is - * called, and eventually we end up in paintLine(), whose - * job is to paint the changed line. It, too, calls - * markTokens(). - * - * The problem was that if the line started a multiline - * token, the first markTokens() (done in offsetToX()) - * would set nextLineRequested (because the line end - * token had changed) but the second would clear it - * (because the line was the same that time) and therefore - * paintLine() would never know that it needed to repaint - * subsequent lines. - * - * This bug took me ages to track down, that's why I wrote - * all the relevant info down so that others wouldn't - * duplicate it. - */ - if(!(lastLine == lineIndex && nextLineRequested)) - nextLineRequested = (oldToken != token); + abstract public void addColoring(String keyword, String coloring); - lastLine = lineIndex; + /** + * An abstract method that splits a line up into tokens. It + * should parse the line, and call addToken() to + * add syntax tokens to the token list. Then, it should return + * the initial token type for the next line.

+ *

+ * For example if the current line contains the start of a + * multiline comment that doesn't end on that line, this method + * should return the comment token type so that it continues on + * the next line. + * + * @param token The initial token type for this line + * @param line The line to be tokenized + * @param lineIndex The index of the line in the document, + * starting at 0 + * @return The initial token type for the next line + */ + protected abstract byte markTokensImpl(byte token, Segment line, + int lineIndex); - addToken(0,Token.END); + protected final void addToken(int length, byte id) { + if (tokenListener != null) { + tokenListener.addToken(length, id); + } + } - return firstToken; - } - - /** - * An abstract method that splits a line up into tokens. It - * should parse the line, and call addToken() to - * add syntax tokens to the token list. Then, it should return - * the initial token type for the next line.

- * - * For example if the current line contains the start of a - * multiline comment that doesn't end on that line, this method - * should return the comment token type so that it continues on - * the next line. - * - * @param token The initial token type for this line - * @param line The line to be tokenized - * @param lineIndex The index of the line in the document, - * starting at 0 - * @return The initial token type for the next line - */ - protected abstract byte markTokensImpl(byte token, Segment line, - int lineIndex); - - /** - * Returns if the token marker supports tokens that span multiple - * lines. If this is true, the object using this token marker is - * required to pass all lines in the document to the - * markTokens() method (in turn).

- * - * The default implementation returns true; it should be overridden - * to return false on simpler token markers for increased speed. - */ - public boolean supportsMultilineTokens() - { - return true; - } - - /** - * Informs the token marker that lines have been inserted into - * the document. This inserts a gap in the lineInfo - * array. - * @param index The first line number - * @param lines The number of lines - */ - public void insertLines(int index, int lines) - { - if(lines <= 0) - return; - length += lines; - ensureCapacity(length); - int len = index + lines; - System.arraycopy(lineInfo,index,lineInfo,len, - lineInfo.length - len); - - for(int i = index + lines - 1; i >= index; i--) - { - lineInfo[i] = new LineInfo(); - } - } - - /** - * Informs the token marker that line have been deleted from - * the document. This removes the lines in question from the - * lineInfo array. - * @param index The first line number - * @param lines The number of lines - */ - public void deleteLines(int index, int lines) - { - if (lines <= 0) - return; - int len = index + lines; - length -= lines; - System.arraycopy(lineInfo,len,lineInfo, - index,lineInfo.length - len); - } - - /** - * Returns the number of lines in this token marker. - */ - public int getLineCount() - { - return length; - } - - /** - * Returns true if the next line should be repainted. This - * will return true after a line has been tokenized that starts - * a multiline token that continues onto the next line. - */ - public boolean isNextLineRequested() - { - return nextLineRequested; - } - - // protected members - - /** - * The first token in the list. This should be used as the return - * value from markTokens(). - */ - protected Token firstToken; - - /** - * The last token in the list. New tokens are added here. - * This should be set to null before a new line is to be tokenized. - */ - protected Token lastToken; - - /** - * An array for storing information about lines. It is enlarged and - * shrunk automatically by the insertLines() and - * deleteLines() methods. - */ - protected LineInfo[] lineInfo; - - /** - * The number of lines in the model being tokenized. This can be - * less than the length of the lineInfo array. - */ - protected int length; - - /** - * The last tokenized line. - */ - protected int lastLine; - - /** - * True if the next line should be painted. - */ - protected boolean nextLineRequested; - - /** - * Creates a new TokenMarker. This DOES NOT create - * a lineInfo array; an initial call to insertLines() - * does that. - */ - protected TokenMarker() - { - lastLine = -1; - } - - /** - * Ensures that the lineInfo array can contain the - * specified index. This enlarges it if necessary. No action is - * taken if the array is large enough already.

- * - * It should be unnecessary to call this under normal - * circumstances; insertLine() should take care of - * enlarging the line info array automatically. - * - * @param index The array index - */ - protected void ensureCapacity(int index) - { - if(lineInfo == null) - lineInfo = new LineInfo[index + 1]; - else if(lineInfo.length <= index) - { - LineInfo[] lineInfoN = new LineInfo[(index + 1) * 2]; - System.arraycopy(lineInfo,0,lineInfoN,0, - lineInfo.length); - lineInfo = lineInfoN; - } - } - - /** - * Adds a token to the token list. - * @param length The length of the token - * @param id The id of the token - */ - protected void addToken(int length, byte id) - { - if(id >= Token.INTERNAL_FIRST && id <= Token.INTERNAL_LAST) - throw new InternalError("Invalid id: " + id); - - if(length == 0 && id != Token.END) - return; - - if(firstToken == null) - { - firstToken = new Token(length,id); - lastToken = firstToken; - } - else if(lastToken == null) - { - lastToken = firstToken; - firstToken.length = length; - firstToken.id = id; - } - else if(lastToken.next == null) - { - lastToken.next = new Token(length,id); - lastToken = lastToken.next; - } - else - { - lastToken = lastToken.next; - lastToken.length = length; - lastToken.id = id; - } - } - - /** - * Inner class for storing information about tokenized lines. - */ - private static class LineInfo - { - /** - * Creates a new LineInfo object with token = Token.NULL - * and obj = null. - */ - public LineInfo() - { - } - - /** - * Creates a new LineInfo object with the specified - * parameters. - */ -// public LineInfo(byte token, Object obj) -// { -// this.token = token; -// this.obj = obj; -// } - - /** - * The id of the last token of the line. - */ - public byte token; - - /** - * This is for use by the token marker implementations - * themselves. It can be used to store anything that - * is an object and that needs to exist on a per-line - * basis. - */ -// public Object obj; - } + /** + * Returns if the token marker supports tokens that span multiple + * lines. If this is true, the object using this token marker is + * required to pass all lines in the document to the + * markTokens() method (in turn).

+ *

+ * The default implementation returns true; it should be overridden + * to return false on simpler token markers for increased speed. + */ + public boolean supportsMultilineTokens() { + return true; + } } diff --git a/app/src/processing/app/syntax/TokenMarkerState.java b/app/src/processing/app/syntax/TokenMarkerState.java new file mode 100644 index 000000000..8b20b2603 --- /dev/null +++ b/app/src/processing/app/syntax/TokenMarkerState.java @@ -0,0 +1,266 @@ +package processing.app.syntax; + +import javax.swing.text.Segment; + +/** + * This class serves only as a workaround to preserve API and should be removed + * in the next major version. Base TokenMarker which serves as superclass for + * token markes for various modes is stateful, but single instance was shared + * between all tabs and Editors. This caused inherent bugs by leaking state + * between contexts. + * + * TokenMarker subclasses now serve two purposes: they keep keyword list and + * they override markTokensImpl to provide the marking logic. + * + * Since each tab and Editor should have it's own token marker state, I extracted + * most of the fields and associated metods working with them from TokenMarker + * into this class, and allowed TokenMarker to create instances of this class + * when requested. + * + * The way marking logic is handled is unfortunate, since markTokensImpl is + * expected to call addToken() of TokenMarker superclass instead of – for + * example - returning a List of tokens. I worked around this by plugging in + * TokenMarkerState instance as listener before markTokensImpl is called. + * This behavior is safe since TokenMarker is always operated from Event + * Dispatch Thread and no multithreading is involved. + * + * This allows having only single instance of TokenMarker in a way it was + * intended before while keeping state separate for each tab. + * + * In the next major version TokenMarker shound be redesigned with following + * requirements in mind: + * - Single instance of keyword list and other common data, initialized by Mode + * - Each tab should have its own instance of TokenMarker containing its state + * - Support multiple flavors for different doc types + * - Other modes should provide logic in a way which is compatible with + * multiple states (pure function? Function object?). + * Currently state, logic and keywords list are tied together into one + * TokenMarker instance, which leads to need for this workaround. + */ +public class TokenMarkerState { + + protected TokenMarker marker; + + protected TokenMarkerState(TokenMarker marker) { + this.marker = marker; + } + + /** + * The first token in the list. This should be used as the return + * value from markTokens(). + */ + protected Token firstToken; + + /** + * The last token in the list. New tokens are added here. + * This should be set to null before a new line is to be tokenized. + */ + protected Token lastToken; + + /** + * An array for storing information about lines. It is enlarged and + * shrunk automatically by the insertLines() and + * deleteLines() methods. + */ + protected byte[] lineInfo; + + /** + * The number of lines in the model being tokenized. This can be + * less than the length of the lineInfo array. + */ + protected int length; + + /** + * The last tokenized line. + */ + protected int lastLine = -1; + + /** + * True if the next line should be painted. + */ + protected boolean nextLineRequested; + + /** + * A wrapper for the lower-level markTokensImpl method + * that is called to split a line up into tokens. + * + * @param line The line + * @param lineIndex The line number + */ + public Token markTokens(Segment line, int lineIndex) { + if (lineIndex >= length) { + throw new IllegalArgumentException("Tokenizing invalid line: " + + lineIndex); + } + + marker.setTokenListener(this::addToken); + + lastToken = null; + + byte prev = (lineIndex == 0) ? Token.NULL : lineInfo[lineIndex - 1]; + + byte oldToken = lineInfo[lineIndex]; + byte token = marker.markTokensImpl(prev, line, lineIndex); + + marker.setTokenListener(null); + + lineInfo[lineIndex] = token; + + /* + * This is a foul hack. It stops nextLineRequested + * from being cleared if the same line is marked twice. + * + * Why is this necessary? It's all JEditTextArea's fault. + * When something is inserted into the text, firing a + * document event, the insertUpdate() method shifts the + * caret (if necessary) by the amount inserted. + * + * All caret movement is handled by the select() method, + * which eventually pipes the new position to scrollTo() + * and calls repaint(). + * + * Note that at this point in time, the new line hasn't + * yet been painted; the caret is moved first. + * + * scrollTo() calls offsetToX(), which tokenizes the line + * unless it is being called on the last line painted + * (in which case it uses the text area's painter cached + * token list). What scrollTo() does next is irrelevant. + * + * After scrollTo() has done it's job, repaint() is + * called, and eventually we end up in paintLine(), whose + * job is to paint the changed line. It, too, calls + * markTokens(). + * + * The problem was that if the line started a multiline + * token, the first markTokens() (done in offsetToX()) + * would set nextLineRequested (because the line end + * token had changed) but the second would clear it + * (because the line was the same that time) and therefore + * paintLine() would never know that it needed to repaint + * subsequent lines. + * + * This bug took me ages to track down, that's why I wrote + * all the relevant info down so that others wouldn't + * duplicate it. + */ + if (!(lastLine == lineIndex && nextLineRequested)) { + nextLineRequested = (oldToken != token); + } + + lastLine = lineIndex; + + addToken(0, Token.END); + + return firstToken; + } + + /** + * Informs the token marker that lines have been inserted into + * the document. This inserts a gap in the lineInfo + * array. + * + * @param index The first line number + * @param lines The number of lines + */ + public void insertLines(int index, int lines) { + if (lines <= 0) + return; + length += lines; + ensureCapacity(length); + int len = index + lines; + System.arraycopy(lineInfo, index, lineInfo, len, + lineInfo.length - len); + + for (int i = index + lines - 1; i >= index; i--) { + lineInfo[i] = Token.NULL; + } + } + + /** + * Informs the token marker that line have been deleted from + * the document. This removes the lines in question from the + * lineInfo array. + * + * @param index The first line number + * @param lines The number of lines + */ + public void deleteLines(int index, int lines) { + if (lines <= 0) + return; + int len = index + lines; + length -= lines; + System.arraycopy(lineInfo, len, lineInfo, + index, lineInfo.length - len); + } + + /** + * Returns the number of lines in this token marker. + */ + public int getLineCount() { + return length; + } + + /** + * Returns true if the next line should be repainted. This + * will return true after a line has been tokenized that starts + * a multiline token that continues onto the next line. + */ + public boolean isNextLineRequested() { + return nextLineRequested; + } + + /** + * Ensures that the lineInfo array can contain the + * specified index. This enlarges it if necessary. No action is + * taken if the array is large enough already.

+ *

+ * It should be unnecessary to call this under normal + * circumstances; insertLine() should take care of + * enlarging the line info array automatically. + * + * @param index The array index + */ + protected void ensureCapacity(int index) { + if (lineInfo == null) { + lineInfo = new byte[index + 1]; + } else if (lineInfo.length <= index) { + byte[] lineInfoN = new byte[(index + 1) * 2]; + System.arraycopy(lineInfo, 0, lineInfoN, 0, + lineInfo.length); + lineInfo = lineInfoN; + } + } + + /** + * Adds a token to the token list. + * + * @param length The length of the token + * @param id The id of the token + */ + protected void addToken(int length, byte id) { + if (id >= Token.INTERNAL_FIRST && id <= Token.INTERNAL_LAST) { + throw new InternalError("Invalid id: " + id); + } + + if (length == 0 && id != Token.END) { + return; + } + + if (firstToken == null) { + firstToken = new Token(length, id); + lastToken = firstToken; + } else if (lastToken == null) { + lastToken = firstToken; + firstToken.length = length; + firstToken.id = id; + } else if (lastToken.next == null) { + lastToken.next = new Token(length, id); + lastToken = lastToken.next; + } else { + lastToken = lastToken.next; + lastToken.length = length; + lastToken.id = id; + } + } +}