Skip to content

Commit

Permalink
feat: Support file link in hover
Browse files Browse the repository at this point in the history
Fixes redhat-developer#376

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Jun 21, 2024
1 parent 69f8a0c commit 5196092
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.redhat.devtools.lsp4ij.features.documentation;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.platform.backend.documentation.DocumentationLinkHandler;
import com.intellij.platform.backend.documentation.DocumentationTarget;
import com.intellij.platform.backend.documentation.LinkResolveResult;
import com.redhat.devtools.lsp4ij.hint.LSPNavigationLinkHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class LSPDocumentationLinkHandler implements DocumentationLinkHandler {

@Override
public @Nullable LinkResolveResult resolveLink(@NotNull DocumentationTarget target, @NotNull String url) {
if (target instanceof LSPDocumentationTarget lspTarget) {
if (url.endsWith(LSPNavigationLinkHandler.PREFIX_OR_SUFFIX)) {
ApplicationManager.getApplication()
.invokeLater(() -> {
String fileUrl = url.substring(0, url.length() - LSPNavigationLinkHandler.PREFIX_OR_SUFFIX.length());
var file = lspTarget.getFile();
LSPNavigationLinkHandler.handleLink(fileUrl, file, file.getProject());
});
return LinkResolveResult.resolvedTarget(target);
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@ public Pointer<? extends DocumentationTarget> createPointer() {
return Pointer.hardPointer(this);
}

public PsiFile getFile() {
return file;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import com.intellij.lang.Language;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import com.redhat.devtools.lsp4ij.features.documentation.markdown.CustomLinkRenderer;
import com.redhat.devtools.lsp4ij.features.documentation.markdown.SyntaxColorationCodeBlockRenderer;
import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
import com.vladsch.flexmark.ext.tables.TablesExtension;
Expand Down Expand Up @@ -103,16 +105,15 @@ public NodeRenderer apply(DataHolder options) {
* @param fileName the file name which must be used to retrieve TextMate (if non-null) for MarkDown code block which defines the language or indented blockquote.
* @return the given <code>markdown</code> to Html.
*/
public @NotNull String toHtml(@NotNull String markdown, @Nullable Language language, @Nullable String fileName) {
public @NotNull String toHtml(@NotNull String markdown,
@Nullable Language language,
@Nullable String fileName) {
var htmlRenderer = this.htmlRenderer;
if (language != null || fileName != null) {
htmlRenderer = HtmlRenderer.builder(options)
.nodeRendererFactory(new NodeRendererFactory() {
@Override
public NodeRenderer apply(DataHolder options) {
return new SyntaxColorationCodeBlockRenderer(project, language, fileName);
}
}).build();
.nodeRendererFactory(new CustomLinkRenderer.Factory())
.nodeRendererFactory(new SyntaxColorationCodeBlockRenderer.Factory(project, language, fileName))
.build();
}
return htmlRenderer.render(htmlParser.parse(markdown));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.redhat.devtools.lsp4ij.features.documentation.markdown;

import com.redhat.devtools.lsp4ij.hint.LSPNavigationLinkHandler;
import com.vladsch.flexmark.ast.Link;
import com.vladsch.flexmark.html.HtmlWriter;
import com.vladsch.flexmark.html.renderer.*;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.misc.CharPredicate;
import com.vladsch.flexmark.util.sequence.BasedSequence;

import java.util.Collections;
import java.util.Set;

public class CustomLinkRenderer implements NodeRenderer {

@Override
public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
return Collections.singleton(new NodeRenderingHandler<>(Link.class, this::render));
}

private void render(Link node, NodeRendererContext context, HtmlWriter html) {
if (context.isDoNotRenderLinks() /*|| isSuppressedLinkPrefix(node.getUrl(), context)*/) {
context.renderChildren(node);
} else {
ResolvedLink resolvedLink = context.resolveLink(LinkType.LINK, node.getUrl().unescape(), null, null);

String url = getUrl(resolvedLink.getUrl());
html.attr("href", url);

// we have a title part, use that
if (node.getTitle().isNotNull()) {
resolvedLink = resolvedLink.withTitle(node.getTitle().unescape());
}

html.attr(resolvedLink.getNonNullAttributes());
html.srcPos(node.getChars()).withAttr(resolvedLink).tag("a");
renderChildrenSourceLineWrapped(node, node.getText(), context, html);
html.tag("/a");
}
}

private String getUrl(String url) {
if (isFileUrl(url)) {
return url + LSPNavigationLinkHandler.PREFIX_OR_SUFFIX;
}
return url;
}

private static boolean isFileUrl(String url) {
int index = url.indexOf("://");
return (index == -1 || url.substring(0, index).equals("file"));
}

private void renderChildrenSourceLineWrapped(
Node node,
BasedSequence nodeChildText,
NodeRendererContext context,
HtmlWriter html
) {
// if have SOFT BREAK or HARD BREAK as child then we open our own span
if (context.getHtmlOptions().sourcePositionParagraphLines && nodeChildText.indexOfAny(CharPredicate.ANY_EOL) >= 0) {
// if (myNextLine > 0) {
// myNextLine--;
// }

// outputSourceLineSpan(node, node, node, html);
context.renderChildren(node);
html.tag("/span");
} else {
context.renderChildren(node);
}
}

public static class Factory implements NodeRendererFactory {
@Override
public NodeRenderer apply(DataHolder options) {
return new CustomLinkRenderer();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.lsp4ij.features.documentation;
package com.redhat.devtools.lsp4ij.features.documentation.markdown;

import com.intellij.lang.Language;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
Expand All @@ -17,15 +17,18 @@
import com.intellij.openapi.editor.richcopy.SyntaxInfoBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.psi.TokenType;
import com.redhat.devtools.lsp4ij.features.documentation.LightQuickDocHighlightingHelper;
import com.redhat.devtools.lsp4ij.internal.SimpleLanguageUtils;
import com.redhat.devtools.lsp4ij.internal.StringUtils;
import com.vladsch.flexmark.ast.FencedCodeBlock;
import com.vladsch.flexmark.ast.IndentedCodeBlock;
import com.vladsch.flexmark.html.HtmlWriter;
import com.vladsch.flexmark.html.renderer.NodeRenderer;
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
import com.vladsch.flexmark.util.ast.ContentNode;
import com.vladsch.flexmark.util.data.DataHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -208,5 +211,22 @@ private static boolean hasTextMateSupport() {
}
}

public static class Factory implements NodeRendererFactory {


private final Project project;
private final Language fileLanguage;
private final String fileName;

public Factory(Project project, Language fileLanguage, String fileName) {
this.project = project;
this.fileLanguage = fileLanguage;
this.fileName = fileName;
}
@Override
public NodeRenderer apply(DataHolder options) {
return new SyntaxColorationCodeBlockRenderer(project, fileLanguage, fileName);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.lsp4ij.features.documentation;
package com.redhat.devtools.lsp4ij.features.documentation.markdown;

import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
Expand All @@ -18,6 +18,7 @@
import com.intellij.openapi.fileTypes.SyntaxHighlighter;
import com.intellij.openapi.util.registry.Registry;
import com.redhat.devtools.lsp4ij.LanguageServersRegistry;
import com.redhat.devtools.lsp4ij.features.documentation.markdown.SyntaxColorationCodeBlockRenderer;
import com.redhat.devtools.lsp4ij.internal.StringUtils;
import com.vladsch.flexmark.html.HtmlWriter;
import org.jetbrains.annotations.NotNull;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@

import com.intellij.codeInsight.highlighting.TooltipLinkHandler;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import com.redhat.devtools.lsp4ij.LSPIJUtils;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jetbrains.annotations.Nullable;

/**
* Handles tooltip links in format {@code #lsp-navigation/file_path:startLine;startChar;endLine;endChar}.
Expand All @@ -35,27 +36,41 @@
*/
public class LSPNavigationLinkHandler extends TooltipLinkHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(LSPNavigationLinkHandler.class);//$NON-NLS-1$
public static final String PREFIX_OR_SUFFIX = "#lsp-navigation/";

private static final String PREFIX = "#lsp-navigation/";
public static final String POS_SEPARATOR = ";";
public static final String HASH_SEPARATOR = "#";

@Override
public boolean handleLink(@NotNull String refSuffix, @NotNull Editor editor) {
int pos = refSuffix.lastIndexOf(':');
if (pos <= 0 || pos == refSuffix.length() - 1) {
LOGGER.info("Malformed suffix: " + refSuffix);
return true;
}
public boolean handleLink(@NotNull String fileUrl,
@NotNull Editor editor) {
return handleLink(fileUrl, null, editor.getProject());
}

String uri = refSuffix.substring(0, pos);
Range range = toRange(refSuffix.substring(pos + 1));
Location location = new Location();
location.setUri(uri);
if (range != null) {
location.setRange(range);
}
return LSPIJUtils.openInEditor(location, editor.getProject());
/**
* Open in an editor the given fileUrl and tries to create the file if it doesn't exist.
*
* <p>
* the fileUrl can have following syntax:
* <ul>
* <li>file:///C:/Users/username/foo.txt</li>
* <li>C:/Users/username/foo.txt</li>
* <li>foo.txt</li>
* <li>file:///C:/Users/username/foo.txt#L1:5</li>
* </ul>
* </p>
* @param fileUrl the file Url to open.
* @param file the psi file which trigger the link. If not null it is used to resolve absolute path.
* @param project the project.
* @return true if file Url can be opened and false otherwise.
*/
public static boolean handleLink(@NotNull String fileUrl,
@Nullable PsiFile file,
@NotNull Project project) {
int pos = fileUrl.lastIndexOf(HASH_SEPARATOR);
boolean hasPosition = pos > 0 && pos != fileUrl.length() - 1;
String uri = hasPosition ? fileUrl.substring(0, pos) : fileUrl;
Position start = hasPosition ? toPosition(fileUrl.substring(pos + 1)) : null;
return LSPIJUtils.openInEditor(uri, start, project);
}

/**
Expand All @@ -69,41 +84,63 @@ public boolean handleLink(@NotNull String refSuffix, @NotNull Editor editor) {
* @return the LSP navigation url from the given location.
*/
public static String toNavigationUrl(@NotNull Location location) {
StringBuilder url = new StringBuilder(PREFIX);
return toNavigationUrl(location, true);
}

public static String toNavigationUrl(@NotNull Location location, boolean prefix) {
StringBuilder url = new StringBuilder(PREFIX_OR_SUFFIX);
if (prefix) {
url.append(PREFIX_OR_SUFFIX);
}
url.append(location.getUri());
url.append(":");
if (location.getRange() != null) {
toString(location.getRange(), url);
appendStartPositionIfNeeded(location.getRange(), url);
if (!prefix) {
url.append(PREFIX_OR_SUFFIX);
}
return url.toString();
}

/**
* Serialize LSP range used in the LSP location Url.
*
* @param range the LSP range.
* @param result
*/
private static void toString(@NotNull Range range, StringBuilder result) {
result.append(range.getStart().getLine());
result.append(POS_SEPARATOR);
result.append(range.getStart().getCharacter());
result.append(POS_SEPARATOR);
result.append(range.getEnd().getLine());
result.append(POS_SEPARATOR);
result.append(range.getEnd().getCharacter());
private static void appendStartPositionIfNeeded(@Nullable Range range, StringBuilder result) {
Position start = range != null ? range.getStart() : null;
if (start == null) {
return;
}
result.append(HASH_SEPARATOR);
result.append("L");
result.append(start.getLine());
result.append(":");
result.append(start.getCharacter());
}

private static Range toRange(String rangeString) {
if (rangeString.isEmpty()) {
private static Position toPosition(String positionString) {
if (positionString == null || positionString.isEmpty()) {
return null;
}
String[] positions = rangeString.split(POS_SEPARATOR);
Position start = new Position(toInt(0, positions), toInt(1, positions));
Position end = new Position(toInt(2, positions), toInt(3, positions));
return new Range(start, end);
if (positionString.charAt(0) != 'L') {
return null;
}
positionString = positionString.substring(1, positionString.length());
String[] positions = positionString.split(":");
if (positions.length == 0) {
return null;
}
int line = toInt(0, positions);
int character = toInt(1, positions);
return new Position(line, character);
}

private static int toInt(int index, String[] positions) {
return Integer.valueOf(positions[index]);
if (index < positions.length) {
try {
return Integer.valueOf(positions[index]);
} catch (Exception e) {}
}
return 0;
}
}
3 changes: 3 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@
id="LSPDocumentationTargetProvider"
implementation="com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationTargetProvider"
order="first"/>
<platform.backend.documentation.linkHandler
id="LSPDocumentationLinkHandler"
implementation="com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationLinkHandler" />

<!-- LSP textDocument/folding -->
<lang.foldingBuilder id="LSPFoldingBuilderForText"
Expand Down

0 comments on commit 5196092

Please sign in to comment.