org.eclipse.lsp4e.operations.hover.LSBasedHover.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.lsp4e.operations.hover.LSBasedHover.java

Source

/*******************************************************************************
 * 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 508458 - Add support for codelens
 *  Angelo Zerr <angelo.zerr@gmail.com> - Bug 525602 - LSBasedHover must check if LS have codelens capability
 *  Lucas Bullen (Red Hat Inc.) - [Bug 517428] Requests sent before initialization
 *******************************************************************************/
package org.eclipse.lsp4e.operations.hover;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.internal.text.html.BrowserInformationControl;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultInformationControl;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextHover;
import org.eclipse.jface.text.ITextHoverExtension;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4e.LanguageServiceAccessor.LSPDocumentInfo;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.MarkedString;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.mylyn.wikitext.markdown.MarkdownLanguage;
import org.eclipse.mylyn.wikitext.parser.MarkupParser;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.EditorsUI;

/**
 * LSP implementation of {@link org.eclipse.jface.text.ITextHover}
 *
 */
public class LSBasedHover implements ITextHover, ITextHoverExtension {

    private static final MarkupParser MARKDOWN_PARSER = new MarkupParser(new MarkdownLanguage());

    private static final LocationListener HYPER_LINK_LISTENER = new LocationListener() {

        @Override
        public void changing(LocationEvent event) {
            if (!"about:blank".equals(event.location)) { //$NON-NLS-1$
                IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
                LSPEclipseUtils.open(event.location, page, null);
                event.doit = false;
            }
        }

        @Override
        public void changed(LocationEvent event) {
        }
    };

    private static class FocusableBrowserInformationControl extends BrowserInformationControl {

        public FocusableBrowserInformationControl(Shell parent) {
            super(parent, JFaceResources.DEFAULT_FONT, EditorsUI.getTooltipAffordanceString());
        }

        @Override
        protected void createContent(Composite parent) {
            super.createContent(parent);
            Browser b = (Browser) (parent.getChildren()[0]);
            b.addProgressListener(ProgressListener.completedAdapter(event -> {
                if (getInput() == null)
                    return;
                Browser browser = (Browser) event.getSource();
                @Nullable
                Point constraints = getSizeConstraints();
                Point hint = computeSizeHint();

                setSize(hint.x, hint.y);
                browser.execute("document.getElementsByTagName(\"html\")[0].style.whiteSpace = \"nowrap\""); //$NON-NLS-1$
                Double width = 20 + (Double) browser.evaluate("return document.body.scrollWidth;"); //$NON-NLS-1$
                if (constraints != null && constraints.x < width) {
                    width = (double) constraints.x;
                }

                setSize(width.intValue(), 0);
                browser.execute("document.getElementsByTagName(\"html\")[0].style.whiteSpace = \"normal\""); //$NON-NLS-1$
                Double height = (Double) browser.evaluate("return document.body.scrollHeight;"); //$NON-NLS-1$
                if (constraints != null && constraints.y < height) {
                    height = (double) constraints.y;
                }

                setSize(width.intValue(), height.intValue());
            }));
            b.setJavascriptEnabled(true);
        }

        @Override
        public IInformationControlCreator getInformationPresenterControlCreator() {
            return new IInformationControlCreator() {
                @Override
                public IInformationControl createInformationControl(Shell parent) {
                    BrowserInformationControl res = new BrowserInformationControl(parent,
                            JFaceResources.DEFAULT_FONT, true);
                    res.addLocationListener(HYPER_LINK_LISTENER);
                    return res;
                }
            };
        }
    }

    private List<CompletableFuture<?>> requests;
    private List<Hover> hoverResults;
    private IRegion lastRegion;
    private ITextViewer textViewer;

    public LSBasedHover() {
    }

    @Override
    public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) {
        if (textViewer == null || hoverRegion == null) {
            return null;
        }
        if (!(hoverRegion.equals(this.lastRegion) && textViewer.equals(this.textViewer) && this.requests != null)) {
            initiateHoverRequest(textViewer, hoverRegion.getOffset());
        }
        try {
            CompletableFuture.allOf(this.requests.toArray(new CompletableFuture[this.requests.size()])).get(500,
                    TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            LanguageServerPlugin.logError(e);
        }
        String result = ""; //$NON-NLS-1$
        if (!(this.hoverResults == null || this.hoverResults.isEmpty())) {
            result += hoverResults.stream().filter(Objects::nonNull).map(LSBasedHover::getHoverString)
                    .filter(Objects::nonNull).collect(Collectors.joining("\n\n")); //$NON-NLS-1$

        }
        if (result.isEmpty()) {
            return null;
        }
        result = MARKDOWN_PARSER.parseToHtml(result);
        // put CSS styling to match Eclipse style
        ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
        Color foreground = colorRegistry.get("org.eclipse.ui.workbench.HOVER_FOREGROUND"); //$NON-NLS-1$
        Color background = colorRegistry.get("org.eclipse.ui.workbench.HOVER_BACKGROUND"); //$NON-NLS-1$
        String style = "<style TYPE='text/css'>html { " + //$NON-NLS-1$
                "font-family: " + JFaceResources.getDefaultFontDescriptor().getFontData()[0].getName() + "; " + //$NON-NLS-1$ //$NON-NLS-2$
                "font-size: " //$NON-NLS-1$
                + Integer.toString(JFaceResources.getDefaultFontDescriptor().getFontData()[0].getHeight()) + "pt; " //$NON-NLS-1$
                + (background != null ? "background-color: " + toHTMLrgb(background.getRGB()) + "; " : "") + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                (foreground != null ? "color: " + toHTMLrgb(foreground.getRGB()) + "; " : "") + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                " }</style>"; //$NON-NLS-1$

        int headIndex = result.indexOf("<head>"); //$NON-NLS-1$
        StringBuilder builder = new StringBuilder(result.length() + style.length());
        builder.append(result.substring(0, headIndex + "<head>".length())); //$NON-NLS-1$
        builder.append(style);
        builder.append(result.substring(headIndex + "<head>".length())); //$NON-NLS-1$
        return builder.toString();
    }

    protected static @Nullable String getHoverString(@NonNull Hover hover) {
        Either<List<Either<String, MarkedString>>, MarkupContent> hoverContent = hover.getContents();
        if (hoverContent.isLeft()) {
            List<Either<String, MarkedString>> contents = hoverContent.getLeft();
            if (contents == null || contents.isEmpty()) {
                return null;
            }
            return contents.stream().map(content -> {
                if (content.isLeft()) {
                    return content.getLeft();
                } else if (content.isRight()) {
                    MarkedString markedString = content.getRight();
                    // TODO this won't work fully until markup parser will support syntax
                    // highlighting but will help display
                    // strings with language tags, e.g. without it things after <?php tag aren't
                    // displayed
                    if (markedString.getLanguage() != null && !markedString.getLanguage().isEmpty()) {
                        return String.format("```%s\n%s\n```", markedString.getLanguage(), markedString.getValue()); //$NON-NLS-1$
                    } else {
                        return markedString.getValue();
                    }
                } else {
                    return ""; //$NON-NLS-1$
                }
            }).filter(((Predicate<String>) String::isEmpty).negate()).collect(Collectors.joining("\n\n")); //$NON-NLS-1$ )
        } else {
            return hoverContent.getRight().getValue();
        }
    }

    private static @NonNull String toHTMLrgb(RGB rgb) {
        StringBuilder builder = new StringBuilder(7);
        builder.append('#');
        appendAsHexString(builder, rgb.red);
        appendAsHexString(builder, rgb.green);
        appendAsHexString(builder, rgb.blue);
        return builder.toString();
    }

    private static void appendAsHexString(StringBuilder buffer, int intValue) {
        String hexValue = Integer.toHexString(intValue);
        if (hexValue.length() == 1) {
            buffer.append('0');
        }
        buffer.append(hexValue);
    }

    @Override
    public IRegion getHoverRegion(ITextViewer textViewer, int offset) {
        if (textViewer == null) {
            return null;
        }
        IRegion res = null;
        initiateHoverRequest(textViewer, offset);
        try {
            CompletableFuture.allOf(this.requests.toArray(new CompletableFuture[this.requests.size()])).get(800,
                    TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e1) {
            LanguageServerPlugin.logError(e1);
        }
        final IDocument document = textViewer.getDocument();
        if (!this.hoverResults.isEmpty()) {
            res = new Region(0, document.getLength());
            for (Hover hover : this.hoverResults) {
                int rangeOffset = offset;
                int rangeLength = 0;
                if (hover != null && hover.getRange() != null) {
                    try {
                        Range range = hover.getRange();
                        rangeOffset = LSPEclipseUtils.toOffset(range.getStart(), document);
                        rangeLength = LSPEclipseUtils.toOffset(range.getEnd(), document) - rangeOffset;
                    } catch (BadLocationException e) {
                        LanguageServerPlugin.logError(e);
                        res = new Region(offset, 1);
                    }
                }
                res = new Region(Math.max(res.getOffset(), rangeOffset), Math.min(res.getLength(), rangeLength));
            }
        } else {
            res = new Region(offset, 1);
        }

        this.lastRegion = res;
        this.textViewer = textViewer;
        return res;
    }

    /**
     * Initialize hover requests with hover (if available) and codelens (if
     * available).
     *
     * @param viewer
     *            the text viewer.
     * @param offset
     *            the hovered offset.
     */
    private void initiateHoverRequest(@NonNull ITextViewer viewer, int offset) {
        this.textViewer = viewer;
        // use intermediary variables to make the lists specific to the request
        // if we directly add/remove from members, we may have thread related issues
        // such as some
        // results from a previous request leaking in the new hover.
        final List<CompletableFuture<?>> requests = new ArrayList<>();
        IDocument document = viewer.getDocument();
        this.hoverResults = getHoverResults(document, offset, requests);
        this.requests = requests;
    }

    /**
     * Returns the list of hover.
     *
     * @param document
     *            the document of text viewer.
     * @param offset
     *            the hovered offset.
     * @param requests
     * @return the list of hover.
     */
    private List<Hover> getHoverResults(@NonNull IDocument document, int offset,
            List<CompletableFuture<?>> requests) {
        List<@NonNull LSPDocumentInfo> docInfos = LanguageServiceAccessor.getLSPDocumentInfosFor(document,
                (capabilities) -> Boolean.TRUE.equals(capabilities.getHoverProvider()));
        final List<Hover> hoverResults = Collections.synchronizedList(new ArrayList<>(docInfos.size()));
        for (@NonNull
        final LSPDocumentInfo info : docInfos) {
            info.getInitializedLanguageClient().thenAccept(languageServer -> {
                try {
                    CompletableFuture<Hover> hover = languageServer.getTextDocumentService().hover(LSPEclipseUtils
                            .toTextDocumentPosistionParams(info.getFileUri(), offset, info.getDocument()));
                    requests.add(hover.thenAccept(hoverResults::add));
                } catch (BadLocationException e) {
                    LanguageServerPlugin.logError(e);
                }
            });
        }
        return hoverResults;
    }

    @Override
    public IInformationControlCreator getHoverControlCreator() {
        return new AbstractReusableInformationControlCreator() {
            @Override
            protected IInformationControl doCreateInformationControl(Shell parent) {
                if (BrowserInformationControl.isAvailable(parent)) {
                    return new FocusableBrowserInformationControl(parent);
                } else {
                    return new DefaultInformationControl(parent);
                }
            }
        };
    }
}