/*
* 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.nodeEditor.highlighter;
import com.intellij.openapi.project.IndexNotReadyException;
import jetbrains.mps.make.IMakeService;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.EditorMessage;
import jetbrains.mps.nodeEditor.NodeHighlightManager;
import jetbrains.mps.nodeEditor.PriorityComparator;
import jetbrains.mps.nodeEditor.checking.EditorChecker;
import jetbrains.mps.nodeEditor.checking.UpdateResult;
import jetbrains.mps.nodeEditor.checking.UpdateResult.Completed;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.typesystem.inference.TypeContextManager;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.module.SRepository;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class HighlighterUpdateSession {
private final IHighlighter myHighlighter;
private final Collection<EditorCheckerWrapper> myCheckers;
private final List<EditorComponent> myAllEditorComponents;
@Nullable
private final EditorComponent myInspector;
public HighlighterUpdateSession(IHighlighter highlighter, Collection<EditorCheckerWrapper> checkers,
List<EditorComponent> allEditorComponents, @Nullable EditorComponent inspector) {
myHighlighter = highlighter;
myCheckers = checkers;
myAllEditorComponents = allEditorComponents;
myInspector = inspector;
}
@NotNull
private static <T> T runLoPrioRead(final Computable<T> computable) {
assert !ModelAccess.instance().canRead() : "Lo-prio read with acquired read can be a reason of a deadlock";
T result;
do {
while (IMakeService.INSTANCE.isSessionActive()) {
try {
Thread.sleep(600);
} catch (InterruptedException ignored) {
}
}
result = ModelAccess.instance().runReadAction(new Computable<T>() {
@Override
public T compute() {
if (IMakeService.INSTANCE.isSessionActive() || ModelAccess.instance().hasScheduledWrites()) return null;
return computable.compute();
}
});
} while (result == null);
return result;
}
private void doUpdate() {
if (myCheckers.isEmpty()) {
return;
}
List<Pair<EditorComponent, Boolean>> input = new ArrayList<Pair<EditorComponent, Boolean>>();
HashSet<SNodePointer> visited = new HashSet<SNodePointer>();
for (EditorComponent ecomp : myAllEditorComponents) {
SNodePointer pointer = new SNodePointer(ecomp.getNodeForTypechecking());
input.add(new Pair<EditorComponent, Boolean>(ecomp, !visited.contains(pointer)));
visited.add(pointer);
}
final boolean[] isUpdated = {false};
for (Pair<EditorComponent, Boolean> pair : input) {
final EditorComponent editorComponent = pair.o1;
final Boolean applyQuickFixes = pair.o2;
if (myHighlighter.isPausedOrStopping()) {
return;
}
TypeContextManager.getInstance().runTypecheckingAction(editorComponent.getTypecheckingContextOwner(), new Runnable() {
@Override
public void run() {
if (updateEditorComponent(editorComponent, false, applyQuickFixes)) {
isUpdated[0] = true;
}
}
});
}
if (myHighlighter.isPausedOrStopping()) {
return;
}
if (myInspector != null) {
TypeContextManager.getInstance().runTypecheckingAction(myInspector.getTypecheckingContextOwner(), new Runnable() {
@Override
public void run() {
updateEditorComponent(myInspector, isUpdated[0], false);
}
});
}
}
private boolean updateEditorComponent(final EditorComponent component, final boolean mainEditorMessagesChanged, final boolean applyQuickFixes) {
HighlighterEditorTracker editorTracker = myHighlighter.getEditorTracker();
final SRepository repository = component.getEditorContext().getRepository();
boolean needsUpdate = new ModelAccessHelper(repository).runReadAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
final SNode editedNode = component.getEditedNode();
return editedNode != null && SNodeUtil.isAccessible(editedNode, repository);
}
});
if (!needsUpdate) return false;
final Set<EditorCheckerWrapper> checkersToRecheck = new LinkedHashSet<>();
boolean rootWasCheckedOnce = editorTracker.wasCheckedOnce(component);
if (!rootWasCheckedOnce) {
checkersToRecheck.addAll(myCheckers);
} else {
repository.getModelAccess().runReadAction(new Runnable() {
@Override
public void run() {
if (myHighlighter.isPausedOrStopping()) return;
for (EditorCheckerWrapper checker : myCheckers) {
if (checker.needsUpdate(component)) {
checkersToRecheck.add(checker);
}
}
}
});
}
if (checkersToRecheck.isEmpty() || myHighlighter.isPausedOrStopping()) return false;
List<EditorCheckerWrapper> checkersToRecheckList = new ArrayList<>(checkersToRecheck);
checkersToRecheckList.sort(new PriorityComparator());
boolean recreateInspectorMessages = mainEditorMessagesChanged || !editorTracker.wereInspectorMessagesCreated();
editorTracker.markCheckedOnce(component);
return updateEditor(component, rootWasCheckedOnce, checkersToRecheckList, recreateInspectorMessages, applyQuickFixes);
}
private boolean updateEditor(final EditorComponent editor, final boolean wasCheckedOnce,
List<EditorCheckerWrapper> checkersToRecheck, boolean recreateInspectorMessages, final boolean applyQuickFixes) {
if (editor == null || editor.getRootCell() == null) return false;
final NodeHighlightManager highlightManager = editor.getHighlightManager();
boolean anyMessageChanged = false;
for (final EditorCheckerWrapper checker : checkersToRecheck) {
UpdateResult checkResult = runLoPrioRead(new Computable<UpdateResult>() {
@Override
public UpdateResult compute() {
if (myHighlighter.isPausedOrStopping()) return UpdateResult.CANCELLED;
SNode node = editor.getEditedNode(); // XXX perhaps, shall use getEditedNodePointer and resolve it, rather than check isAccessible?
if (node == null) {
return UpdateResult.CANCELLED;
}
if (!SNodeUtil.isAccessible(node, editor.getEditorContext().getRepository())) {
// asking runLoPrioRead() implementation to re-execute this task later:
// editor was not updated in accordance with last modelReload event yet.
return null;
}
return checker.withChecker(checker -> {
try {
return checker.update(editor, wasCheckedOnce, applyQuickFixes,
new HighlighterUpdateSessionCancellable(myHighlighter, checker.toString(), editor));
} catch (IndexNotReadyException ex) {
highlightManager.clearForOwner(checker.getEditorMessageOwner(), true);
throw ex;
}
}, UpdateResult.CANCELLED);
}
});
if (myHighlighter.isStopping()) return false;
if (checkResult instanceof Completed) {
Completed completed = (Completed) checkResult;
if (completed.myMessagesChanged || myHighlighter.getEditorTracker().isInspector(editor) && recreateInspectorMessages) {
anyMessageChanged = true;
highlightManager.clearForOwner(checker.getEditorMessageOwner(), false);
for (EditorMessage message : completed.myMessages) {
highlightManager.mark(message);
}
}
}
}
if (myHighlighter.isStopping()) return false;
if (anyMessageChanged) {
highlightManager.repaintAndRebuildEditorMessages();
editor.updateStatusBarMessage();
}
return anyMessageChanged;
}
private void doneUpdating() {
for (EditorCheckerWrapper checker : myCheckers) {
checker.doneUpdating();
}
}
public void update() {
doUpdate();
doneUpdating();
}
}