/* * 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.ide.projectPane; import com.intellij.ide.SelectInTarget; import com.intellij.ide.projectView.ProjectView; import com.intellij.ide.projectView.impl.ProjectViewPane; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DataProvider; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.ToggleAction; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.components.StoragePathMacros; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManagerEvent; import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.WriteExternalException; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowId; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.Update; import jetbrains.mps.RuntimeFlags; import jetbrains.mps.icons.MPSIcons; import jetbrains.mps.ide.ThreadUtils; import jetbrains.mps.ide.editor.MPSFileNodeEditor; import jetbrains.mps.ide.platform.watching.ReloadListener; import jetbrains.mps.ide.platform.watching.ReloadManager; import jetbrains.mps.ide.projectPane.logicalview.ProjectPaneTree; import jetbrains.mps.ide.projectPane.logicalview.ProjectTree; import jetbrains.mps.ide.projectPane.logicalview.ProjectTreeFindHelper; import jetbrains.mps.ide.projectView.ProjectViewPaneOverride; import jetbrains.mps.ide.ui.tree.MPSTree; import jetbrains.mps.ide.ui.tree.MPSTreeNode; import jetbrains.mps.ide.ui.tree.TreeHighlighterExtension; import jetbrains.mps.openapi.editor.EditorComponent; import jetbrains.mps.project.MPSProject; import jetbrains.mps.smodel.ModelReadRunnable; import jetbrains.mps.util.annotation.Hack; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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.SNodeReference; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepositoryListenerBase; import javax.swing.Icon; import javax.swing.JComponent; import java.awt.Component; import java.util.ArrayList; import java.util.Collections; import java.util.List; @State( name = "MPSProjectPane", storages = @Storage(StoragePathMacros.WORKSPACE_FILE) ) public class ProjectPane extends BaseLogicalViewProjectPane implements ProjectViewPaneOverride { private static final Logger LOG = LogManager.getLogger(ProjectPane.class); private final SRepositoryListenerBase myRepositoryListener = new SRepositoryListenerBase() { @Override public void moduleAdded(@NotNull SModule module) { ProjectPane.this.updateFromRoot(true); } @Override public void moduleRemoved(@NotNull SModuleReference module) { ProjectPane.this.updateFromRoot(true); } }; private final ReloadListener myReloadListener; private MyScrollPane myScrollPane; // FIXME there's update queue in MPSTree, do really we need both? private final MergingUpdateQueue myUpdateQueue = new MergingUpdateQueue("Project Pane Updates Queue", 500, true, myScrollPane, null, null, true); public static final String ID = ProjectViewPane.ID; private final FileEditorManagerListener myEditorListener = new FileEditorManagerListener() { @Override public void selectionChanged(@NotNull FileEditorManagerEvent event) { FileEditor fileEditor = event.getNewEditor(); if (fileEditor instanceof MPSFileNodeEditor) { final MPSFileNodeEditor editor = (MPSFileNodeEditor) fileEditor; if (getProjectView().isAutoscrollFromSource(ID)) { EditorComponent editorComponent = editor.getNodeEditor().getCurrentEditorComponent(); if (editorComponent == null) { return; } final SNode sNode = editorComponent.getEditedNode(); selectNodeWithoutExpansion(sNode.getReference()); } } } }; private List<List<String>> myExpandedPathsRaw = Collections.emptyList(); private List<List<String>> mySelectedPathsRaw = Collections.emptyList(); private MessageBusConnection myConnection; private final ShowDescriptorModelsAction myShowDescriptorModelsAction; public ProjectPane(final Project project, ProjectView projectView) { super(project, projectView); myUpdateQueue.setRestartTimerOnAdd(true); myReloadListener = new ReloadListener() { @Override public void reloadStarted() { } @Override public void reloadFinished() { rebuild(); } }; ApplicationManager.getApplication().getComponent(ReloadManager.class).addReloadListener(myReloadListener); myShowDescriptorModelsAction = new ShowDescriptorModelsAction(this); } @Override public void dispose() { myUpdateQueue.dispose(); ApplicationManager.getApplication().getComponent(ReloadManager.class).removeReloadListener(myReloadListener); super.dispose(); } @Override protected void removeListeners() { super.removeListeners(); myConnection.disconnect(); myConnection = null; getMPSProject().getRepository().removeRepositoryListener(myRepositoryListener); } @Override protected void addListeners() { super.addListeners(); assert myConnection == null; // double initialization myConnection = getProject().getMessageBus().connect(); myConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myEditorListener); getMPSProject().getRepository().addRepositoryListener(myRepositoryListener); } @Hack public static ProjectPane getInstance(Project project) { final ProjectView projectView = ProjectView.getInstance(project); if (ApplicationManager.getApplication().isUnitTestMode()) { //to ensure panes are initialized // despite http://jetbrains.net/tracker/issue/IDEA-24732 is fixed, ProjectViewImpl doesn't load extensions in unit test model // Perhaps, shall fix ProjectCreationTest (not to rely on != null result), instead? projectView.getSelectInTargets(); } return (ProjectPane) projectView.getProjectViewPaneById(ID); } // FIXME perhaps, shall be explicit about parameter type, seems that it's always invoked with MPSProject anyway // and there's hardly need to access ProjectPane without knowledge about IDE. public static ProjectPane getInstance(jetbrains.mps.project.Project mpsProject) { if (mpsProject instanceof MPSProject) { return getInstance(((MPSProject) mpsProject).getProject()); } return null; } @Override public ProjectTree getTree() { return (jetbrains.mps.ide.projectPane.logicalview.ProjectTree) myTree; } /*package*/ MPSProject getMPSProject() { // Shall I use getTree().getProject() instead? return getProject().getComponent(MPSProject.class); } @Override public String getTitle() { return "Logical View"; } @Override @NotNull public String getId() { return ID; } @Override public int getWeight() { return 0; } @Override public SelectInTarget createSelectInTarget() { return new ProjectPaneSelectInTarget(getMPSProject(), true); } @Override public Icon getIcon() { return MPSIcons.ProjectPane.LogicalView; } @NotNull @Override public ActionCallback updateFromRoot(boolean restoreExpandedPaths) { // XXX why not MPSTree.rebuildLater? // FIXME what's the difference with #rebuildTree? myUpdateQueue.queue(new AbstractUpdate(UpdateID.REBUILD) { @Override public void run() { if (getTree() == null) { return; } getTree().rebuildNow(); } }); return new ActionCallback(); // todo } @Override public void select(Object element, final VirtualFile file, final boolean requestFocus) { } @Override public JComponent createComponent() { if (isComponentCreated()) { return myScrollPane; } ProjectPaneTree tree = new ProjectPaneTree(this, myProject); Disposer.register(this, tree); tree.setShowStructureCondition(this::showNodeStructure); myTree = tree; myScrollPane = new MyScrollPane(getTree()); addListeners(); if (!RuntimeFlags.isTestMode()) { rebuild(); } TreeHighlighterExtension.attachHighlighters(tree, myProject); return myScrollPane; } @Override protected boolean isComponentCreated() { return myScrollPane != null; } public void rebuildTree() { // @see #updateFromRoot myUpdateQueue.queue(new AbstractUpdate(UpdateID.REBUILD) { @Override public void run() { if (getTree() == null || getProject().isDisposed()) { return; } getTree().rebuildNow(); getTree().expandProjectNode(); } }); } public void activate() { ThreadUtils.assertEDT(); activatePane(null, true); } @Override public void rebuild() { // This method can be called from different threads, however rebuildTree() // merely adds an update to the update queue, and thus it's safe to invoke it // without runReadInEDT or runInUIThreadNoWait as it used to be. rebuildTree(); } @Override public void addToolbarActions(DefaultActionGroup group) { super.addToolbarActions(group); group.addAction(myShowDescriptorModelsAction).setAsSecondary(true); } @Override protected void saveExpandedPaths() { // this gets called from the IDEA's implementation of ProjectViewImpl // thankfully, the method is declared protected if (myTree != null) { myExpandedPathsRaw = ((MPSTree) myTree).getExpandedPathsRaw(); mySelectedPathsRaw = ((MPSTree) myTree).getSelectedPathsRaw(); } else { myExpandedPathsRaw = Collections.emptyList(); mySelectedPathsRaw = Collections.emptyList(); } } @Override public void restoreExpandedPathsOverride() { // this gets called from the MPS's implementation of ProjectViewImpl // we must resort to this hack because the method in the superclass is declared private if (myTree != null) { myUpdateQueue.queue(new AbstractUpdate(UpdateID.RESTORE_EXPAND) { @Override public void run() { ((MPSTree) myTree).loadState(myExpandedPathsRaw, mySelectedPathsRaw); } }); } } @Override public void writeExternal(Element element) throws WriteExternalException { saveExpandedPaths(); // keep the binary format in sync with what IDEA writes Element subPane = new Element("subPane"); // we probably don't need this... if (getSubId() != null) { subPane.setAttribute("subId", getSubId()); } writePaths(subPane, myExpandedPathsRaw, "PATH"); writePaths(subPane, mySelectedPathsRaw, "SELECTED"); if (!myShowDescriptorModelsAction.isDefaultState()) { Element option1 = new Element(ShowDescriptorModelsAction.KEY); option1.setAttribute("value", Boolean.toString(myShowDescriptorModelsAction.isSelected())); subPane.addContent(option1); } element.addContent(subPane); } private void writePaths(Element parentElement, List<List<String>> pathsRaw, String elementName) { for (List<String> path : pathsRaw) { Element pathElement = new Element(elementName); writePath(path, pathElement); parentElement.addContent(pathElement); } } private void writePath(List<String> path, Element pathElement) { for (String treeNodeId : path) { Element elm = new Element("PATH_ELEMENT"); writeNodeId(treeNodeId, elm); pathElement.addContent(elm); } } private void writeNodeId(String treeNodeId, Element elm) { Element option1 = new Element("option"); option1.setAttribute("name", "myItemId"); option1.setAttribute("value", treeNodeId); elm.addContent(option1); Element option2 = new Element("option"); option2.setAttribute("name", "myItemType"); option2.setAttribute("value", ""); elm.addContent(option2); } @Override public void readExternal(Element element) throws InvalidDataException { // emulate the superclass's readExternal using the same binary format List<Element> subPanes = element.getChildren("subPane"); for (Element subPane : subPanes) { myExpandedPathsRaw = readPaths(subPane, "PATH"); mySelectedPathsRaw = readPaths(subPane, "SELECTED"); Element option1 = subPane.getChild(ShowDescriptorModelsAction.KEY); if (option1 != null) { myShowDescriptorModelsAction.setState(Boolean.parseBoolean(option1.getAttributeValue("value"))); } } } private List<List<String>> readPaths(Element parentElement, String name) { List<List<String>> result = new ArrayList<>(); for (Element pathElement : parentElement.getChildren(name)) { List<String> path = readPath(pathElement); result.add(path); } return result; } @NotNull private List<String> readPath(Element pathElement) { List<String> path = new ArrayList<>(); for (Element elm : pathElement.getChildren("PATH_ELEMENT")) { String treeNodeId = readNodeId(elm); if (treeNodeId != null) { path.add(treeNodeId); } } return path; } @Nullable private String readNodeId(Element elm) { List<Element> options = elm.getChildren("option"); String treeNodeId = null; for (Element option : options) { if ("myItemId".equals(option.getAttributeValue("name"))) { treeNodeId = option.getAttributeValue("value"); break; } } return treeNodeId; } //----selection---- public void selectModule(@NotNull final SModule module, final boolean autofocus) { ThreadUtils.assertEDT(); Runnable lookupAndSelect = new LookupAndSelect(module.getModuleReference()); activatePane(new ScheduleUpdateRunnable(myUpdateQueue, createModelReadUpdate(UpdateID.SELECT, lookupAndSelect)), autofocus); } public void selectModel(@NotNull final SModel model, boolean autofocus) { ThreadUtils.assertEDT(); Runnable lookupAndSelect = new LookupAndSelect(model.getReference()); activatePane(new ScheduleUpdateRunnable(myUpdateQueue, createModelReadUpdate(UpdateID.SELECT, lookupAndSelect)), autofocus); } private void activatePane(@Nullable final Runnable postActivate, boolean autoFocusContents) { // This method may be executed asynchronously, so checking for isDisposed first. if (isDisposed()) { return; } ToolWindowManager windowManager = ToolWindowManager.getInstance(getProject()); ToolWindow projectViewToolWindow = windowManager.getToolWindow(ToolWindowId.PROJECT_VIEW); //In unit test mode projectViewToolWindow == null // besides, https://youtrack.jetbrains.com/issue/MPS-24516 suggests tool window may be missing even in non-test mode (in plugin?) if (!ApplicationManager.getApplication().isUnitTestMode() && projectViewToolWindow != null) { projectViewToolWindow.activate(() -> { // I'm not quite sure next changeView is essential (what does toolWindow.activate() does then?), // but since there's no documentation what to expect, leave it the way it used to be in PaneActivator. getProjectView().changeView(getId()); if (postActivate != null) { postActivate.run(); } }, autoFocusContents); } } public void selectNode(@NotNull final SNode node, boolean autofocus) { ThreadUtils.assertEDT(); final Runnable lookupAndSelect = new LookupAndSelect(node.getReference()); activatePane(new ScheduleUpdateRunnable(myUpdateQueue, createModelReadUpdate(UpdateID.SELECT, lookupAndSelect)), autofocus); } private void selectNodeWithoutExpansion(@NotNull SNodeReference nodeRef) { final Runnable lookupAndSelect = new LookupAndSelect(nodeRef); myUpdateQueue.queue(createModelReadUpdate(UpdateID.SELECT, () -> getTree().runWithoutExpansion(lookupAndSelect))); } /** * @return update code block with the given id, that runs supplied delegate with read access to project repository */ private Update createModelReadUpdate(@NotNull UpdateID id, @NotNull Runnable delegate) { return new UpdateAdapter(id, new ModelReadRunnable(getMPSProject().getModelAccess(), delegate)); } //----select next queries---- @Override public void selectNextModel(SModel modelDescriptor) { final MPSTreeNode mpsTreeNode = createFindHelper().findNextTreeNode(modelDescriptor); // FIXME selectNextNode does the same, refactor. Check callers if need ThreadUtils at all ThreadUtils.runInUIThreadNoWait(() -> { ProjectTree tree = getTree(); if (tree != null) { tree.selectNode(mpsTreeNode); } }); } public void selectNextNode(SNode node) { final MPSTreeNode mpsTreeNode = createFindHelper().findNextTreeNode(node); ThreadUtils.runInUIThreadNoWait(() -> getTree().selectNode(mpsTreeNode)); } //----tree node selection queries--- public MPSTreeNode findNextTreeNode(SNode node) { return createFindHelper().findNextTreeNode(node); } public boolean isDescriptorModelInGeneratorVisible() { return myShowDescriptorModelsAction.isSelected(); } @NotNull /*package*/ ProjectTreeFindHelper createFindHelper() { return new ProjectTreeFindHelper(getTree()); } //----UI---- private class MyScrollPane extends JBScrollPane implements DataProvider { private MyScrollPane(Component view) { super(view); } @Override @Nullable public Object getData(@NonNls String dataId) { return ProjectPane.this.getData(dataId); } } private enum UpdateID { REBUILD(20), SELECT(30), RESTORE_EXPAND(40); private int myPriority; UpdateID(int priority) { myPriority = priority; } public int getPriority() { return myPriority; } } private abstract static class AbstractUpdate extends Update { /*package*/ AbstractUpdate(UpdateID id) { super(id, id.getPriority()); } } private static class UpdateAdapter extends Update { private final Runnable myDelegate; /*package*/ UpdateAdapter(@NotNull UpdateID id, @NotNull Runnable delegate) { super(id, id.getPriority()); myDelegate = delegate; } @Override public void run() { myDelegate.run(); } } // handy runnable that places an update into given queue private static class ScheduleUpdateRunnable implements Runnable { private final MergingUpdateQueue myQueue; private final Update myUpdate; /*package*/ ScheduleUpdateRunnable(@NotNull MergingUpdateQueue queue, @NotNull Update update) { myQueue = queue; myUpdate = update; } @Override public void run() { myQueue.queue(myUpdate); } } // resolve a reference, look up a corresponding tree node, and select it if found // XXX split to Computable<TreeNode> and runnable that takes it? private class LookupAndSelect implements Runnable { private SNodeReference myNode; private SModelReference myModel; private SModuleReference myModule; public LookupAndSelect(SModuleReference module) { myModule = module; } public LookupAndSelect(SModelReference model) { myModel = model; } public LookupAndSelect(SNodeReference node) { myNode = node; } @Override public void run() { MPSTreeNode toSelect = null; if (myModule != null) { SModule module = myModule.resolve(getMPSProject().getRepository()); if (module == null) { // likely, by the time we got to this point (selection update), the reference is no longer valid, exit gracefully return; } toSelect = createFindHelper().findMostSuitableModuleTreeNode(module); if (toSelect == null) { LOG.warn("Couldn't select module \"" + myModule.getModuleName() + "\" : tree node not found."); return; } } else if (myModel != null) { SModel model = myModel.resolve(getMPSProject().getRepository()); if (model == null) { return; } toSelect = createFindHelper().findMostSuitableModelTreeNode(model); if (toSelect == null) { LOG.warn("Couldn't select model \"" + myModel.getModelName() + "\" : tree node not found."); return; } } else if (myNode != null) { SNode node = myNode.resolve(getMPSProject().getRepository()); if (node == null) { return; } toSelect = createFindHelper().findMostSuitableSNodeTreeNode(node); if (toSelect == null) { LOG.warn("Couldn't select node \"" + myNode.toString() + "\" : tree node not found."); return; } } if (toSelect != null) { getTree().selectNode(toSelect); } } } private static class ShowDescriptorModelsAction extends ToggleAction { private final ProjectPane myProjectPane; private boolean myState = false; /*package*/ static final String KEY = "showGeneratorDescriptor"; ShowDescriptorModelsAction(ProjectPane projectPane) { super("Show @descriptor models in Generators"); myProjectPane = projectPane; } public boolean isSelected() { return myState; } /*package*/ boolean isDefaultState() { return !isSelected(); } /*package*/ void setState(boolean selected) { myState = selected; } @Override public boolean isSelected(AnActionEvent e) { return isSelected(); } @Override public void setSelected(AnActionEvent e, boolean state) { myState = state; myProjectPane.rebuild(); } } }