Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Show LSP completion item documentation in popup #1115

Merged
merged 1 commit into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
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 com.redhat.devtools.intellij.lsp4ij.operations.documentation.LSPTextHoverForFile;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DocumentLink;
import org.eclipse.lsp4j.MarkupContent;

import java.util.List;
import java.util.Collection;
Expand Down Expand Up @@ -109,7 +110,7 @@ public Collection<LSPDocumentLinkForServer> getAllDocumentLink() {
}


public String getHoverContent(PsiElement element, int targetOffset, Editor editor) {
public List<MarkupContent> getHoverContent(PsiElement element, int targetOffset, Editor editor) {
return hover.getHoverContent(element, targetOffset, editor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class SupportedFeatures {
.setDocumentationFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT));
completionItemCapabilities.setInsertTextModeSupport(new CompletionItemInsertTextModeSupportCapabilities(List.of(InsertTextMode.AsIs, InsertTextMode.AdjustIndentation)));

// completionItemCapabilities.setResolveSupport(new CompletionItemResolveSupportCapabilities(List.of("documentation", "detail", "additionalTextEdits"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
completionItemCapabilities.setResolveSupport(new CompletionItemResolveSupportCapabilities(List.of("documentation" /*, "detail", "additionalTextEdits" */)));
CompletionCapabilities completionCapabilities = new CompletionCapabilities(completionItemCapabilities);
completionCapabilities.setCompletionList(new CompletionListCapabilities(List.of("editRange")));
textDocumentClientCapabilities.setCompletion(completionCapabilities);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,20 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No
async processing is occuring on a separate thread.
*/
CompletionParams params = LSPIJUtils.toCompletionParams(LSPIJUtils.toUri(document), offset, document);
BlockingDeque<Pair<Either<List<CompletionItem>, CompletionList>, LanguageServer>> proposals = new LinkedBlockingDeque<>();
BlockingDeque<Pair<Either<List<CompletionItem>, CompletionList>, LanguageServerItem>> proposals = new LinkedBlockingDeque<>();

CompletableFuture<Void> future = completionLanguageServersFuture
.thenComposeAsync(languageServers -> cancellationSupport.execute(
CompletableFuture.allOf(languageServers.stream()
.map(languageServer ->
cancellationSupport.execute(languageServer.getServer().getTextDocumentService().completion(params))
.thenAcceptAsync(completion -> proposals.add(new Pair<>(completion, languageServer.getServer()))))
.thenAcceptAsync(completion -> proposals.add(new Pair<>(completion, languageServer))))
.toArray(CompletableFuture[]::new))));

ProgressManager.checkCanceled();
while (!future.isDone() || !proposals.isEmpty()) {
ProgressManager.checkCanceled();
Pair<Either<List<CompletionItem>, CompletionList>, LanguageServer> pair = proposals.poll(25, TimeUnit.MILLISECONDS);
Pair<Either<List<CompletionItem>, CompletionList>, LanguageServerItem> pair = proposals.poll(25, TimeUnit.MILLISECONDS);
if (pair != null) {
Either<List<CompletionItem>, CompletionList> completion = pair.getFirst();
if (completion != null) {
Expand All @@ -101,7 +101,7 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No
}

private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix completionPrefix, Either<List<CompletionItem>,
CompletionList> completion, LanguageServer languageServer, @NotNull CompletionResultSet result, CancellationSupport cancellationSupport) {
CompletionList> completion, LanguageServerItem languageServer, @NotNull CompletionResultSet result, CancellationSupport cancellationSupport) {
CompletionItemDefaults itemDefaults = null;
List<CompletionItem> items = null;
if (completion.isLeft()) {
Expand Down Expand Up @@ -138,7 +138,7 @@ private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix co

private static LSPCompletionProposal createLookupItem(PsiFile file, Editor editor, int offset,
CompletionItem item,
CompletionItemDefaults itemDefaults, LanguageServer languageServer) {
CompletionItemDefaults itemDefaults, LanguageServerItem languageServer) {
// Update text edit range with item defaults if needed
updateWithItemDefaults(item, itemDefaults);
return new LSPCompletionProposal(file, editor, offset, item, languageServer);
Expand Down Expand Up @@ -179,4 +179,4 @@ private static CompletableFuture<List<LanguageServerItem>> initiateLanguageServe
return false;
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,27 @@
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorModificationUtil;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiFile;
import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils;
import com.redhat.devtools.intellij.lsp4ij.LanguageServerItem;
import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor;
import com.redhat.devtools.intellij.lsp4ij.command.internal.CommandExecutor;
import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetIndentOptions;
import org.apache.commons.lang.StringUtils;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageServer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static com.redhat.devtools.intellij.lsp4ij.operations.completion.CompletionProposalTools.createLspIndentOptions;
import static com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetVariableConstants.*;
Expand All @@ -53,19 +56,23 @@ public class LSPCompletionProposal extends LookupElement {
private final CompletionItem item;
private final int initialOffset;
private final PsiFile file;
private final Boolean supportResolveCompletion;
private int currentOffset;
private int bestOffset;
private final Editor editor;
private final LanguageServer languageServer;
private final LanguageServerItem languageServer;
private String documentation;

public LSPCompletionProposal(PsiFile file, Editor editor, int offset, CompletionItem item, LanguageServer languageServer) {
public LSPCompletionProposal(PsiFile file, Editor editor, int offset, CompletionItem item, LanguageServerItem languageServer) {
this.file = file;
this.item = item;
this.editor = editor;
this.languageServer = languageServer;
this.initialOffset = offset;
this.currentOffset = offset;
this.bestOffset = getPrefixCompletionStart(editor.getDocument(), offset);
ServerCapabilities serverCapabilities = languageServer.getServerWrapper().getServerCapabilities();
this.supportResolveCompletion = serverCapabilities != null && serverCapabilities.getCompletionProvider() != null && serverCapabilities.getCompletionProvider().getResolveProvider();
putUserData(CodeCompletionHandlerBase.DIRECT_INSERTION, true);
}

Expand Down Expand Up @@ -274,7 +281,7 @@ private void executeCustomCommand(@NotNull Command command, Document document) {
Project project = editor.getProject();
// Execute custom command of the completion item.
LanguageServiceAccessor.getInstance(project)
.resolveServerDefinition(languageServer).map(definition -> definition.id)
.resolveServerDefinition(languageServer.getServer()).map(definition -> definition.id)
.ifPresent(id -> {
CommandExecutor.executeCommand(project, command, document, id);
});
Expand Down Expand Up @@ -347,4 +354,38 @@ public CompletionItem getItem() {
}
}

public MarkupContent getDocumentation() {
if (item.getDocumentation() == null && supportResolveCompletion) {
try {
CompletionItem resolved = languageServer.getServer()
.getTextDocumentService()
.resolveCompletionItem(item)
.get(1000, TimeUnit.MILLISECONDS);
if (resolved != null) {
item.setDocumentation(resolved.getDocumentation());
}
} catch (ExecutionException e) {
if (!(e.getCause() instanceof CancellationException)) {
LOGGER.warn(e.getLocalizedMessage(), e);
}
} catch (TimeoutException e) {
LOGGER.warn(e.getLocalizedMessage(), e);
} catch (InterruptedException e) {
LOGGER.warn(e.getLocalizedMessage(), e);
Thread.currentThread().interrupt();
}
}
return getDocumentation(item.getDocumentation());
}

private static MarkupContent getDocumentation(Either<String, MarkupContent> documentation) {
if (documentation == null) {
return null;
}
if (documentation.isLeft()) {
String content = documentation.getLeft();
return new MarkupContent(content, MarkupKind.PLAINTEXT);
}
return documentation.getRight();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*******************************************************************************
* Copyright (c) 2020 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.documentation;

import com.intellij.lang.documentation.DocumentationProviderEx;
import com.intellij.lang.documentation.ExternalDocumentationHandler;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
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.operations.completion.LSPCompletionProposal;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import org.eclipse.lsp4j.MarkupContent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* {@link DocumentationProviderEx} implementation for LSP to support:
*
* <ul>
* <li>textDocument/hover</li>
* <li>documentation for completion item</li>
* </ul>.
*/
public class LSPDocumentationProvider extends DocumentationProviderEx implements ExternalDocumentationHandler {

private static final Parser PARSER = Parser.builder().build();
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build();

private static final Key<Integer> TARGET_OFFSET_KEY = new Key<>(LSPDocumentationProvider.class.getName());

@Nullable
@Override
public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) {
return generateDoc(element, originalElement);
}

@Override
public @Nullable PsiElement getCustomDocumentationElement(@NotNull Editor editor, @NotNull PsiFile file, @Nullable PsiElement contextElement, int targetOffset) {
if (contextElement != null) {
// Store the offset where the hover has been triggered
contextElement.putUserData(TARGET_OFFSET_KEY, targetOffset);
angelozerr marked this conversation as resolved.
Show resolved Hide resolved
}
return super.getCustomDocumentationElement(editor, file, contextElement, targetOffset);
}

@Nullable
@Override
public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
try {
Project project = element.getProject();
if (project.isDisposed()) {
return null;
}
Editor editor = LSPIJUtils.editorForElement(element);
if (editor == null) {
return null;
}
List<MarkupContent> result = getMarkupContents(element, originalElement);
if (result == null || result.isEmpty()) {
return null;
}
String s = result
.stream()
.map(m -> m.getValue())
.collect(Collectors.joining("\n\n"));
return styleHtml(editor, RENDERER.render(PARSER.parse(s)));
} finally {
if (originalElement != null) {
originalElement.putUserData(TARGET_OFFSET_KEY, null);
}
}
}

@Nullable
public List<MarkupContent> getMarkupContents(PsiElement element, @Nullable PsiElement originalElement) {
if (element instanceof LSPPsiElementForLookupItem) {
// Show documentation for a given completion item in the "documentation popup" (see IJ Completion setting)
// (LSP textDocument/completion request)
return ((LSPPsiElementForLookupItem) element).getDocumentation();
}
if (originalElement == null || !Objects.equals(element.getContainingFile(), originalElement.getContainingFile())) {
return null;
}
Editor editor = LSPIJUtils.editorForElement(element);
if (editor == null) {
return null;
}

// Show documentation for a hovered element (LSP textDocument/hover request).
VirtualFile file = originalElement.getContainingFile().getVirtualFile();
if (LSPVirtualFileWrapper.hasWrapper(file)) {
int targetOffset = getTargetOffset(originalElement);
return LSPVirtualFileWrapper.getLSPVirtualFileWrapper(file).getHoverContent(element, targetOffset, editor);
}
return null;
}

private static int getTargetOffset(PsiElement originalElement) {
Integer targetOffset = originalElement.getUserData(TARGET_OFFSET_KEY);
if (targetOffset != null) {
return targetOffset;
}
int startOffset = originalElement.getTextOffset();
int textLength = originalElement.getTextLength();
return startOffset + textLength / 2;
}

@Nullable
@Override
public PsiElement getDocumentationElementForLookupItem(PsiManager psiManager, Object object, PsiElement element) {
if (object instanceof LSPCompletionProposal) {
MarkupContent documentation = ((LSPCompletionProposal) object).getDocumentation();
if (documentation != null) {
return new LSPPsiElementForLookupItem(documentation, psiManager, element);
}
}
return null;
}

@Nullable
@Override
public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) {
return null;
}

@Override
public boolean handleExternal(PsiElement element, PsiElement originalElement) {
return false;
}

@Override
public boolean handleExternalLink(PsiManager psiManager, String link, PsiElement context) {
VirtualFile file = LSPIJUtils.findResourceFor(link);
if (file != null) {
FileEditorManager.getInstance(psiManager.getProject()).openFile(file, true, true);
return true;
}
return false;
}

@Override
public boolean canFetchDocumentationLink(String link) {
return false;
}

@Override
public @NotNull String fetchExternalDocumentation(@NotNull String link, @Nullable PsiElement element) {
return null;
}


public static String styleHtml(Editor editor, String htmlBody) {
if (htmlBody == null || htmlBody.isEmpty()) {
return htmlBody;
}
Color background = editor.getColorsScheme().getDefaultBackground();
Color foreground = editor.getColorsScheme().getDefaultForeground();

StringBuilder html = new StringBuilder("<html><head><style TYPE='text/css'>html { ");
if (background != null) {
html.append("background-color: ")
.append(toHTMLrgb(background))
.append(";");
}
if (foreground != null) {
html.append("color: ")
.append(toHTMLrgb(foreground))
.append(";");
}
html
.append(" }</style></head><body>")
.append(htmlBody)
.append("</body></html>");
return html.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);
}

}
Loading
Loading