/**
* Aptana Studio
* Copyright (c) 2005-2012 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
* Please see the license.html included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.editor.php.internal.contentAssist;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TypedRegion;
import org2.eclipse.php.internal.core.documentModel.parser.regions.PHPRegionTypes;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.StringUtil;
import com.aptana.editor.common.contentassist.ILexemeProvider;
import com.aptana.editor.common.contentassist.LexemeProvider;
import com.aptana.editor.php.PHPEditorPlugin;
import com.aptana.editor.php.internal.core.IPHPConstants;
import com.aptana.parsing.lexer.Range;
/**
* Utilities for parsing.
*
* @author Denis Denisenko
*/
public final class ParsingUtils
{
/**
* Create a {@link LexemeProvider} for the given partition that is at the offset of the given document.
*
* @param document
* @param offset
* @return A new {@link LexemeProvider} for the partition on that offset.
*/
public static ILexemeProvider<PHPTokenType> createLexemeProvider(IDocument document, int offset)
{
if (offset == document.getLength() && offset > 0)
{
offset--;
}
return new LexemeProvider<PHPTokenType>(document, offset, new PHPScopeScanner())
{
@Override
protected PHPTokenType getTypeFromData(Object data)
{
if (data != null)
{
return new PHPTokenType(data.toString());
}
else
{
return new PHPTokenType(PHPRegionTypes.UNKNOWN_TOKEN);
}
}
};
}
/**
* Create a {@link LexemeProvider} for the entire given document.
*
* @param document
* @return A new {@link LexemeProvider} for the document.
*/
public static ILexemeProvider<PHPTokenType> createLexemeProvider(IDocument document)
{
return new LexemeProvider<PHPTokenType>(document, new Range(0, document.getLength() - 1), new PHPScopeScanner())
{
@Override
protected PHPTokenType getTypeFromData(Object data)
{
if (data != null)
{
return new PHPTokenType(data.toString());
}
else
{
return new PHPTokenType(PHPRegionTypes.UNKNOWN_TOKEN);
}
}
};
}
/**
* Create a {@link LexemeProvider} for the given range in the document.
*
* @param document
* @param start
* - start offset
* @param end
* - end offset
* @return A new {@link LexemeProvider} for the document.
*/
public static ILexemeProvider<PHPTokenType> createLexemeProvider(IDocument document, int start, int end)
{
if (end == document.getLength() && end > 0)
{
end--;
}
return new LexemeProvider<PHPTokenType>(document, new Range(start, end), new PHPScopeScanner())
{
@Override
protected PHPTokenType getTypeFromData(Object data)
{
if (data != null)
{
return new PHPTokenType(data.toString());
}
else
{
return new PHPTokenType(PHPRegionTypes.UNKNOWN_TOKEN);
}
}
};
}
/**
* Gets dereferencing parts.
*
* @param region
* - An {@link ITypedRegion} that will bound the call path search to the region itself (can be null)
* @param content
* - content.
* @param possibleReferenceOperators
* - possible reference operators.
* @param offset
* - offset to start parsing from.
* @return list where the full dereferencing path is encoded or null In example for the dereferencing
* "A::constField->method()->field2" there would be ["A", "::", "constField", "->", "method()", "->",
* "field2"] The last entry may be an empty string if we are completing right after the reference operator.
*/
public static List<String> parseCallPath(ITypedRegion region, String content, int offset,
String[] possibleReferenceOperators)
{
return parseCallPath(region, content, offset, possibleReferenceOperators, false, null);
}
/**
* Gets dereferencing parts.
*
* @param region
* - An {@link ITypedRegion} that will bound the call path search to the region itself (can be null)
* @param content
* - content.
* @param possibleReferenceOperators
* - possible reference operators.
* @param offset
* - offset to start parsing from.
* @param skipInitialSpaces
* - whether to skip initial spaces while parsing.
* @return list where the full dereferencing path is encoded or null In example for the dereferencing
* "A::constField->method()->field2" there would be ["A", "::", "constField", "->", "method()", "->",
* "field2"] The last entry may be an empty string if we are completing right after the reference operator.
*/
public static List<String> parseCallPath(ITypedRegion region, String content, int offset,
String[] possibleReferenceOperators, boolean skipInitialSpaces, IDocument document)
{
try
{
// Try to collect more PHP regions that exist before the given region. This resolve a situation where we get
// PHP String regions that breaks the default regions into a few parts.
if (region != null && document != null)
{
try
{
ITypedRegion prevRegion = null;
int rOffset = region.getOffset() - 1;
int totalLength = region.getLength();
while (rOffset > 0)
{
ITypedRegion partition = document.getPartition(rOffset);
if (!partition.getType().startsWith(IPHPConstants.PREFIX))
{
break;
}
else
{
prevRegion = partition;
rOffset = prevRegion.getOffset() - 1;
totalLength += prevRegion.getLength();
}
}
if (prevRegion != null)
{
// Wrap all regions as one
region = new TypedRegion(prevRegion.getOffset(), totalLength, region.getType());
}
}
catch (BadLocationException e)
{
IdeLog.logError(PHPEditorPlugin.getDefault(), "Error while parsing the call path", e); //$NON-NLS-1$
}
}
List<String> result = new ArrayList<String>();
if (content.length() == 0)
{
result.add(StringUtil.EMPTY);
return result;
}
offset = fixOffset(content, offset);
int currentPos = offset;
// skipping initial white spaces
if (skipInitialSpaces)
{
skipWhiteSpaces(offset, content);
}
// parsing right-most name
String entry = parseFunctionCall(region, content, currentPos, false);
if (entry == null)
{
entry = parseName(region, content, currentPos, false);
}
currentPos -= entry.length();
result.add(entry);
// parsing pairs [name, reference]
while (true)
{
// allowing whites paces before the reference operator
currentPos = skipWhiteSpaces(currentPos, content);
// parsing for reference
String operator = parseReferenceOperator(region, possibleReferenceOperators, currentPos, content);
// if no reference found, returning the result
if (operator == null)
{
return result;
}
currentPos -= operator.length();
// allowing whites paces before the reference operator
currentPos = skipWhiteSpaces(currentPos, content);
// parsing for method call or identifier
entry = parseFunctionCall(region, content, currentPos, false);
if (entry == null)
{
entry = parseName(region, content, currentPos, false);
}
// if no entry found, returning null as call path is incomplete.
if (entry == null || entry.length() == 0)
{
return null;
}
currentPos -= entry.length();
// adding reference operator and the entry to the result.
result.add(0, operator);
result.add(0, entry);
}
}
catch (Throwable th)
{
return null;
}
}
/*
* Fix the offset by searching the closing bracket of a function call.
* @param content
* @param offset
* @return The fixed offset (the closing bracket offset); Or the original offset in case of an error or in case no
* fix was made.
*/
private static int fixOffset(String content, int offset)
{
int originalOffset = offset;
char ch = content.charAt(offset);
if (ch == '(')
{
// scan to the closing and continue
int openings = 1;
int start = offset + 1;
int searchLimit = Math.min(start + 500, content.length());
for (int c = start; c < searchLimit; c++)
{
offset++;
ch = content.charAt(c);
if (ch == '(')
{
openings++;
}
else if (ch == ')')
{
openings--;
}
if (openings == 0)
{
break;
}
}
if (openings != 0)
{
return originalOffset; // The function is not closed
}
}
return offset;
}
/**
* Checks if entry is a function call. Method can only be applied to {@link #parseCallPath(String, int, String[])}
* method result entries.
*
* @param call
* - possible call entry.
* @return true if function call, false otherwise.
*/
public static boolean isFunctionCall(String call)
{
return call.indexOf('(') >= 1;
}
/**
* Gets function name from function call. Method can only be applied to
* {@link #parseCallPath(String, int, String[])} method result entries.
*
* @param call
* - call string.
* @return function name or null.
*/
public static String getFunctionNameFromCall(String call)
{
int openingBracketIndex = call.indexOf('(');
if (openingBracketIndex < 1)
{
return null;
}
return call.substring(0, openingBracketIndex);
}
/**
* Checks whether current content is an identifier.
*
* @param region
* An {@link ITypedRegion} that can be used to limit the search range (can be null)
* @param content
* - current content.
* @param offset
* - current offset.
* @param skipInitialWhitespaces
* - whether to skip initial white spaces.
* @return identifier or null
*/
private static String parseName(ITypedRegion region, String content, int offset, boolean skipInitialWhitespaces)
{
if (content.length() == 0)
{
return null;
}
if (offset < 0)
{
return null;
}
StringBuffer name = new StringBuffer();
int start = offset;
int partitionStart = (region == null) ? 0 : region.getOffset();
if (skipInitialWhitespaces)
{
for (int i = start; i >= partitionStart; i--)
{
char ch = content.charAt(i);
if (!Character.isWhitespace(ch))
{
start = i;
break;
}
}
}
for (int i = start; i >= partitionStart; i--)
{
char ch = content.charAt(i);
if (Character.isJavaIdentifierPart(ch))
{
name.insert(0, ch);
}
else if (ch == '\\')
{
name.insert(0, ch);
}
else if (ch == '$')
{
name.insert(0, ch);
return name.toString();
}
else
{
return name.toString();
}
}
return name.toString();
}
/**
* Parses function call from position.
*
* @param region
* @param content
* - current content.
* @param offset
* - current offset.
* @param skipInitialWhitespaces
* - whether to skip initial white spaces.
* @return identifier or null
*/
private static String parseFunctionCall(ITypedRegion region, String content, int offset,
boolean skipInitialWhitespaces)
{
if (offset < 0)
{
return null;
}
StringBuffer call = new StringBuffer();
int start = offset;
if (skipInitialWhitespaces)
{
start = skipWhiteSpaces(start, content);
}
// initial state
final int initialState = 0;
// state indicating closing bracket is met
final int insideMethodArguments = 1;
// state indicating opening bracket is met
final int argumentsEndMet = 2;
int state = initialState;
int level = 0;
int partitionStart = (region == null) ? 0 : region.getOffset();
for (int i = start; i >= partitionStart; i--)
{
char ch = content.charAt(i);
switch (state)
{
case initialState:
if (ch != ')')
{
return null;
}
call.insert(0, ch);
state = insideMethodArguments;
level++;
break;
case insideMethodArguments:
if (ch == '(')
{
level--;
call.insert(0, ch);
if (level == 0)
{
state = argumentsEndMet;
}
}
else if (ch == ')')
{
level++;
call.insert(0, ch);
}
else
{
call.insert(0, ch);
}
break;
case argumentsEndMet:
if (Character.isJavaIdentifierPart(ch))
{
call.insert(0, ch);
}
else
{
if (isFunctionCall(call.toString()))
{
return call.toString();
}
else
{
return null;
}
}
break;
}
}
// if we haven't met the opening bracket, function call is not parsed
if (state != argumentsEndMet)
{
return null;
}
// checking if we have one or more symbols before the opening bracket
// (method name is not empty)
if (!isFunctionCall(call.toString()))
{
return null;
}
return call.toString();
}
/**
* Parses references.
*
* @param region
* An {@link ITypedRegion} that will bound the reference search to the region itself (can be null)
* @param possibleOperators
* - possible reference operators to parse.
* @param pos
* - position.
* @param contents
* - contents.
* @return parsed reference.
*/
private static String parseReferenceOperator(ITypedRegion region, String[] possibleOperators, int pos,
String contents)
{
for (String possibleReference : possibleOperators)
{
if (parseConstantString(region, possibleReference, pos, contents) != -1)
{
return possibleReference;
}
}
return null;
}
/**
* Skips white spaces.
*
* @param pos
* - position to start from.
* @param contents
* - contents.
* @return the position right before spaces.
*/
private static int skipWhiteSpaces(int pos, String contents)
{
if (pos < 0)
{
return pos;
}
for (int i = pos; i >= 0; i--)
{
if (!Character.isWhitespace(contents.charAt(i)))
{
return i;
}
}
return -1;
}
/**
* Parses simple reference.
*
* @param region
* A region (partition) that bounds the search for the reference. The search will not go left to the
* region's start offset (passing a null region will not limit the scan).
* @param constant
* - constant to parse
* @param start
* - position to start parsing from.
* @param contents
* - contents to parse.
* @return the offset right before the end of the constant or -1 if constant not found.
*/
private static int parseConstantString(ITypedRegion region, String constant, int start, String contents)
{
if (start < constant.length() - 1)
{
return -1;
}
int partitionStart = (region == null) ? 0 : region.getOffset();
int posInConstant = constant.length() - 1;
for (int i = start; i >= partitionStart; i--)
{
if (posInConstant == -1)
{
return i;
}
char contentsChar = contents.charAt(i);
char constantChar = constant.charAt(posInConstant);
if (contentsChar != constantChar)
{
return -1;
}
posInConstant--;
}
return -1;
}
/**
* ParsingUtils parsing constructor.
*/
private ParsingUtils()
{
}
}