diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java
index 58c637911..7636f34a8 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java
@@ -12,6 +12,7 @@
import com.intellij.openapi.Disposable;
import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.psi.PsiFile;
import com.redhat.devtools.lsp4ij.features.codeAction.intention.LSPIntentionCodeActionSupport;
import com.redhat.devtools.lsp4ij.features.codeLens.LSPCodeLensSupport;
@@ -30,13 +31,15 @@
import com.redhat.devtools.lsp4ij.features.rename.LSPRenameSupport;
import com.redhat.devtools.lsp4ij.features.signatureHelp.LSPSignatureHelpSupport;
import com.redhat.devtools.lsp4ij.features.typeDefinition.LSPTypeDefinitionSupport;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* LSP file support stored in the opened {@link PsiFile} with key "lsp.file.support"
* which manages and caches LSP codeLens, inlayHint, color futures.
*/
-public class LSPFileSupport implements Disposable {
+@ApiStatus.Internal
+public class LSPFileSupport extends UserDataHolderBase implements Disposable {
private static final Key
+ * the following syntax is supported for fileUrl:
+ *
+ *
+ *
markdown
to Html.
@@ -91,11 +104,43 @@ public NodeRenderer apply(DataHolder options) {
* the syntax coloration to use if MarkDown content contains some code block or blockquote to highlight.
* @return the given markdown
to Html.
*/
- public @NotNull String toHtml(@NotNull String markdown, @Nullable PsiFile file) {
- return toHtml(markdown, file != null ? file.getLanguage() : null, file != null ? file.getName() : null);
+ public @NotNull String toHtml(@NotNull String markdown,
+ @Nullable PsiFile file) {
+ var htmlRenderer = this.htmlRenderer;
+ if (file != null) {
+ // The HtmlRenderer is stored in LSPFileSupport instead of PsiFile
+ // to evict it when file is closed.
+ var fileSupport = LSPFileSupport.getSupport(file);
+ htmlRenderer = fileSupport.getUserData(HTML_RENDERER_KEY);
+ if (htmlRenderer == null) {
+ htmlRenderer = getHtmlRenderer(fileSupport);
+ }
+ }
+ return htmlRenderer.render(htmlParser.parse(markdown));
+ }
+
+ private synchronized HtmlRenderer getHtmlRenderer(@NotNull LSPFileSupport fileSupport) {
+ var file = fileSupport.getFile();
+ var htmlRenderer = fileSupport.getUserData(HTML_RENDERER_KEY);
+ if (htmlRenderer != null) {
+ return htmlRenderer;
+ }
+
+ MutableDataSet fileOptions = new MutableDataSet(options);
+ fileOptions.set(LANGUAGE_CONTEXT, file.getLanguage());
+ fileOptions.set(FILE_NAME_CONTEXT, file.getName());
+ Path baseDir = file.getVirtualFile().getParent().getFileSystem().getNioPath(file.getVirtualFile().getParent());
+ fileOptions.set(FILE_BASE_DIR, baseDir);
+
+ htmlRenderer = createHtmlRenderer(fileOptions);
+ fileSupport.putUserData(HTML_RENDERER_KEY, htmlRenderer);
+ file.putUserData(HTML_RENDERER_KEY, htmlRenderer);
+ return htmlRenderer;
}
/**
+ * This method is just used by Junit tests.
+ *
* Convert the given
- * This handler looks like {@link com.intellij.codeInsight.hint.NavigationLinkHandler} but as LSP works with position (line, character)
- * instead of offset, we provide this handler to avoid resolving offset from LSP position when
- * IntelliJ annotation is created with the tooltip.
+ * This handler looks like {@link com.intellij.codeInsight.hint.NavigationLinkHandler} but as LSP works with position (line, character)
+ * instead of offset, we provide this handler to avoid resolving offset from LSP position when
+ * IntelliJ annotation is created with the tooltip.
*
- * {@code #lsp-navigation/file_path:startLine;startChar;endLine;endChar}
+ * {@code #lsp-navigation/file_path:startLine;startChar;endLine;endChar}
* markdown
to Html.
*
* @param markdown the MarkDown content to convert to Html.
@@ -103,16 +148,17 @@ 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 Path baseDir,
+ @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();
+ MutableDataSet fileOptions = new MutableDataSet(options);
+ fileOptions.set(FILE_BASE_DIR, baseDir);
+ fileOptions.set(LANGUAGE_CONTEXT, language);
+ fileOptions.set(FILE_NAME_CONTEXT, fileName);
+ htmlRenderer = createHtmlRenderer(fileOptions);
}
return htmlRenderer.render(htmlParser.parse(markdown));
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/LSPLinkResolver.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/LSPLinkResolver.java
new file mode 100644
index 000000000..c10ff427e
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/LSPLinkResolver.java
@@ -0,0 +1,125 @@
+/*******************************************************************************
+ * Copyright (c) 2024 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.lsp4ij.features.documentation.markdown;
+
+import com.redhat.devtools.lsp4ij.LSPIJUtils;
+import com.redhat.devtools.lsp4ij.features.documentation.MarkdownConverter;
+import com.vladsch.flexmark.ast.Image;
+import com.vladsch.flexmark.ast.Link;
+import com.vladsch.flexmark.ast.Reference;
+import com.vladsch.flexmark.html.LinkResolver;
+import com.vladsch.flexmark.html.LinkResolverFactory;
+import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
+import com.vladsch.flexmark.html.renderer.LinkStatus;
+import com.vladsch.flexmark.html.renderer.ResolvedLink;
+import com.vladsch.flexmark.util.ast.Node;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Set;
+
+/**
+ * Custom link resolver used to resolve relative path by using the {@link com.intellij.psi.PsiFile}
+ * path which triggers the MarkDown converter for hover and completion documentation.
+ */
+public class LSPLinkResolver implements LinkResolver {
+
+ private final Path fileBaseDir;
+
+ private enum FileUrlKind {
+ RELATIVE,
+ ABSOLUTE,
+ NONE;
+ }
+
+ public LSPLinkResolver(LinkResolverBasicContext context) {
+ this.fileBaseDir = MarkdownConverter.FILE_BASE_DIR.get(context.getOptions());
+ }
+
+ @Override
+ public @NotNull ResolvedLink resolveLink(@NotNull Node node, @NotNull LinkResolverBasicContext context, @NotNull ResolvedLink link) {
+ if (node instanceof Image || node instanceof Link || node instanceof Reference) {
+ String url = link.getUrl();
+ FileUrlKind fileUrlKind = getFileUrlKind(url);
+ if (fileBaseDir != null && fileUrlKind != FileUrlKind.NONE) {
+ String position = "";
+ int hashIndex= url.indexOf("#");
+ if (hashIndex != -1) {
+ position = url.substring(hashIndex, url.length());
+ url = url.substring(0, hashIndex);
+ }
+ try {
+ File resolvedFile = getResolvedFile(url, fileUrlKind);
+ String resolvedUri = LSPIJUtils.toUri(resolvedFile).toASCIIString() + position;
+ return link.withStatus(LinkStatus.VALID)
+ .withUrl(resolvedUri);
+ }
+ catch(Exception e) {
+
+ }
+ }
+ }
+ return link;
+ }
+
+ private @NotNull File getResolvedFile(String url, FileUrlKind fileUrlKind) {
+ if (fileUrlKind == FileUrlKind.RELATIVE) {
+ return fileBaseDir.resolve(url).toFile();
+ }
+ return Paths.get(url).toFile();
+ }
+
+
+ private static FileUrlKind getFileUrlKind(String url) {
+ if (url.isBlank()) {
+ return FileUrlKind.NONE;
+ }
+ int index = url.indexOf("://");
+ if (index == -1) {
+ return FileUrlKind.RELATIVE;
+ }
+ if (url.charAt(0) == '/' || url.substring(0, index).equals("file")) {
+ return FileUrlKind.ABSOLUTE;
+ }
+ return FileUrlKind.NONE;
+ }
+
+
+ public static class Factory implements LinkResolverFactory {
+
+ @Nullable
+ @Override
+ public Set<project>
element is the root of the descriptor.
ONLY_THIS_TEXT
\n", toHtml("ONLY_THIS_TEXT")); - assertEquals("This is
\nbold
This is\nbold
\n", toHtml(""" This is **bold** """)); @@ -92,9 +94,9 @@ public void testMultiLineConversion() { """; String html = """ -multi
- line
- HTML
+
multi
+ line
+ HTML
stuff
function foo(): void\n" + + "
Some content...
\n" + + "\n" + + "\n" + + "\n"; + assertEquals(html, toHtml(markdown, null, null, "test.ts")); } private String toHtml(String markdown) { return MarkdownConverter.getInstance(myFixture.getProject()).toHtml(markdown); } - private String toHtml(@NotNull String markdown, @Nullable Language language, @Nullable String fileName) { - return MarkdownConverter.getInstance(myFixture.getProject()).toHtml(markdown,language, fileName); + private String toHtml(@NotNull String markdown, + @Nullable Path baseDir, + @Nullable Language language, + @Nullable String fileName) { + return MarkdownConverter.getInstance(myFixture.getProject()).toHtml(markdown, baseDir, language, fileName); } } \ No newline at end of file diff --git a/src/test/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverterWithPsiFileTest.java b/src/test/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverterWithPsiFileTest.java new file mode 100644 index 000000000..d212c0373 --- /dev/null +++ b/src/test/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverterWithPsiFileTest.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2024 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.lsp4ij.features.documentation; + +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPIJUtils; +import com.redhat.devtools.lsp4ij.fixtures.LSPCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Test Markdown conversion to HTML by using PsiFile. + */ +public class MarkdownConverterWithPsiFileTest extends LSPCodeInsightFixtureTestCase { + + private static final String USER_HOME = System.getProperty("user.home"); + + public void testHighlightCodeBlockConversion() { + // Here code block language is not set, the language is retrieved from the PsiFile. + String fileName = "test.txt"; + + String markdown = """ + Here's some XML code: + + ``` + +
Here's some XML code:
+<?xml version="1.0" encoding="UTF-8"?>
+ <note>
+ <to>Angelo</to>
+ <from>Fred</from>
+ <heading>Tests</heading>
+ <body>I wrote them!</body>
+ </note>
+
+ """;
+
+ assertMarkdownConverter(fileName, markdown, html);
+ }
+
+ public void testXmlHighlightCodeBlockConversion() {
+ // Here code block language is not set, the language is retrieved from the PsiFile.
+ String fileName = "test.xml";
+
+ String markdown = """
+ Here's some XML code:
+
+ ```
+
+ Here's some XML code:
+<?xml version="1.0" encoding="UTF-8"?>+ """; + + assertMarkdownConverter(fileName, markdown, html); + } + + public void testAbsoluteFileLink() { + String markdown = "[foo](file://" + USER_HOME + "/lsp/foo.txt)"; + assertMarkdownConverter("bar.txt", markdown, "\n"); + } + + public void testAbsoluteFileLinkWithSlash() { + String markdown = "[foo](/lsp/foo.txt)"; + assertMarkdownConverter("bar.txt", markdown, "\n"); + } + + public void testRelativeFileLink() { + String markdown = "[foo](lsp/foo.txt)"; + assertMarkdownConverter("bar.txt", markdown, "\n"); + } + + private void assertMarkdownConverter(String fileName, String markdown, String html) { + myFixture.configureByText(fileName, ""); + var file = myFixture.getFile(); + + String systemBaseDir = getSystemBaseDir(); + html = html.replace("$SYSTEM_BASE_DIR$", systemBaseDir); + + String fileBaseDir = getFileBaseDir(file); + html = html.replace("$FILE_BASE_DIR$", fileBaseDir); + + String actual = MarkdownConverter.getInstance(file.getProject()).toHtml(markdown, file); + assertEquals("$SYSTEM_BASE_DIR$=" + systemBaseDir + ", $FILE_BASE_DIR$=" + fileBaseDir, html, actual); + } + + private String getSystemBaseDir() { + String systemBaseDir = LSPIJUtils.toUri(Paths.get("/").toFile()).toASCIIString(); + systemBaseDir = systemBaseDir.substring("file://".length()); + if (systemBaseDir.endsWith("/")) { + systemBaseDir = systemBaseDir.substring(0, systemBaseDir.length() - 1); + } + return systemBaseDir; + } + + private static @NotNull String getFileBaseDir(PsiFile file) { + Path baseDir = file.getVirtualFile().getParent().getFileSystem().getNioPath(file.getVirtualFile().getParent()); + String fileBaseDir = LSPIJUtils.toUri(baseDir.toFile()).toASCIIString(); + fileBaseDir = fileBaseDir.substring("file://".length()); + if (fileBaseDir.endsWith("/")) { + fileBaseDir = fileBaseDir.substring(0, fileBaseDir.length() - 1); + } + return fileBaseDir; + } + +} \ No newline at end of file
<note>
<to>Angelo</to>
<from>Fred</from>
<heading>Tests</heading>
<body>I wrote them!</body>
</note>