diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LSPVirtualFileWrapper.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LSPVirtualFileWrapper.java index 4090dd9c5..64ca86cb0 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LSPVirtualFileWrapper.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LSPVirtualFileWrapper.java @@ -15,12 +15,15 @@ package com.redhat.devtools.intellij.lsp4ij; import com.intellij.openapi.Disposable; +import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; import com.redhat.devtools.intellij.lsp4ij.operations.diagnostics.LSPDiagnosticsForServer; import com.redhat.devtools.intellij.lsp4ij.operations.documentLink.LSPDocumentLinkForServer; +import com.redhat.devtools.intellij.lsp4ij.operations.hover.LSPTextHoverForFile; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DocumentLink; @@ -45,10 +48,12 @@ public class LSPVirtualFileWrapper implements Disposable { private final VirtualFile file; private final Map dataPerServer; + private final LSPTextHoverForFile hover; LSPVirtualFileWrapper(VirtualFile file) { this.file = file; this.dataPerServer = new HashMap<>(); + this.hover = new LSPTextHoverForFile(); Module project = LSPIJUtils.getProject(file); if (project != null) { Disposer.register(project, this); @@ -103,6 +108,11 @@ public Collection getAllDocumentLink() { return getData(LSPVirtualFileData::getDocumentLinkForServer); } + + public String getHoverContent(PsiElement element, int targetOffset, Editor editor) { + return hover.getHoverContent(element, targetOffset, editor); + } + // ------------------------ Other methods private LSPVirtualFileData getLSPVirtualFileData(LanguageServerWrapper languageServerWrapper) { @@ -176,4 +186,5 @@ public static void dispose(VirtualFile file) { wrapper.dispose(); } } + } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java index ada173cc0..a24b3b118 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java @@ -15,11 +15,14 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.lsp4ij.LSPVirtualFileWrapper; import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import com.vladsch.flexmark.html.HtmlRenderer; @@ -44,62 +47,11 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +/** + * LSP testDocument/hover support. + * + */ public class LSPTextHover extends DocumentationProviderEx implements ExternalDocumentationHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(LSPTextHover.class); - - private static final String HEAD = ""; //$NON-NLS-1$ - - private static final Parser PARSER = Parser.builder().build(); - private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build(); - - private PsiElement lastElement; - private int lastOffset = -1; - private CompletableFuture> lspRequest; - private CancellationSupport cancellationSupport; - - public LSPTextHover() { - LOGGER.info("LSPTextHover"); - } - - public static String styleHtml(Editor editor, String html) { - if (html == null || html.isEmpty()) { - return html; - } - Color background = editor.getColorsScheme().getDefaultBackground(); - Color foreground = editor.getColorsScheme().getDefaultForeground(); - // put CSS styling to match Eclipse style - 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();*/ - StringBuilder builder = new StringBuilder(style); - builder.append(html).append(""); - return builder.toString(); - } - - private static String toHTMLrgb(Color rgb) { - StringBuilder builder = new StringBuilder(7); - builder.append('#'); - appendAsHexString(builder, rgb.getRed()); - appendAsHexString(builder, rgb.getGreen()); - appendAsHexString(builder, rgb.getBlue()); - 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); - } @Nullable @Override @@ -110,6 +62,10 @@ public String getQuickNavigateInfo(PsiElement element, PsiElement originalElemen @Nullable @Override public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { + Project project = element.getProject(); + if (project.isDisposed()) { + return null; + } if (originalElement == null || !Objects.equals(element.getContainingFile(), originalElement.getContainingFile())) { return null; } @@ -117,102 +73,18 @@ public String generateDoc(PsiElement element, @Nullable PsiElement originalEleme if (editor == null) { return null; } + + VirtualFile file = originalElement.getContainingFile().getVirtualFile(); int targetOffset = getTargetOffset(originalElement); - initiateHoverRequest(element, targetOffset); - try { - String result = lspRequest.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(editor, RENDERER.render(PARSER.parse(result))); - } - } catch (ExecutionException | TimeoutException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } catch (InterruptedException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); - } finally { - this.cancellationSupport = null; - } - return null; + return LSPVirtualFileWrapper.getLSPVirtualFileWrapper(file).getHoverContent(element, targetOffset, editor); } - private int getTargetOffset(PsiElement originalElement) { + private static int getTargetOffset(PsiElement originalElement) { int startOffset = originalElement.getTextOffset(); int textLength = originalElement.getTextLength(); return startOffset + textLength / 2; } - private static @Nullable String getHoverString(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(); - } - } - - - /** - * Initialize hover requests with hover (if available) and codelens (if - * available). - * - * @param element the PSI element. - * @param offset the target offset. - */ - private void initiateHoverRequest(PsiElement element, int offset) { - if (this.cancellationSupport != null) { - this.cancellationSupport.cancel(); - this.cancellationSupport = null; - } - PsiDocumentManager manager = PsiDocumentManager.getInstance(element.getProject()); - final Document document = manager.getDocument(element.getContainingFile()); - if (offset != -1 && (this.lspRequest == null || !element.equals(this.lastElement) || offset != this.lastOffset)) { - this.lastElement = element; - this.lastOffset = offset; - this.cancellationSupport = new CancellationSupport(); - this.lspRequest = LanguageServiceAccessor.getInstance(element.getProject()) - .getLanguageServers(document, capabilities -> isHoverCapable(capabilities)) - .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 -> - cancellationSupport.execute( - languageServer.getServer().getTextDocumentService() - .hover(LSPIJUtils.toHoverParams(offset, document))) - .join() - ).filter(Objects::nonNull).collect(Collectors.toList())); - } - } - - private boolean isHoverCapable(ServerCapabilities capabilities) { - return (capabilities.getHoverProvider().isLeft() && capabilities.getHoverProvider().getLeft()) || capabilities.getHoverProvider().isRight(); - } - @Nullable @Override public PsiElement getDocumentationElementForLookupItem(PsiManager psiManager, Object object, PsiElement element) { diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHoverForFile.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHoverForFile.java new file mode 100644 index 000000000..a5e4f9e8c --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHoverForFile.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.hover; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.MarkedString; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +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; + +/** + * LSP textDocument/hover support for a given file. + */ +public class LSPTextHoverForFile { + + private static final Logger LOGGER = LoggerFactory.getLogger(LSPTextHoverForFile.class); + private static final Parser PARSER = Parser.builder().build(); + private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build(); + + private PsiElement lastElement; + private int lastOffset = -1; + private CompletableFuture> lspRequest; + private CancellationSupport previousCancellationSupport; + + public String getHoverContent(PsiElement element, int targetOffset, Editor editor) { + initiateHoverRequest(element, targetOffset); + try { + String result = lspRequest.get(500, TimeUnit.MILLISECONDS).stream() + .filter(Objects::nonNull) + .map(LSPTextHoverForFile::getHoverString) + .filter(Objects::nonNull) + .collect(Collectors.joining("\n\n")) //$NON-NLS-1$ + .trim(); + if (!result.isEmpty()) { + return styleHtml(editor, RENDERER.render(PARSER.parse(result))); + } + // The LSP hover request are finished, don't need to cancel the previous LSP requests. + previousCancellationSupport = null; + } catch (ExecutionException | TimeoutException e) { + LOGGER.warn(e.getLocalizedMessage(), e); + } catch (InterruptedException e) { + LOGGER.warn(e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } + return null; + } + + + /** + * Initialize hover requests with hover (if available). + * + * @param element the PSI element. + * @param offset the target offset. + */ + + private void initiateHoverRequest(PsiElement element, int offset) { + if (this.previousCancellationSupport != null) { + // The prvious LSP hover request is not finished,cancel it + this.previousCancellationSupport.cancel(); + } + PsiDocumentManager manager = PsiDocumentManager.getInstance(element.getProject()); + final Document document = manager.getDocument(element.getContainingFile()); + if (offset != -1 && (this.lspRequest == null || !element.equals(this.lastElement) || offset != this.lastOffset)) { + this.lastElement = element; + this.lastOffset = offset; + CancellationSupport cancellationSupport = new CancellationSupport(); + this.lspRequest = LanguageServiceAccessor.getInstance(element.getProject()) + .getLanguageServers(document, capabilities -> isHoverCapable(capabilities)) + .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 -> + cancellationSupport.execute( + languageServer.getServer().getTextDocumentService() + .hover(LSPIJUtils.toHoverParams(offset, document))) + .join() + ).filter(Objects::nonNull).collect(Collectors.toList())); + // store the current cancel support as previous + previousCancellationSupport = cancellationSupport; + } + } + + private static @Nullable String getHoverString(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 boolean isHoverCapable(ServerCapabilities capabilities) { + return (capabilities.getHoverProvider().isLeft() && capabilities.getHoverProvider().getLeft()) || capabilities.getHoverProvider().isRight(); + } + + + public static String styleHtml(Editor editor, String html) { + if (html == null || html.isEmpty()) { + return html; + } + Color background = editor.getColorsScheme().getDefaultBackground(); + Color foreground = editor.getColorsScheme().getDefaultForeground(); + // put CSS styling to match Eclipse style + 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();*/ + StringBuilder builder = new StringBuilder(style); + builder.append(html).append(""); + return builder.toString(); + } + + private static String toHTMLrgb(Color rgb) { + StringBuilder builder = new StringBuilder(7); + builder.append('#'); + appendAsHexString(builder, rgb.getRed()); + appendAsHexString(builder, rgb.getGreen()); + appendAsHexString(builder, rgb.getBlue()); + 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); + } + +}