From 865b8477729048068c6e1fe498fb90e147cecb7e Mon Sep 17 00:00:00 2001
From: azerr
Date: Thu, 20 Jun 2024 15:26:06 +0200
Subject: [PATCH] feat: Support file link in hover
Fixes #376
Signed-off-by: azerr
---
.../redhat/devtools/lsp4ij/LSPIJUtils.java | 94 ++++++++++++++-
...LSPDocumentLinkGotoDeclarationHandler.java | 23 +---
.../LSPDocumentationLinkHandler.java | 29 +++++
.../documentation/LSPDocumentationTarget.java | 3 +
.../documentation/MarkdownConverter.java | 80 ++++++++----
.../markdown/LSPLinkResolver.java | 114 ++++++++++++++++++
.../SyntaxColorationCodeBlockRenderer.java | 20 ++-
.../TextMateHighlighterHelper.java | 3 +-
.../lsp4ij/hint/LSPNavigationLinkHandler.java | 113 +++++++++++------
src/main/resources/META-INF/plugin.xml | 3 +
.../documentation/MarkdownConverterTest.java | 15 ++-
.../MarkdownConverterWithPsiFileTest.java | 88 ++++++++++++++
12 files changed, 494 insertions(+), 91 deletions(-)
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationLinkHandler.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/LSPLinkResolver.java
rename src/main/java/com/redhat/devtools/lsp4ij/features/documentation/{ => markdown}/SyntaxColorationCodeBlockRenderer.java (87%)
rename src/main/java/com/redhat/devtools/lsp4ij/features/documentation/{ => markdown}/TextMateHighlighterHelper.java (94%)
create mode 100644 src/test/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverterWithPsiFileTest.java
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
index b9319dded..7c08fd68d 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
@@ -28,6 +28,7 @@
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.*;
import com.intellij.psi.PsiElement;
@@ -63,6 +64,10 @@ public class LSPIJUtils {
private static final String JRT_SCHEME = JRT_PROTOCOL + ":";
+ private static final String HASH_SEPARATOR = "#";
+
+ private static final String ENCODED_HASH_SEPARATOR = "%23";
+
private static final Comparator TEXT_EDITS_ASCENDING_COMPARATOR = (a, b) -> {
int diff = a.getRange().getStart().getLine() - b.getRange().getStart().getLine();
if (diff == 0) {
@@ -121,10 +126,98 @@ public static boolean openInEditor(@NotNull String fileUri,
@Nullable Position position,
boolean focusEditor,
@NotNull Project project) {
+ return openInEditor(fileUri, position, focusEditor, false, project);
+ }
+
+ /**
+ * Open the given fileUri with the given position in an editor.
+ *
+ * @param fileUri the file Uri.
+ * @param position the position.
+ * @param focusEditor true if editor will take the focus and false otherwise.
+ * @param project the project.
+ * @return true if the file was opened and false otherwise.
+ */
+ public static boolean openInEditor(@NotNull String fileUri,
+ @Nullable Position position,
+ boolean focusEditor,
+ boolean createFileIfNeeded,
+ @NotNull Project project) {
+ if (position == null) {
+ // Try to get position information from the fileUri
+ // ex :
+ // - file:///c:/Users/azerr/Downloads/simpleTest/simpleTest/yes.lua#L2
+ // - file:///c:/Users/azerr/Downloads/simpleTest/simpleTest/yes.lua#L2:5
+ // - file:///c%3A/Users/azerr/Downloads/simpleTest/simpleTest/yes.lua%23L2
+ String findHash = HASH_SEPARATOR;
+ int hashIndex = fileUri.lastIndexOf(findHash);
+ if (hashIndex == -1) {
+ findHash = ENCODED_HASH_SEPARATOR;
+ hashIndex = fileUri.lastIndexOf(findHash);
+ }
+ boolean hasPosition = hashIndex > 0 && hashIndex != fileUri.length() - 1;
+ if (hasPosition) {
+ position = toPosition(fileUri.substring(hashIndex + findHash.length()));
+ fileUri = fileUri.substring(0, hashIndex);
+ }
+ }
VirtualFile file = findResourceFor(fileUri);
+ if (file == null && createFileIfNeeded) {
+ // The file doesn't exit,
+ // open a dialog to confirm the creation of the file.
+ int result = Messages.showYesNoDialog(LanguageServerBundle.message("lsp.create.file.confirm.dialog.message", fileUri),
+ LanguageServerBundle.message("lsp.create.file.confirm.dialog.title"), Messages.getQuestionIcon());
+ if (result == Messages.YES) {
+ try {
+ // Create file
+ VirtualFile newFile = LSPIJUtils.createFile(fileUri);
+ if (newFile != null) {
+ // Open it in an editor
+ return LSPIJUtils.openInEditor(newFile, null, project);
+ }
+ } catch (Exception e) {
+ Messages.showErrorDialog(LanguageServerBundle.message("lsp.create.file.error.dialog.message", fileUri, e.getMessage()),
+ LanguageServerBundle.message("lsp.create.file.error.dialog.title"));
+ }
+ }
+ }
return openInEditor(file, position, focusEditor, project);
}
+ /**
+ * Convert position String 'L1:2' to an LSP {@link Position} and null otherwise.
+ *
+ * @param positionString the position string (ex: 'L1:2')
+ *
+ * @return position String 'L1:2' to an LSP {@link Position} and null otherwise.
+ */
+ private static Position toPosition(String positionString) {
+ if (positionString == null || positionString.isEmpty()) {
+ return null;
+ }
+ 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) - 1; // Line numbers should be 1-based
+ int character = toInt(1, positions);
+ return new Position(line, character);
+ }
+
+ private static int toInt(int index, String[] positions) {
+ if (index < positions.length) {
+ try {
+ return Integer.valueOf(positions[index]);
+ } catch (Exception e) {
+ }
+ }
+ return 0;
+ }
+
/**
* Open the given file with the given position in an editor.
*
@@ -412,7 +505,6 @@ public static URI toUri(Module module) {
* Return top-level directories which contain files related to the project.
*
* @param project the project.
- *
* @return top-level directories which contain files related to the project.
*/
@NotNull
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentLink/LSPDocumentLinkGotoDeclarationHandler.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentLink/LSPDocumentLinkGotoDeclarationHandler.java
index 1e8be0bb3..f5e8a2f41 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentLink/LSPDocumentLinkGotoDeclarationHandler.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentLink/LSPDocumentLinkGotoDeclarationHandler.java
@@ -20,15 +20,12 @@
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
-import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.TextRange;
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.lsp4ij.LSPFileSupport;
import com.redhat.devtools.lsp4ij.LSPIJUtils;
-import com.redhat.devtools.lsp4ij.LanguageServerBundle;
import com.redhat.devtools.lsp4ij.LanguageServersRegistry;
import org.eclipse.lsp4j.DocumentLink;
import org.eclipse.lsp4j.DocumentLinkParams;
@@ -36,7 +33,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
@@ -100,22 +96,9 @@ public class LSPDocumentLinkGotoDeclarationHandler implements GotoDeclarationHan
// which asks if user want to create the file.
// At this step we cannot open a dialog directly, we need to open the dialog
// with invoke later.
- ApplicationManager.getApplication().invokeLater(() -> {
- int result = Messages.showYesNoDialog(LanguageServerBundle.message("lsp.create.file.confirm.dialog.message", target),
- LanguageServerBundle.message("lsp.create.file.confirm.dialog.title"), Messages.getQuestionIcon());
- if (result == Messages.YES) {
- try {
- // Create file
- VirtualFile newFile = LSPIJUtils.createFile(target);
- if (newFile != null) {
- // Open it in an editor
- LSPIJUtils.openInEditor(newFile, null, project);
- }
- } catch (IOException e) {
- Messages.showErrorDialog(LanguageServerBundle.message("lsp.create.file.error.dialog.message", target, e.getMessage()),
- LanguageServerBundle.message("lsp.create.file.error.dialog.title"));
- }
- }
+ ApplicationManager.getApplication()
+ .invokeLater(() -> {
+ LSPIJUtils.openInEditor(target, null, true, true, project);
});
// Return an empty result here.
// If user accepts to create the file, the open is done after the creation of teh file.
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..c6435dcf7
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationLinkHandler.java
@@ -0,0 +1,29 @@
+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.LSPIJUtils;
+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.startsWith("file://")) {
+ ApplicationManager.getApplication()
+ .invokeLater(() -> {
+ var file = lspTarget.getFile();
+ LSPIJUtils.openInEditor(url, null, true, true, 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..07920e1f0 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
@@ -12,19 +12,21 @@
import com.intellij.lang.Language;
import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiFile;
+import com.redhat.devtools.lsp4ij.features.documentation.markdown.LSPLinkResolver;
+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;
import com.vladsch.flexmark.html.HtmlRenderer;
-import com.vladsch.flexmark.html.renderer.NodeRenderer;
-import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
import com.vladsch.flexmark.parser.Parser;
-import com.vladsch.flexmark.util.data.DataHolder;
+import com.vladsch.flexmark.util.data.DataKey;
import com.vladsch.flexmark.util.data.MutableDataSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.nio.file.Path;
import java.util.Arrays;
/**
@@ -32,8 +34,14 @@
*/
public class MarkdownConverter {
- private final Project project;
+ private static final Key HTML_RENDERER_KEY = Key.create("lsp.html.renderer");
+
+ public static final DataKey PROJECT_CONTEXT = new DataKey<>("LSP_PROJECT", (Project) null);
+ public static final DataKey FILE_NAME_CONTEXT = new DataKey<>("LSP_FILE_NAME", "");
+ public static final DataKey LANGUAGE_CONTEXT = new DataKey<>("LSP_LANGUAGE", (Language)null);
+ public static final DataKey BASE_DIR_CONTEXT = new DataKey<>("LSP_BASE_DIR", (Path)null);
+ private final Project project;
private final Parser htmlParser;
private final HtmlRenderer htmlRenderer;
private final MutableDataSet options;
@@ -62,16 +70,18 @@ private MarkdownConverter(Project project) {
options.set(HtmlRenderer.SOFT_BREAK, "
\n");
options.set(HtmlRenderer.GENERATE_HEADER_ID, true);
- htmlRenderer = HtmlRenderer.builder(options)
- .nodeRendererFactory(new NodeRendererFactory() {
- @Override
- public NodeRenderer apply(DataHolder options) {
- return new SyntaxColorationCodeBlockRenderer(project, null, null);
- }
- }).build();
+ options.set(PROJECT_CONTEXT, project);
+ htmlRenderer = createHtmlRenderer(options);
htmlParser = Parser.builder(options).build();
}
+ @NotNull
+ private static HtmlRenderer createHtmlRenderer(MutableDataSet options) {
+ return HtmlRenderer.builder(options)
+ .linkResolverFactory(new LSPLinkResolver.Factory())
+ .nodeRendererFactory(new SyntaxColorationCodeBlockRenderer.Factory())
+ .build();
+ }
/**
* Convert the given markdown
to Html.
@@ -91,11 +101,38 @@ 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) {
+ htmlRenderer = file.getUserData(HTML_RENDERER_KEY);
+ if (htmlRenderer == null) {
+ htmlRenderer = getHtmlRenderer(file);
+ }
+ }
+ return htmlRenderer.render(htmlParser.parse(markdown));
+ }
+
+ private synchronized HtmlRenderer getHtmlRenderer(PsiFile file) {
+ var htmlRenderer = file.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(BASE_DIR_CONTEXT, baseDir);
+
+ htmlRenderer = createHtmlRenderer(fileOptions);
+ file.putUserData(HTML_RENDERER_KEY, htmlRenderer);
+ return htmlRenderer;
}
/**
+ * This method is just used by Junit tests.
+ *
* Convert the given markdown
to Html.
*
* @param markdown the MarkDown content to convert to Html.
@@ -103,16 +140,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(BASE_DIR_CONTEXT, baseDir);
+ fileOptions.set(LANGUAGE_CONTEXT, language);
+ fileOptions.set(FILE_NAME_CONTEXT, fileName);
+ htmlRenderer = createHtmlRenderer(options);
}
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..71e298a85
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/markdown/LSPLinkResolver.java
@@ -0,0 +1,114 @@
+/*******************************************************************************
+ * 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.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 baseDir;
+
+ private enum FileUrlKind {
+ RELATIVE,
+ ABSOLUTE,
+ NONE;
+ }
+
+ public LSPLinkResolver(LinkResolverBasicContext context) {
+ this.baseDir = MarkdownConverter.BASE_DIR_CONTEXT.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 (baseDir != null && fileUrlKind == FileUrlKind.RELATIVE) {
+ String position = "";
+ int hashIndex= url.indexOf("#");
+ if (hashIndex != -1) {
+ position = url.substring(hashIndex, url.length());
+ url = url.substring(0, hashIndex);
+ }
+ try {
+ File resolvedFile = baseDir.resolve(url).toFile();
+ String resolvedUri = LSPIJUtils.toUri(resolvedFile).toASCIIString() + position;
+ return link.withStatus(LinkStatus.VALID)
+ .withUrl(resolvedUri);
+ }
+ catch(Exception e) {
+
+ }
+ }
+ }
+ return link;
+ }
+
+
+ private static FileUrlKind getFileUrlKind(String url) {
+ int index = url.indexOf("://");
+ if (index == -1) {
+ return FileUrlKind.RELATIVE;
+ }
+ if (url.substring(0, index).equals("file")) {
+ return FileUrlKind.ABSOLUTE;
+ }
+ return FileUrlKind.NONE;
+ }
+
+
+ public static class Factory implements LinkResolverFactory {
+
+ @Nullable
+ @Override
+ public Set> getAfterDependents() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Set> getBeforeDependents() {
+ return null;
+ }
+
+ @Override
+ public boolean affectsGlobalScope() {
+ return false;
+ }
+
+ @NotNull
+ @Override
+ public LinkResolver apply(@NotNull LinkResolverBasicContext context) {
+ return new LSPLinkResolver(context);
+ }
+ }
+
+}
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..800f67be7 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,8 @@
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.features.documentation.MarkdownConverter;
import com.redhat.devtools.lsp4ij.internal.SimpleLanguageUtils;
import com.redhat.devtools.lsp4ij.internal.StringUtils;
import com.vladsch.flexmark.ast.FencedCodeBlock;
@@ -24,8 +26,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;
@@ -54,10 +58,10 @@ public class SyntaxColorationCodeBlockRenderer implements NodeRenderer {
private final String fileName;
- public SyntaxColorationCodeBlockRenderer(Project project, Language fileLanguage, String fileName) {
- this.project = project;
- this.fileLanguage = fileLanguage;
- this.fileName = fileName;
+ public SyntaxColorationCodeBlockRenderer(DataHolder options) {
+ this.project = MarkdownConverter.PROJECT_CONTEXT.get(options);
+ this.fileLanguage = MarkdownConverter.LANGUAGE_CONTEXT.get(options);
+ this.fileName = MarkdownConverter.FILE_NAME_CONTEXT.get(options);
}
@Override
@@ -208,5 +212,11 @@ private static boolean hasTextMateSupport() {
}
}
+ public static class Factory implements NodeRendererFactory {
+ @Override
+ public NodeRenderer apply(DataHolder options) {
+ return new SyntaxColorationCodeBlockRenderer(options);
+ }
+ }
}
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"/>
+
""";
- assertEquals(html, toHtml(markdown, null, "test.ts"));
+ assertEquals(html, toHtml(markdown, null, null, "test.ts"));
}
public void testTypeScriptHighlightIndentedBlockquoteWithFileNameConversion() {
@@ -236,7 +238,7 @@ public void testTypeScriptHighlightIndentedBlockquoteWithFileNameConversion() {
""";
- assertEquals(html, toHtml(markdown, null, "test.ts"));
+ assertEquals(html, toHtml(markdown, null, null, "test.ts"));
}
public void testXmlHighlightIndentedBlockquoteWithLanguageConversion() {
@@ -255,14 +257,17 @@ public void testXmlHighlightIndentedBlockquoteWithLanguageConversion() {
""";
- assertEquals(html, toHtml(markdown, XMLLanguage.INSTANCE, null));
+ assertEquals(html, toHtml(markdown, null, XMLLanguage.INSTANCE, null));
}
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..849e0c271
--- /dev/null
+++ b/src/test/java/com/redhat/devtools/lsp4ij/features/documentation/MarkdownConverterWithPsiFileTest.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * 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.redhat.devtools.lsp4ij.fixtures.LSPCodeInsightFixtureTestCase;
+
+/**
+ * Test Markdown conversion to HTML by using PsiFile.
+ */
+public class MarkdownConverterWithPsiFileTest extends LSPCodeInsightFixtureTestCase {
+
+ 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:
+
+ ```
+
+
+ Angelo
+ Fred
+ Tests
+ I wrote them!
+
+ ```
+ """;
+
+ // As file is NOT an XML file, there are no syntax coloration
+ String html = """
+ 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:
+
+ ```
+
+
+ Angelo
+ Fred
+ Tests
+ I wrote them!
+
+ ```
+ """;
+
+ // As file is an XML file, the XML syntax coloration is used.
+ String html = """
+ 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);
+ }
+
+ private void assertMarkdownConverter(String fileName, String markdown, String html) {
+ myFixture.configureByText(fileName, "");
+ var file = myFixture.getFile();
+ String actual = MarkdownConverter.getInstance(file.getProject()).toHtml(markdown, file);
+ assertEquals(html, actual);
+ }
+
+}
\ No newline at end of file