Skip to content

Commit

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

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Jun 22, 2024
1 parent 6f183e1 commit 34ce82c
Show file tree
Hide file tree
Showing 13 changed files with 494 additions and 104 deletions.
14 changes: 13 additions & 1 deletion src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,7 +37,7 @@
* 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 {
public class LSPFileSupport extends UserDataHolderBase implements Disposable {

private static final Key<LSPFileSupport> LSP_FILE_SUPPORT_KEY = Key.create("lsp.file.support");
private final PsiFile file;
Expand Down Expand Up @@ -118,6 +119,14 @@ public void dispose() {
getReferenceSupport().cancel();
getDeclarationSupport().cancel();
getTypeDefinitionSupport().cancel();
var map = getUserMap();
var keys= map.getKeys();
for(var key : keys) {
var value = map.get(key);
if (value instanceof Disposable) {
((Disposable) value).dispose();
}
}
}

/**
Expand Down Expand Up @@ -306,4 +315,7 @@ public static boolean hasSupport(@NotNull PsiFile file) {
return file.getUserData(LSP_FILE_SUPPORT_KEY) != null;
}

public PsiFile getFile() {
return file;
}
}
104 changes: 103 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 + ":";

public 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,108 @@ 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 fileUrl in an editor.
*
* <p>
* the following syntax is supported for fileUrl:
* <ul>
* <li>file:///C:/Users/username/foo.txt</li>
* <li>C:/Users/username/foo.txt</li>
* <li>foo.txt</li>
* <li>file:///C:/Users/username/foo.txt#L1:5</li>
* </ul>
* </p>
* @param fileUri the file Uri to open.
* @param position the position.
* @param focusEditor true if editor will take the focus and false otherwise.
* @param createFileIfNeeded true if file must be created if doesn't exist and false otherwise.
* @param project the project.
* @return true if file Url can be 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 +515,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,43 @@
/*******************************************************************************
* 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 http://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.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;

/**
* LSP {@link DocumentationLinkHandler} to open file in an
* IJ Editor (and with a given position if declared) declared as link in HTML.
*/
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;
}
}
Loading

0 comments on commit 34ce82c

Please sign in to comment.