Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

import com.intellij.codeInsight.navigation.CtrlMouseHandler2;
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProcessCanceledException;
Expand All @@ -27,7 +31,10 @@
import com.redhat.devtools.lsp4ij.LSPFileSupport;
import com.redhat.devtools.lsp4ij.LSPIJUtils;
import com.redhat.devtools.lsp4ij.LanguageServersRegistry;
import com.redhat.devtools.lsp4ij.features.references.LSPReferenceCollector;
import com.redhat.devtools.lsp4ij.features.semanticTokens.viewProvider.LSPSemanticTokensFileViewProvider;
import com.redhat.devtools.lsp4ij.usages.LSPUsageType;
import com.redhat.devtools.lsp4ij.usages.LSPUsagesManager;
import com.redhat.devtools.lsp4ij.usages.LocationData;
import org.eclipse.lsp4j.SemanticTokenTypes;
import org.eclipse.lsp4j.TextDocumentIdentifier;
Expand All @@ -37,6 +44,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
Expand All @@ -56,17 +64,10 @@ public class LSPGotoDeclarationHandler implements GotoDeclarationHandler {
public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement,
int offset,
Editor editor) {
Project project = editor.getProject();
if (project == null || project.isDisposed()) {
return PsiElement.EMPTY_ARRAY;
}
PsiFile psiFile = sourceElement != null ? sourceElement.getContainingFile() : null;
PsiFile psiFile = getPsiFile(sourceElement, editor);
if (psiFile == null) {
return PsiElement.EMPTY_ARRAY;
}
if (!LanguageServersRegistry.getInstance().isFileSupported(psiFile)) {
return PsiElement.EMPTY_ARRAY;
}

// If this was called for a populated semantic tokens view provider, try to get the target directly from it
LSPSemanticTokensFileViewProvider semanticTokensFileViewProvider = LSPSemanticTokensFileViewProvider.getInstance(psiFile);
Expand All @@ -77,19 +78,23 @@ public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement
PsiElement target = reference != null ? reference.resolve() : null;
return target != null ? new PsiElement[]{target} : PsiElement.EMPTY_ARRAY;
}
// If it's definitely a declaration, just return an empty set of targets
// If it's definitely a declaration, skip definition request and go straight to references
else if (semanticTokensFileViewProvider.isDeclaration(offset)) {
return PsiElement.EMPTY_ARRAY;
return handleReferenceFallback(psiFile, editor, offset);
}
}

// Use LSP to find targets
PsiElement[] targets = getGotoDeclarationTargets(sourceElement, offset);
// First try the regular definition request; when it only points back to the caret,
// fall back to references so the user still sees usages for "Declaration or Usages".
PsiElement[] resolvedTargets = getGotoDeclarationTargets(sourceElement, offset);
if (shouldFallBackToReferences(sourceElement, resolvedTargets)) {
return handleReferenceFallback(psiFile, editor, offset);
}

// If this is a semantic token-backed file and there were targets but this wasn't represented in semantic tokens
// as a reference, stub a reference for the word at the current offset
if (semanticTokensFileViewProvider != null) {
if (!ArrayUtil.isEmpty(targets)) {
if (!ArrayUtil.isEmpty(resolvedTargets)) {
TextRange wordRange = LSPIJUtils.getWordRangeAt(editor.getDocument(), psiFile, offset);
if (wordRange != null) {
// This will ensure it's stubbed as a generic reference
Expand All @@ -102,12 +107,12 @@ else if (semanticTokensFileViewProvider.isDeclaration(offset)) {
// declaration. Otherwise things that can't act as references will show up as hyperlinked incorrectly.
// Unfortunately there's no symbolic state available as to whether or not this was invoked that way, so
// we have to check the stack trace for the known caller.
if (ExceptionUtil.currentStackTrace().contains(CtrlMouseHandler2.class.getName())) {
if (isCtrlMouseInvocation()) {
throw new ProcessCanceledException();
}
}

return targets;
return resolvedTargets;
}

/**
Expand Down Expand Up @@ -137,9 +142,6 @@ else if (semanticTokensFileViewProvider.isDeclaration(offset)) {
CompletableFuture<List<LocationData>> definitionsFuture = definitionSupport.getDefinitions(params);
try {
waitUntilDone(definitionsFuture, psiFile);
} catch (ProcessCanceledException ex) {
// cancel the LSP requests textDocument/definition
definitionSupport.cancel();
} catch (CancellationException ex) {
// cancel the LSP requests textDocument/definition
definitionSupport.cancel();
Expand All @@ -161,4 +163,79 @@ else if (semanticTokensFileViewProvider.isDeclaration(offset)) {
}
return PsiElement.EMPTY_ARRAY;
}

private static boolean shouldFallBackToReferences(@Nullable PsiElement sourceElement,
@Nullable PsiElement[] targets) {
if (sourceElement == null || targets == null || targets.length == 0) {
return sourceElement != null;
}
return Arrays.stream(targets)
.allMatch(target -> target == null || isSameElement(target, sourceElement));
}

private PsiElement[] handleReferenceFallback(@NotNull PsiFile psiFile, @NotNull Editor editor, int offset) {
List<LocationData> referenceLocations = LSPReferenceCollector.collect(psiFile, editor.getDocument(), offset);
if (!referenceLocations.isEmpty()) {
if (isCtrlMouseInvocation()) {
return toPsiElements(referenceLocations, psiFile.getProject());
}
showReferencesPopup(psiFile, editor, referenceLocations);
throw new ProcessCanceledException();
}
return PsiElement.EMPTY_ARRAY;
}

@Nullable
private static PsiFile getPsiFile(@Nullable PsiElement sourceElement, @NotNull Editor editor) {
Project project = editor.getProject();
if (project == null || project.isDisposed()) {
return null;
}
PsiFile psiFile = sourceElement != null ? sourceElement.getContainingFile() : null;
if (psiFile == null || !LanguageServersRegistry.getInstance().isFileSupported(psiFile)) {
return null;
}
return psiFile;
}

private static boolean isSameElement(@Nullable PsiElement target, @Nullable PsiElement source) {
if (target == null || source == null) {
return false;
}
VirtualFile targetFile = getVirtualFile(target);
VirtualFile sourceFile = getVirtualFile(source);
return targetFile != null && targetFile.equals(sourceFile) && target.getTextOffset() == source.getTextOffset();
}

@Nullable
private static VirtualFile getVirtualFile(@NotNull PsiElement element) {
PsiFile file = element.getContainingFile();
return file != null ? file.getVirtualFile() : null;
}

private void showReferencesPopup(@NotNull PsiFile psiFile,
@NotNull Editor editor,
@NotNull List<LocationData> locations) {
Project project = psiFile.getProject();
ApplicationManager.getApplication().invokeLater(() -> {
var dataContext = SimpleDataContext.builder()
.add(CommonDataKeys.PSI_FILE, psiFile)
.add(CommonDataKeys.EDITOR, editor)
.add(PlatformCoreDataKeys.CONTEXT_COMPONENT, editor.getContentComponent())
.build();
LSPUsagesManager.getInstance(project)
.findShowUsagesInPopup(locations, LSPUsageType.References, dataContext, null);
});
}

private PsiElement[] toPsiElements(@NotNull List<LocationData> locations, @NotNull Project project) {
return locations.stream()
.map(location -> toPsiElement(location.location(), location.languageServer().getClientFeatures(), project))
.filter(Objects::nonNull)
.toArray(PsiElement[]::new);
}

private static boolean isCtrlMouseInvocation() {
return ExceptionUtil.currentStackTrace().contains(CtrlMouseHandler2.class.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,34 @@

import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.psi.PsiFile;
import com.redhat.devtools.lsp4ij.LSPFileSupport;
import com.redhat.devtools.lsp4ij.LSPIJUtils;
import com.redhat.devtools.lsp4ij.LanguageServerBundle;
import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures;
import com.redhat.devtools.lsp4ij.features.AbstractLSPGoToAction;
import com.redhat.devtools.lsp4ij.usages.LSPUsageType;
import com.redhat.devtools.lsp4ij.usages.LocationData;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone;
import com.redhat.devtools.lsp4ij.LanguageServerBundle;
import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures;
import com.redhat.devtools.lsp4ij.features.AbstractLSPGoToAction;
import com.redhat.devtools.lsp4ij.usages.LSPUsageType;
import com.redhat.devtools.lsp4ij.usages.LocationData;
import org.jetbrains.annotations.NotNull;

import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
* LSP Go To Reference.
*/
public class LSPGoToReferenceAction extends AbstractLSPGoToAction {

private static final Logger LOGGER = LoggerFactory.getLogger(LSPGoToReferenceAction.class);

public LSPGoToReferenceAction() {
super(LSPUsageType.References);
}

@Override
protected CompletableFuture<List<LocationData>> getLocations(@NotNull PsiFile psiFile,
@NotNull Document document,
@NotNull Editor editor,
int offset) {
LSPReferenceSupport referenceSupport = LSPFileSupport.getSupport(psiFile).getReferenceSupport();
var params = new LSPReferenceParams(new TextDocumentIdentifier(), LSPIJUtils.toPosition(offset, document), offset);
CompletableFuture<List<LocationData>> referencesFuture = referenceSupport.getReferences(params);
try {
waitUntilDone(referencesFuture, psiFile);
} catch (ProcessCanceledException ex) {
// cancel the LSP requests textDocument/references
referenceSupport.cancel();
} catch (CancellationException ex) {
// cancel the LSP requests textDocument/references
referenceSupport.cancel();
} catch (ExecutionException e) {
LOGGER.error("Error while consuming LSP 'textDocument/references' request", e);
}
return referencesFuture;
}
protected CompletableFuture<List<LocationData>> getLocations(@NotNull PsiFile psiFile,
@NotNull Document document,
@NotNull Editor editor,
int offset) {
List<LocationData> locations = LSPReferenceCollector.collect(psiFile, document, offset);
return CompletableFuture.completedFuture(locations);
}

@Override
protected boolean canSupportFeature(@NotNull LSPClientFeatures clientFeatures, @NotNull PsiFile file) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.redhat.devtools.lsp4ij.features.references;

import com.intellij.openapi.editor.Document;
import com.intellij.psi.PsiFile;
import com.redhat.devtools.lsp4ij.LSPFileSupport;
import com.redhat.devtools.lsp4ij.LSPIJUtils;
import com.redhat.devtools.lsp4ij.usages.LocationData;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally;
import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone;

public final class LSPReferenceCollector {

private static final Logger LOGGER = LoggerFactory.getLogger(LSPReferenceCollector.class);

public static List<LocationData> collect(@NotNull PsiFile psiFile,
@NotNull Document document,
int offset) {
LSPReferenceSupport referenceSupport = LSPFileSupport.getSupport(psiFile).getReferenceSupport();
var params = new LSPReferenceParams(new TextDocumentIdentifier(), LSPIJUtils.toPosition(offset, document), offset);
CompletableFuture<List<LocationData>> referencesFuture = referenceSupport.getReferences(params);
try {
waitUntilDone(referencesFuture, psiFile);
} catch (CancellationException ex) {
referenceSupport.cancel();
} catch (ExecutionException e) {
LOGGER.error("Error while consuming LSP 'textDocument/references' request", e);
}

if (isDoneNormally(referencesFuture)) {
List<LocationData> locations = referencesFuture.getNow(null);
if (locations != null) {
return locations;
}
}
return Collections.emptyList();
}
}
Loading