/**
* 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.mapping;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.ui.IEditorInput;
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.php.PHPEditorPlugin;
import com.aptana.editor.php.indexer.IElementEntry;
import com.aptana.editor.php.indexer.IElementsIndex;
import com.aptana.editor.php.indexer.IIndexReporter;
import com.aptana.editor.php.indexer.IReportable;
import com.aptana.editor.php.indexer.PHPGlobalIndexer;
import com.aptana.editor.php.internal.contentAssist.ContentAssistFilters;
import com.aptana.editor.php.internal.contentAssist.PHPContentAssistProcessor;
import com.aptana.editor.php.internal.contentAssist.PHPContextCalculator;
import com.aptana.editor.php.internal.contentAssist.PHPTokenType;
import com.aptana.editor.php.internal.contentAssist.ParsingUtils;
import com.aptana.editor.php.internal.core.IPHPConstants;
import com.aptana.editor.php.internal.core.builder.IBuildPath;
import com.aptana.editor.php.internal.core.builder.IModule;
import com.aptana.editor.php.internal.indexer.AbstractPHPEntryValue;
import com.aptana.editor.php.internal.indexer.ModuleSubstitutionIndex;
import com.aptana.editor.php.internal.indexer.PDTPHPModuleIndexer;
import com.aptana.editor.php.internal.indexer.UnpackedElementIndex;
import com.aptana.editor.php.internal.ui.editor.PHPSourceEditor;
import com.aptana.ide.ui.io.internal.UniformFileStoreEditorInput;
import com.aptana.parsing.lexer.Lexeme;
/**
* PHPOffsetMapper
*
* @author Denis Denisenko, Shalom Gibly
*/
public class PHPOffsetMapper
{
private static final String NEW = "new"; //$NON-NLS-1$
/**
* Whether reported stack is global.
*/
private boolean reportedStackIsGlobal;
private PHPSourceEditor phpSourceEditor;
private String namespace;
private Map<String, String> aliases;
/**
* Constructs a new PHP offset mapper with a given PHP editor.
*
* @param phpSourceEditor
*/
public PHPOffsetMapper(PHPSourceEditor phpSourceEditor)
{
this.phpSourceEditor = phpSourceEditor;
}
/**
* Global imports.
*/
private Set<String> globalImports;
/**
* Returns the first matching {@link IElementEntry} origin for the given lexeme.
*
* @param lexeme
* @param lexemeProvider
* @return An {@link IElementEntry} matching the lexeme origin, or null if none was found.
*/
public IElementEntry findEntry(Lexeme<PHPTokenType> lexeme, ILexemeProvider<PHPTokenType> lexemeProvider)
{
IDocument document = phpSourceEditor.getDocumentProvider().getDocument(phpSourceEditor.getEditorInput());
String source = document.get();
Set<IElementEntry> entries = collectEntries(source, lexeme);
if (entries.isEmpty())
{
return null;
}
List<IElementEntry> sortedEntries = sortByModule(entries);
return sortedEntries.get(0);
}
/**
* Find the ICodeLocation for the given lexeme.
*
* @param lexeme
* The current lexeme
* @param lexemeProvider
* The lexeme provider, for cases that require lexeme inspection
*/
public ICodeLocation[] findTargets(Lexeme<PHPTokenType> lexeme, ILexemeProvider<PHPTokenType> lexemeProvider)
{
String source = null;
IEditorInput editorInput = phpSourceEditor.getEditorInput();
try
{
// Check if we are in an 'include' or 'require'
IDocument document = phpSourceEditor.getDocumentProvider().getDocument(phpSourceEditor.getEditorInput());
source = document.get();
ITypedRegion partition = document.getPartition(lexeme.getStartingOffset());
int previousPartitionEnd = (partition != null) ? partition.getOffset() - 1 : -1;
if (previousPartitionEnd > 0
&& (IPHPConstants.PHP_STRING_SINGLE.equals(partition.getType()) || IPHPConstants.PHP_STRING_DOUBLE
.equals(partition.getType())))
{
// Because we have a different partition type for strings, we have to check the previous partition for
// any include or require lexemes.
// We also have to create a new lexeme provider just for this region check
ILexemeProvider<PHPTokenType> newLexemeProvider = ParsingUtils.createLexemeProvider(document,
previousPartitionEnd);
int lexemePosition = newLexemeProvider.getLexemeFloorIndex(previousPartitionEnd - 1);
Lexeme<PHPTokenType> importLexeme = PHPContextCalculator.findLexemeBackward(newLexemeProvider,
lexemePosition, new String[] { PHPRegionTypes.PHP_INCLUDE, PHPRegionTypes.PHP_INCLUDE_ONCE,
PHPRegionTypes.PHP_REQUIRE, PHPRegionTypes.PHP_REQUIRE_ONCE }, new String[] {
PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN });
if (importLexeme != null)
{
return getIncludeLocation(lexeme, source);
}
}
}
catch (Exception e)
{
IdeLog.logError(PHPEditorPlugin.getDefault(), "Error locating code-location target", e); //$NON-NLS-1$
}
String fullPath = null;
IFileStore remoteFileStore = null;
List<IElementEntry> sortedEntries = sortByModule(collectEntries(source, lexeme));
List<CodeLocation> locations = new ArrayList<CodeLocation>(5);
for (IElementEntry entry : sortedEntries)
{
Object value = entry.getValue();
if (value instanceof AbstractPHPEntryValue)
{
if (entry.getModule() != null)
{
fullPath = entry.getModule().getFullPath();
if (editorInput instanceof UniformFileStoreEditorInput)
{
UniformFileStoreEditorInput uniformInput = (UniformFileStoreEditorInput) editorInput;
if (uniformInput.isRemote())
{
if (uniformInput.getLocalFileStore().toString().equals(fullPath))
{
remoteFileStore = uniformInput.getFileStore();
}
fullPath = null;
}
}
if (fullPath != null || remoteFileStore != null)
{
AbstractPHPEntryValue phpEntryValue = (AbstractPHPEntryValue) value;
int startOffset = phpEntryValue.getStartOffset();
Lexeme<PHPTokenType> startLexeme = new Lexeme<PHPTokenType>(new PHPTokenType(
PHPRegionTypes.UNKNOWN_TOKEN), startOffset, startOffset, StringUtil.EMPTY);
if (remoteFileStore != null)
{
locations.add(new CodeLocation(remoteFileStore, startLexeme));
}
locations.add(new CodeLocation(fullPath, startLexeme));
}
}
}
}
return locations.toArray(new CodeLocation[locations.size()]);
}
/**
* Collect a set of {@link IElementEntry}s.
*
* @param offset
* @param source
* @param lexeme
* @return A collection of IElementEntries
*/
@SuppressWarnings("unchecked")
private Set<IElementEntry> collectEntries(String source, Lexeme<PHPTokenType> lexeme)
{
boolean isFunctionCall = isFunctionCall(lexeme, source);
boolean isConstructor = isConstructorCall(lexeme, source);
int offset = lexeme.getEndingOffset();
IModule module = phpSourceEditor.getModule();
if (module == null)
{
return Collections.EMPTY_SET;
}
Set<IElementEntry> entries = null;
IElementsIndex index = getIndex(source, offset);
// trying to get dereference entries
IDocument document = phpSourceEditor.getDocumentProvider().getDocument(phpSourceEditor.getEditorInput());
List<String> callPath = ParsingUtils.parseCallPath(null, source, offset, PHPContentAssistProcessor.OPS, false,
document);
if (callPath == null || callPath.isEmpty())
{
return Collections.EMPTY_SET;
}
if (callPath.size() > 1)
{
if (PHPContentAssistProcessor.DEREFERENCE_OP.equals(callPath.get(1)))
{
entries = PHPContentAssistProcessor.computeDereferenceEntries(index, callPath, offset, module, true,
aliases, namespace);
}
else
{
entries = PHPContentAssistProcessor.computeStaticDereferenceEntries(index, callPath, offset, module,
true, aliases, namespace);
}
}
else
{
String toFind = callPath.get(callPath.size() - 1);
boolean variableCompletion = false;
List<IElementEntry> res = new ArrayList<IElementEntry>();
if (toFind.length() > 0 && toFind.charAt(0) == '$')
{
variableCompletion = true;
toFind = toFind.substring(1);
}
else
{
// Constants first
res.addAll(PHPContentAssistProcessor.computeDefines(toFind, index, null, true));
}
toFind = toFind.toLowerCase();
res.addAll(PHPContentAssistProcessor.computeSimpleIdentifierEntries(reportedStackIsGlobal, globalImports,
toFind, variableCompletion, index, true, module, false, namespace, aliases));
if (res != null)
{
entries = new LinkedHashSet<IElementEntry>();
entries.addAll(res);
}
}
if (entries == null)
{
return Collections.EMPTY_SET;
}
if (isFunctionCall && !isConstructor)
{
entries = ContentAssistFilters.filterAllButFunctions(entries, index);
}
else if (isConstructor)
{
entries = ContentAssistFilters.filterAllButClasses(entries, index);
}
else
{
entries = ContentAssistFilters.filterAllButVariablesAndClasses(entries, index);
}
if (entries == null || entries.size() == 0)
{
return Collections.EMPTY_SET;
}
return entries;
}
/**
* Gets include location.
*
* @param lexeme
* - include lexeme.
* @param source
* - source.
* @return location or null.
*/
private ICodeLocation[] getIncludeLocation(Lexeme<PHPTokenType> lexeme, String source)
{
String moduleName = getIncludeModuleName(lexeme, source);
if (moduleName == null)
{
return null;
}
IModule module = phpSourceEditor.getModule();
if (module == null)
{
return null;
}
IBuildPath buildPath = module.getBuildPath();
if (buildPath == null)
{
return null;
}
try
{
Path path = new Path(moduleName);
if (path.isAbsolute())
{
return null;
}
IModule includedModule = buildPath.resolveRelativePath(module, path);
if (includedModule == null)
{
return null;
}
Lexeme<PHPTokenType> startLexeme = new Lexeme<PHPTokenType>(new PHPTokenType(PHPRegionTypes.UNKNOWN_TOKEN),
0, 0, StringUtil.EMPTY);
return new CodeLocation[] { new CodeLocation(includedModule.getFullPath(), startLexeme) };
}
catch (Throwable th) // $codepro.audit.disable emptyCatchClause
{
// skip
}
return null;
}
/**
* Gets include module name.
*
* @param lexeme
* - include lexeme.
* @param source
* - source.
* @return module name or null
*/
private String getIncludeModuleName(Lexeme<PHPTokenType> lexeme, String source)
{
if (lexeme != null)
{
String includeString = lexeme.getText();
if (includeString != null && includeString.length() > 2)
{
return includeString.substring(1, includeString.length() - 1);
}
}
return null;
}
/**
* Checks whether lexeme has "new" in the left.
*
* @param lexeme
* - lexeme.
* @param source
* - source.
* @return true if constructor, false otherwise.
*/
private boolean isConstructorCall(Lexeme<PHPTokenType> lexeme, String source)
{
int searchStringPos = NEW.length() - 1;
// going left searching for the "new" sequence
for (int i = lexeme.getStartingOffset() - 1; i >= 0; i--)
{
if (searchStringPos == -1)
{
return true;
}
if (i > source.length() - 1)
{
return false;
}
char ch = source.charAt(i);
if (ch == NEW.charAt(searchStringPos))
{
searchStringPos--;
}
else if (!Character.isWhitespace(ch))
{
return false;
}
}
return searchStringPos == -1;
}
/**
* Checks whether lexeme is function call.
*
* @param lexeme
* - lexeme.
* @param source
* - source.
* @return true if function call, false otherwise
*/
private boolean isFunctionCall(Lexeme<PHPTokenType> lexeme, String source)
{
// going right searching for "(" character
for (int i = lexeme.getEndingOffset() + 1; i < source.length(); i++)
{
char ch = source.charAt(i);
if (ch == '(')
{
return true;
}
else if (!Character.isWhitespace(ch))
{
return false;
}
}
return false;
}
/**
* Gets elements index for a module.
*
* @param content
* - module content.
* @param offset
* @return elements index
*/
public IElementsIndex getIndex(String content, int offset)
{
IModule currentModule = phpSourceEditor.getModule();
if (currentModule == null)
{
return PHPGlobalIndexer.getInstance().getIndex();
}
final UnpackedElementIndex index = new UnpackedElementIndex();
PDTPHPModuleIndexer indexer = new PDTPHPModuleIndexer(false, offset);
indexer.setUpdateTaskTags(false);
indexer.indexModule(content, currentModule, new IIndexReporter()
{
public IElementEntry reportEntry(int category, String entryPath, IReportable value, IModule module)
{
return index.addEntry(category, entryPath, value, module);
}
});
reportedStackIsGlobal = indexer.isReportedScopeGlobal();
globalImports = indexer.getGlobalImports();
namespace = indexer.getNamespace();
aliases = indexer.getAliases();
ModuleSubstitutionIndex result = new ModuleSubstitutionIndex(currentModule, index, PHPGlobalIndexer
.getInstance().getIndex());
return result;
}
/**
* Sorts entries by module.
*
* @param entries
* - entries.
* @return sorted entries.
*/
private List<IElementEntry> sortByModule(Set<IElementEntry> entries)
{
if (entries == null)
{
return null;
}
// current implementation just puts entries from the current module first, other entries last.
List<IElementEntry> currentModuleEntries = new ArrayList<IElementEntry>();
List<IElementEntry> otherEntries = new ArrayList<IElementEntry>();
IModule currentModule = phpSourceEditor.getModule();
for (IElementEntry entry : entries)
{
if (currentModule != null && entry.getModule() != null && currentModule.equals(entry.getModule()))
{
currentModuleEntries.add(entry);
}
else
{
otherEntries.add(entry);
}
}
List<IElementEntry> toReturn = new ArrayList<IElementEntry>();
toReturn.addAll(currentModuleEntries);
toReturn.addAll(otherEntries);
return toReturn;
}
}