/*
* Copyright 2000-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 com.intellij.execution.impl;
import com.intellij.execution.filters.Filter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.ProgressIndicatorUtils;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.NullableComputable;
import com.intellij.openapi.util.Ref;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author peter
*/
class AsyncFilterRunner {
private static final Logger LOG = Logger.getInstance("#com.intellij.execution.impl.FilterRunner");
private static final ExecutorService ourExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("console filters", 1);
private final EditorHyperlinkSupport myHyperlinks;
private final Editor myEditor;
private final Map<AtomicBoolean, Future<FilterResults>> myPendingFilterResults = new LinkedHashMap<>();
AsyncFilterRunner(EditorHyperlinkSupport hyperlinks, Editor editor) {
myHyperlinks = hyperlinks;
myEditor = editor;
}
void highlightHyperlinks(final Filter customFilter, final int startLine, final int endLine) {
if (endLine < 0) return;
Computable<FilterResults> bgComputation = highlightHyperlinksAsync(customFilter, startLine, endLine);
if (ApplicationManager.getApplication().isWriteAccessAllowed()) {
bgComputation.compute().applyHighlights(myHyperlinks);
}
else {
runFiltersInBackground(bgComputation);
}
}
private void runFiltersInBackground(Computable<FilterResults> bgComputation) {
AtomicBoolean handled = new AtomicBoolean();
Future<FilterResults> future = ourExecutor.submit(() -> {
FilterResults results = computeWithWritePriority(bgComputation);
if (!results.myResults.isEmpty()) {
ApplicationManager.getApplication().invokeLater(() -> {
results.applyHighlights(myHyperlinks);
myPendingFilterResults.remove(handled);
}, ModalityState.any(), o -> handled.get());
}
return results;
});
myPendingFilterResults.put(handled, future);
handleSynchronouslyIfQuick(handled, future, 5);
}
@NotNull
private FilterResults computeWithWritePriority(Computable<FilterResults> bgComputation) {
Ref<FilterResults> applyResults = Ref.create(FilterResults.EMPTY);
Runnable computeInReadAction = () -> {
if (myEditor.isDisposed()) return;
applyResults.set(bgComputation.compute());
};
while (!ProgressIndicatorUtils.runInReadActionWithWriteActionPriority(computeInReadAction)) {
ProgressIndicatorUtils.yieldToPendingWriteActions();
}
return applyResults.get();
}
private boolean handleSynchronouslyIfQuick(AtomicBoolean handled, Future<FilterResults> future, long timeout) {
try {
future.get(timeout, TimeUnit.MILLISECONDS).applyHighlights(myHyperlinks);
handled.set(true);
myPendingFilterResults.remove(handled);
return true;
}
catch (TimeoutException ignored) {
return false;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public boolean waitForPendingFilters(long timeoutMs) {
ApplicationManager.getApplication().assertIsDispatchThread();
long started = System.currentTimeMillis();
while (!myPendingFilterResults.isEmpty()) {
Map.Entry<AtomicBoolean, Future<FilterResults>> next = myPendingFilterResults.entrySet().iterator().next();
timeoutMs -= System.currentTimeMillis() - started;
if (timeoutMs < 1) return false;
if (!handleSynchronouslyIfQuick(next.getKey(), next.getValue(), timeoutMs)) return false;
}
return true;
}
@NotNull
private Computable<FilterResults> highlightHyperlinksAsync(Filter filter, int startLine, int endLine) {
Document document = myEditor.getDocument();
int markerOffset = document.getLineEndOffset(endLine);
RangeMarker marker = document.createRangeMarker(markerOffset, markerOffset);
List<LineHighlighter> tasks = new ArrayList<>();
for (int i = startLine; i <= endLine; i++) {
tasks.add(processLine(document, filter, i));
}
return () -> {
List<Filter.Result> results = new ArrayList<>();
for (LineHighlighter task : tasks) {
ProgressManager.checkCanceled();
if (!marker.isValid()) return FilterResults.EMPTY;
ContainerUtil.addIfNotNull(results, task.compute());
}
return new FilterResults(markerOffset, marker, results);
};
}
@NotNull
private static LineHighlighter processLine(Document document, Filter filter, int line) {
int lineEnd = document.getLineEndOffset(line);
int endOffset = lineEnd + (lineEnd < document.getTextLength() ? 1 /* for \n */ : 0);
String text = EditorHyperlinkSupport.getLineText(document, line, true);
return () -> checkRange(filter, endOffset, filter.applyFilter(text, endOffset));
}
private static Filter.Result checkRange(Filter filter, int endOffset, Filter.Result result) {
if (result != null) {
for (Filter.ResultItem resultItem : result.getResultItems()) {
int start = resultItem.getHighlightStartOffset();
int end = resultItem.getHighlightEndOffset();
if (end < start || end > endOffset) {
LOG.error("Filter returned wrong range: start=" + start + "; end=" + end + "; max=" + endOffset + "; filter=" + filter);
}
}
}
return result;
}
private interface LineHighlighter extends NullableComputable<Filter.Result> {
}
private static class FilterResults {
static final FilterResults EMPTY = new FilterResults(0, null, Collections.emptyList());
private int myInitialMarkerOffset;
private RangeMarker myMarker;
private List<Filter.Result> myResults;
FilterResults(int initialMarkerOffset, RangeMarker marker, List<Filter.Result> results) {
myInitialMarkerOffset = initialMarkerOffset;
myMarker = marker;
myResults = results;
}
void applyHighlights(EditorHyperlinkSupport hyperlinks) {
if (myResults.isEmpty() || !myMarker.isValid()) return;
int delta = myMarker.getStartOffset() - myInitialMarkerOffset;
for (Filter.Result result : myResults) {
hyperlinks.highlightHyperlinks(result, delta);
}
}
}
}