/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.plugin.ij.lang.psi.impl;
import com.google.common.base.Strings;
import com.intellij.codeInsight.completion.CompletionInitializationContext;
import com.intellij.extapi.psi.PsiFileBase;
import com.intellij.injected.editor.VirtualFileWindow;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.psi.impl.source.tree.injected.InjectedFileViewProvider;
import com.intellij.psi.stubs.StubElement;
import com.intellij.psi.util.ClassUtil;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.IncorrectOperationException;
import gw.fs.IFile;
import gw.internal.gosu.parser.ErrorType;
import gw.internal.gosu.parser.TypeLoaderAccess;
import gw.internal.gosu.parser.TypeLord;
import gw.lang.parser.*;
import gw.lang.parser.exceptions.ParseResultsException;
import gw.lang.parser.exceptions.SymbolNotFoundException;
import gw.lang.parser.expressions.ITypeLiteralExpression;
import gw.lang.parser.statements.IClassFileStatement;
import gw.lang.reflect.*;
import gw.lang.reflect.gs.ClassType;
import gw.lang.reflect.gs.IGosuClass;
import gw.lang.reflect.gs.ISourceFileHandle;
import gw.lang.reflect.gs.StringSourceFileHandle;
import gw.lang.reflect.module.IModule;
import gw.plugin.ij.completion.GosuClassNameInsertHandler;
import gw.plugin.ij.filesystem.IDEAFile;
import gw.plugin.ij.formatting.GosuLanguageCodeStyleSettingsProvider;
import gw.plugin.ij.intentions.IntentionsFilter;
import gw.plugin.ij.lang.GosuTokenTypes;
import gw.plugin.ij.lang.parser.GosuElementTypes;
import gw.plugin.ij.lang.psi.IGosuFile;
import gw.plugin.ij.lang.psi.IGosuFileBase;
import gw.plugin.ij.lang.psi.IGosuPsiElement;
import gw.plugin.ij.lang.psi.api.IGosuPackageDefinition;
import gw.plugin.ij.lang.psi.api.statements.IGosuUsesStatementList;
import gw.plugin.ij.lang.psi.api.statements.typedef.IGosuClassDefinition;
import gw.plugin.ij.lang.psi.api.statements.typedef.IGosuTypeDefinition;
import gw.plugin.ij.lang.psi.impl.expressions.GosuTypeLiteralImpl;
import gw.plugin.ij.lang.psi.stubs.elements.GosuStubFileElementType;
import gw.plugin.ij.lang.psi.util.GosuPsiParseUtil;
import gw.plugin.ij.util.ExceptionUtil;
import gw.plugin.ij.util.FileUtil;
import gw.plugin.ij.util.GosuModuleUtil;
import gw.plugin.ij.util.InjectedElementEditor;
import gw.util.fingerprint.FP64;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.*;
import java.util.List;
import static com.google.common.collect.Iterables.filter;
public abstract class AbstractGosuClassFileImpl extends PsiFileBase implements IGosuFileBase, PsiClassOwnerEx, IGosuFile, IntentionFilterOwner {
private static final Logger LOG = Logger.getInstance(AbstractGosuClassFileImpl.class);
private boolean _bReparse;
private IntentionActionsFilter myIntentionActionsFilter;
// private GosuClassParseData parseData;
public AbstractGosuClassFileImpl(@NotNull FileViewProvider viewProvider, @NotNull Language language) {
super(viewProvider, language);
myIntentionActionsFilter = new IntentionsFilter();
}
@NotNull
public FileType getFileType() {
return getViewProvider().getVirtualFile().getFileType();
}
public void accept(@NotNull GosuElementVisitor visitor) {
visitor.visitElement(this);
}
public void acceptChildren(@NotNull GosuElementVisitor visitor) {
PsiElement child = getFirstChild();
while (child != null) {
if (child instanceof IGosuPsiElement) {
((IGosuPsiElement) child).accept(visitor);
}
child = child.getNextSibling();
}
}
public IGosuTypeDefinition[] getTypeDefinitions() {
final StubElement<?> stub = getStub();
if (stub != null) {
return stub.getChildrenByType(GosuElementTypes.TYPE_DEFINITION_TYPES, IGosuTypeDefinition.ARRAY_FACTORY);
} else {
return calcTreeElement().getChildrenAsPsiElements(GosuElementTypes.TYPE_DEFINITION_TYPES, IGosuTypeDefinition.ARRAY_FACTORY);
}
}
@NotNull
public PsiClass[] getClasses() {
try {
return getTypeDefinitions();
} catch (ProcessCanceledException e) {
throw e;
} catch (Throwable e) {
if (ExceptionUtil.isWrappedCanceled(e)) {
throw new ProcessCanceledException(e.getCause());
} else {
LOG.error(e);
return PsiClass.EMPTY_ARRAY;
}
}
}
@Nullable
public PsiClass getPsiClass() {
final PsiClass[] classes = getClasses();
return classes.length > 0 ? classes[0] : null;
}
@Override
public PsiModifierList getModifierList() {
return null;
}
@Override
public boolean hasModifierProperty(@PsiModifier.ModifierConstant @NonNls @NotNull String name) {
return false;
}
protected boolean isForComplection(@NotNull CharSequence contents) {
final int offset = contents.toString().indexOf(CompletionInitializationContext.DUMMY_IDENTIFIER);
return offset >= 0;
}
protected IGosuParser createParser(CharSequence contents) {
final IGosuParser parser = GosuParserFactory.createParser(TypeSystem.getCompiledGosuClassSymbolTable(), ScriptabilityModifiers.SCRIPTABLE);
parser.setThrowParseExceptionForWarnings(true);
parser.setDontOptimizeStatementLists(true);
parser.setWarnOnCaseIssue(true);
parser.setEditorParser(true);
parser.setScript(contents);
return parser;
}
@Nullable
protected SymbolNotFoundException getExceptionWithSymbolTable(@NotNull ParseResultsException parseException) {
for (SymbolNotFoundException e : filter(parseException.getParseExceptions(), SymbolNotFoundException.class)) {
if (e.getSymbolName().endsWith(CompletionInitializationContext.DUMMY_IDENTIFIER_TRIMMED)) {
return e;
}
}
return null;
}
// "package" + "." + "class"
public String getQualifiedClassNameFromFile() {
VirtualFile psiFile = FileUtil.getFileFromPsi(this);
if( psiFile == null ) {
return "#Err#NullFile";
}
IModule module = getModule();
if( module == null ) {
return "Err#NullModule";
}
return FileUtil.getSourceQualifiedName(psiFile, getModule());
}
// TODO: merge into getModule() ?
public IModule getModuleForPsi() {
PsiFile targetFile = this;
// Special case for code injection
final FileViewProvider provider = getViewProvider();
if (provider instanceof InjectedFileViewProvider) {
VirtualFile file = provider.getVirtualFile();
if (file instanceof VirtualFileWindow) {
file = FileUtil.getOriginalFile((VirtualFileWindow) file);
}
targetFile = PsiManager.getInstance(getProject()).findFile(file);
}
IModule module = GosuModuleUtil.findModuleForPsiElement(targetFile);
if (module == null) {
final VirtualFile file = FileUtil.getFileFromPsi(targetFile);
if (file != null) {
module = GosuModuleUtil.findModuleForFile(file, getProject());
}
}
return module;
}
public IModule getModule() {
final IModule module = getModuleForPsi();
if (module == null) {
final VirtualFile vfile = getVirtualFile();
if (getName().equals(GosuLanguageCodeStyleSettingsProvider.FORMATTING_FILE_NAME) &&
(vfile == null || vfile instanceof LightVirtualFile)) {
return GosuModuleUtil.getGlobalModule(getProject()); // this is the code style class case
} else {
// throw new IllegalStateException("Module should not be null");
return GosuModuleUtil.getGlobalModule(getProject()); // TODO: For code fragment editor (it contains a copy of injected code fragment)
}
}
return module;
}
public abstract ASTNode parse(ASTNode chameleon);
public final IGosuClass parseType(boolean forceRefresh) {
final String strClassName = getQualifiedClassNameFromFile();
final String contents = (this instanceof GosuProgramFileImpl) ? GosuPsiImplUtil.getFileSource(this) : getViewProvider().getContents().toString();
final int completionMarkerOffset = contents.indexOf(CompletionInitializationContext.DUMMY_IDENTIFIER);
final boolean isForCompletion = completionMarkerOffset >= 0;
// long t1 = System.nanoTime();
try {
IGosuClass gsClass;
if (!isForCompletion) {
final IModule module = getModule();
TypeSystem.pushModule(module);
TypeSystem.lock();
try {
IType type = TypeSystem.getByFullNameIfValid(strClassName, module);
if (type != null) {
gsClass = (IGosuClass) type;
// if the class is not compiled yet gsClass.getSourceFingerprint() will return 0 and we will NOT refresh the type
// if the class is already compiled but it is old, we will refresh the type first
final long contentsFingerPrint = new FP64(contents).getRawFingerprint();
if (forceRefresh || (contentsFingerPrint != gsClass.getSourceFingerprint())) {
refreshFile(gsClass);
}
gsClass.setCreateEditorParser(true);
try {
gsClass.isValid();
} finally {
gsClass.setCreateEditorParser(false);
}
if (contentsFingerPrint == gsClass.getSourceFingerprint()) {
saveParseData(contents, gsClass);
// System.out.println("Class source: " + gsClass.getSource());
return gsClass;
}
}
} finally {
TypeSystem.unlock();
TypeSystem.popModule(module);
}
}
return parseType(strClassName, contents, completionMarkerOffset);
} finally {
// System.out.println((isForCompletion ? "Compl. parse: " : "Normal parse: ") + "(" + forceRefresh + ")" + "" + ", in: " + (System.nanoTime() - t1) * 1e-6 + "ms");
}
}
private void refreshFile( IGosuClass gsClass ) {
IFile[] sourceFiles = gsClass.getSourceFiles();
if( sourceFiles.length > 0 ) {
for( IFile file: sourceFiles ) {
TypeLoaderAccess.instance().refreshed( file, null, RefreshKind.MODIFICATION);
}
}
else {
TypeSystem.refresh( (ITypeRef)gsClass);
}
}
private void saveParseData(@NotNull String contents, @NotNull IGosuClass gsClass) {
if (getOriginalFile().getVirtualFile() != null) {
final GosuClassParseData parseData = getParseData();
if (parseData != null) {
parseData.setClassFileStatement(gsClass.getClassStatement().getClassFileStatement());
parseData.setSource(contents);
}
}
}
protected abstract IGosuClass parseType(String strClassName, String contents, int completionMarkerOffset);
protected void refreshTheOldType() {
if (!DumbService.isDumb(getProject())) {
TypeSystem.refreshed(getFilePath());
}
}
@Nullable
protected IDEAFile getFilePath() {
final VirtualFile file = FileUtil.getFileFromPsi(this);
return file != null ? FileUtil.toIFile(file) : null;
}
@Nullable
public String getFileExtension() {
final VirtualFile file = FileUtil.getFileFromPsi(this);
return file.getExtension();
}
public String toString() {
return String.format( "%s [%s]", getClass().getName(), getQualifiedClassNameFromFile() );
}
// PsiClassOwner
@NotNull
public String getPackageName() {
// TODO: use stab to get package name
return ClassUtil.extractPackageName( getQualifiedClassNameFromFile() );
}
public void setPackageName(@NotNull String packageName) throws IncorrectOperationException {
final IModule module = getModule();
TypeSystem.pushModule(module);
try {
final ASTNode node = getNode();
final ASTNode packageNode = calcTreeElement().findChildByType(GosuElementTypes.ELEM_TYPE_NamespaceStatement);
final IGosuPackageDefinition currentPackage = packageNode != null ? (IGosuPackageDefinition) packageNode.getPsi() : null;
if (Strings.isNullOrEmpty(packageName)) {
// Empty name
if (currentPackage != null) {
node.removeChild(currentPackage.getNode());
}
} else {
// Non-empty name
redefineImports(packageName);
final PsiElement newPackage = GosuPsiParseUtil.parsePackageStatement(packageName, this);
final ASTNode newNode = newPackage.getNode();
if (currentPackage != null) {
if (!packageName.equals(currentPackage.getPackageName())) {
final ASTNode currNode = currentPackage.getNode();
CodeEditUtil.setOldIndentation((TreeElement) newNode, 0); // this is to avoid a stupid exception
node.replaceChild(currNode, newNode);
}
} else {
final ASTNode anchor = node.getFirstChildNode();
node.addChild(newNode, anchor);
node.addLeaf(GosuTokenTypes.TT_WHITESPACE, "\n", anchor);
}
}
reparseAfterSetPackageName();
} finally {
TypeSystem.popModule(module);
}
}
private void redefineImports(String packageName) {
List<PsiElement> typeLiterals = findTypeLiterals();
if (typeLiterals.isEmpty()) {
return;
}
IGosuUsesStatementList oldUsesList = findUsesStatementList();
final StringBuilder sb = new StringBuilder();
Set<String> types = new HashSet<>();
for (PsiElement typeLiteral : typeLiterals) {
GosuTypeLiteralImpl gsTypeLiteral = (GosuTypeLiteralImpl) typeLiteral;
String relativeStr = gsTypeLiteral.getText();
if (!relativeStr.startsWith(packageName)) {
ITypeLiteralExpression parsedElement = gsTypeLiteral.getParsedElement();
if(parsedElement == null) {
continue;
}
IType type = parsedElement.getType().getType();
if( type.getEnclosingType() == null) {
String namespace = type.getNamespace();
if (!(type instanceof ErrorType) &&
!type.isArray() &&
namespace != null &&
!namespace.equals(packageName)) {
String fqn = type.getName();
//fqn is used
if (relativeStr.equals(fqn)) {
continue;
}
if (isTypeVisibleInsideFile(fqn, relativeStr)) {
continue;
}
String name = TypeLord.getPureGenericType(type).getName();
if (!types.add(name)) {
sb.append(String.format("uses %s\n", name));
}
}
}
}
}
final String usesList = sb.toString();
IGosuUsesStatementList newUsesList = !usesList.isEmpty() ? GosuPsiParseUtil.parseUsesList(sb.toString(), this) : null;
if (oldUsesList == null && newUsesList != null) {
addAfter(newUsesList, findChildByClass(IGosuPackageDefinition.class));
} else {
if (newUsesList != null) {
oldUsesList.replace(newUsesList);
} else if (oldUsesList != null) {
oldUsesList.removeStatement();
}
}
}
private boolean isTypeVisibleInsideFile(String fqn, String relativeStr){
GosuClassParseData parseData = getParseData();
if (parseData != null) {
IClassFileStatement classFileStatement = parseData.getClassFileStatement();
if (classFileStatement != null) {
IGosuClass gosuClass = classFileStatement.getGosuClass();
if (gosuClass != null) {
if(GosuClassNameInsertHandler.resolveRelativeTypeInParser(fqn, relativeStr, gosuClass) ) {
return true;
}
}
}
}
return false;
}
private List<PsiElement> findTypeLiterals() {
List<PsiElement> list = new ArrayList<>();
GosuBaseElementImpl.findElements(this, GosuElementTypes.ELEM_TYPE_TypeLiteral, list);
return list;
}
protected void reparseAfterSetPackageName() {
reparseGosuFromPsi();
}
// PsiClassOwnerEx
@Override
public Set<String> getClassNames() {
final VirtualFile file = getVirtualFile();
return file == null ? Collections.<String>emptySet() : Collections.singleton(file.getNameWithoutExtension());
}
// IGosuFile
@Override
public void addImport(String qualifiedName) {
Boolean singleLine = getUserData( InjectedElementEditor.SINGLE_LINE_EDITOR );
if (singleLine != null && singleLine) {
return;
}
final PsiElement uses = GosuPsiParseUtil.parseImport(qualifiedName, this);
final IGosuUsesStatementList usesList = findUsesStatementList();
if (usesList != null) {
usesList.add(uses);
} else {
addAfter(uses, findChildByClass(IGosuPackageDefinition.class));
}
// Do we need reparseFromPsi() here?
}
public IGosuUsesStatementList findUsesStatementList() {
IGosuUsesStatementList usesStmtList = findChildByClass( IGosuUsesStatementList.class );
if( usesStmtList == null ) {
PsiElement firstChild = getFirstChild();
if( firstChild instanceof IGosuClassDefinition ) {
for( PsiElement child : firstChild.getChildren() ) {
if( child instanceof IGosuUsesStatementList ) {
usesStmtList = (IGosuUsesStatementList) child;
break;
}
}
}
}
return usesStmtList;
}
public GosuClassParseData getParseData() {
// if (parseData == null) {
return GosuClassParseDataCache.getParseData(null, getOriginalFile().getVirtualFile(), getModule());
// }
// return parseData;
}
/**
* Call after manipulating PSI so that Gosu parse tree results attached to the psi file are in sync
*/
public IGosuClass reparseGosuFromPsi() {
return parseType(true);
}
/**
* If need be force the PSI to reparse from the psiFile's existing content
*/
public boolean reparsePsiFromContent() {
if( !reparsePsiFromContentIfNecessary() ) {
if( needsGosuReparsing() ) {
reparseGosuFromPsi();
}
return false;
}
return true;
}
private boolean reparsePsiFromContentIfNecessary() {
if( needsPsiReparse() ) {
if( EventQueue.isDispatchThread() ) {
reloadPsi();
}
else {
EventQueue.invokeLater(
new Runnable() {
public void run() {
reloadPsi();
}
} );
}
return true;
}
return false;
}
private boolean needsPsiReparse() {
PsiElement firstChild = getFirstChild();
if(firstChild == null) {
return false;
}
Boolean isRawText = firstChild.getUserData( GosuStubFileElementType.KEY_RAW_TEXT );
return isRawText != null && isRawText;
}
private boolean needsGosuReparsing() {
if( isReparse() ) {
setReparse( false );
return false;
}
AbstractGosuClassFileImpl origFile = (AbstractGosuClassFileImpl)getOriginalFile();
if( origFile != null && origFile.isReparse() ) {
origFile.setReparse( false );
return false;
}
return true;
}
private void reloadPsi() {
ApplicationManager.getApplication().runWriteAction(
new Runnable() {
public void run() {
// Mark this file so that it reparses
((AbstractGosuClassFileImpl)getOriginalFile()).setReparse( true );
// Force this file to reload its contents which in turn leads to a reparse
onContentReload();
}
} );
}
public void setReparse( boolean bReparse ) {
_bReparse = bReparse;
}
public boolean isReparse() {
return _bReparse;
}
// PsiImportHolder
public boolean importClass(@NotNull PsiClass psiClass) {
addImport(psiClass.getQualifiedName());
return true;
}
protected IGosuClass parseGosuClassDirectly(String strClassName, String contents, int completionMarkerOffset, ClassType classType) {
long t1 = System.nanoTime();
final IModule module = getModule();
TypeSystem.pushModule(module);
// DP: this line should execute outside the TS lock to avoid deadlock
final IGosuParser parser = createParser(contents);
TypeSystem.lock();
try {
if (completionMarkerOffset >= 0) {
parser.setSnapshotSymbols();
}
refreshTheOldType();
IGosuClass gsClass = null;
ISymbolTable snapshotSymbols = null;
IType ctxType = null;
try {
final ISourceFileHandle sourceFileHandle = new StringSourceFileHandle(strClassName, contents, getFilePath(), false, classType);
gsClass = parser.parseClass(strClassName, sourceFileHandle, true, true);
} catch (ParseResultsException e) {
if (e.getParsedElement() instanceof IClassFileStatement) {
IClassFileStatement classFileStmt = (IClassFileStatement) e.getParsedElement();
if (classFileStmt.getClassStatement() != null) {
gsClass = classFileStmt.getClassStatement().getGosuClass();
} else {
gsClass = classFileStmt.getGosuClass();
}
}
final SymbolNotFoundException issue = getExceptionWithSymbolTable(e);
if (issue != null) {
snapshotSymbols = issue.getSymbolTable();
ctxType = issue.getExpectedType();
}
}
if (getOriginalFile().getVirtualFile() != null) {
GosuClassParseData parseData = getParseData();
if(parseData != null) {
parseData.setClassFileStatement(gsClass.getClassStatement().getClassFileStatement());
parseData.setSource(contents);
if (completionMarkerOffset >= 0) {
parseData.setSnapshotSymbols(snapshotSymbols);
parseData.setContextType(ctxType);
}
}
}
return gsClass;
} finally {
TypeSystem.unlock();
TypeSystem.popModule( module );
boolean compl = completionMarkerOffset >= 0;
// System.out.println("\t" + strClassName + " - " + (System.nanoTime() - t1) * 1e-6 + " (direct) " + compl);
}
}
@Override
public void setIntentionActionsFilter(final IntentionActionsFilter filter) {
myIntentionActionsFilter = filter;
}
@Override
public IntentionActionsFilter getIntentionActionsFilter() {
return myIntentionActionsFilter;
}
@Override
public void accept(@NotNull PsiElementVisitor visitor) {
if( visitor instanceof GosuElementVisitor) {
((GosuElementVisitor)visitor).visitFile(this);
}
else {
visitor.visitFile( this );
}
}
@Nullable
@Override
public IParsedElement getParsedElement() {
PsiElement element = getFirstChild();
while (!(element instanceof IGosuPsiElement) && element != null) {
element = element.getNextSibling();
}
if (element != null) {
return ((IGosuPsiElement) element).getParsedElement();
}
return null;
}
}