diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationLinkHandler.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationLinkHandler.java new file mode 100644 index 000000000..6a34cbabb --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationLinkHandler.java @@ -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; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java index 2eb40b604..4d2692086 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java @@ -60,4 +60,7 @@ public Pointer createPointer() { return Pointer.hardPointer(this); } + public PsiFile getFile() { + return file; + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverter.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverter.java index d2df185d6..5612cf9e3 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverter.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverter.java @@ -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; @@ -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 markdown 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)); } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/CustomLinkRenderer.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/CustomLinkRenderer.java new file mode 100644 index 000000000..550710f07 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/CustomLinkRenderer.java @@ -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> 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/SyntaxColorationCodeBlockRenderer.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/SyntaxColorationCodeBlockRenderer.java similarity index 87% rename from src/main/java/com/redhat/devtools/lsp4ij/features/documentation/SyntaxColorationCodeBlockRenderer.java rename to src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/SyntaxColorationCodeBlockRenderer.java index 1f7e23221..6d1752e50 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/SyntaxColorationCodeBlockRenderer.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/SyntaxColorationCodeBlockRenderer.java @@ -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; @@ -17,6 +17,7 @@ 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; @@ -24,8 +25,10 @@ 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; @@ -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); + } + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/TextMateHighlighterHelper.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/TextMateHighlighterHelper.java similarity index 94% rename from src/main/java/com/redhat/devtools/lsp4ij/features/documentation/TextMateHighlighterHelper.java rename to src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/TextMateHighlighterHelper.java index 94b83f4c0..6aa9733c5 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/TextMateHighlighterHelper.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/TextMateHighlighterHelper.java @@ -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; @@ -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; diff --git a/src/main/java/com/redhat/devtools/lsp4ij/hint/LSPNavigationLinkHandler.java b/src/main/java/com/redhat/devtools/lsp4ij/hint/LSPNavigationLinkHandler.java index 51c1b801d..3e4079177 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/hint/LSPNavigationLinkHandler.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/hint/LSPNavigationLinkHandler.java @@ -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}. @@ -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. + * + *

+ * the fileUrl can have following syntax: + *

+ *

+ * @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); } /** @@ -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; } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 41c2ddc77..97ae01b9c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -216,6 +216,9 @@ id="LSPDocumentationTargetProvider" implementation="com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationTargetProvider" order="first"/> +