blob: 81f949d61531ad9c6ec823af276b169b34c8cbda [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2019 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Mickael Istria (Red Hat Inc.) - initial implementation
*******************************************************************************/
package org.eclipse.lsp4e.outline;
import java.net.URI;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.viewers.DecorationOverlayIcon;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServiceAccessor.LSPDocumentInfo;
import org.eclipse.lsp4e.outline.SymbolsModel.DocumentSymbolWithFile;
import org.eclipse.lsp4e.ui.LSPImages;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.progress.ProgressManager;
import org.eclipse.ui.navigator.ICommonContentExtensionSite;
import org.eclipse.ui.navigator.ICommonLabelProvider;
import com.google.common.collect.RangeMap;
import com.google.common.collect.TreeRangeMap;
public class SymbolsLabelProvider extends LabelProvider
implements ICommonLabelProvider, IStyledLabelProvider, IPreferenceChangeListener {
private Map<IResource, RangeMap<Integer, Integer>> severities = new HashMap<>();
private final IResourceChangeListener listener = e -> {
try {
IResourceDelta delta = e.getDelta();
if (delta != null) {
delta.accept(d -> {
if (d.getMarkerDeltas().length > 0) {
severities.remove(d.getResource());
}
return true;
});
}
} catch (CoreException ex) {
LanguageServerPlugin.logError(ex);
}
};
private Map<Image, Image[]> overlays = new HashMap<>();
private boolean showLocation;
private boolean showKind;
public SymbolsLabelProvider() {
this(false, InstanceScope.INSTANCE.getNode(LanguageServerPlugin.PLUGIN_ID)
.getBoolean(CNFOutlinePage.SHOW_KIND_PREFERENCE, false));
}
public SymbolsLabelProvider(boolean showLocation, boolean showKind) {
this.showLocation = showLocation;
this.showKind = showKind;
InstanceScope.INSTANCE.getNode(LanguageServerPlugin.PLUGIN_ID).addPreferenceChangeListener(this);
ResourcesPlugin.getWorkspace().addResourceChangeListener(listener);
}
@Override
public void dispose() {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(listener);
InstanceScope.INSTANCE.getNode(LanguageServerPlugin.PLUGIN_ID).removePreferenceChangeListener(this);
super.dispose();
}
@Override
public Image getImage(Object element) {
if (element == null){
return null;
}
if (element == LSSymbolsContentProvider.COMPUTING) {
return JFaceResources.getImage(ProgressManager.WAITING_JOB_KEY);
}
if (element instanceof Throwable) {
return PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_ERROR_TSK);
}
if (element instanceof Either) {
element = ((Either<?, ?>) element).get();
}
Image res = null;
if (element instanceof SymbolInformation) {
res = LSPImages.imageFromSymbolKind(((SymbolInformation) element).getKind());
} else if (element instanceof DocumentSymbol) {
res = LSPImages.imageFromSymbolKind(((DocumentSymbol) element).getKind());
} else if (element instanceof DocumentSymbolWithFile) {
res = LSPImages.imageFromSymbolKind(((DocumentSymbolWithFile) element).symbol.getKind());
}
IResource file = null;
if (element instanceof SymbolInformation) {
file = LSPEclipseUtils.findResourceFor(((SymbolInformation) element).getLocation().getUri());
} else if (element instanceof DocumentSymbolWithFile) {
file = ((DocumentSymbolWithFile) element).file;
}
/*
* Implementation node: for problem decoration,m aybe consider using a ILabelDecorator/IDelayedLabelDecorator?
*/
if (file != null) {
Range range = null;
if (element instanceof SymbolInformation) {
range = ((SymbolInformation) element).getLocation().getRange();
} else if (element instanceof DocumentSymbol) {
range = ((DocumentSymbol) element).getRange();
} else if (element instanceof DocumentSymbolWithFile) {
range = ((DocumentSymbolWithFile) element).symbol.getRange();
}
if (range != null) {
try {
// use existing documents only to calculate the severity
// to avoid extra documents being created (and connected
// to the language server just for this (bug 550968)
IDocument doc = LSPEclipseUtils.getExistingDocument(file);
if (doc != null) {
int maxSeverity = getMaxSeverity(file, doc, range);
if (maxSeverity > IMarker.SEVERITY_INFO) {
return getOverlay(res, maxSeverity);
}
}
} catch (CoreException | BadLocationException e) {
LanguageServerPlugin.logError(e);
}
}
}
return res;
}
protected int getMaxSeverity(@NonNull IResource resource, @NonNull IDocument doc, @NonNull Range range)
throws CoreException, BadLocationException {
if (!severities.containsKey(resource)) {
refreshMarkersByLine(resource);
}
RangeMap<Integer, Integer> severitiesForResource = severities.get(resource);
if (severitiesForResource == null) {
return -1;
}
int bound1 = LSPEclipseUtils.toOffset(range.getStart(), doc);
int bound2 = LSPEclipseUtils.toOffset(range.getEnd(), doc);
// using bounds here because doc may have changed in the meantime so toOffset can return wrong results.
com.google.common.collect.Range<Integer> subRange = com.google.common.collect.Range.closed(
Math.min(bound1, bound2), // we guard that lower <= endOffset
bound2);
return severitiesForResource.subRangeMap(subRange)
.asMapOfRanges()
.values()
.stream()
.max(Comparator.naturalOrder())
.orElse(-1);
}
private void refreshMarkersByLine(IResource resource) throws CoreException {
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
Arrays.stream(resource.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_ZERO))
.filter(marker -> marker.getAttribute(IMarker.SEVERITY, -1) > IMarker.SEVERITY_INFO)
.sorted(Comparator.comparingInt(marker -> marker.getAttribute(IMarker.SEVERITY, -1)))
.forEach(marker -> {
int start = marker.getAttribute(IMarker.CHAR_START, -1);
int end = marker.getAttribute(IMarker.CHAR_END, -1);
if (end < start) {
end = start;
}
int severity = marker.getAttribute(IMarker.SEVERITY, -1);
if (start != end) {
com.google.common.collect.Range<Integer> markerRange = com.google.common.collect.Range.closed(start,end - 1);
rangeMap.remove(markerRange);
rangeMap.put(markerRange, severity);
}
});
severities.put(resource, rangeMap);
}
private Image getOverlay(Image res, int maxSeverity) {
if (maxSeverity != 1 && maxSeverity != 2) {
throw new IllegalArgumentException("Severity " + maxSeverity + " not supported."); //$NON-NLS-1$ //$NON-NLS-2$
}
Image[] currentOverlays = this.overlays.computeIfAbsent(res, key -> new Image[2]);
if (currentOverlays[maxSeverity - 1] == null) {
String overlayId = null;
if (maxSeverity == IMarker.SEVERITY_ERROR) {
overlayId = ISharedImages.IMG_DEC_FIELD_ERROR;
} else if (maxSeverity == IMarker.SEVERITY_WARNING) {
overlayId = ISharedImages.IMG_DEC_FIELD_WARNING;
}
currentOverlays[maxSeverity - 1] = new DecorationOverlayIcon(res,
PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(overlayId),
IDecoration.BOTTOM_LEFT).createImage();
}
return currentOverlays[maxSeverity - 1];
}
@Override
public String getText(Object element) {
return getStyledText(element).getString();
}
@Override
public StyledString getStyledText(Object element) {
if (element == LSSymbolsContentProvider.COMPUTING) {
return new StyledString(Messages.outline_computingSymbols);
}
if (element instanceof Throwable) {
String message = ((Throwable) element).getMessage();
if (message == null) {
message = element.getClass().getName();
}
return new StyledString(message);
}
if (element instanceof LSPDocumentInfo) {
return new StyledString(((LSPDocumentInfo)element).getFileUri().getPath());
}
StyledString res = new StyledString();
if (element == null){
return res;
}
if (element instanceof Either) {
element = ((Either<?, ?>) element).get();
}
String name = null;
SymbolKind kind = null;
URI location = null;
if (element instanceof SymbolInformation) {
name = ((SymbolInformation) element).getName();
kind = ((SymbolInformation) element).getKind();
try {
location = URI.create(((SymbolInformation) element).getLocation().getUri());
} catch (IllegalArgumentException e) {
LanguageServerPlugin.logError("Invalid URI: " + ((SymbolInformation) element).getLocation().getUri(), e); //$NON-NLS-1$
}
} else if (element instanceof DocumentSymbol) {
name = ((DocumentSymbol) element).getName();
kind = ((DocumentSymbol) element).getKind();
} else if (element instanceof DocumentSymbolWithFile) {
name = ((DocumentSymbolWithFile) element).symbol.getName();
kind = ((DocumentSymbolWithFile) element).symbol.getKind();
IFile file = ((DocumentSymbolWithFile) element).file;
if (file != null) {
location = file.getLocationURI();
}
}
if (name != null) {
res.append(name, null);
}
if (showKind && kind != null) {
res.append(" :", null); //$NON-NLS-1$
res.append(kind.toString(), StyledString.DECORATIONS_STYLER);
}
if (showLocation && location != null) {
res.append(' ');
res.append(location.getPath(), StyledString.QUALIFIER_STYLER);
}
return res;
}
@Override
public void restoreState(IMemento aMemento) {
}
@Override
public void saveState(IMemento aMemento) {
}
@Override
public String getDescription(Object anElement) {
return null;
}
@Override
public void init(ICommonContentExtensionSite aConfig) {
}
@Override
public void preferenceChange(PreferenceChangeEvent event) {
if (event.getKey().equals(CNFOutlinePage.SHOW_KIND_PREFERENCE)) {
this.showKind = Boolean.valueOf(event.getNewValue().toString());
for (Object listener : this.getListeners()) {
if (listener instanceof ILabelProviderListener) {
((ILabelProviderListener) listener).labelProviderChanged(new LabelProviderChangedEvent(this));
}
}
}
}
}