| /******************************************************************************* |
| * Copyright (c) 2016, 2018 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 |
| * Lucas Bullen (Red Hat Inc.) - [Bug 528333] Performance problem with diagnostics |
| *******************************************************************************/ |
| package org.eclipse.lsp4e.operations.diagnostics; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| |
| import org.eclipse.core.filebuffers.FileBuffers; |
| import org.eclipse.core.filebuffers.ITextFileBuffer; |
| import org.eclipse.core.filebuffers.ITextFileBufferManager; |
| import org.eclipse.core.filebuffers.LocationKind; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IMarker; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IWorkspaceRunnable; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.source.Annotation; |
| import org.eclipse.jface.text.source.IAnnotationModel; |
| import org.eclipse.jface.text.source.IAnnotationModelExtension; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| import org.eclipse.lsp4e.LSPEclipseUtils; |
| import org.eclipse.lsp4e.LanguageServerPlugin; |
| import org.eclipse.lsp4j.Diagnostic; |
| import org.eclipse.lsp4j.PublishDiagnosticsParams; |
| import org.eclipse.ui.texteditor.MarkerUtilities; |
| |
| public class LSPDiagnosticsToMarkers implements Consumer<PublishDiagnosticsParams> { |
| |
| public static final String LSP_DIAGNOSTIC = "lspDiagnostic"; //$NON-NLS-1$ |
| public static final String LANGUAGE_SERVER_ID = "languageServerId"; //$NON-NLS-1$ |
| public static final String LS_DIAGNOSTIC_MARKER_TYPE = "org.eclipse.lsp4e.diagnostic"; //$NON-NLS-1$ |
| private final @NonNull String languageServerId; |
| |
| public LSPDiagnosticsToMarkers(@NonNull String serverId) { |
| this.languageServerId = serverId; |
| } |
| |
| /** |
| * |
| * @param project |
| * @param serverId |
| * ignored |
| * @deprecated |
| */ |
| @Deprecated |
| public LSPDiagnosticsToMarkers(IProject project, @NonNull String serverId) { |
| this(serverId); |
| } |
| |
| @Override |
| public void accept(PublishDiagnosticsParams diagnostics) { |
| try { |
| String uri = diagnostics.getUri(); |
| IResource resource = LSPEclipseUtils.findResourceFor(uri); |
| if (resource != null && resource.exists()) { |
| updateMarkers(diagnostics, resource); |
| } else { |
| LSPEclipseUtils.findOpenEditorsFor(LSPEclipseUtils.toUri(uri)).stream() |
| .map(reference -> reference.getEditor(true)) |
| .filter(Objects::nonNull) |
| .map(editor -> editor.getAdapter(ITextViewer.class)) |
| .filter(Objects::nonNull) |
| .filter(ISourceViewer.class::isInstance) |
| .map(ISourceViewer.class::cast) |
| .forEach(sourceViewer -> updateEditorAnnotations(sourceViewer, diagnostics)); |
| } |
| } catch (CoreException ex) { |
| LanguageServerPlugin.logError(ex); |
| } |
| } |
| |
| private void updateEditorAnnotations(@NonNull ISourceViewer sourceViewer, PublishDiagnosticsParams diagnostics) { |
| IAnnotationModel annotationModel = sourceViewer.getAnnotationModel(); |
| if (annotationModel == null) { |
| return; |
| } |
| if (annotationModel instanceof IAnnotationModelExtension) { |
| Set<Annotation> toRemove = new HashSet<>(); |
| annotationModel.getAnnotationIterator().forEachRemaining(annotation -> { |
| if (annotation instanceof DiagnosticAnnotation) { |
| toRemove.add(annotation); |
| } |
| }); |
| Map<Annotation, Position> toAdd = new HashMap<>(diagnostics.getDiagnostics().size(), 1.f); |
| diagnostics.getDiagnostics().forEach(diagnostic -> { |
| try { |
| int startOffset = LSPEclipseUtils.toOffset(diagnostic.getRange().getStart(), sourceViewer.getDocument()); |
| int endOffset = LSPEclipseUtils.toOffset(diagnostic.getRange().getEnd(), sourceViewer.getDocument()); |
| toAdd.put(new DiagnosticAnnotation(diagnostic), new Position(startOffset, endOffset - startOffset)); |
| } catch (BadLocationException ex) { |
| LanguageServerPlugin.logError(ex); |
| } |
| }); |
| ((IAnnotationModelExtension)annotationModel).replaceAnnotations(toRemove.toArray(new Annotation[toRemove.size()]), toAdd); |
| } |
| } |
| |
| private void updateMarkers(PublishDiagnosticsParams diagnostics, IResource resource) throws CoreException { |
| Set<IMarker> toDeleteMarkers = new HashSet<>( |
| Arrays.asList(resource.findMarkers(LS_DIAGNOSTIC_MARKER_TYPE, false, IResource.DEPTH_ONE))); |
| toDeleteMarkers |
| .removeIf(marker -> !Objects.equals(marker.getAttribute(LANGUAGE_SERVER_ID, ""), languageServerId)); //$NON-NLS-1$ |
| List<Diagnostic> newDiagnostics = new ArrayList<>(); |
| Map<IMarker, Diagnostic> toUpdate = new HashMap<>(); |
| for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { |
| IMarker associatedMarker = getExistingMarkerFor(resource, diagnostic, toDeleteMarkers); |
| if (associatedMarker == null) { |
| newDiagnostics.add(diagnostic); |
| } else { |
| toDeleteMarkers.remove(associatedMarker); |
| toUpdate.put(associatedMarker, diagnostic); |
| } |
| } |
| IWorkspaceRunnable runnable = new IWorkspaceRunnable() { |
| @Override |
| public void run(IProgressMonitor monitor) throws CoreException { |
| if (resource.exists()) { |
| for (Diagnostic diagnostic : newDiagnostics) { |
| resource.createMarker(LS_DIAGNOSTIC_MARKER_TYPE, computeMarkerAttributes(resource, diagnostic)); |
| } |
| for (Entry<IMarker, Diagnostic> entry : toUpdate.entrySet()) { |
| updateMarker(resource, entry.getValue(), entry.getKey()); |
| } |
| toDeleteMarkers.forEach(t -> { |
| try { |
| t.delete(); |
| } catch (CoreException e) { |
| LanguageServerPlugin.logError(e); |
| } |
| }); |
| } |
| } |
| }; |
| ResourcesPlugin.getWorkspace().run(runnable, new NullProgressMonitor()); |
| } |
| |
| protected void updateMarker(IResource resource, Diagnostic diagnostic, IMarker marker) { |
| try { |
| Map<String, Object> targetAttributes = computeMarkerAttributes(resource, diagnostic); |
| if (!targetAttributes.equals(marker.getAttributes())) { |
| marker.setAttributes(targetAttributes); |
| } |
| } catch (CoreException e) { |
| LanguageServerPlugin.logError(e); |
| } |
| } |
| |
| private Map<String, Object> computeMarkerAttributes(IResource resource, Diagnostic diagnostic) |
| throws CoreException { |
| Map<String, Object> targetAttributes = new HashMap<>(7); |
| targetAttributes.put(LSP_DIAGNOSTIC, diagnostic); |
| targetAttributes.put(LANGUAGE_SERVER_ID, this.languageServerId); |
| targetAttributes.put(IMarker.MESSAGE, diagnostic.getMessage()); |
| targetAttributes.put(IMarker.SEVERITY, LSPEclipseUtils.toEclipseMarkerSeverity(diagnostic.getSeverity())); |
| if (resource.getType() == IResource.FILE) { |
| try { |
| IFile file = (IFile) resource; |
| ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager(); |
| ITextFileBuffer textFileBuffer = manager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); |
| |
| if (textFileBuffer == null) { |
| manager.connect(file.getFullPath(), LocationKind.IFILE, new NullProgressMonitor()); |
| textFileBuffer = manager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); |
| } |
| |
| IDocument document = textFileBuffer.getDocument(); |
| int start = Math.min(LSPEclipseUtils.toOffset(diagnostic.getRange().getStart(), document), |
| document.getLength()); |
| int end = Math.min(LSPEclipseUtils.toOffset(diagnostic.getRange().getEnd(), document), |
| document.getLength()); |
| if (start == end && document.getLength() > end) { |
| end++; |
| if (document.getLineOfOffset(end) != document.getLineOfOffset(start)) { |
| start--; |
| end--; |
| } |
| } |
| targetAttributes.put(IMarker.CHAR_START, start); |
| targetAttributes.put(IMarker.CHAR_END, end); |
| targetAttributes.put(IMarker.LINE_NUMBER, document.getLineOfOffset(start) + 1); |
| } catch (BadLocationException ex) { |
| LanguageServerPlugin.logError(ex); |
| } |
| } |
| return targetAttributes; |
| } |
| |
| private IMarker getExistingMarkerFor(IResource resource, Diagnostic diagnostic, Set<IMarker> remainingMarkers) { |
| ITextFileBuffer textFileBuffer = FileBuffers.getTextFileBufferManager() |
| .getTextFileBuffer(resource.getFullPath(), LocationKind.IFILE); |
| if (textFileBuffer == null) { |
| return null; |
| } |
| |
| IDocument document = textFileBuffer.getDocument(); |
| for (IMarker marker : remainingMarkers) { |
| int startOffset = MarkerUtilities.getCharStart(marker); |
| int endOffset = MarkerUtilities.getCharEnd(marker); |
| try { |
| if (LSPEclipseUtils.toOffset(diagnostic.getRange().getStart(), document) == startOffset |
| && LSPEclipseUtils.toOffset(diagnostic.getRange().getEnd(), document) == endOffset |
| && Objects.equals(marker.getAttribute(IMarker.MESSAGE), diagnostic.getMessage()) |
| && Objects.equals(marker.getAttribute(LANGUAGE_SERVER_ID), this.languageServerId)) { |
| return marker; |
| } |
| } catch (CoreException | BadLocationException e) { |
| LanguageServerPlugin.logError(e); |
| } |
| } |
| return null; |
| } |
| } |