package org.jetbrains.plugins.cucumber.steps;
import com.intellij.ProjectTopics;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleRootListener;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.cucumber.psi.GherkinFile;
import java.util.*;
public abstract class NotIndexedCucumberExtension extends AbstractCucumberExtension {
public Object getDataObject(@NotNull final Project project) {
final DataObject result = new DataObject();
result.myUpdateQueue.setPassThrough(false);
PsiManager.getInstance(project).addPsiTreeChangeListener(result.myCucumberPsiTreeListener);
PsiManager.getInstance(project).addPsiTreeChangeListener(new PsiTreeChangeAdapter() {
@Override
public void childAdded(@NotNull PsiTreeChangeEvent event) {
final PsiElement parent = event.getParent();
PsiElement child = event.getChild();
if (isStepLikeFile(child, parent)) {
final PsiFile file = (PsiFile)child;
result.myUpdateQueue.queue(new Update(parent) {
public void run() {
if (file.isValid()) {
reloadAbstractStepDefinitions(file);
createWatcher(file);
}
}
});
}
}
@Override
public void childRemoved(@NotNull PsiTreeChangeEvent event) {
final PsiElement parent = event.getParent();
final PsiElement child = event.getChild();
if (isStepLikeFile(child, parent)) {
result.myUpdateQueue.queue(new Update(parent) {
public void run() {
removeAbstractStepDefinitionsRelatedTo((PsiFile)child);
}
});
}
}
});
// clear caches after modules roots were changed
final MessageBusConnection connection = project.getMessageBus().connect();
connection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
final List<VirtualFile> myPreviousStepDefsProviders = new ArrayList<>();
public void beforeRootsChange(ModuleRootEvent event) {
myPreviousStepDefsProviders.clear();
collectAllStepDefsProviders(myPreviousStepDefsProviders, project);
}
public void rootsChanged(ModuleRootEvent event) {
// compare new and previous content roots
final List<VirtualFile> newStepDefsProviders = new ArrayList<>();
collectAllStepDefsProviders(newStepDefsProviders, project);
if (!compareRoots(newStepDefsProviders)) {
// clear caches on roots changed
reset(project);
}
}
private boolean compareRoots(final List<VirtualFile> newStepDefsProviders) {
if (myPreviousStepDefsProviders.size() != newStepDefsProviders.size()) {
return false;
}
for (VirtualFile root : myPreviousStepDefsProviders) {
if (!newStepDefsProviders.contains(root)) {
return false;
}
}
return true;
}
});
Disposer.register(project, connection);
return result;
}
@Override
public Collection<? extends PsiFile> getStepDefinitionContainers(@NotNull final GherkinFile featureFile) {
final Set<PsiDirectory> stepDefRoots = findStepDefsRoots(featureFile);
final Set<PsiFile> stepDefs = ContainerUtil.newHashSet();
for (PsiDirectory root : stepDefRoots) {
stepDefs.addAll(gatherStepDefinitionsFilesFromDirectory(root, true));
}
return stepDefs.isEmpty() ? Collections.emptySet() : stepDefs;
}
protected Set<PsiDirectory> findStepDefsRoots(@NotNull final GherkinFile featureFile) {
final Module module = ModuleUtilCore.findModuleForPsiElement(featureFile);
final VirtualFile file = featureFile.getVirtualFile();
if (file == null || module == null) {
return Collections.emptySet();
}
final List<PsiDirectory> result = new ArrayList<>();
findRelatedStepDefsRoots(module, featureFile, result, new HashSet<>());
return new HashSet<>(result);
}
private void createWatcher(final PsiFile file) {
if (file.getProject().isDisposed()) {
return;
}
final DataObject dataObject = (DataObject)CucumberStepsIndex.getInstance(file.getProject()).getExtensionDataObject(this);
dataObject.myCucumberPsiTreeListener.addChangesWatcher(file, new CucumberPsiTreeListener.ChangesWatcher() {
public void onChange(PsiElement parentPsiElement) {
dataObject.myUpdateQueue.queue(new Update(file) {
public void run() {
if (!file.getProject().isDisposed()) {
reloadAbstractStepDefinitions(file);
}
DaemonCodeAnalyzer.getInstance(file.getProject()).restart();
}
});
}
});
}
private void reloadAbstractStepDefinitions(final PsiFile file) {
if (file.getProject().isDisposed()) {
return;
}
final DataObject dataObject = (DataObject)CucumberStepsIndex.getInstance(file.getProject()).getExtensionDataObject(this);
// Do not commit document if file was deleted
final PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(file.getProject());
final Document document = psiDocumentManager.getDocument(file);
if (document != null) {
psiDocumentManager.commitDocument(document);
}
// remove old definitions related to current file
removeAbstractStepDefinitionsRelatedTo(file);
// read definitions from file
if (file.isValid()) {
synchronized (dataObject.myStepDefinitions) {
dataObject.myStepDefinitions.addAll(getStepDefinitions(file));
}
}
}
private void removeAbstractStepDefinitionsRelatedTo(final PsiFile file) {
if (file.getProject().isDisposed()) {
return;
}
final DataObject dataObject = (DataObject)CucumberStepsIndex.getInstance(file.getProject()).getExtensionDataObject(this);
// file may be invalid !!!!
synchronized (dataObject.myStepDefinitions) {
for (Iterator<AbstractStepDefinition> iterator = dataObject.myStepDefinitions.iterator(); iterator.hasNext(); ) {
AbstractStepDefinition definition = iterator.next();
final PsiElement element = definition.getElement();
if (element == null || element.getContainingFile().equals(file)) {
iterator.remove();
}
}
}
}
@NotNull
private List<PsiFile> gatherStepDefinitionsFilesFromDirectory(@NotNull final PsiDirectory dir, final boolean writableOnly) {
final List<PsiFile> result = new ArrayList<>();
// find step definitions in current folder
for (PsiFile file : dir.getFiles()) {
final VirtualFile virtualFile = file.getVirtualFile();
final PsiDirectory parent = file.getParent();
if (parent != null) {
boolean isStepFile = writableOnly ? isWritableStepLikeFile(file, parent) : isStepLikeFile(file, parent);
if (isStepFile && virtualFile != null) {
result.add(file);
}
}
}
// process subfolders
for (PsiDirectory subDir : dir.getSubdirectories()) {
result.addAll(gatherStepDefinitionsFilesFromDirectory(subDir, writableOnly));
}
return result;
}
public static void collectDependencies(Module module, Set<Module> modules) {
if (modules.contains(module)) return;
final Module[] dependencies = ModuleRootManager.getInstance(module).getDependencies();
for (Module dependency : dependencies) {
if (!modules.contains(dependency)) {
modules.add(dependency);
collectDependencies(dependency, modules);
}
}
}
public List<AbstractStepDefinition> loadStepsFor(@Nullable final PsiFile featureFile, @NotNull final Module module) {
final Set<Module> modules = new HashSet<>();
collectDependencies(module, modules);
modules.add(module);
final List<AbstractStepDefinition> result = new ArrayList<>();
for (Module current : modules) {
result.addAll(loadStepsForModule(featureFile, current));
}
return result;
}
public List<AbstractStepDefinition> loadStepsForModule(@Nullable final PsiFile featureFile, @NotNull final Module module) {
final DataObject dataObject = (DataObject)CucumberStepsIndex.getInstance(module.getProject()).getExtensionDataObject(this);
// New step definitions folders roots
final List<PsiDirectory> notLoadedStepDefinitionsRoots = new ArrayList<>();
try {
if (featureFile != null) {
findRelatedStepDefsRoots(module, featureFile, notLoadedStepDefinitionsRoots, dataObject.myProcessedStepDirectories);
}
loadStepDefinitionRootsFromLibraries(module, notLoadedStepDefinitionsRoots, dataObject.myProcessedStepDirectories);
}
catch (ProcessCanceledException e) {
// just stop items gathering
return Collections.emptyList();
}
synchronized (dataObject.myStepDefinitions) {
// Parse new folders
final List<AbstractStepDefinition> stepDefinitions = new ArrayList<>();
for (PsiDirectory root : notLoadedStepDefinitionsRoots) {
stepDefinitions.clear();
// let's process each folder separately
try {
dataObject.myProcessedStepDirectories.add(root.getVirtualFile().getPath());
final List<PsiFile> files = gatherStepDefinitionsFilesFromDirectory(root, false);
for (final PsiFile file : files) {
removeAbstractStepDefinitionsRelatedTo(file);
stepDefinitions.addAll(getStepDefinitions(file));
createWatcher(file);
}
dataObject.myStepDefinitions.addAll(stepDefinitions);
}
catch (ProcessCanceledException e) {
// remove from processed
dataObject.myProcessedStepDirectories.remove(root.getVirtualFile().getPath());
// remove new step definitions
if (!stepDefinitions.isEmpty()) {
dataObject.myStepDefinitions.removeAll(stepDefinitions);
}
throw e;
}
}
}
synchronized (dataObject.myStepDefinitions) {
return new ArrayList<>(dataObject.myStepDefinitions);
}
}
protected static void addStepDefsRootIfNecessary(final VirtualFile root,
@NotNull final List<PsiDirectory> newStepDefinitionsRoots,
@NotNull final Set<String> processedStepDirectories,
@NotNull final Project project) {
if (root == null || !root.isValid()) {
return;
}
final String path = root.getPath();
if (processedStepDirectories.contains(path)) {
return;
}
final PsiDirectory rootPathDir = PsiManager.getInstance(project).findDirectory(root);
if (rootPathDir != null && rootPathDir.isValid()) {
if (!newStepDefinitionsRoots.contains(rootPathDir)) {
newStepDefinitionsRoots.add(rootPathDir);
}
}
}
@Nullable
protected static VirtualFile findContentRoot(final Module module, final VirtualFile file) {
if (file == null || module == null) return null;
final VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots();
for (VirtualFile root : contentRoots) {
if (VfsUtilCore.isAncestor(root, file, false)) {
return root;
}
}
return null;
}
protected abstract void loadStepDefinitionRootsFromLibraries(Module module, List<PsiDirectory> roots, Set<String> directories);
protected abstract Collection<AbstractStepDefinition> getStepDefinitions(@NotNull final PsiFile file);
protected abstract void collectAllStepDefsProviders(@NotNull final List<VirtualFile> providers, @NotNull final Project project);
public abstract void findRelatedStepDefsRoots(@NotNull final Module module, @NotNull final PsiFile featureFile,
final List<PsiDirectory> newStepDefinitionsRoots,
final Set<String> processedStepDirectories);
public void reset(@NotNull final Project project) {
final DataObject dataObject = (DataObject)CucumberStepsIndex.getInstance(project).getExtensionDataObject(this);
dataObject.myUpdateQueue.cancelAllUpdates();
synchronized (dataObject.myStepDefinitions) {
dataObject.myStepDefinitions.clear();
}
dataObject.myProcessedStepDirectories.clear();
}
public void flush(@NotNull final Project project) {
final DataObject dataObject = (DataObject)CucumberStepsIndex.getInstance(project).getExtensionDataObject(this);
dataObject.myUpdateQueue.flush();
}
public List<AbstractStepDefinition> getAllStepDefinitions(Project project) {
final DataObject dataObject = (DataObject)CucumberStepsIndex.getInstance(project).getExtensionDataObject(this);
synchronized (dataObject.myStepDefinitions) {
return new ArrayList<>(dataObject.myStepDefinitions);
}
}
public static class DataObject {
final List<AbstractStepDefinition> myStepDefinitions = new ArrayList<>();
final Set<String> myProcessedStepDirectories = new HashSet<>();
final MergingUpdateQueue myUpdateQueue = new MergingUpdateQueue("Steps reparse", 500, true, null);
final CucumberPsiTreeListener myCucumberPsiTreeListener = new CucumberPsiTreeListener();
}
}