/*
* Copyright 2003-2017 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.warningPanel;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.vcs.FileStatusListener;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.messages.MessageBusConnection;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.classloading.ClassLoaderManager;
import jetbrains.mps.classloading.MPSClassesListener;
import jetbrains.mps.classloading.MPSClassesListenerAdapter;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.ide.editor.MPSFileNodeEditor;
import jetbrains.mps.module.ReloadableModuleBase;
import jetbrains.mps.nodefs.MPSNodeVirtualFile;
import jetbrains.mps.openapi.editor.Editor;
import jetbrains.mps.openapi.editor.EditorComponent;
import jetbrains.mps.project.MPSProject;
import jetbrains.mps.smodel.ModelReadRunnable;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.util.containers.MultiMap;
import org.apache.log4j.LogManager;
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.model.SModel;
import org.jetbrains.mps.openapi.model.SModel.Problem;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
public class MPSEditorWarningsManager implements ProjectComponent {
public static final Logger LOG = LogManager.getLogger(MPSEditorWarningsManager.class);
private final MPSProject myProject;
private final FileEditorManager myFileEditorManager;
private final FileStatusManager myFileStatusManager;
private ClassLoaderManager myClassLoaderManager;
private final MPSClassesListener myClassesListener = new EditorWarningsListenerAdapter();
private final MyFileStatusListener myFileStatusListener = new MyFileStatusListener();
private MessageBusConnection myProjectBus;
// I don't truly need atomic boolean here, regular boolean would suffice in most cases, as requests generally come
// from same thread sequentially (e.g. modelLoaded). Nevertheless, it doesn't hurt to account for more complicated scenario.
private final AtomicBoolean myScheduledUpdateAllWarnings = new AtomicBoolean(false);
private final SRepositoryContentAdapter myRepoListener = new SRepositoryContentAdapter() {
@Override
protected void startListening(SModel model) {
model.addModelListener(this);
}
@Override
protected void stopListening(SModel model) {
model.removeModelListener(this);
}
@Override
public void modelLoaded(SModel model, boolean partially) {
updateAllWarningsLater();
}
@Override
public void modelUnloaded(SModel model) {
updateAllWarningsLater();
}
@Override
public void problemsDetected(SModel model, Iterable<Problem> problems) {
updateAllWarningsLater();
}
@Override
public void modelSaved(SModel model) {
updateAllWarningsLater();
}
};
private MultiMap<MPSFileNodeEditor, WarningPanel> myWarnings = new MultiMap<MPSFileNodeEditor, WarningPanel>();
public MPSEditorWarningsManager(MPSProject project, FileEditorManager fileEditorManager, FileStatusManager fileStatusManager, MPSCoreComponents coreComponents) {
myProject = project;
myFileEditorManager = fileEditorManager;
myFileStatusManager = fileStatusManager;
myClassLoaderManager = coreComponents.getClassLoaderManager();
}
@Override
public void projectOpened() {
myProjectBus = myProject.getProject().getMessageBus().connect();
myProjectBus.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new MyFileEditorManagerListener());
myClassLoaderManager.addClassesHandler(myClassesListener);
myFileStatusManager.addFileStatusListener(myFileStatusListener);
new RepoListenerRegistrar(myProject.getRepository(), myRepoListener).attach();
}
@Override
public void projectClosed() {
new RepoListenerRegistrar(myProject.getRepository(), myRepoListener).detach();
myFileStatusManager.removeFileStatusListener(myFileStatusListener);
myClassLoaderManager.removeClassesHandler(myClassesListener);
myProjectBus.disconnect();
}
@Override
@NonNls
@NotNull
public String getComponentName() {
return "MPS Editor Warnings Manager";
}
@Override
public void initComponent() {
}
@Override
public void disposeComponent() {
}
private void updateWarnings(@NotNull final MPSFileNodeEditor editor) {
DumbService.getInstance(myProject.getProject()).smartInvokeLater(new ModelReadRunnable(myProject.getModelAccess(), () -> doUpdateWarnings(editor)));
}
private void doUpdateWarnings(final MPSFileNodeEditor editor) {
List<WarningPanel> newWarnings = new ArrayList<WarningPanel>();
Editor nodeEditor = editor.getNodeEditor();
if (nodeEditor == null) return;
EditorComponent editorComponent = nodeEditor.getCurrentEditorComponent();
if (editorComponent != null && editorComponent.isDisposed()) return;
SNode node;
if (editorComponent != null) {
node = editorComponent.getEditedNode();
} else {
MPSNodeVirtualFile file = editor.getFile();
node = file != null && file.isValid() ? file.getNode() : null;
}
if (node == null || !SNodeUtil.isAccessible(node, myProject.getRepository())) return;
EditorWarningsProvider[] providers = Extensions.getExtensions(EditorWarningsProvider.EP_NAME);
for (EditorWarningsProvider provider : providers) {
WarningPanel panel = provider.getWarningPanel(node, myProject.getProject());
if (panel != null) {
newWarnings.add(panel);
}
}
replaceWarningPanels(editor, newWarnings);
}
private void updateAllWarnings(@Nullable VirtualFile vf) {
if (RuntimeFlags.isTestMode()) return;
for (FileEditor editor : myFileEditorManager.getAllEditors()) {
if (editor instanceof MPSFileNodeEditor) {
MPSFileNodeEditor mpsEditor = (MPSFileNodeEditor) editor;
if (!mpsEditor.isDisposed()) {
if (vf == null || vf.equals(mpsEditor.getFile())) {
updateWarnings(mpsEditor);
}
}
}
}
}
// re-dispatch updateAllWarnings from an EDT thread
/*package*/ void updateAllWarningsLater() {
if (myScheduledUpdateAllWarnings.get()) {
// there's already scheduled update in the EDT queue
return;
}
myScheduledUpdateAllWarnings.set(true);
ThreadUtils.runInUIThreadNoWait(new Runnable() {
@Override
public void run() {
myScheduledUpdateAllWarnings.set(false);
if (myProject.isDisposed()) {
return;
}
updateAllWarnings();
}
});
}
/*package*/ void updateAllWarnings() {
updateAllWarnings(null);
}
private void replaceWarningPanels(MPSFileNodeEditor editor, List<WarningPanel> newPanels) {
Collection<WarningPanel> oldPanels = myWarnings.get(editor);
List<WarningPanel> toRemove = new ArrayList<WarningPanel>(oldPanels);
toRemove.removeAll(newPanels);
List<WarningPanel> toAdd = new ArrayList<WarningPanel>(newPanels);
toAdd.removeAll(oldPanels);
for (WarningPanel panel : toRemove) {
myFileEditorManager.removeTopComponent(editor, panel);
myWarnings.removeValue(editor, panel);
}
for (WarningPanel panel : toAdd) {
myFileEditorManager.addTopComponent(editor, panel);
myWarnings.putValue(editor, panel);
}
}
private class MyFileEditorManagerListener implements FileEditorManagerListener {
@Override
public void fileOpened(FileEditorManager source, VirtualFile file) {
if (file instanceof MPSNodeVirtualFile) {
for (FileEditor fe : myFileEditorManager.getEditors(file)) {
if (fe instanceof MPSFileNodeEditor) {
updateWarnings((MPSFileNodeEditor) fe);
}
}
}
}
@Override
public void fileClosed(FileEditorManager source, VirtualFile file) {
myWarnings.keySet().retainAll(Arrays.asList(source.getAllEditors()));
}
@Override
public void selectionChanged(FileEditorManagerEvent event) {
updateAllWarnings();
}
}
private class EditorWarningsListenerAdapter extends MPSClassesListenerAdapter {
@Override
public void afterClassesLoaded(Set<? extends ReloadableModuleBase> modules) {
updateAllWarningsLater();
}
}
private class MyFileStatusListener implements FileStatusListener {
@Override
public void fileStatusChanged(@NotNull final VirtualFile virtualFile) {
updateAllWarnings(virtualFile);
}
@Override
public void fileStatusesChanged() {
updateAllWarnings();
}
}
}