/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.idea.core.psi.impl; import com.intellij.ide.impl.ProjectViewSelectInTarget; import com.intellij.ide.projectView.impl.ProjectViewPane; import com.intellij.lang.FileASTNode; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.util.Ref; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileSystemItem; import com.intellij.psi.PsiManager; import com.intellij.psi.impl.PsiManagerEx; import com.intellij.psi.impl.PsiManagerImpl; import com.intellij.psi.impl.file.PsiDirectoryImpl; import com.intellij.psi.impl.file.impl.FileManager; import com.intellij.psi.search.PsiElementProcessor; import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.BidirectionalMap; import jetbrains.mps.extapi.persistence.FileDataSource; import jetbrains.mps.icons.MPSIcons.Nodes; import jetbrains.mps.ide.icons.GlobalIconManager; import jetbrains.mps.ide.vfs.VirtualFileUtils; import jetbrains.mps.idea.core.MPSBundle; import jetbrains.mps.nodefs.NodeVirtualFileSystem; import jetbrains.mps.persistence.FilePerRootDataSource; import jetbrains.mps.project.MPSExtentions; import jetbrains.mps.smodel.DynamicReference; import jetbrains.mps.smodel.ModelAccessHelper; import jetbrains.mps.smodel.StaticReference; import jetbrains.mps.util.JavaNameUtil; import jetbrains.mps.vfs.IFile; import org.apache.log4j.Logger; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SAbstractConcept; import org.jetbrains.mps.openapi.language.SAbstractLink; import org.jetbrains.mps.openapi.language.SProperty; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.model.SReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.DataSource; import javax.swing.Icon; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; /** * evgeny, 1/25/13 */ public class MPSPsiModel extends MPSPsiNodeBase implements PsiDirectory { private static final Logger LOG = Logger.getLogger(MPSPsiModel.class); private static final PsiDirectory[] EMPTY_PSI_DIRECTORIES = new PsiDirectory[0]; private final SModelReference myModelReference; private final Map<SNodeId, MPSPsiNode> myNodes = new HashMap<>(); private final BidirectionalMap<MPSPsiNodeBase, Integer> myNodesOrder = new BidirectionalMap<>(); private VirtualFile mySourceVirtualFile; private PsiDirectoryImpl myPsiDirectory; public MPSPsiModel(SModelReference reference, PsiManager manager) { super(manager); this.myModelReference = reference; } @Override public boolean equals(Object obj) { /* TODO: remove after fix in platform: This override fixes check for equality of SmartPointerElementInfo and MPSPsiModel in SmartPsiElementPointerImpl.createElementInfo() */ if (obj instanceof PsiDirectory && !(obj instanceof MPSPsiNodeBase)) { return this.getVirtualFile().equals(((PsiDirectory) obj).getVirtualFile()); } return super.equals(obj); } @Override protected Icon getBaseIcon() { final GlobalIconManager globalIconManager = ApplicationManager.getApplication().getComponent(GlobalIconManager.class); if (globalIconManager == null) { return Nodes.Model; } return new ModelAccessHelper(getProjectRepository()).runReadAction(() -> globalIconManager.getIconFor(myModelReference.resolve(getProjectRepository()))); } @Nullable @Override public Icon getIcon(int flags) { return getBaseIcon(); } @Nullable @Override protected Icon getElementIcon(@IconFlags int flags) { return getBaseIcon(); } @NotNull @Override public String getName() { return JavaNameUtil.shortName(getQualifiedName()); } public SModelReference getSModelReference() { return myModelReference; } @Override public boolean isValid() { if (myPsiDirectory == null || !(myPsiDirectory.isValid())) return false; final SRepository repository = getProjectRepository(); final Ref<Boolean> result = new Ref<>(false); repository.getModelAccess().runReadAction(() -> { SModel model = myModelReference.resolve(repository); result.set(model != null); }); return result.get(); } @Override public boolean isPhysical() { // See SmartPsiElementPointerImpl.doCreateElementInfo() and LOG.error() in createElementInfo() in the same class // We implement PsiDirectory but don't want DirElementInfo to be created for us, because when it's restored // it creates not an MPSPsiModel obviously but a PsiDirectoryFactory.getInstance().findDirectory() which happens // to be PsiDirectoryImpl or something. Hence, LOG.error() is called. // It affects project pane tests. // // Currently waiting for the change element instanceof PsiDirectory && element.isPhysical() to happen in idea code return false; } @Override public String toString() { return "Model:" + myModelReference.toString(); } public MPSPsiNode[] getRootNodes() { return getRootNodesOfType(MPSPsiNode.class); } public <T extends PsiElement> T[] getRootNodesOfType(Class<T> aClass) { PsiElement[] surrogateRoots = getChildren(); List<T> result = new ArrayList<>(); for (PsiElement r : surrogateRoots) { assert r instanceof MPSPsiRootNode; // a layer between model and real root for (PsiElement c : r.getChildren()) { if (aClass.isInstance(c)) { result.add((T) c); } } } return ArrayUtil.toObjectArray(result, aClass); } /* PsiFile */ @Override public void checkSetName(String name) throws IncorrectOperationException { throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.not.implemented")); } @NotNull @Override public VirtualFile getVirtualFile() { SRepository repository = getProjectRepository(); NodeVirtualFileSystem fs = NodeVirtualFileSystem.getInstance(); return new ModelAccessHelper(repository).runReadAction(() -> fs.getFileFor(repository, myModelReference)); } @Override public boolean processChildren(PsiElementProcessor<PsiFileSystemItem> processor) { return false; } @Override public boolean isDirectory() { return true; } @Nullable @Override public PsiDirectory getParent() { return null; } @NotNull @Override public PsiDirectory[] getSubdirectories() { return EMPTY_PSI_DIRECTORIES; //To change body of implemented methods use File | Settings | File Templates. } @NotNull @Override public PsiFile[] getFiles() { return getChildren(PsiFile.class); } @Nullable @Override public PsiDirectory findSubdirectory(@NotNull String name) { return null; } @Nullable @Override public PsiFile findFile(@NotNull @NonNls String name) { return null; } @Override public final boolean isWritable() { return myPsiDirectory != null && myPsiDirectory.isWritable(); } @NotNull @Override public PsiDirectory createSubdirectory(@NotNull String name) throws IncorrectOperationException { if (myPsiDirectory == null) throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent")); return myPsiDirectory.createSubdirectory(name); } @Override public void checkCreateSubdirectory(@NotNull String name) throws IncorrectOperationException { if (myPsiDirectory == null) throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent")); myPsiDirectory.checkCreateSubdirectory(name); } @NotNull @Override public PsiFile createFile(@NotNull @NonNls String name) throws IncorrectOperationException { if (myPsiDirectory == null) throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent")); return myPsiDirectory.createFile(name); } @NotNull @Override public PsiFile copyFileFrom(@NotNull String newName, @NotNull PsiFile originalFile) throws IncorrectOperationException { throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.not.implemented")); } @Override public void checkCreateFile(@NotNull String name) throws IncorrectOperationException { if (myPsiDirectory == null) throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent")); myPsiDirectory.checkCreateFile(name); } @Override public PsiElement add(@NotNull PsiElement element) throws IncorrectOperationException { if (myPsiDirectory == null) throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent")); return myPsiDirectory.add(element); } @Override public void checkAdd(@NotNull PsiElement element) throws IncorrectOperationException { if (myPsiDirectory == null) throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent")); myPsiDirectory.checkAdd(element); } @Override public FileASTNode getNode() { return null; } @NotNull @Override public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException { throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.not.implemented")); } @Nullable @Override public PsiDirectory getParentDirectory() { VirtualFile parentFile = getSourceVirtualFile().getParent(); if (parentFile == null) return null; return myManager.findDirectory(parentFile); } @Override public boolean canNavigateToSource() { return false; } @Override public boolean canNavigate() { return true; } @Override public void navigate(boolean requestFocus) { ProjectViewSelectInTarget.select(getProject(), this, ProjectViewPane.ID, null, getSourceVirtualFile(), requestFocus); } public String getQualifiedName() { return myModelReference.getModelName(); } public VirtualFile getSourceVirtualFile() { if (mySourceVirtualFile == null) { final SRepository repo = getProjectRepository(); repo.getModelAccess().runReadAction(() -> { SModel model = myModelReference.resolve(repo); // fixme when DefaultModelRoot.createModel() doesn't do register() anymore, it shouldn't be needed // happens in tests for creating file-per-root persisted model in a directory, when no languages is chosen // i.e. creation of model is cancelled if (model == null) { return; } DataSource source = model.getSource(); if (source instanceof FileDataSource) { mySourceVirtualFile = VirtualFileUtils.getProjectVirtualFile(((FileDataSource) source).getFile()); } else if (source instanceof FilePerRootDataSource) { // todo remove knowledge about particular PerRoot persistence, should be more generic mySourceVirtualFile = VirtualFileUtils.getProjectVirtualFile(((FilePerRootDataSource) source).getFolder()).findChild(MPSExtentions.DOT_MODEL_HEADER); } }); } return mySourceVirtualFile; } @Override public PsiFile getContainingFile() { // if it's singe-file model then return that file final SRepository repository = getProjectRepository(); return new ModelAccessHelper(repository.getModelAccess()).runReadAction(() -> { SModel model = myModelReference.resolve(repository); if (model.getSource() instanceof FileDataSource) { IFile iModelFile = ((FileDataSource) model.getSource()).getFile(); VirtualFile vModelFile = VirtualFileUtils.getProjectVirtualFile(iModelFile); if (vModelFile == null) { // extra check due to MPS-21363 LOG.warn(String.format(MPSBundle.message("mps.psi.model.warning.containing.file"), iModelFile.toPath().toString())); return null; } return PsiManager.getInstance(getProject()).findFile(vModelFile); } else { return null; } }); } /* package */ boolean isRoot(MPSPsiNode psiNode) { return psiNode.getParent() instanceof MPSPsiRootNode; } MPSPsiNode reload(SNodeId sNodeId) { SRepository repository = getProjectRepository(); repository.getModelAccess().checkWriteAccess(); MPSPsiNode mpsPsiNode = lookupNode(sNodeId); if (mpsPsiNode == null) { return null; } SNode sNode = mpsPsiNode.getSNodeReference().resolve(repository); MPSPsiNode replacement = convert(sNode); if (isRoot(mpsPsiNode)) { MPSPsiRootNode rootNode = (MPSPsiRootNode) mpsPsiNode.getParent(); assert rootNode.getContainingModel().equals(this); MPSPsiRootNode replacementRoot; if (sNode.getContainingRoot() == sNode && sNode.getModel().getSource() instanceof FilePerRootDataSource) { final String name = extractName(sNode); final VirtualFile virtualFile = VirtualFileUtils.getProjectVirtualFile(((FilePerRootDataSource) sNode.getModel().getSource()).getFile(name + MPSExtentions.DOT_MODEL_ROOT)); replacementRoot = new MPSPsiRootNode(sNode.getNodeId(), name, this, getManager(), virtualFile); } else { replacementRoot = new MPSPsiRootNode(sNode.getNodeId(), extractName(sNode), this, getManager()); } replaceChild(rootNode, replacementRoot); ((PsiManagerEx) getManager()).getFileManager().setViewProvider(rootNode.getVirtualFile(), null); replacementRoot.addChildLast(replacement); } else { ((MPSPsiNodeBase) mpsPsiNode.getParent()).replaceChild(mpsPsiNode, replacement); } enumerateNodes(); return replacement; } public void reloadAll() { final SRepository repository = getProjectRepository(); repository.getModelAccess().checkReadAccess(); SModel sModel = myModelReference.resolve(repository); for (SNode root : sModel.getRootNodes()) { MPSPsiNode mpsPsiNode = lookupNode(root.getNodeId()); if (mpsPsiNode == null) continue; drop(mpsPsiNode); } myNodes.clear(); reload(sModel); } private PsiFile tryReuseRootPsiFile(VirtualFile vfile) { // It's important that we only try to take cached psiFile. Otherwise we could end up creating psiFile // for this virtual file, which would require psiModel, but we're in the process of reloading it. FileManager fileManager = ((PsiManagerEx) PsiManagerEx.getInstance(getProject())).getFileManager(); return fileManager.getCachedPsiFile(vfile); } void reload(SModel model) { clearChildren(); for (SNode root : model.getRootNodes()) { String rootName; rootName = extractName(root); MPSPsiRootNode rootNode; if (model.getSource() instanceof FilePerRootDataSource) { final IFile iFile = ((FilePerRootDataSource) model.getSource()).getFile(rootName + MPSExtentions.DOT_MODEL_ROOT); VirtualFile virtualFile = VirtualFileUtils.getProjectVirtualFile(iFile); if (virtualFile == null) virtualFile = VirtualFileUtils.getVirtualFile(iFile.toPath().toString()); PsiFile psiFile = virtualFile != null ? tryReuseRootPsiFile(virtualFile) : null; rootNode = psiFile != null && psiFile instanceof MPSPsiRootNode ? (MPSPsiRootNode) psiFile : new MPSPsiRootNode(root.getNodeId(), rootName, this, getManager(), virtualFile); } else { rootNode = new MPSPsiRootNode(root.getNodeId(), rootName, this, getManager()); } addChildLast(rootNode); if (rootNode.getChildren().length == 0) rootNode.addChildLast(convert(root)); else { rootNode.updateChildren(); fillNodes(rootNode); } } enumerateNodes(); getSourceVirtualFile(); if (mySourceVirtualFile == null || mySourceVirtualFile.getParent() == null) myPsiDirectory = null; else myPsiDirectory = new PsiDirectoryImpl((PsiManagerImpl) myManager, getSourceVirtualFile().getParent()); /*MPSModuleRepository.getInstance().getModelAccess().runReadAction(new Runnable() { @Override public void run() { mySourceVirtualFile = ModelUtil.getFileByModel(myModelReference.resolve(MPSModuleRepository.getInstance())); } });*/ } private void fillNodes(MPSPsiRootNode rootNode) { Queue<MPSPsiNode> psiNodes = new LinkedList<>(); for (PsiElement element : rootNode.getChildren()) { if (element instanceof MPSPsiNode) psiNodes.add((MPSPsiNode) element); } while (!psiNodes.isEmpty()) { MPSPsiNode mpsPsiNode = psiNodes.poll(); myNodes.put(mpsPsiNode.getId(), mpsPsiNode); for (PsiElement element : mpsPsiNode.getChildren()) { if (element instanceof MPSPsiNode) psiNodes.add((MPSPsiNode) element); } } } private MPSPsiNode convert(SNode node) { MPSPsiNode psiNode = MPSPsiProvider.getInstance(getProject()).create(node.getNodeId(), node.getConcept(), node.getContainmentLink() == null ? null : node.getContainmentLink().getName()); myNodes.put(node.getNodeId(), psiNode); // properties for (SProperty property : node.getProperties()) { psiNode.setProperty(property.getName(), node.getProperty(property)); } // refs for (SReference ref : node.getReferences()) { SAbstractLink link = ref.getLink(); SAbstractConcept linkTargetConcept = null; if (link != null) { linkTargetConcept = link.getTargetConcept(); } MPSPsiRef psiRef = null; if (ref instanceof StaticReference) { psiRef = MPSPsiProvider.getInstance(getProject()).createReferenceNode(ref.getLink().getName(), linkTargetConcept, ref.getTargetSModelReference(), ref.getTargetNodeId()); } else if (ref instanceof DynamicReference) { psiRef = MPSPsiProvider.getInstance(getProject()).createReferenceNode(ref.getLink().getName(), linkTargetConcept, ((DynamicReference) ref).getResolveInfo()); } if (psiRef != null) { psiNode.addChild(null, psiRef); } } // children for (SNode root : node.getChildren()) { MPSPsiNode psiChild = convert(root); MPSPsiNodeBase artificialParent = psiNode.getParentFor(psiChild); MPSPsiNodeBase wouldBeParent = artificialParent == null ? psiNode : artificialParent; wouldBeParent.addChildLast(psiChild); } return psiNode; } private void drop(MPSPsiNode psiNode) { myNodes.remove(psiNode.getId()); for (MPSPsiNodeBase node : psiNode.children()) { if (node instanceof MPSPsiNode) { drop((MPSPsiNode) node); } } } MPSPsiNode lookupNode(SNodeId nodeId) { return myNodes.get(nodeId); } Integer getNodePosition(MPSPsiNodeBase node) { return myNodesOrder.get(node); } public MPSPsiNodeBase findNodeByPosition(int pos) { List<MPSPsiNodeBase> nodes = myNodesOrder.getKeysByValue(pos); if (nodes == null) { return null; } assert nodes.size() <= 1 : "Non-unique mapping from psi nodes to their positions in the tree"; return nodes.isEmpty() ? null : nodes.get(0); } private String extractName(SNode sNode) { return sNode.getName() != null && !sNode.getName().isEmpty() ? sNode.getName() : sNode.getNodeId().toString(); } // todo replace with depth-first; depth is never very big in real models, and memory consumption will be less, // also the order will be more natural private void enumerateNodes() { myNodesOrder.clear(); Deque<MPSPsiNodeBase> stack = new ArrayDeque<>(); stack.add(this); int k = 0; while (!stack.isEmpty()) { MPSPsiNodeBase curr = stack.pop(); // remembering position myNodesOrder.put(curr, k++); PsiElement[] children = curr.getChildren(); for (int i = children.length - 1; i >= 0; i--) { PsiElement c = children[i]; if (c instanceof MPSPsiNodeBase) { stack.push((MPSPsiNodeBase) c); } } } } @Override public void checkDelete() throws IncorrectOperationException { } @Override public void delete() throws IncorrectOperationException { // todo unregister model try { getSourceVirtualFile().delete(getManager()); } catch (IOException e) { throw new IncorrectOperationException(e.toString(), e); } } }