blob: 4b950c468d6107650a67d90d67c8c031e9cb71be [file] [log] [blame]
/**
* Copyright (c) 2017 Angelo ZERR.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Angelo Zerr <angelo.zerr@gmail.com> - [CodeMining] Provide CodeMining support with CodeMiningManager - Bug 527720
*/
package org.eclipse.jface.internal.text.codemining;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.osgi.framework.Bundle;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.codemining.ICodeMining;
import org.eclipse.jface.text.codemining.ICodeMiningProvider;
import org.eclipse.jface.text.codemining.LineHeaderCodeMining;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.inlined.AbstractInlinedAnnotation;
import org.eclipse.jface.text.source.inlined.InlinedAnnotationSupport;
/**
* Code Mining manager implementation.
*
* @since 3.13
*/
public class CodeMiningManager implements Runnable {
/**
* The source viewer
*/
private final ISourceViewer fViewer;
/**
* The inlined annotation support used to draw CodeMining in the line spacing.
*/
private final InlinedAnnotationSupport fInlinedAnnotationSupport;
/**
* The list of codemining providers.
*/
private List<ICodeMiningProvider> fCodeMiningProviders;
/**
* The current progress monitor.
*/
private IProgressMonitor fMonitor;
/**
* Constructor of codemining manager with the given arguments.
*
* @param viewer the source viewer
* @param inlinedAnnotationSupport the inlined annotation support used to draw code minings
* @param codeMiningProviders the array of codemining providers, must not be empty
*/
public CodeMiningManager(ISourceViewer viewer, InlinedAnnotationSupport inlinedAnnotationSupport,
ICodeMiningProvider[] codeMiningProviders) {
Assert.isNotNull(viewer);
Assert.isNotNull(inlinedAnnotationSupport);
Assert.isNotNull(codeMiningProviders);
fViewer= viewer;
fInlinedAnnotationSupport= inlinedAnnotationSupport;
setCodeMiningProviders(codeMiningProviders);
}
/**
* Set the codemining providers.
*
* @param codeMiningProviders the codemining providers.
*/
public void setCodeMiningProviders(ICodeMiningProvider[] codeMiningProviders) {
cancel();
if (fCodeMiningProviders != null) {
fCodeMiningProviders.stream().forEach(ICodeMiningProvider::dispose);
}
fCodeMiningProviders= Arrays.asList(codeMiningProviders);
}
/**
* Uninstalls this codemining manager.
*/
public void uninstall() {
cancel();
if (fInlinedAnnotationSupport != null) {
fInlinedAnnotationSupport.updateAnnotations(Collections.emptySet());
}
}
/**
* Collect, resolve and render the code minings of the viewer.
*/
@Override
public void run() {
if (fViewer == null || fInlinedAnnotationSupport == null || fCodeMiningProviders == null
|| fCodeMiningProviders.isEmpty() || fViewer.getAnnotationModel() == null) {
return;
}
// Cancel the last progress monitor to cancel last resolve and render of code
// minings
cancel();
// Update the code minings
updateCodeMinings();
}
/**
* Update the code minings.
*/
private void updateCodeMinings() {
// Refresh the code minings by using the new progress monitor.
fMonitor= new CancellationExceptionMonitor();
IProgressMonitor monitor= fMonitor;
// Collect the code minings for the viewer
getCodeMinings(fViewer, fCodeMiningProviders, monitor).thenAccept(symbols -> {
// check if request was canceled.
monitor.isCanceled();
// then group code minings by lines position
Map<Position, List<ICodeMining>> groups= groupByLines(symbols, fCodeMiningProviders);
// resolve and render code minings
renderCodeMinings(groups, fViewer, monitor);
});
}
/**
* Cancel the codemining process.
*/
private void cancel() {
// Cancel the last progress monitor.
if (fMonitor != null) {
fMonitor.setCanceled(true);
}
}
private static void logCodeMiningProviderException(Throwable e) {
if (e != null && (e instanceof CancellationException || (e.getCause() != null && e.getCause() instanceof CancellationException))) {
return;
}
String PLUGIN_ID= "org.eclipse.jface.text"; //$NON-NLS-1$
Bundle plugin= Platform.getBundle(PLUGIN_ID);
if (plugin != null) {
// In OSGi context, uses Platform Text log
ILog log= Platform.getLog(plugin);
log.log(new Status(IStatus.ERROR, PLUGIN_ID, IStatus.OK, e.getMessage(), e));
} else {
// In java main context, print stack trace
System.err.println("Error while code mining process: " + e.getMessage()); //$NON-NLS-1$
}
}
/**
* Return the list of {@link CompletableFuture} which provides the list of {@link ICodeMining}
* for the given <code>viewer</code> by using the given providers.
*
* @param viewer the text viewer.
* @param providers the CodeMining list providers.
* @param monitor the progress monitor.
* @return the list of {@link CompletableFuture} which provides the list of {@link ICodeMining}
* for the given <code>viewer</code> by using the given providers.
*/
private static CompletableFuture<List<? extends ICodeMining>> getCodeMinings(ITextViewer viewer,
List<ICodeMiningProvider> providers, IProgressMonitor monitor) {
List<CompletableFuture<List<? extends ICodeMining>>> com= providers.stream()
.map(provider -> provider.provideCodeMinings(viewer, monitor))
.filter(c -> c != null)
.map(future -> future.exceptionally(e -> {
logCodeMiningProviderException(e);
return Collections.emptyList();
}))
.collect(Collectors.toList());
return CompletableFuture.allOf(com.toArray(new CompletableFuture[com.size()])).thenApply(
v -> com.stream().map(CompletableFuture::join).flatMap(java.util.Collection::stream).collect(Collectors.toList()));
}
/**
* Returns a sorted Map which groups the given code minings by same position line.
*
* @param codeMinings list of code minings to group.
* @param providers CodeMining providers used to retrieve code minings.
* @return a sorted Map which groups the given code minings by same position line.
*/
private static Map<Position, List<ICodeMining>> groupByLines(List<? extends ICodeMining> codeMinings,
List<ICodeMiningProvider> providers) {
// sort code minings by lineNumber and provider-rank if
Collections.sort(codeMinings, (a, b) -> {
if (a.getPosition().offset < b.getPosition().offset) {
return -1;
} else if (a.getPosition().offset > b.getPosition().offset) {
return 1;
} else if (providers.indexOf(a.getProvider()) < providers.indexOf(b.getProvider())) {
return -1;
} else if (providers.indexOf(a.getProvider()) > providers.indexOf(b.getProvider())) {
return 1;
} else {
return 0;
}
});
return codeMinings.stream().collect(Collectors.groupingBy(ICodeMining::getPosition, LinkedHashMap::new,
Collectors.mapping(Function.identity(), Collectors.toList())));
}
/**
* Render the codemining grouped by line position.
*
* @param groups code minings grouped by lines position
* @param viewer the viewer
* @param monitor the progress monitor
*/
private void renderCodeMinings(Map<Position, List<ICodeMining>> groups, ISourceViewer viewer,
IProgressMonitor monitor) {
// check if request was canceled.
monitor.isCanceled();
IDocument document= viewer != null ? viewer.getDocument() : null;
if (document == null) {
// this case comes from when editor is closed before codemining rendered is
// done.
return;
}
Set<ICodeMiningAnnotation> annotationsToRedraw= new HashSet<>();
Set<AbstractInlinedAnnotation> currentAnnotations= new HashSet<>();
// Loop for grouped code minings
groups.entrySet().stream().forEach(g -> {
// check if request was canceled.
monitor.isCanceled();
Position pos= new Position(g.getKey().offset, g.getKey().length);
List<ICodeMining> minings= g.getValue();
boolean inLineHeader= !minings.isEmpty() ? (minings.get(0) instanceof LineHeaderCodeMining) : true;
// Try to find existing annotation
AbstractInlinedAnnotation ann= fInlinedAnnotationSupport.findExistingAnnotation(pos);
if (ann == null) {
// The annotation doesn't exists, create it.
ann= inLineHeader ? new CodeMiningLineHeaderAnnotation(pos, viewer) : new CodeMiningLineContentAnnotation(pos, viewer);
} else if (ann instanceof ICodeMiningAnnotation && ((ICodeMiningAnnotation) ann).isInVisibleLines()) {
// annotation is in visible lines
annotationsToRedraw.add((ICodeMiningAnnotation) ann);
}
((ICodeMiningAnnotation) ann).update(minings, monitor);
currentAnnotations.add(ann);
});
// check if request was canceled.
monitor.isCanceled();
fInlinedAnnotationSupport.updateAnnotations(currentAnnotations);
// redraw the existing codemining annotations since their content can change
annotationsToRedraw.stream().forEach(ICodeMiningAnnotation::redraw);
}
/**
* Returns <code>true</code> if the given mining has a non empty label and <code>false</code>
* otherwise.
*
* @param mining the mining to check
* @return <code>true</code> if the given mining has a non empty label and <code>false</code>
* otherwise.
*/
static boolean isValidMining(ICodeMining mining) {
return mining != null && mining.getLabel() != null && !mining.getLabel().isEmpty();
}
/**
* Returns the valid code mining at the given location by using the bounds of codemining
* annotations which stores only the valid code mining.
*
* @param minings the list of mining of the codemining annotation.
* @param bounds the bounds of the valid minings of the codemining annotation.
* @param x the x location
* @param y the y location
* @return the valid code mining at the given location by using the bounds of codemining
* annotations which stores only the valid code mining.
*/
static ICodeMining getValidCodeMiningAtLocation(ICodeMining[] minings, List<Rectangle> bounds, int x, int y) {
for (int i= 0; i < bounds.size(); i++) {
Rectangle bound= bounds.get(i);
if (bound.contains(x, y)) {
return getCodeValidMiningAtIndex(minings, i);
}
}
return null;
}
/**
* Returns the valid code mining at the given index.
*
* @param minings the list of minings
* @param index the index
* @return the valid code mining at the given index.
*/
private static ICodeMining getCodeValidMiningAtIndex(ICodeMining[] minings, int index) {
int validIndex= 0;
for (ICodeMining mining : minings) {
if (isValidMining(mining)) {
if (validIndex == index) {
return mining;
}
validIndex++;
}
}
return null;
}
}