/******************************************************************************* | |
* Copyright (c) 2013 Sierra Wireless and others. | |
* All rights reserved. This program and the accompanying materials | |
* are made available under the terms of the Eclipse Public License v1.0 | |
* which accompanies this distribution, and is available at | |
* http://www.eclipse.org/legal/epl-v10.html | |
* | |
* Contributors: | |
* Sierra Wireless - initial API and implementation | |
*******************************************************************************/ | |
package org.eclipse.koneki.ldt.ui.internal.editor.text; | |
import java.util.Arrays; | |
import org.eclipse.core.runtime.Assert; | |
import org.eclipse.jface.text.BadLocationException; | |
import org.eclipse.jface.text.IDocument; | |
import org.eclipse.jface.text.IRegion; | |
import org.eclipse.jface.text.ITypedRegion; | |
import org.eclipse.jface.text.TextUtilities; | |
import org.eclipse.jface.text.TypedRegion; | |
import org.eclipse.koneki.ldt.core.internal.ast.models.api.Item; | |
import org.eclipse.koneki.ldt.core.internal.ast.models.api.UnknownItem; | |
import org.eclipse.koneki.ldt.core.internal.ast.models.file.Call; | |
import org.eclipse.koneki.ldt.core.internal.ast.models.file.Identifier; | |
import org.eclipse.koneki.ldt.core.internal.ast.models.file.Index; | |
import org.eclipse.koneki.ldt.core.internal.ast.models.file.Invoke; | |
import org.eclipse.koneki.ldt.core.internal.ast.models.file.LuaExpression; | |
/** | |
* Utility methods for heuristic based Lua manipulations in an incomplete Lua source file. | |
* | |
* <p> | |
* An instance holds some internal position in the document and is therefore not threadsafe. | |
* </p> | |
* | |
* inspired by org.eclipse.jdt.internal.ui.text.JavaHeuristicScanner | |
*/ | |
public class LuaHeuristicScanner { | |
/** | |
* Returned by all methods when the requested position could not be found, or if a {@link BadLocationException} was thrown while scanning. | |
*/ | |
public static final int NOT_FOUND = -1; | |
/** | |
* Special bound parameter that means either -1 (backward scanning) or <code>fDocument.getLength()</code> (forward scanning). | |
*/ | |
public static final int UNBOUND = -2; | |
/* character constants */ | |
private static final char LBRACE = '{'; | |
private static final char RBRACE = '}'; | |
private static final char LBRACKET = '['; | |
private static final char RBRACKET = ']'; | |
private static final char LPAREN = '('; | |
private static final char RPAREN = ')'; | |
private static final char SEMICOLON = ';'; | |
private static final char COMMA = ','; | |
private static final char COLON = ':'; | |
private static final char DOT = '.'; | |
private static final char EQUAL = '='; | |
private static final char LANGLE = '<'; | |
private static final char RANGLE = '>'; | |
private static final char PLUS = '+'; | |
private static final char MINUS = '-'; | |
private static final char ASTERISK = '*'; | |
private static final char SLASH = '/'; | |
private static final char PERSENT = '%'; | |
private static final char CARET = '^'; | |
private static final char SHARP = '#'; | |
/* preset stop conditions */ | |
private static final StopCondition FNONWS = new NonWhitespace(); | |
private final StopCondition fNonWSDefaultPart = new NonWhitespaceDefaultPartition(); | |
private final StopCondition fNonIdent = new NonJavaIdentifierPartDefaultPartition(); | |
/** | |
* Specifies the stop condition, upon which the <code>scanXXX</code> methods will decide whether to keep scanning or not. This interface may | |
* implemented by clients. | |
*/ | |
private abstract static class StopCondition { | |
/** | |
* Instructs the scanner to return the current position. | |
* | |
* @param ch | |
* the char at the current position | |
* @param position | |
* the current position | |
* @param forward | |
* the iteration direction | |
* @return <code>true</code> if the stop condition is met. | |
*/ | |
public abstract boolean stop(char ch, int position, boolean forward); | |
/** | |
* Asks the condition to return the next position to query. The default is to return the next/previous position. | |
* | |
* @param position | |
* the position | |
* @param forward | |
* <code>true</code> if next position should be returned | |
* @return the next position to scan | |
*/ | |
public int nextPosition(int position, boolean forward) { | |
return forward ? position + 1 : position - 1; | |
} | |
} | |
/** | |
* Stops upon a non-whitespace (as defined by {@link Character#isWhitespace(char)}) character. | |
*/ | |
private static class NonWhitespace extends StopCondition { | |
@Override | |
public boolean stop(char ch, int position, boolean forward) { | |
return !Character.isWhitespace(ch); | |
} | |
} | |
/** | |
* Stops upon a non-whitespace character in the default partition. | |
*/ | |
private final class NonWhitespaceDefaultPartition extends NonWhitespace { | |
@Override | |
public boolean stop(char ch, int position, boolean forward) { | |
return super.stop(ch, position, true) && isAcceptedPartition(position); | |
} | |
@Override | |
public int nextPosition(int position, boolean forward) { | |
ITypedRegion partition = getPartition(position); | |
if (isAcceptedPartition(partition)) | |
return super.nextPosition(position, forward); | |
if (forward) { | |
int end = partition.getOffset() + partition.getLength(); | |
if (position < end) | |
return end; | |
} else { | |
int offset = partition.getOffset(); | |
if (position > offset) | |
return offset - 1; | |
} | |
return super.nextPosition(position, forward); | |
} | |
} | |
/** | |
* Stops upon a non-java identifier (as defined by {@link Character#isJavaIdentifierPart(char)}) character. | |
*/ | |
private static class NonJavaIdentifierPart extends StopCondition { | |
@Override | |
public boolean stop(char ch, int position, boolean forward) { | |
return !Character.isJavaIdentifierPart(ch); | |
} | |
} | |
/** | |
* Stops upon a non-java identifier character in the default partition. | |
* | |
* @see JavaHeuristicScanner.NonJavaIdentifierPart | |
*/ | |
private final class NonJavaIdentifierPartDefaultPartition extends NonJavaIdentifierPart { | |
@Override | |
public boolean stop(char ch, int position, boolean forward) { | |
return super.stop(ch, position, true) || !isAcceptedPartition(position); | |
} | |
@Override | |
public int nextPosition(int position, boolean forward) { | |
ITypedRegion partition = getPartition(position); | |
if (isAcceptedPartition(partition)) | |
return super.nextPosition(position, forward); | |
if (forward) { | |
int end = partition.getOffset() + partition.getLength(); | |
if (position < end) | |
return end; | |
} else { | |
int offset = partition.getOffset(); | |
if (position > offset) | |
return offset - 1; | |
} | |
return super.nextPosition(position, forward); | |
} | |
} | |
/** | |
* Stops upon a character in the default partition that matches the given character list. | |
*/ | |
private final class CharacterMatch extends StopCondition { | |
private final char[] fChars; | |
/** | |
* Creates a new instance. | |
* | |
* @param ch | |
* the single character to match | |
*/ | |
public CharacterMatch(char ch) { | |
this(new char[] { ch }); | |
} | |
/** | |
* Creates a new instance. | |
* | |
* @param chars | |
* the chars to match. | |
*/ | |
public CharacterMatch(char[] chars) { | |
Assert.isNotNull(chars); | |
Assert.isTrue(chars.length > 0); | |
fChars = chars; | |
Arrays.sort(chars); | |
} | |
@Override | |
public boolean stop(char ch, int position, boolean forward) { | |
return Arrays.binarySearch(fChars, ch) >= 0 && isAcceptedPartition(position); | |
} | |
@Override | |
public int nextPosition(int position, boolean forward) { | |
ITypedRegion partition = getPartition(position); | |
if (isAcceptedPartition(partition)) | |
return super.nextPosition(position, forward); | |
if (forward) { | |
int end = partition.getOffset() + partition.getLength(); | |
if (position < end) | |
return end; | |
} else { | |
int offset = partition.getOffset(); | |
if (position > offset) | |
return offset - 1; | |
} | |
return super.nextPosition(position, forward); | |
} | |
} | |
/** The document being scanned. */ | |
private final IDocument fDocument; | |
/* internal scan state */ | |
/** the most recently read character. */ | |
private char fChar; | |
/** the most recently read position. */ | |
private int fPos; | |
/** | |
* The most recently used partition. | |
*/ | |
private ITypedRegion fCachedPartition = new TypedRegion(-1, 0, "__no_partition_at_all"); //$NON-NLS-1$ | |
/** | |
* Calls <code>this(document, ILuaPartitions.LUA_PARTITIONING, IDocument.DEFAULT_CONTENT_TYPE)</code>. | |
* | |
* @param document | |
* the document to scan. | |
*/ | |
public LuaHeuristicScanner(IDocument document) { | |
Assert.isLegal(document != null); | |
fDocument = document; | |
} | |
/** | |
* Returns the most recent internal scan position. | |
* | |
* @return the most recent internal scan position. | |
*/ | |
public int getPosition() { | |
return fPos; | |
} | |
/** | |
* Returns the next token in forward direction, starting at <code>start</code>, and not extending further than <code>bound</code>. The return | |
* value is one of the constants defined in {@link Symbols}. After a call, {@link #getPosition()} will return the position just after the scanned | |
* token (i.e. the next position that will be scanned). | |
* | |
* @param start | |
* the first character position in the document to consider | |
* @param bound | |
* the first position not to consider any more | |
* @return a constant from {@link Symbols} describing the next token | |
*/ | |
public int nextToken(int start, int bound) { | |
int pos = scanForward(start, bound, fNonWSDefaultPart); | |
if (pos == NOT_FOUND) | |
return LuaSymbols.TOKEN_EOF; | |
// first, detect string | |
ITypedRegion partition = getPartition(pos); | |
if (partition.getType().equals(ILuaPartitions.LUA_STRING) || partition.getType().equals(ILuaPartitions.LUA_SINGLE_QUOTE_STRING) | |
|| partition.getType().equals(ILuaPartitions.LUA_MULTI_LINE_STRING)) { | |
fPos += partition.getLength(); | |
return LuaSymbols.TOKEN_STRING; | |
} | |
fPos++; | |
// detect key char | |
switch (fChar) { | |
case LBRACE: | |
return LuaSymbols.TOKEN_LBRACE; | |
case RBRACE: | |
return LuaSymbols.TOKEN_RBRACE; | |
case LBRACKET: | |
return LuaSymbols.TOKEN_LBRACKET; | |
case RBRACKET: | |
return LuaSymbols.TOKEN_RBRACKET; | |
case LPAREN: | |
return LuaSymbols.TOKEN_LPAREN; | |
case RPAREN: | |
return LuaSymbols.TOKEN_RPAREN; | |
case SEMICOLON: | |
return LuaSymbols.TOKEN_SEMICOLON; | |
case COMMA: | |
return LuaSymbols.TOKEN_COMMA; | |
case EQUAL: | |
return LuaSymbols.TOKEN_EQUAL; | |
case LANGLE: | |
return LuaSymbols.TOKEN_LESSTHAN; | |
case RANGLE: | |
return LuaSymbols.TOKEN_GREATERTHAN; | |
case PLUS: | |
return LuaSymbols.TOKEN_PLUS; | |
case MINUS: | |
return LuaSymbols.TOKEN_MINUS; | |
case SHARP: | |
return LuaSymbols.TOKEN_SHARP; | |
case DOT: | |
return LuaSymbols.TOKEN_DOT; | |
case COLON: | |
return LuaSymbols.TOKEN_COLON; | |
case ASTERISK: | |
return LuaSymbols.TOKEN_ASTERISK; | |
case SLASH: | |
return LuaSymbols.TOKEN_SLASH; | |
case PERSENT: | |
return LuaSymbols.TOKEN_PERSENT; | |
case CARET: | |
return LuaSymbols.TOKEN_CARET; | |
default: | |
break; | |
} | |
// else | |
if (Character.isJavaIdentifierPart(fChar)) { | |
// assume an identifier or keyword | |
int from = pos; | |
int to; | |
pos = scanForward(pos + 1, bound, fNonIdent); | |
if (pos == NOT_FOUND) | |
to = bound == UNBOUND ? fDocument.getLength() : bound; | |
else | |
to = pos; | |
String identOrKeyword; | |
try { | |
identOrKeyword = fDocument.get(from, to - from); | |
} catch (BadLocationException e) { | |
return LuaSymbols.TOKEN_EOF; | |
} | |
return getToken(identOrKeyword); | |
} else { | |
// operators, number literals etc | |
return LuaSymbols.TOKEN_OTHER; | |
} | |
} | |
/** | |
* Returns the next token in backward direction, starting at <code>start</code>, and not extending further than <code>bound</code>. The return | |
* value is one of the constants defined in {@link Symbols}. After a call, {@link #getPosition()} will return the position just before the scanned | |
* token starts (i.e. the next position that will be scanned). | |
* | |
* @param start | |
* the first character position in the document to consider | |
* @param bound | |
* the first position not to consider any more | |
* @return a constant from {@link Symbols} describing the previous token | |
*/ | |
public int previousToken(int start, int bound) { | |
int pos = scanBackward(start, bound, fNonWSDefaultPart); | |
if (pos == NOT_FOUND) | |
return LuaSymbols.TOKEN_EOF; | |
// first, detect string | |
ITypedRegion partition = getPartition(pos); | |
if (partition.getType().equals(ILuaPartitions.LUA_STRING) || partition.getType().equals(ILuaPartitions.LUA_SINGLE_QUOTE_STRING) | |
|| partition.getType().equals(ILuaPartitions.LUA_MULTI_LINE_STRING)) { | |
fPos -= partition.getLength(); | |
return LuaSymbols.TOKEN_STRING; | |
} | |
fPos--; | |
// detect key char | |
switch (fChar) { | |
case LBRACE: | |
return LuaSymbols.TOKEN_LBRACE; | |
case RBRACE: | |
return LuaSymbols.TOKEN_RBRACE; | |
case LBRACKET: | |
return LuaSymbols.TOKEN_LBRACKET; | |
case RBRACKET: | |
return LuaSymbols.TOKEN_RBRACKET; | |
case LPAREN: | |
return LuaSymbols.TOKEN_LPAREN; | |
case RPAREN: | |
return LuaSymbols.TOKEN_RPAREN; | |
case SEMICOLON: | |
return LuaSymbols.TOKEN_SEMICOLON; | |
case COLON: | |
return LuaSymbols.TOKEN_COLON; | |
case COMMA: | |
return LuaSymbols.TOKEN_COMMA; | |
case EQUAL: | |
return LuaSymbols.TOKEN_EQUAL; | |
case LANGLE: | |
return LuaSymbols.TOKEN_LESSTHAN; | |
case RANGLE: | |
return LuaSymbols.TOKEN_GREATERTHAN; | |
case PLUS: | |
return LuaSymbols.TOKEN_PLUS; | |
case MINUS: | |
return LuaSymbols.TOKEN_MINUS; | |
case SHARP: | |
return LuaSymbols.TOKEN_SHARP; | |
case DOT: | |
return LuaSymbols.TOKEN_DOT; | |
case ASTERISK: | |
return LuaSymbols.TOKEN_ASTERISK; | |
case SLASH: | |
return LuaSymbols.TOKEN_SLASH; | |
case PERSENT: | |
return LuaSymbols.TOKEN_PERSENT; | |
case CARET: | |
return LuaSymbols.TOKEN_CARET; | |
default: | |
break; | |
} | |
// else | |
if (Character.isJavaIdentifierPart(fChar)) { | |
// assume an ident or keyword | |
int from; | |
int to = pos + 1; | |
pos = scanBackward(pos - 1, bound, fNonIdent); | |
if (pos == NOT_FOUND) | |
from = bound == UNBOUND ? 0 : bound + 1; | |
else | |
from = pos + 1; | |
String identOrKeyword; | |
try { | |
identOrKeyword = fDocument.get(from, to - from); | |
} catch (BadLocationException e) { | |
return LuaSymbols.TOKEN_EOF; | |
} | |
return getToken(identOrKeyword); | |
} else { | |
// operators, number literals etc | |
return LuaSymbols.TOKEN_OTHER; | |
} | |
} | |
/** | |
* Returns one of the keyword constants or <code>TOKEN_IDENT</code> for a scanned identifier. | |
* | |
* @param s | |
* a scanned identifier | |
* @return one of the constants defined in {@link Symbols} | |
*/ | |
private int getToken(String s) { | |
Assert.isNotNull(s); | |
switch (s.length()) { | |
case 2: | |
if ("if".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_IF; | |
if ("do".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_DO; | |
if ("or".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_OR; | |
break; | |
case 3: | |
if ("for".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_FOR; | |
if ("end".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_END; | |
if ("nil".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_NIL; | |
if ("and".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_AND; | |
if ("not".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_NOT; | |
break; | |
case 4: | |
if ("else".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_ELSE; | |
if ("then".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_THEN; | |
if ("true".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_TRUE; | |
break; | |
case 5: | |
if ("break".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_BREAK; | |
if ("while".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_WHILE; | |
if ("local".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_LOCAL; | |
if ("false".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_FALSE; | |
break; | |
case 6: | |
if ("return".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_RETURN; | |
if ("repeat".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_REPEAT; | |
if ("elseif".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_ELSEIF; | |
break; | |
case 8: | |
if ("function".equals(s)) //$NON-NLS-1$ | |
return LuaSymbols.TOKEN_FUNCTION; | |
break; | |
default: | |
break; | |
} | |
return LuaSymbols.TOKEN_IDENT; | |
} | |
/** | |
* Finds the smallest position in <code>fDocument</code> such that the position is >= <code>position</code> and < <code>bound</code> and | |
* <code>Character.isWhitespace(fDocument.getChar(pos))</code> evaluates to <code>false</code> and the position is in the default partition. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> > <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @return the smallest position of a non-whitespace character in [<code>position</code>, <code>bound</code>) that resides in a Lua partition, or | |
* <code>NOT_FOUND</code> if none can be found | |
*/ | |
public int findNonWhitespaceForward(int position, int bound) { | |
return scanForward(position, bound, fNonWSDefaultPart); | |
} | |
/** | |
* Finds the smallest position in <code>fDocument</code> such that the position is >= <code>position</code> and < <code>bound</code> and | |
* <code>Character.isWhitespace(fDocument.getChar(pos))</code> evaluates to <code>false</code>. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> > <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @return the smallest position of a non-whitespace character in [<code>position</code>, <code>bound</code>), or <code>NOT_FOUND</code> if none | |
* can be found | |
*/ | |
public int findNonWhitespaceForwardInAnyPartition(int position, int bound) { | |
return scanForward(position, bound, FNONWS); | |
} | |
/** | |
* Finds the highest position in <code>fDocument</code> such that the position is <= <code>position</code> and > <code>bound</code> and | |
* <code>Character.isWhitespace(fDocument.getChar(pos))</code> evaluates to <code>false</code> and the position is in the default partition. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> < <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @return the highest position of a non-whitespace character in (<code>bound</code>, <code>position</code>] that resides in a Lua partition, or | |
* <code>NOT_FOUND</code> if none can be found | |
*/ | |
public int findNonWhitespaceBackward(int position, int bound) { | |
return scanBackward(position, bound, fNonWSDefaultPart); | |
} | |
/** | |
* Finds the highest position in <code>fDocument</code> such that the position is <= <code>position</code> and > <code>bound</code> and | |
* <code>Character.isWhitespace(fDocument.getChar(pos))</code> evaluates to <code>false</code> and the position can be in any partition. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> < <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @return the highest position of a non-whitespace character in (<code>bound</code>, <code>position</code>] that resides in a Lua partition, or | |
* <code>NOT_FOUND</code> if none can be found | |
*/ | |
public int findNonWhitespaceBackwardInAnyPartition(int position, int bound) { | |
return scanBackward(position, bound, FNONWS); | |
} | |
/** | |
* Finds the lowest position <code>p</code> in <code>fDocument</code> such that <code>start</code> <= p < <code>bound</code> and | |
* <code>condition.stop(fDocument.getChar(p), p)</code> evaluates to <code>true</code>. | |
* | |
* @param start | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> > <code>start</code>, or | |
* <code>UNBOUND</code> | |
* @param condition | |
* the <code>StopCondition</code> to check | |
* @return the lowest position in [<code>start</code>, <code>bound</code>) for which <code>condition</code> holds, or <code>NOT_FOUND</code> if | |
* none can be found | |
*/ | |
public int scanForward(int start, int bound, StopCondition condition) { | |
Assert.isLegal(start >= 0); | |
if (bound == UNBOUND) | |
bound = fDocument.getLength(); | |
Assert.isLegal(bound <= fDocument.getLength()); | |
try { | |
fPos = start; | |
while (fPos < bound) { | |
fChar = fDocument.getChar(fPos); | |
if (condition.stop(fChar, fPos, true)) | |
return fPos; | |
fPos = condition.nextPosition(fPos, true); | |
} | |
} catch (BadLocationException e) { | |
} | |
return NOT_FOUND; | |
} | |
/** | |
* Finds the lowest position in <code>fDocument</code> such that the position is >= <code>position</code> and < <code>bound</code> and | |
* <code>fDocument.getChar(position) == ch</code> evaluates to <code>true</code> and the position is in the default partition. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> > <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @param ch | |
* the <code>char</code> to search for | |
* @return the lowest position of <code>ch</code> in (<code>bound</code>, <code>position</code>] that resides in a Lua partition, or | |
* <code>NOT_FOUND</code> if none can be found | |
*/ | |
public int scanForward(int position, int bound, char ch) { | |
return scanForward(position, bound, new CharacterMatch(ch)); | |
} | |
/** | |
* Finds the lowest position in <code>fDocument</code> such that the position is >= <code>position</code> and < <code>bound</code> and | |
* <code>fDocument.getChar(position) == ch</code> evaluates to <code>true</code> for at least one ch in <code>chars</code> and the position is in | |
* the default partition. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> > <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @param chars | |
* an array of <code>char</code> to search for | |
* @return the lowest position of a non-whitespace character in [<code>position</code>, <code>bound</code>) that resides in a Lua partition, or | |
* <code>NOT_FOUND</code> if none can be found | |
*/ | |
public int scanForward(int position, int bound, char[] chars) { | |
return scanForward(position, bound, new CharacterMatch(chars)); | |
} | |
/** | |
* Finds the highest position <code>p</code> in <code>fDocument</code> such that <code>bound</code> < <code>p</code> <= <code>start</code> | |
* and <code>condition.stop(fDocument.getChar(p), p)</code> evaluates to <code>true</code>. | |
* | |
* @param start | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> < <code>start</code>, or | |
* <code>UNBOUND</code> | |
* @param condition | |
* the <code>StopCondition</code> to check | |
* @return the highest position in (<code>bound</code>, <code>start</code> for which <code>condition</code> holds, or <code>NOT_FOUND</code> if | |
* none can be found | |
*/ | |
public int scanBackward(int start, int bound, StopCondition condition) { | |
if (bound == UNBOUND) | |
bound = -1; | |
Assert.isLegal(bound >= -1); | |
Assert.isLegal(start < fDocument.getLength()); | |
try { | |
fPos = start; | |
while (fPos > bound) { | |
fChar = fDocument.getChar(fPos); | |
if (condition.stop(fChar, fPos, false)) | |
return fPos; | |
fPos = condition.nextPosition(fPos, false); | |
} | |
// CHECKSTYLE:OFF | |
} catch (BadLocationException e) { | |
// CHECKSTYLE:ON | |
} | |
return NOT_FOUND; | |
} | |
/** | |
* Finds the highest position in <code>fDocument</code> such that the position is <= <code>position</code> and > <code>bound</code> and | |
* <code>fDocument.getChar(position) == ch</code> evaluates to <code>true</code> for at least one ch in <code>chars</code> and the position is in | |
* the default partition. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> < <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @param ch | |
* the <code>char</code> to search for | |
* @return the highest position of one element in <code>chars</code> in (<code>bound</code>, <code>position</code>] that resides in a Lua | |
* partition, or <code>NOT_FOUND</code> if none can be found | |
*/ | |
public int scanBackward(int position, int bound, char ch) { | |
return scanBackward(position, bound, new CharacterMatch(ch)); | |
} | |
/** | |
* Finds the highest position in <code>fDocument</code> such that the position is <= <code>position</code> and > <code>bound</code> and | |
* <code>fDocument.getChar(position) == ch</code> evaluates to <code>true</code> for at least one ch in <code>chars</code> and the position is in | |
* the default partition. | |
* | |
* @param position | |
* the first character position in <code>fDocument</code> to be considered | |
* @param bound | |
* the first position in <code>fDocument</code> to not consider any more, with <code>bound</code> < <code>position</code>, or | |
* <code>UNBOUND</code> | |
* @param chars | |
* an array of <code>char</code> to search for | |
* @return the highest position of one element in <code>chars</code> in (<code>bound</code>, <code>position</code>] that resides in a Lua | |
* partition, or <code>NOT_FOUND</code> if none can be found | |
*/ | |
public int scanBackward(int position, int bound, char[] chars) { | |
return scanBackward(position, bound, new CharacterMatch(chars)); | |
} | |
/** | |
* Checks whether <code>position</code> resides in a default (Lua) partition of <code>fDocument</code> or in the LUA string partition. | |
* | |
* @param position | |
* the position to be checked | |
* @return <code>true</code> if <code>position</code> is in the default partition of <code>fDocument</code> or in the Lua string partition, | |
* <code>false</code> otherwise | |
*/ | |
public boolean isAcceptedPartition(int position) { | |
return isAcceptedPartition(getPartition(position)); | |
} | |
/** | |
* Checks whether <code>partition</code> is a default (Lua) partition of <code>fDocument</code> or the LUA string partition. | |
* | |
* @param partition | |
* the partition to be checked | |
* @return <code>true</code> if <code>position</code> is the default partition of <code>fDocument</code> or the Lua string partition, | |
* <code>false</code> otherwise | |
*/ | |
public boolean isAcceptedPartition(ITypedRegion partition) { | |
return IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType()) || ILuaPartitions.LUA_STRING.equals(partition.getType()) | |
|| ILuaPartitions.LUA_SINGLE_QUOTE_STRING.equals(partition.getType()) | |
|| ILuaPartitions.LUA_MULTI_LINE_STRING.equals(partition.getType()); | |
} | |
/** | |
* Returns the partition at <code>position</code>. | |
* | |
* @param position | |
* the position to get the partition for | |
* @return the partition at <code>position</code> or a dummy zero-length partition if accessing the document fails | |
*/ | |
private ITypedRegion getPartition(int position) { | |
if (!contains(fCachedPartition, position)) { | |
Assert.isTrue(position >= 0); | |
Assert.isTrue(position <= fDocument.getLength()); | |
try { | |
fCachedPartition = TextUtilities.getPartition(fDocument, ILuaPartitions.LUA_PARTITIONING, position, false); | |
} catch (BadLocationException e) { | |
fCachedPartition = new TypedRegion(position, 0, "__no_partition_at_all"); //$NON-NLS-1$ | |
} | |
} | |
return fCachedPartition; | |
} | |
/** | |
* Returns <code>true</code> if <code>region</code> contains <code>position</code>. | |
* | |
* @param region | |
* a region | |
* @param position | |
* an offset | |
* @return <code>true</code> if <code>region</code> contains <code>position</code> | |
*/ | |
private boolean contains(IRegion region, int position) { | |
int offset = region.getOffset(); | |
return offset <= position && position < offset + region.getLength(); | |
} | |
public boolean eatForwardBetweenSepartor(int start, int bound, int leftSeparatorToken, int rightSeparatorToken) { | |
int position = start; | |
// first token should be the left separator | |
int currenttoken = nextToken(position, bound); | |
if (currenttoken != leftSeparatorToken) | |
return false; | |
// eat everything until we found the right separator | |
position = getPosition(); | |
do { | |
currenttoken = nextToken(position, bound); | |
// if we found the right separator, so we eat all between the 2 separators | |
if (currenttoken == rightSeparatorToken) { | |
return true; | |
} | |
// if this is a left separator we must eat if completely | |
else if (currenttoken == leftSeparatorToken) { | |
// if we don't eat it correctly return false; | |
if (!eatForwardBetweenSepartor(position, bound, leftSeparatorToken, rightSeparatorToken)) | |
return false; | |
// else we continue to search ending separator | |
} else if (currenttoken == LuaSymbols.TOKEN_EOF) { | |
// End of File we stop the search, we can't not eat the pattern completely. | |
return false; | |
} | |
position = getPosition(); | |
} while (true); | |
} | |
public boolean eatBackwardBetweenSepartor(int start, int bound, int leftSeparatorToken, int rightSeparatorToken) { | |
int position = start; | |
// first token should be the right separator | |
int currenttoken = previousToken(position, bound); | |
if (currenttoken != rightSeparatorToken) | |
return false; | |
// eat everything until we found the left separator | |
position = getPosition(); | |
do { | |
currenttoken = previousToken(position, bound); | |
// if we found the left separator, so we eat all between the 2 separators | |
if (currenttoken == leftSeparatorToken) { | |
return true; | |
} | |
// if this is a right separator we must eat if completely | |
else if (currenttoken == rightSeparatorToken) { | |
// if we don't eat it correctly return false; | |
if (!eatBackwardBetweenSepartor(position, bound, leftSeparatorToken, rightSeparatorToken)) | |
return false; | |
// else we continue to search ending separator | |
} else if (currenttoken == LuaSymbols.TOKEN_EOF) { | |
// End of File we stop the search, we can't not eat the pattern completely. | |
return false; | |
} | |
position = getPosition(); | |
} while (true); | |
} | |
/** | |
* try to guess if cursor is in an expression | |
* | |
* @throws BadLocationException | |
*/ | |
public LuaExpression guessLuaExpression(int start) { | |
try { | |
final int tokenStuffBetweenParentheses = -2000; | |
final int tokenStuffBetweenSquareBracket = -2001; | |
// find begin of expression | |
// ---------------------------------- | |
int position = start - 1; | |
boolean stop = false; | |
// eat first token | |
int previousToken = previousToken(position, UNBOUND); | |
if (previousToken == LuaSymbols.TOKEN_RPAREN) { | |
if (!eatBackwardBetweenSepartor(position, UNBOUND, LuaSymbols.TOKEN_LPAREN, LuaSymbols.TOKEN_RPAREN)) { | |
return null; | |
} else { | |
previousToken = tokenStuffBetweenParentheses; | |
} | |
} else if (previousToken == LuaSymbols.TOKEN_RBRACKET) { | |
if (!eatBackwardBetweenSepartor(position, UNBOUND, LuaSymbols.TOKEN_LBRACKET, LuaSymbols.TOKEN_RBRACKET)) { | |
return null; | |
} else { | |
previousToken = tokenStuffBetweenSquareBracket; | |
} | |
} | |
position = getPosition(); | |
do { | |
// get previous token | |
int currenttoken = previousToken(position, UNBOUND); | |
switch (previousToken) { | |
case LuaSymbols.TOKEN_COLON: | |
case LuaSymbols.TOKEN_DOT: | |
case tokenStuffBetweenParentheses: | |
case tokenStuffBetweenSquareBracket: | |
// manage case where there are 2 dots | |
if (previousToken == LuaSymbols.TOKEN_DOT && currenttoken == LuaSymbols.TOKEN_DOT) { | |
position = position + 1; | |
stop = true; | |
break; | |
} | |
// before dot and colon or left parent, we must found '(...)' or an identifier | |
if (currenttoken != LuaSymbols.TOKEN_RPAREN && currenttoken != LuaSymbols.TOKEN_IDENT | |
&& currenttoken != LuaSymbols.TOKEN_RBRACKET) | |
return null; | |
if (currenttoken == LuaSymbols.TOKEN_RPAREN) { | |
// case : stuff between parentheses | |
if (!eatBackwardBetweenSepartor(position, UNBOUND, LuaSymbols.TOKEN_LPAREN, LuaSymbols.TOKEN_RPAREN)) { | |
return null; | |
} else { | |
previousToken = tokenStuffBetweenParentheses; | |
} | |
} else if (currenttoken == LuaSymbols.TOKEN_RBRACKET) { | |
// case : stuff between parentheses | |
if (!eatBackwardBetweenSepartor(position, UNBOUND, LuaSymbols.TOKEN_LBRACKET, LuaSymbols.TOKEN_RBRACKET)) { | |
return null; | |
} else { | |
previousToken = tokenStuffBetweenSquareBracket; | |
} | |
} else { | |
// case : identifier | |
previousToken = currenttoken; | |
} | |
position = getPosition(); | |
break; | |
case LuaSymbols.TOKEN_IDENT: | |
// before an identifier, we must found dot or colon | |
if (currenttoken == LuaSymbols.TOKEN_COLON || currenttoken == LuaSymbols.TOKEN_DOT) { | |
position = getPosition(); | |
previousToken = currenttoken; | |
} else { | |
stop = true; | |
} | |
break; | |
default: | |
return null; | |
} | |
} while (!stop); | |
// parse expression | |
// ---------------------------------- | |
position = position + 1; | |
int nextToken = nextToken(position, start); | |
// 1 - first token must be an identifier | |
if (nextToken != LuaSymbols.TOKEN_IDENT) | |
return null; | |
String itemname = fDocument.get(position, getPosition() - position).trim(); | |
Item item = new UnknownItem(); | |
item.setName(itemname); | |
Identifier identifier = new Identifier(); | |
identifier.setDefinition(item); | |
identifier.setStart(position); | |
identifier.setEnd(getPosition()); | |
// 2 - next token should be expression (index, call, invoke) | |
stop = false; | |
position = getPosition(); | |
LuaExpression exp = identifier; | |
do { | |
nextToken = nextToken(position, start); | |
switch (nextToken) { | |
case LuaSymbols.TOKEN_EOF: | |
stop = true; | |
break; | |
case LuaSymbols.TOKEN_LBRACKET: | |
// create index | |
Index squareIndex = new Index(); | |
squareIndex.setLeft(exp); | |
// if this is a string we try to find the right value of the index. | |
int contentbegin = getPosition(); | |
nextToken = nextToken(getPosition(), start); | |
if (nextToken == LuaSymbols.TOKEN_STRING) { | |
String fieldname = fDocument.get(contentbegin, getPosition() - contentbegin).trim(); | |
if (fieldname != null) { | |
if (fieldname.startsWith("\"") && fieldname.endsWith("\"")) {//$NON-NLS-1$//$NON-NLS-2$ | |
fieldname = fieldname.replaceAll("^\"(.*)\"$", "$1"); //$NON-NLS-1$//$NON-NLS-2$ | |
} else if (fieldname.startsWith("'") && fieldname.endsWith("'")) {//$NON-NLS-1$//$NON-NLS-2$ | |
fieldname = fieldname.replaceAll("^'(.*)'$", "$1"); //$NON-NLS-1$//$NON-NLS-2$ | |
} else { | |
fieldname = null; | |
} | |
squareIndex.setRight(fieldname); | |
} | |
} | |
// eat bracket | |
eatForwardBetweenSepartor(position, start, LuaSymbols.TOKEN_LBRACKET, LuaSymbols.TOKEN_RBRACKET); | |
// update loop variable | |
exp = squareIndex; | |
position = getPosition(); | |
break; | |
case LuaSymbols.TOKEN_DOT: | |
// manage dot index | |
position = getPosition(); | |
nextToken = nextToken(getPosition(), start); | |
// next token should be an identifier or EOF for incomplete index | |
String indexfieldname; | |
if (nextToken == LuaSymbols.TOKEN_EOF) { | |
indexfieldname = ""; //$NON-NLS-1$ | |
} else if (nextToken == LuaSymbols.TOKEN_IDENT) { | |
indexfieldname = fDocument.get(position, getPosition() - position).trim(); | |
} else { | |
// if it's not an identifier or a EOF, we do not manage this case | |
return null; | |
} | |
// create index | |
Index index = new Index(); | |
index.setLeft(exp); | |
index.setRight(indexfieldname); | |
if (nextToken == LuaSymbols.TOKEN_EOF) | |
index.setIncomplete(true); // no identifier after the '.' this is an incomplete Index | |
// update loop variable | |
exp = index; | |
position = getPosition(); | |
break; | |
case LuaSymbols.TOKEN_COLON: | |
// manage invoke | |
position = getPosition(); | |
// next token should be an identifier or EOF for incomplete invocation | |
nextToken = nextToken(getPosition(), start); | |
String invokefunctionname; | |
if (nextToken == LuaSymbols.TOKEN_EOF) { | |
invokefunctionname = ""; //$NON-NLS-1$ | |
} else if (nextToken == LuaSymbols.TOKEN_IDENT) { | |
invokefunctionname = fDocument.get(position, getPosition() - position).trim(); | |
} else { | |
// if it's not an identifier or a EOF, we do not manage this case | |
return null; | |
} | |
// create Invoke | |
Invoke invoke = new Invoke(); | |
invoke.setRecord(exp); | |
invoke.setFunctionName(invokefunctionname); | |
// consume the full invocation ( left parent until right parent) | |
position = getPosition(); | |
nextToken = nextToken(getPosition(), start); | |
if (nextToken == LuaSymbols.TOKEN_LPAREN) { | |
if (!eatForwardBetweenSepartor(position, start, LuaSymbols.TOKEN_LPAREN, LuaSymbols.TOKEN_RPAREN)) | |
invoke.setIncomplete(true); // no right parentheses so this is an incomplete invoke | |
} else if (nextToken != LuaSymbols.TOKEN_EOF) { | |
return null; // invalid invocation | |
} else { | |
invoke.setIncomplete(true); // no left parentheses so this is an incomplete invoke | |
} | |
// update loop variable | |
exp = invoke; | |
position = getPosition(); | |
break; | |
case LuaSymbols.TOKEN_LPAREN: | |
// manage call | |
Call call = new Call(); | |
call.setFunction(exp); | |
// consume the call invocation ( left parent until right parent) | |
if (!eatForwardBetweenSepartor(position, start, LuaSymbols.TOKEN_LPAREN, LuaSymbols.TOKEN_RPAREN)) { | |
call.setIncomplete(true); // no right parentheses so this is an incomplete invoke | |
} | |
// update loop variable | |
exp = call; | |
position = getPosition(); | |
break; | |
default: | |
return null; | |
} | |
} while (!stop); | |
return exp; | |
} catch (BadLocationException e) { | |
} | |
return null; | |
} | |
} |