/* * 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.editor; import com.intellij.codeHighlighting.BackgroundEditorHighlighter; import com.intellij.ide.structureView.StructureViewBuilder; import com.intellij.openapi.actionSystem.DataProvider; import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.editor.Document; import com.intellij.openapi.fileEditor.DocumentsEditor; import com.intellij.openapi.fileEditor.FileEditorDataProviderManager; import com.intellij.openapi.fileEditor.FileEditorLocation; import com.intellij.openapi.fileEditor.FileEditorState; import com.intellij.openapi.fileEditor.FileEditorStateLevel; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.openapi.vfs.VirtualFile; import jetbrains.mps.ide.editor.BaseNodeEditor.BaseEditorState; import jetbrains.mps.ide.vfs.VirtualFileUtils; import jetbrains.mps.nodefs.MPSNodeVirtualFile; import jetbrains.mps.nodefs.NodeVirtualFileSystem; import jetbrains.mps.openapi.editor.Editor; import jetbrains.mps.openapi.editor.EditorState; import jetbrains.mps.project.MPSProject; import jetbrains.mps.smodel.CommandListenerAdapter; import jetbrains.mps.smodel.ModelAccessHelper; import jetbrains.mps.smodel.SModelFileTracker; import jetbrains.mps.util.AbstractComputeRunnable; import jetbrains.mps.util.Computable; import jetbrains.mps.vfs.IFile; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.EditableSModel; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.module.SRepository; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import java.awt.BorderLayout; import java.awt.Font; import java.beans.PropertyChangeListener; import java.util.List; public class MPSFileNodeEditor extends UserDataHolderBase implements DocumentsEditor { private Editor myNodeEditor; private final JPanel myComponent = new MPSFileNodeEditorComponent(); private final MPSProject myProject; private MPSNodeVirtualFile myFile; private boolean myDisposed = false; // See: https://youtrack.jetbrains.com/issue/MPS-24409 private EditorState myDelayedState = null; private boolean mySelected; // do not duplicate code that obtains MPSNodeVirtualFile from regular IDEA VirtualFile // in MPSFileNodeEditorProvider and MPSFileNodeEditor /*package*/ static class NodeFileComputable implements Computable<MPSNodeVirtualFile> { private final SRepository myRepository; private final IFile myFile; private final String myNameToMatch; public NodeFileComputable(SRepository repository, VirtualFile file) { myRepository = repository; myFile = VirtualFileUtils.toIFile(file.getParent()); myNameToMatch = file.getNameWithoutExtension(); } @Override public MPSNodeVirtualFile compute() { SModel model = SModelFileTracker.getInstance(myRepository).findModel(myFile); if (model != null) { for (SNode node : model.getRootNodes()) { if (myNameToMatch.equals(node.getName()) || myNameToMatch.equals(node.getNodeId().toString())) { return NodeVirtualFileSystem.getInstance().getFileFor(myRepository, node); } } } return null; } } public MPSFileNodeEditor(@NotNull MPSProject project, @NotNull VirtualFile file) { this(project, null); // FIXME MPSNodeVirtualFile is subclass of VirtualFile, how do we ensure proper cons is invoked? assert !(file instanceof MPSNodeVirtualFile); final SRepository repository = project.getRepository(); final NodeFileComputable nodeFileComputable = new NodeFileComputable(repository, file); // we expect new models (that may come from the file) could show up in the repository only as a command(repository modification) result repository.getModelAccess().addCommandListener(new CommandListenerAdapter() { @Override public void commandFinished() { MPSNodeVirtualFile mpsNodeVirtualFile = nodeFileComputable.compute(); if (mpsNodeVirtualFile != null) { myFile = mpsNodeVirtualFile; MPSFileNodeEditor.this.initEditor(); repository.getModelAccess().removeCommandListener(this); } } }); } public MPSFileNodeEditor(@NotNull MPSProject project, MPSNodeVirtualFile file) { myProject = project; myFile = file; myProject.getModelAccess().runReadAction(this::initEditor); } @Nullable public MPSNodeVirtualFile getFile() { return myFile; } public Editor getNodeEditor() { return myNodeEditor; } @Override @NotNull public JComponent getComponent() { return myComponent; } @Override @Nullable public JComponent getPreferredFocusedComponent() { JPanel panel = new JPanel(new BorderLayout()); JLabel label = new JLabel("Loading..."); final Font font = label.getFont(); label.setFont(new Font(font.getName(), font.getStyle(), font.getSize() * 2)); // double size for better visibility panel.add(label, BorderLayout.CENTER); return isDisposed() ? null : (myNodeEditor == null ? panel : (JComponent) myNodeEditor.getCurrentEditorComponent()); } @Override @NonNls @NotNull public String getName() { return new ModelAccessHelper(myProject.getModelAccess()).runReadAction(() -> { if (waitingForNodeFile()) { return "Editor waiting for node"; } assert myFile.getNode() != null : String.format("File does not contain node: %s", myFile.toString()); return myFile.getNode().getName(); }); } @Override @NotNull public MPSEditorStateWrapper getState(@NotNull final FileEditorStateLevel level) { final MPSEditorStateWrapper state = new MPSEditorStateWrapper(); if (!isDisposed() && myNodeEditor != null) { myProject.getModelAccess().runReadAction(() -> { EditorState editorState = myNodeEditor.saveState(); if (level == FileEditorStateLevel.FULL) { editorState.clearSessionState(); } state.setEditorState(editorState); }); } else { state.setEditorState(new BaseEditorState()); } state.setLevel(level); return state; } @Override public void setState(final @NotNull FileEditorState state) { if (myNodeEditor == null) { return; } final MPSEditorStateWrapper wrapper = (MPSEditorStateWrapper) state; setState(wrapper.getEditorState(), wrapper.getLevel() == FileEditorStateLevel.UNDO); } private void setState(EditorState editorState, boolean isUndo) { myDelayedState = null; if (isUndo) { //we need it here since undo might need to flush events which requires write action myProject.getModelAccess().runWriteAction(() -> myNodeEditor.loadState(editorState)); } else { myNodeEditor.loadState(editorState); AbstractComputeRunnable<EditorState> runnable = new AbstractComputeRunnable<EditorState>() { @Override protected EditorState compute() { return myNodeEditor.saveState(); } }; myProject.getModelAccess().runReadAction(runnable); if (runnable.getResult().getClass() != editorState.getClass()) { myDelayedState = editorState; } } } @Override public boolean isModified() { if (waitingForNodeFile()) { return false; } return new ModelAccessHelper(myProject.getModelAccess()).runReadAction(() -> { assert myFile.getNode() != null : String.format("File does not contain node: %s", myFile.toString()); SModel md = myFile.getNode().getModel(); return md instanceof EditableSModel && ((EditableSModel) md).isChanged(); }); } @Override public boolean isValid() { // allowing myFile==null as it currently designates delayed editor: waiting for the model to become ready // in the repo and then becoming a normal fully-fledged editor return (waitingForNodeFile() || myFile.isValid()) && !myDisposed; } @Override public void selectNotify() { mySelected = true; if (myNodeEditor != null) { myNodeEditor.selectNotify(); } } @Override public void deselectNotify() { if (myNodeEditor != null) { myNodeEditor.deselectNotify(); } mySelected = false; } @Override public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { } @Override public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { } @Override @Nullable public BackgroundEditorHighlighter getBackgroundHighlighter() { return null; } @Override @Nullable public FileEditorLocation getCurrentLocation() { return null; } @Override @Nullable public StructureViewBuilder getStructureViewBuilder() { if (waitingForNodeFile()) { return null; } return new ModelAccessHelper(myProject.getModelAccess()).runReadAction(() -> { for (NodeStructureViewProvider provider : NodeStructureViewProvider.EP_NODE_STRUCTURE_VIEW_PROVIDER.getExtensions()) { // FIXME NodeStructureViewProvider shall not be shy to accept MPSProject directly, as it's what the only implementation out there does. StructureViewBuilder builder = provider.getStructureViewBuilder(myFile, myProject.getProject()); if (builder != null) { return builder; } } return null; }); } @Override public void dispose() { if (myNodeEditor != null) { myNodeEditor.dispose(); } myComponent.removeAll(); myDisposed = true; } public boolean isDisposed() { return myDisposed; } // expects model read, and likely EDT? private void recreateEditor(EditorState state) { if (myProject.isDisposed() || !isValid() || waitingForNodeFile()) { return; } myComponent.removeAll(); Editor oldNodeEditor = myNodeEditor; myNodeEditor = new MPSEditorOpener(myProject).createEditorFor(myFile.getNode()); if (oldNodeEditor != null) { oldNodeEditor.dispose(); } if (state != null) { setState(state, false); } if (mySelected) { myNodeEditor.selectNotify(); } myComponent.add(((BaseNodeEditor) myNodeEditor).getComponent(), BorderLayout.CENTER); myComponent.validate(); } private void initEditor() { recreateEditor(myNodeEditor != null ? getState(FileEditorStateLevel.FULL).getEditorState() : null); } public void recreateEditorOnTabChange() { EditorState currentState = myNodeEditor != null ? getState(FileEditorStateLevel.FULL).getEditorState() : null; if (myDelayedState == null) { recreateEditor(currentState); } else { recreateEditor(myDelayedState); } } @Override public Document[] getDocuments() { if (!isDisposed() && myNodeEditor != null) { List<Document> result = ((BaseNodeEditor) myNodeEditor).getAllEditedDocuments(); return result.toArray(new Document[result.size()]); } return new Document[0]; } private boolean waitingForNodeFile() { return myFile == null; } private class MPSFileNodeEditorComponent extends JPanel implements DataProvider { private MPSFileNodeEditorComponent() { super(new BorderLayout()); } @Override public Object getData(@NonNls String dataId) { if (getParent() == null) { if (dataId.equals(PlatformDataKeys.FILE_EDITOR.getName())) { return MPSFileNodeEditor.this; } if (dataId.equals(PlatformDataKeys.PROJECT.getName())) { return myProject.getProject(); } } else { if (!myProject.isDisposed() && !waitingForNodeFile()) { final Object data = FileEditorDataProviderManager.getInstance(myProject.getProject()).getData(dataId, MPSFileNodeEditor.this, myFile); if (data != null) { return data; } } } return null; } } }