Skip to content

Commit

Permalink
feat: Show LSP completion item documentation in popup
Browse files Browse the repository at this point in the history
Fixes #1046

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Aug 21, 2023
1 parent 13d4b7b commit 5a6dc9f
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 214 deletions.
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,26 @@
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.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 +55,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 +280,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 +353,35 @@ 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 (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
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,204 @@
/*******************************************************************************
* 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>testDocument/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);
}
return super.getCustomDocumentationElement(editor, file, contextElement, targetOffset);
}

@Nullable
@Override
public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
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)));
}

@Nullable
public List<MarkupContent> getMarkupContents(PsiElement element, @Nullable PsiElement originalElement) {
if (element instanceof LSPPsiElementForLookupItem) {
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;
}

VirtualFile file = originalElement.getContainingFile().getVirtualFile();
int targetOffset = getTargetOffset(originalElement);
if (LSPVirtualFileWrapper.hasWrapper(file)) {
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 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 = "<html><head><style TYPE='text/css'>html { " + //$NON-NLS-1$
(background != null ? "background-color: " + toHTMLrgb(background) + "; " : "") + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
(foreground != null ? "color: " + toHTMLrgb(foreground) + "; " : "") + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
" }</style></head><body>"; //$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("</body></html>");
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);
}

}
Loading

0 comments on commit 5a6dc9f

Please sign in to comment.