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 25, 2024
1 parent 6f183e1 commit a9bb547
Show file tree
Hide file tree
Showing 14 changed files with 609 additions and 122 deletions.
16 changes: 15 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 @@ -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<LSPFileSupport> LSP_FILE_SUPPORT_KEY = Key.create("lsp.file.support");
private final PsiFile file;
Expand Down Expand Up @@ -118,6 +121,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 +317,7 @@ public static boolean hasSupport(@NotNull PsiFile file) {
return file.getUserData(LSP_FILE_SUPPORT_KEY) != null;
}

public PsiFile getFile() {
return file;
}
}
149 changes: 141 additions & 8 deletions 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 All @@ -48,6 +49,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* Utilities class for LSP.
Expand All @@ -63,6 +65,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 +127,121 @@ 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>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.
final String uri = fileUri;
if (ApplicationManager.getApplication().isDispatchThread()) {
return createFileAndOpenInEditor(uri, project);
} else {
AtomicBoolean result = new AtomicBoolean(false);
ApplicationManager.getApplication().invokeAndWait(() -> {
result.set(createFileAndOpenInEditor(uri, project));
});
return result.get();
}
}
return openInEditor(file, position, focusEditor, project);
}

private static boolean createFileAndOpenInEditor(@NotNull String fileUri, @NotNull Project project) {
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 false;
}

/**
* 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 @@ -153,19 +270,36 @@ public static boolean openInEditor(@Nullable VirtualFile file,
boolean focusEditor,
@NotNull Project project) {
if (file != null) {
if (position == null) {
return FileEditorManager.getInstance(project).openFile(file, true).length > 0;
final Document document = position != null ? LSPIJUtils.getDocument(file) : null;
if (ApplicationManager.getApplication().isDispatchThread()) {
return doOpenInEditor(file, position, document, focusEditor, project);
} else {
Document document = FileDocumentManager.getInstance().getDocument(file);
if (document != null) {
OpenFileDescriptor desc = new OpenFileDescriptor(project, file, LSPIJUtils.toOffset(position, document));
return FileEditorManager.getInstance(project).openTextEditor(desc, focusEditor) != null;
}
AtomicBoolean result = new AtomicBoolean(false);
ApplicationManager.getApplication().invokeAndWait(() -> {
result.set(doOpenInEditor(file, position, document, focusEditor, project));
});
return result.get();
}
}
return false;
}

private static boolean doOpenInEditor(@NotNull VirtualFile file,
@Nullable Position position,
@Nullable Document document,
boolean focusEditor,
@NotNull Project project) {
if (position == null) {
return FileEditorManager.getInstance(project).openFile(file, true).length > 0;
} else {
if (document != null) {
OpenFileDescriptor desc = new OpenFileDescriptor(project, file, LSPIJUtils.toOffset(position, document));
return FileEditorManager.getInstance(project).openTextEditor(desc, focusEditor) != null;
}
return false;
}
}

/**
* Returns the file language of the given file and null otherwise.
*
Expand Down Expand Up @@ -412,7 +546,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 @@ -176,7 +176,7 @@ public static CompletableFuture<ShowDocumentResult> showDocument(@NotNull ShowDo

CompletableFuture<ShowDocumentResult> future = new CompletableFuture<>();
ApplicationManager.getApplication()
.invokeLater(() -> {
.executeOnPooledThread(() -> {
if (LSPIJUtils.openInEditor(uri, position, focusEditor, project)) {
future.complete(SHOW_DOCUMENT_RESULT_WITH_SUCCESS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,24 @@
package com.redhat.devtools.lsp4ij.features.documentLink;

import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
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 @@ -71,7 +66,8 @@ public class LSPDocumentLinkGotoDeclarationHandler implements GotoDeclarationHan
CompletableFuture<List<DocumentLinkData>> documentLinkFuture = documentLinkSupport.getDocumentLinks(params);
try {
waitUntilDone(documentLinkFuture, psiFile);
} catch (ProcessCanceledException e) {//Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility
} catch (
ProcessCanceledException e) {//Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility
//TODO delete block when minimum required version is 2024.2
documentLinkSupport.cancel();
return null;
Expand Down Expand Up @@ -100,28 +96,12 @@ 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"));
}
}
});
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.
// If user accepts to create the file, the open is done after the creation of the file.
return PsiElement.EMPTY_ARRAY;
}
return new PsiElement[]{ LSPIJUtils.getPsiFile(targetFile, project)};
return new PsiElement[]{LSPIJUtils.getPsiFile(targetFile, project)};
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*******************************************************************************
* 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.ApiStatus;
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.
*/
@ApiStatus.Internal
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()
.executeOnPooledThread(() -> {
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 a9bb547

Please sign in to comment.