blob: 7024c7a525e46511a4123d4aa126c04148abaf9b [file] [log] [blame]
/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.engine.scanner;
import com.google.dart.engine.error.AnalysisErrorListener;
import com.google.dart.engine.source.Source;
import com.google.dart.engine.utilities.collection.TokenMap;
/**
* Instances of the class {@code IncrementalScanner} implement a scanner that scans a subset of a
* string and inserts the resulting tokens into the middle of an existing token stream.
*
* @coverage dart.engine.parser
*/
public class IncrementalScanner extends Scanner {
/**
* The reader used to access the characters in the source.
*/
private CharacterReader reader;
/**
* A map from tokens that were copied to the copies of the tokens.
*/
private TokenMap tokenMap = new TokenMap();
/**
* The token in the new token stream immediately to the left of the range of tokens that were
* inserted, or the token immediately to the left of the modified region if there were no new
* tokens.
*/
private Token leftToken;
/**
* The token in the new token stream immediately to the right of the range of tokens that were
* inserted, or the token immediately to the right of the modified region if there were no new
* tokens.
*/
private Token rightToken;
/**
* A flag indicating whether there were any tokens changed as a result of the modification.
*/
private boolean hasNonWhitespaceChange = false;
/**
* Initialize a newly created scanner.
*
* @param source the source being scanned
* @param reader the character reader used to read the characters in the source
* @param errorListener the error listener that will be informed of any errors that are found
*/
public IncrementalScanner(Source source, CharacterReader reader,
AnalysisErrorListener errorListener) {
super(source, reader, errorListener);
this.reader = reader;
}
/**
* Return the token in the new token stream immediately to the left of the range of tokens that
* were inserted, or the token immediately to the left of the modified region if there were no new
* tokens.
*
* @return the token to the left of the inserted tokens
*/
public Token getLeftToken() {
return leftToken;
}
/**
* Return the token in the new token stream immediately to the right of the range of tokens that
* were inserted, or the token immediately to the right of the modified region if there were no
* new tokens.
*
* @return the token to the right of the inserted tokens
*/
public Token getRightToken() {
return rightToken;
}
/**
* Return a map from tokens that were copied to the copies of the tokens.
*
* @return a map from tokens that were copied to the copies of the tokens
*/
public TokenMap getTokenMap() {
return tokenMap;
}
/**
* Return {@code true} if there were any tokens either added or removed (or both) as a result of
* the modification.
*
* @return {@code true} if there were any tokens changed as a result of the modification
*/
public boolean hasNonWhitespaceChange() {
return hasNonWhitespaceChange;
}
/**
* Given the stream of tokens scanned from the original source, the modified source (the result of
* replacing one contiguous range of characters with another string of characters), and a
* specification of the modification that was made, return a stream of tokens scanned from the
* modified source. The original stream of tokens will not be modified.
*
* @param originalStream the stream of tokens scanned from the original source
* @param index the index of the first character in both the original and modified source that was
* affected by the modification
* @param removedLength the number of characters removed from the original source
* @param insertedLength the number of characters added to the modified source
*/
public Token rescan(Token originalStream, int index, int removedLength, int insertedLength) {
//
// Copy all of the tokens in the originalStream whose end is less than the replacement start.
// (If the replacement start is equal to the end of an existing token, then it means that the
// existing token might have been modified, so we need to rescan it.)
//
while (originalStream.getType() != TokenType.EOF && originalStream.getEnd() < index) {
originalStream = copyAndAdvance(originalStream, 0);
}
Token oldFirst = originalStream;
Token oldLeftToken = originalStream.getPrevious();
leftToken = getTail();
//
// Skip tokens in the original stream until we find a token whose offset is greater than the end
// of the removed region. (If the end of the removed region is equal to the beginning of an
// existing token, then it means that the existing token might have been modified, so we need to
// rescan it.)
//
int removedEnd = index + (removedLength == 0 ? 0 : removedLength - 1);
while (originalStream.getType() != TokenType.EOF && originalStream.getOffset() <= removedEnd) {
originalStream = originalStream.getNext();
}
Token oldLast;
Token oldRightToken;
if (originalStream.getType() != TokenType.EOF && removedEnd + 1 == originalStream.getOffset()) {
oldLast = originalStream;
originalStream = originalStream.getNext();
oldRightToken = originalStream;
} else {
oldLast = originalStream.getPrevious();
oldRightToken = originalStream;
}
//
// Compute the delta between the character index of characters after the modified region in the
// original source and the index of the corresponding character in the modified source.
//
int delta = insertedLength - removedLength;
//
// Compute the range of characters that are known to need to be rescanned. If the index is
// within an existing token, then we need to start at the beginning of the token.
//
int scanStart = Math.min(oldFirst.getOffset(), index);
int oldEnd = oldLast.getEnd() + delta - 1;
int newEnd = index + insertedLength - 1;
int scanEnd = Math.max(newEnd, oldEnd);
//
// Starting at the start of the scan region, scan tokens from the modifiedSource until the end
// of the just scanned token is greater than or equal to end of the scan region in the modified
// source. Include trailing characters of any token that was split as a result of inserted text,
// as in "ab" --> "a.b".
//
reader.setOffset(scanStart - 1);
int next = reader.advance();
while (next != -1 && reader.getOffset() <= scanEnd) {
next = bigSwitch(next);
}
//
// Copy the remaining tokens in the original stream, but apply the delta to the token's offset.
//
if (originalStream.getType() == TokenType.EOF) {
copyAndAdvance(originalStream, delta);
rightToken = getTail();
rightToken.setNextWithoutSettingPrevious(rightToken);
} else {
originalStream = copyAndAdvance(originalStream, delta);
rightToken = getTail();
while (originalStream.getType() != TokenType.EOF) {
originalStream = copyAndAdvance(originalStream, delta);
}
Token eof = copyAndAdvance(originalStream, delta);
eof.setNextWithoutSettingPrevious(eof);
}
//
// If the index is immediately after an existing token and the inserted characters did not
// change that original token, then adjust the leftToken to be the next token. For example, in
// "a; c;" --> "a;b c;", the leftToken was ";", but this code advances it to "b" since "b" is
// the first new token.
//
Token newFirst = leftToken.getNext();
while (newFirst != rightToken && oldFirst != oldRightToken
&& newFirst.getType() != TokenType.EOF && equalTokens(oldFirst, newFirst)) {
tokenMap.put(oldFirst, newFirst);
oldLeftToken = oldFirst;
oldFirst = oldFirst.getNext();
leftToken = newFirst;
newFirst = newFirst.getNext();
}
Token newLast = rightToken.getPrevious();
while (newLast != leftToken && oldLast != oldLeftToken && newLast.getType() != TokenType.EOF
&& equalTokens(oldLast, newLast)) {
tokenMap.put(oldLast, newLast);
oldRightToken = oldLast;
oldLast = oldLast.getPrevious();
rightToken = newLast;
newLast = newLast.getPrevious();
}
hasNonWhitespaceChange = leftToken.getNext() != rightToken
|| oldLeftToken.getNext() != oldRightToken;
//
// TODO(brianwilkerson) Begin tokens are not getting associated with the corresponding end
// tokens (because the end tokens have not been copied when we're copying the begin tokens).
// This could have implications for parsing.
// TODO(brianwilkerson) Update the lineInfo.
//
return getFirstToken();
}
private Token copyAndAdvance(Token originalToken, int delta) {
Token copiedToken = originalToken.copy();
tokenMap.put(originalToken, copiedToken);
copiedToken.applyDelta(delta);
appendToken(copiedToken);
Token originalComment = originalToken.getPrecedingComments();
Token copiedComment = originalToken.getPrecedingComments();
while (originalComment != null) {
tokenMap.put(originalComment, copiedComment);
originalComment = originalComment.getNext();
copiedComment = copiedComment.getNext();
}
return originalToken.getNext();
}
/**
* Return {@code true} if the two tokens are equal to each other. For the purposes of the
* incremental scanner, two tokens are equal if they have the same type and lexeme.
*
* @param oldToken the token from the old stream that is being compared
* @param newToken the token from the new stream that is being compared
* @return {@code true} if the two tokens are equal to each other
*/
private boolean equalTokens(Token oldToken, Token newToken) {
return oldToken.getType() == newToken.getType() && oldToken.getLength() == newToken.getLength()
&& oldToken.getLexeme().equals(newToken.getLexeme());
}
}