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 extends DocumentationTarget> 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:
+ *
+ * - file:///C:/Users/username/foo.txt
+ * - C:/Users/username/foo.txt
+ * - foo.txt
+ * - file:///C:/Users/username/foo.txt#L1:5
+ *
+ *
+ * @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"/>
+