Skip to content

Commit

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

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jun 21, 2024
1 parent 6f183e1 commit 865b847
Show file tree
Hide file tree
Showing 12 changed files with 494 additions and 91 deletions.
94 changes: 93 additions & 1 deletion src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TextEdit> TEXT_EDITS_ASCENDING_COMPARATOR = (a, b) -> {
int diff = a.getRange().getStart().getLine() - b.getRange().getStart().getLine();
if (diff == 0) {
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,19 @@
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;
import org.jetbrains.annotations.Nullable;
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;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
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 @@ -12,28 +12,36 @@

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;

/**
* Converts Markdown to HTML
*/
public class MarkdownConverter {

private final Project project;
private static final Key<HtmlRenderer> HTML_RENDERER_KEY = Key.create("lsp.html.renderer");

public static final DataKey<Project> PROJECT_CONTEXT = new DataKey<>("LSP_PROJECT", (Project) null);
public static final DataKey<String> FILE_NAME_CONTEXT = new DataKey<>("LSP_FILE_NAME", "");
public static final DataKey<Language> LANGUAGE_CONTEXT = new DataKey<>("LSP_LANGUAGE", (Language)null);
public static final DataKey<Path> 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;
Expand Down Expand Up @@ -62,16 +70,18 @@ private MarkdownConverter(Project project) {

options.set(HtmlRenderer.SOFT_BREAK, "<br />\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 <code>markdown</code> to Html.
Expand All @@ -91,28 +101,56 @@ public NodeRenderer apply(DataHolder options) {
* the syntax coloration to use if MarkDown content contains some code block or blockquote to highlight.
* @return the given <code>markdown</code> 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 <code>markdown</code> to Html.
*
* @param markdown the MarkDown content to convert to Html.
* @param language the {@link Language} which must be used (if non-null) for MarkDown code block which defines the language or indented blockquote.
* @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 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));
}
Expand Down
Loading

0 comments on commit 865b847

Please sign in to comment.