org.springframework.tooling.ls.eclipse.commons.STS4LanguageClientImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.tooling.ls.eclipse.commons.STS4LanguageClientImpl.java

Source

/*******************************************************************************
 * Copyright (c) 2017, 2019 Pivotal, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Pivotal, Inc. - initial API and implementation
 *******************************************************************************/
package org.springframework.tooling.ls.eclipse.commons;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.internal.ui.javaeditor.JavaSourceViewer;
import org.eclipse.jdt.internal.ui.viewsupport.JavaElementLinks;
import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.preference.IPreferenceStore;
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.TextSelection;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationPainter;
import org.eclipse.jface.text.source.AnnotationPainter.IDrawingStrategy;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.ISourceViewerExtension5;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageClientImpl;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.MarkupKind;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditor;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
import org.springframework.ide.vscode.commons.protocol.CursorMovement;
import org.springframework.ide.vscode.commons.protocol.HighlightParams;
import org.springframework.ide.vscode.commons.protocol.ProgressParams;
import org.springframework.ide.vscode.commons.protocol.STS4LanguageClient;
import org.springframework.ide.vscode.commons.protocol.java.ClasspathListenerParams;
import org.springframework.ide.vscode.commons.protocol.java.JavaDataParams;
import org.springframework.ide.vscode.commons.protocol.java.JavaSearchParams;
import org.springframework.ide.vscode.commons.protocol.java.JavaTypeHierarchyParams;
import org.springframework.ide.vscode.commons.protocol.java.TypeData;
import org.springframework.ide.vscode.commons.protocol.java.TypeDescriptorData;
import org.springframework.tooling.jdt.ls.commons.Logger;
import org.springframework.tooling.jdt.ls.commons.classpath.ReusableClasspathListenerHandler;
import org.springframework.tooling.jdt.ls.commons.java.JavaData;
import org.springframework.tooling.jdt.ls.commons.java.JavaFluxSearch;
import org.springframework.tooling.jdt.ls.commons.java.TypeHierarchy;
import org.springframework.tooling.jdt.ls.commons.javadoc.JavadocUtils;
import org.springframework.tooling.jdt.ls.commons.resources.ResourceUtils;
import org.springframework.tooling.ls.eclipse.commons.javadoc.JavaDoc2MarkdownConverter;
import org.springframework.tooling.ls.eclipse.commons.preferences.PreferenceConstants;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

@SuppressWarnings("restriction")
public class STS4LanguageClientImpl extends LanguageClientImpl implements STS4LanguageClient {

    private static ReusableClasspathListenerHandler classpathService = new ReusableClasspathListenerHandler(
            Logger.forEclipsePlugin(LanguageServerCommonsActivator::getInstance), new LSP4ECommandExecutor(),
            () -> new ProjectSorter());

    private static final long LABEL_FLAGS = JavaElementLabels.ALL_FULLY_QUALIFIED
            | JavaElementLabels.M_PRE_RETURNTYPE | JavaElementLabels.M_PARAMETER_ANNOTATIONS
            | JavaElementLabels.M_PARAMETER_TYPES | JavaElementLabels.M_PARAMETER_NAMES
            | JavaElementLabels.M_EXCEPTIONS | JavaElementLabels.F_PRE_TYPE_SIGNATURE
            | JavaElementLabels.M_PRE_TYPE_PARAMETERS | JavaElementLabels.T_TYPE_PARAMETERS
            | JavaElementLabels.USE_RESOLVED;

    private static final long LOCAL_VARIABLE_FLAGS = LABEL_FLAGS & ~JavaElementLabels.F_FULLY_QUALIFIED
            | JavaElementLabels.F_POST_QUALIFIED;

    private static final long COMMON_SIGNATURE_FLAGS = LABEL_FLAGS & ~JavaElementLabels.ALL_FULLY_QUALIFIED
            | JavaElementLabels.T_FULLY_QUALIFIED | JavaElementLabels.M_FULLY_QUALIFIED;

    private static String label(IJavaElement element) {
        try {
            if (element instanceof ILocalVariable) {
                return JavaElementLabels.getElementLabel(element, LOCAL_VARIABLE_FLAGS);
            } else {
                return JavaElementLabels.getElementLabel(element, COMMON_SIGNATURE_FLAGS);
            }
        } catch (Exception e) {
            return null;
        }
    }

    final private JavaData javaData = new JavaData(STS4LanguageClientImpl::label,
            Logger.forEclipsePlugin(LanguageServerCommonsActivator::getInstance));

    final private JavaFluxSearch javaFluxSearch = new JavaFluxSearch(
            Logger.forEclipsePlugin(LanguageServerCommonsActivator::getInstance), javaData);

    final private TypeHierarchy typeHierarchy = new TypeHierarchy(
            Logger.forEclipsePlugin(LanguageServerCommonsActivator::getInstance), javaData);

    private static final String ANNOTION_TYPE_ID = "org.springframework.tooling.bootinfo";

    private static final String ALT_ANNOTATION_DRAWING_STRATEGY_ID = "boot.hint.strategy";
    private static final String ALT_ANNOTATION_TYPE_ID = "org.springframework.tooling.bootinfoCodeLens";

    /**
     * Latest highlight request params. It is sufficient to only remember the last request per uri, because
     * each new request is expected to replace the previous highlights.
     */
    static final Map<String, HighlightParams> currentHighlights = new ConcurrentHashMap<>();

    /**
     * Current markers... indexed per document uri, needed sp we to be removed upon next update.
     */
    private static Map<String, Annotation[]> currentAnnotations = new ConcurrentHashMap<>();

    private static final IDrawingStrategy BOOT_RANGE_HIGHLIGHT_DRAWING_STRATEGY = new IDrawingStrategy() {

        @Override
        public void draw(Annotation annotation, GC gc, StyledText textWidget, int offset, int length, Color color) {

            if (gc == null) {
                textWidget.redrawRange(offset, length, true);
            } else {
                int oldAlpha = gc.getAlpha();
                Font oldFont = gc.getFont();

                Point left = textWidget.getLocationAtOffset(offset);
                Point right = textWidget.getLocationAtOffset(offset + length);
                gc.setFont(textWidget.getFont());
                int fontHeight = gc.getFontMetrics().getHeight();
                Rectangle r = new Rectangle(left.x, left.y + textWidget.getLineHeight(offset) - fontHeight,
                        right.x - left.x, fontHeight);
                gc.setAlpha(0x40);
                gc.setBackground(color);
                gc.fillRectangle(r);

                gc.setAlpha(oldAlpha);
                gc.setFont(oldFont);
            }
        }

    };

    static class UpdateHighlights extends UIJob {

        private String target;

        UpdateHighlights(String target) {
            super("Update highlights");
            this.target = target;
            setSystem(true);
            schedule();
        }

        @Override
        public IStatus runInUIThread(IProgressMonitor monitor) {
            Utils.getActiveEditors().forEach(this::updateSourceViewer);
            return Status.OK_STATUS;
        }

        protected void updateSourceViewer(IEditorPart editor) {
            ITextViewer viewer = editor.getAdapter(ITextViewer.class);
            if (viewer instanceof ISourceViewer) {
                ISourceViewer sourceViewer = (ISourceViewer) viewer;
                IAnnotationModel annotationModel = sourceViewer.getAnnotationModel();
                if (annotationModel != null) {
                    IDocument doc = sourceViewer.getDocument();
                    if (doc != null && sourceViewer != null) {
                        if (target != null) {
                            HighlightParams highlightParams = currentHighlights.get(target);
                            if (Utils.isProperDocumentIdFor(doc, highlightParams.getDoc())) {
                                updateHighlightAnnotations(editor, sourceViewer, annotationModel, target);
                            }
                        } else {
                            URI uri = Utils.findDocUri(doc);
                            if (uri != null) {
                                updateHighlightAnnotations(editor, sourceViewer, annotationModel, uri.toString());
                            }
                        }
                    }
                }
            }
        }

    };

    private static void updateHighlightAnnotations(IEditorPart editor, ISourceViewer sourceViewer,
            IAnnotationModel annotationModel, String docUri) {
        if (annotationModel instanceof IAnnotationModelExtension) {
            if (isCodeLensHighlightOn()) {
                addBootRangeHighlightSupport(editor, sourceViewer);
            }
            updateAnnotations(docUri, sourceViewer, (IAnnotationModelExtension) annotationModel);
        }
        if (sourceViewer instanceof ISourceViewerExtension5) {
            if (sourceViewer instanceof JavaSourceViewer) {
                // JavaSourceViewer#updateCodeMinings() is overridden and doesn't do anything
                try {
                    Method method = JavaSourceViewer.class.getDeclaredMethod("doUpdateCodeMinings");
                    method.setAccessible(true);
                    method.invoke(sourceViewer);
                } catch (InvocationTargetException | NoSuchMethodException | SecurityException
                        | IllegalAccessException | IllegalArgumentException e) {
                    // don't log the exception
                    ((ISourceViewerExtension5) sourceViewer).updateCodeMinings();
                }
            } else {
                ((ISourceViewerExtension5) sourceViewer).updateCodeMinings();
            }
        }
    }

    private static void addBootRangeHighlightSupport(IEditorPart editor, ISourceViewer sourceViewer) {
        if (editor instanceof AbstractDecoratedTextEditor) {
            addBootRangeHighlightSupport((AbstractDecoratedTextEditor) editor, sourceViewer);
        }
        //      else if () {
        //         // TODO: XML multi page editor handling for highlight support of XML source editor
        //      }
    }

    @SuppressWarnings("unchecked")
    private static void addBootRangeHighlightSupport(AbstractDecoratedTextEditor editor,
            ISourceViewer sourceViewer) {
        try {
            Field f = AbstractDecoratedTextEditor.class.getDeclaredField("fSourceViewerDecorationSupport");
            f.setAccessible(true);
            SourceViewerDecorationSupport support = (SourceViewerDecorationSupport) f.get(editor);
            f = SourceViewerDecorationSupport.class.getDeclaredField("fAnnotationPainter");
            f.setAccessible(true);
            AnnotationPainter painter = (AnnotationPainter) f.get(support);

            f = AnnotationPainter.class.getDeclaredField("fAnnotationType2Color");
            f.setAccessible(true);
            Color highlightColor = LanguageServerCommonsActivator.getInstance().getBootHighlightRangeColor();
            if (highlightColor != null
                    && ((Map<Object, Color>) f.get(painter)).get(ALT_ANNOTATION_TYPE_ID) != highlightColor) {
                painter.setAnnotationTypeColor(ALT_ANNOTATION_TYPE_ID, highlightColor);
                painter.addDrawingStrategy(ALT_ANNOTATION_DRAWING_STRATEGY_ID,
                        BOOT_RANGE_HIGHLIGHT_DRAWING_STRATEGY);
                painter.addAnnotationType(ALT_ANNOTATION_TYPE_ID, ALT_ANNOTATION_DRAWING_STRATEGY_ID);
            }

        } catch (Exception e) {
            LanguageServerCommonsActivator.logError(e,
                    "Failed to contribute alternative range highlight annotation. Switch off highlight CodeLense under STS Language Server preferences!");
        }
    }

    private static boolean isCodeLensHighlightOn() {
        IPreferenceStore store = LanguageServerCommonsActivator.getInstance().getPreferenceStore();
        return store.getBoolean(PreferenceConstants.HIGHLIGHT_CODELENS_PREFS);
    }

    static synchronized void updateAnnotations(String target, ISourceViewer sourceViewer,
            IAnnotationModelExtension annotationModel) {
        Annotation[] toRemove = currentAnnotations.get(target);
        if (toRemove == null) {
            toRemove = new Annotation[0];
        }
        HighlightParams highlightParams = currentHighlights.get(target);
        List<CodeLens> highlights = highlightParams == null ? null : highlightParams.getCodeLenses();
        String annotationType = isCodeLensHighlightOn() ? ALT_ANNOTATION_TYPE_ID : ANNOTION_TYPE_ID;
        Map<Annotation, Position> newAnnotations = createAnnotations(sourceViewer.getDocument(), highlights,
                annotationType);
        annotationModel.replaceAnnotations(toRemove, newAnnotations);
        currentAnnotations.put(target, newAnnotations.keySet().toArray(new Annotation[newAnnotations.size()]));
    }

    private static Map<Annotation, Position> createAnnotations(IDocument doc, List<CodeLens> highlights,
            String annotationType) {
        ImmutableMap.Builder<Annotation, Position> annotations = ImmutableMap.builder();
        if (highlights == null) {
            highlights = ImmutableList.of();
        }
        highlights.stream().map(CodeLens::getRange).distinct().forEach(rng -> {
            try {
                int start = LSPEclipseUtils.toOffset(rng.getStart(), doc);
                int end = LSPEclipseUtils.toOffset(rng.getEnd(), doc);
                Position e_rng = new Position(start, Math.max(0, end - start));
                annotations.put(new Annotation(annotationType, false, null), e_rng);
            } catch (BadLocationException e) {
                //ignore invalid highlights
            }
        });
        return annotations.build();
    }

    @Override
    public synchronized void highlight(HighlightParams highlights) {
        String target = highlights.getDoc().getUri();
        if (target != null) {
            currentHighlights.put(target, highlights);
            new UpdateHighlights(target);
        }
    }

    @Override
    public void progress(ProgressParams progressEvent) {
        String status = progressEvent.getStatusMsg() != null ? progressEvent.getStatusMsg() : "";
        showStatusMessage(status);
    }

    private void showStatusMessage(final String status) {
        PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
            @Override
            public void run() {
                IStatusLineManager statusLineManager = getStatusLineManager();
                if (statusLineManager != null) {
                    statusLineManager.setMessage(status);
                }
            }
        });
    }

    private IStatusLineManager getStatusLineManager() {
        try {
            return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor()
                    .getEditorSite().getActionBars().getStatusLineManager();
        } catch (NullPointerException e) {
            return null;
        }
    }

    @Override
    public CompletableFuture<Object> addClasspathListener(ClasspathListenerParams params) {
        return CompletableFuture
                .completedFuture(classpathService.addClasspathListener(params.getCallbackCommandId()));
    }

    @Override
    public CompletableFuture<Object> removeClasspathListener(ClasspathListenerParams params) {
        return CompletableFuture
                .completedFuture(classpathService.removeClasspathListener(params.getCallbackCommandId()));
    }

    @Override
    public CompletableFuture<MarkupContent> javadoc(JavaDataParams params) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                MarkupContent mc = new MarkupContent();
                mc.setKind(MarkupKind.MARKDOWN);
                mc.setValue(JavadocUtils.javadoc(JavaDoc2MarkdownConverter::getMarkdownContentReader,
                        URI.create(params.getProjectUri()), params.getBindingKey(),
                        JavaDataParams.isLookInOtherProjects(params)));
                return mc;
            } catch (Exception e) {
                LanguageServerCommonsActivator.logError(e, "Failed getting javadoc for " + params.toString());
            }
            return null;
        });
    }

    @Override
    public CompletableFuture<Object> moveCursor(CursorMovement cursorMovement) {
        Utils.getActiveEditors().forEach(_editor -> {
            try {
                if (_editor instanceof AbstractTextEditor) {
                    AbstractTextEditor editor = (AbstractTextEditor) _editor;
                    IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());
                    if (doc != null) {
                        URI uri = Utils.findDocUri(doc);
                        if (cursorMovement.getUri().equals(uri.toString())) {
                            org.eclipse.lsp4j.Position pos = cursorMovement.getPosition();
                            int offset = LSPEclipseUtils.toOffset(pos, doc);
                            Display.getDefault().asyncExec(() -> {
                                editor.getSelectionProvider().setSelection(new TextSelection(offset, 0));
                            });
                        }
                    }
                }
            } catch (Exception e) {
                LanguageServerCommonsActivator.logError(e, "sts/moveCursor failed");
            }
        });
        return CompletableFuture.completedFuture("ok");
    }

    @Override
    public CompletableFuture<TypeData> javaType(JavaDataParams params) {
        return CompletableFuture.supplyAsync(() -> javaData.typeData(params.getProjectUri(), params.getBindingKey(),
                JavaDataParams.isLookInOtherProjects(params)));
    }

    @Override
    public CompletableFuture<String> javadocHoverLink(JavaDataParams params) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                URI projectUri = params.getProjectUri() == null ? null : URI.create(params.getProjectUri());
                IJavaElement element = JavaData.findElement(projectUri, params.getBindingKey(),
                        JavaDataParams.isLookInOtherProjects(params));
                if (element != null) {
                    return JavaElementLinks.createURI(JavaElementLinks.OPEN_LINK_SCHEME, element);
                }
            } catch (Exception e) {
                LanguageServerCommonsActivator.logError(e,
                        "Failed to find java element for key " + params.getBindingKey());
            }
            return null;
        });
    }

    @Override
    public CompletableFuture<Location> javaLocation(JavaDataParams params) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                URI projectUri = params.getProjectUri() == null ? null : URI.create(params.getProjectUri());
                IJavaElement element = JavaData.findElement(projectUri, params.getBindingKey(),
                        JavaDataParams.isLookInOtherProjects(params));
                if (element != null) {
                    IJavaProject project = element.getJavaProject() == null
                            ? ResourceUtils.getJavaProject(projectUri)
                            : element.getJavaProject();
                    if (project != null) {
                        return new Location(
                                Utils.eclipseIntroUri(project.getElementName(), params.getBindingKey()).toString(),
                                null);
                    }
                }
            } catch (Exception e) {
                LanguageServerCommonsActivator.logError(e,
                        "Failed to find java element for key " + params.getBindingKey());
            }
            return null;
        });
    }

    @Override
    public CompletableFuture<List<TypeDescriptorData>> javaSearchTypes(JavaSearchParams params) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return javaFluxSearch.fuzzySearchTypes(params);
            } catch (Exception e) {
                LanguageServerCommonsActivator.logError(e, "Failed to search type with term '" + params.getTerm()
                        + "' in project " + params.getProjectUri());
                return Collections.emptyList();
            }
        });
    }

    @Override
    public CompletableFuture<List<String>> javaSearchPackages(JavaSearchParams params) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return javaFluxSearch.fuzzySearchPackages(params);
            } catch (Exception e) {
                LanguageServerCommonsActivator.logError(e, "Failed to search package with term '" + params.getTerm()
                        + "' in project " + params.getProjectUri());
                return Collections.emptyList();
            }
        });
    }

    @Override
    public CompletableFuture<List<TypeDescriptorData>> javaSubTypes(JavaTypeHierarchyParams params) {
        return CompletableFuture.supplyAsync(() -> typeHierarchy.subTypes(params).collect(Collectors.toList()));
    }

    @Override
    public CompletableFuture<List<TypeDescriptorData>> javaSuperTypes(JavaTypeHierarchyParams params) {
        return CompletableFuture.supplyAsync(() -> typeHierarchy.superTypes(params).collect(Collectors.toList()));
    }

}