From e02c3b1a2e78ea4ffa1df6e15aa526b047499cbf Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Wed, 7 Aug 2019 12:37:24 -0700 Subject: [PATCH] Copy code for LSPTextHover from lsp4e This is to avoid compilation issues after class renamed in lsp4e. If lsp4e undoes the rename as requested in this bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=549859 Then this commit can/should be reverted. --- eclipse-language-servers/local-build.sh | 2 +- .../META-INF/MANIFEST.MF | 5 +- .../FocusableBrowserInformationControl.java | 102 +++++++ .../tooling/boot/ls/jdt/LSPTextHover.java | 262 ++++++++++++++++++ .../ls/jdt/SpringBootJavaHoverProvider.java | 6 +- 5 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/FocusableBrowserInformationControl.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/LSPTextHover.java diff --git a/eclipse-language-servers/local-build.sh b/eclipse-language-servers/local-build.sh index 075d55731..055ace96d 100755 --- a/eclipse-language-servers/local-build.sh +++ b/eclipse-language-servers/local-build.sh @@ -5,4 +5,4 @@ cd ../headless-services ./mvnw clean install -Dmaven.test.skip=true cd $workdir -./mvnw -Pe411 clean install -Dmaven.test.skip=true +./mvnw -Pe412 clean install -Dmaven.test.skip=true diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/META-INF/MANIFEST.MF b/eclipse-language-servers/org.springframework.tooling.boot.ls/META-INF/MANIFEST.MF index c7327b875..935925238 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/META-INF/MANIFEST.MF +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/META-INF/MANIFEST.MF @@ -26,7 +26,10 @@ Require-Bundle: org.eclipse.jdt.launching;bundle-version="3.9.0", org.springsource.ide.eclipse.commons.livexp, org.apache.commons.lang3, org.eclipse.wst.sse.ui;bundle-version="1.5.0";resolution:=optional, - org.json;bundle-version="1.0.0" + org.json;bundle-version="1.0.0", + org.eclipse.jdt.annotation, + org.eclipse.mylyn.wikitext.markdown, + org.eclipse.mylyn.wikitext Import-Package: com.google.common.base, com.google.common.collect, com.google.gson;version="2.7.0", diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/FocusableBrowserInformationControl.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/FocusableBrowserInformationControl.java new file mode 100644 index 000000000..5db17b3cc --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/FocusableBrowserInformationControl.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * 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 + * Kris De Volder - copied from lsp4e see: https://www.pivotaltracker.com/story/show/167763955 + *******************************************************************************/ +package org.springframework.tooling.boot.ls.jdt; + +import org.eclipse.core.runtime.Platform; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.internal.text.html.BrowserInformationControl; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.text.IInformationControlCreator; +import org.eclipse.lsp4e.LSPEclipseUtils; +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.FontData; +import org.eclipse.swt.graphics.Point; +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; +import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; + +class FocusableBrowserInformationControl extends BrowserInformationControl { + + 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) { + // comment requested by sonar + } + }; + + 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(), hint.y); + 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; + } + if (Platform.getPreferencesService().getBoolean(EditorsUI.PLUGIN_ID, + AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_TEXT_HOVER_AFFORDANCE, true, + null)) { + FontData[] fontDatas = JFaceResources.getDialogFont().getFontData(); + height = fontDatas[0].getHeight() + height; + } + setSize(width.intValue(), height.intValue()); + })); + b.setJavascriptEnabled(true); + } + + @Override + public IInformationControlCreator getInformationPresenterControlCreator() { + return parent -> { + BrowserInformationControl res = new BrowserInformationControl(parent, JFaceResources.DEFAULT_FONT, + true); + res.addLocationListener(HYPER_LINK_LISTENER); + return res; + }; + } +} \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/LSPTextHover.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/LSPTextHover.java new file mode 100644 index 000000000..0b599d479 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/LSPTextHover.java @@ -0,0 +1,262 @@ +/******************************************************************************* + * 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 - Bug 525602 - LSBasedHover must check if LS have codelens capability + * Lucas Bullen (Red Hat Inc.) - [Bug 517428] Requests sent before initialization + * Kris De Volder - copied from lsp4e see: https://www.pivotaltracker.com/story/show/167763955 + *******************************************************************************/ +package org.springframework.tooling.boot.ls.jdt; + +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.lsp4j.Hover; +import org.eclipse.lsp4j.MarkedString; +import org.eclipse.lsp4j.MarkupContent; +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.graphics.Color; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Shell; + +/** + * LSP implementation of {@link org.eclipse.jface.text.ITextHover} + * + */ +public class LSPTextHover implements ITextHover, ITextHoverExtension { + + private static final String HEAD = ""; //$NON-NLS-1$ + + + private static final MarkupParser MARKDOWN_PARSER = new MarkupParser(new MarkdownLanguage()); + + + private IRegion lastRegion; + private ITextViewer lastViewer; + private CompletableFuture> request; + + public LSPTextHover() { + // nothing to init yet, comment requested by sonar + } + + public static String styleHtml(String html) { + if (html == null || html.isEmpty()) { + return html; + } + // 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 = ""; //$NON-NLS-1$ + + int headIndex = html.indexOf(HEAD); + StringBuilder builder = new StringBuilder(html.length() + style.length()); + builder.append(html.substring(0, headIndex + HEAD.length())); + builder.append(style); + builder.append(html.substring(headIndex + HEAD.length())); + return builder.toString(); + } + + @Override + public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) { + if (textViewer == null || hoverRegion == null) { + return null; + } + if (this.request == null || !textViewer.equals(this.lastViewer) || !hoverRegion.equals(this.lastRegion)) { + initiateHoverRequest(textViewer, hoverRegion.getOffset()); + } + try { + String result = request.get(500, TimeUnit.MILLISECONDS).stream() + .filter(Objects::nonNull) + .map(LSPTextHover::getHoverString) + .filter(Objects::nonNull) + .collect(Collectors.joining("\n\n")) //$NON-NLS-1$ + .trim(); + if (!result.isEmpty()) { + return styleHtml(MARKDOWN_PARSER.parseToHtml(result)); + } + } catch (ExecutionException | TimeoutException e) { + LanguageServerPlugin.logError(e); + } catch (InterruptedException e) { + LanguageServerPlugin.logError(e); + Thread.currentThread().interrupt(); + } + return null; + } + + protected static @Nullable String getHoverString(@NonNull Hover hover) { + Either>, MarkupContent> hoverContent = hover.getContents(); + if (hoverContent.isLeft()) { + List> 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 ) 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; + } + if (this.request == null || this.lastRegion == null || !textViewer.equals(this.lastViewer) + || offset < this.lastRegion.getOffset() || offset > lastRegion.getOffset() + lastRegion.getLength()) { + initiateHoverRequest(textViewer, offset); + } + try { + final IDocument document = textViewer.getDocument(); + boolean[] oneHoverAtLeast = new boolean[] { false }; + int[] regionStartOffset = new int[] { 0 }; + int[] regionEndOffset = new int[] { document.getLength() }; + this.request.get(500, TimeUnit.MILLISECONDS).stream() + .filter(Objects::nonNull) + .map(Hover::getRange) + .filter(Objects::nonNull) + .forEach(range -> { + try { + regionStartOffset[0] = Math.max(regionStartOffset[0], + LSPEclipseUtils.toOffset(range.getStart(), document)); + regionEndOffset[0] = Math.min(regionEndOffset[0], + LSPEclipseUtils.toOffset(range.getEnd(), document)); + oneHoverAtLeast[0] = true; + } catch (BadLocationException e) { + LanguageServerPlugin.logError(e); + } + }); + if (oneHoverAtLeast[0]) { + this.lastRegion = new Region(regionStartOffset[0], regionEndOffset[0] - regionStartOffset[0]); + return this.lastRegion; + } + } catch (ExecutionException | TimeoutException e1) { + LanguageServerPlugin.logError(e1); + } catch (InterruptedException e1) { + LanguageServerPlugin.logError(e1); + Thread.currentThread().interrupt(); + } + this.lastRegion = new Region(offset, 0); + return this.lastRegion; + } + + /** + * 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) { + final IDocument document = viewer.getDocument(); + this.lastViewer = viewer; + this.request = LanguageServiceAccessor + .getLanguageServers(document, capabilities -> Boolean.TRUE.equals(capabilities.getHoverProvider())) + .thenApplyAsync(languageServers -> // Async is very important here, otherwise the LS Client thread is in + // deadlock and doesn't read bytes from LS + languageServers.stream() + .map(languageServer -> { + try { + return languageServer.getTextDocumentService() + .hover(LSPEclipseUtils.toTextDocumentPosistionParams(offset, document)).get(); + } catch (ExecutionException | BadLocationException e) { + LanguageServerPlugin.logError(e); + return null; + } catch (InterruptedException e) { + LanguageServerPlugin.logError(e); + Thread.currentThread().interrupt(); + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList())); + } + + @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); + } + } + }; + } +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/SpringBootJavaHoverProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/SpringBootJavaHoverProvider.java index b80adec90..f284c6e74 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/SpringBootJavaHoverProvider.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/jdt/SpringBootJavaHoverProvider.java @@ -20,7 +20,7 @@ import org.eclipse.jdt.internal.ui.text.java.hover.JavadocBrowserInformationCont import org.eclipse.jdt.internal.ui.text.java.hover.JavadocHover; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; -import org.eclipse.lsp4e.operations.hover.LSBasedHover; +import org.eclipse.lsp4e.operations.hover.LSPTextHover; /** * @author Martin Lippert @@ -28,11 +28,11 @@ import org.eclipse.lsp4e.operations.hover.LSBasedHover; @SuppressWarnings("restriction") public class SpringBootJavaHoverProvider extends JavadocHover { - private LSBasedHover lsBasedHover; + private LSPTextHover lsBasedHover; public SpringBootJavaHoverProvider() { super(); - lsBasedHover = new LSBasedHover(); + lsBasedHover = new LSPTextHover(); } @Override