/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.tools.syntax; /* * TokenMarker.java - Generic token marker Copyright (C) 1998, 1999 Slava Pestov * * You may use and modify this package for any purpose. Redistribution is permitted, in both source * and binary form, provided that this notice remains intact in all source distributions of this * package. */ 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. * <p> * * For performance reasons, the linked list of tokens is reused after each line is tokenized. * Therefore, the return value of <code>markTokens</code> should only be used for immediate * painting. Notably, it cannot be cached. * * @author Slava Pestov, Ingo Mierswa */ public abstract class TokenMarker { /** * A wrapper for the lower-level <code>markTokensImpl</code> 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); } lastToken = null; LineInfo info = lineInfo[lineIndex]; LineInfo prev; if (lineIndex == 0) { prev = null; } else { prev = lineInfo[lineIndex - 1]; } byte oldToken = info.token; byte token = markTokensImpl(prev == null ? Token.NULL : prev.token, line, lineIndex); info.token = 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; } /** * An abstract method that splits a line up into tokens. It should parse the line, and call * <code>addToken()</code> to add syntax tokens to the token list. Then, it should return the * initial token type for the next line. * <p> * * 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 * <code>markTokens()</code> method (in turn). * <p> * * 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 <code>lineInfo</code> 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 <code>lineInfo</code> 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 * <code>markTokens()</code>. */ 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 * <code>insertLines()</code> and <code>deleteLines()</code> methods. */ protected LineInfo[] lineInfo; /** * The number of lines in the model being tokenized. This can be less than the length of the * <code>lineInfo</code> 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 <code>TokenMarker</code>. This DOES NOT create a lineInfo array; an initial * call to <code>insertLines()</code> does that. */ protected TokenMarker() { lastLine = -1; } /** * Ensures that the <code>lineInfo</code> array can contain the specified index. This enlarges * it if necessary. No action is taken if the array is large enough already. * <p> * * It should be unnecessary to call this under normal circumstances; <code>insertLine()</code> * 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. */ public 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; } }