-
Notifications
You must be signed in to change notification settings - Fork 10
Enable Library Source Navigation for AppMap References #937
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package appland.files; | ||
|
|
||
| import com.intellij.openapi.extensions.ExtensionPointName; | ||
| import com.intellij.openapi.project.Project; | ||
| import com.intellij.openapi.vfs.VirtualFile; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| /** | ||
| * Extension point to locate files referenced in AppMaps, e.g. from external libraries. | ||
| */ | ||
| public interface AppMapFileLookup { | ||
| ExtensionPointName<AppMapFileLookup> EP_NAME = ExtensionPointName.create("appland.files.fileLookup"); | ||
|
|
||
| /** | ||
| * Context data for file lookup. | ||
| * | ||
| * @param relativePath Path from AppMap, typically relative to appmap.yml | ||
| * @param line 1-based line number from AppMap, or null if not available | ||
| */ | ||
| record Data(@NotNull String relativePath, @Nullable Integer line) { | ||
| } | ||
|
|
||
| /** | ||
| * @param project Current project | ||
| * @param data Context data for the lookup | ||
| * @return The target file, if found by this contributor. | ||
| */ | ||
| @Nullable | ||
| VirtualFile findFile(@NotNull Project project, @NotNull Data data); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm not mistaken, data.line is currently always null and |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package appland.files; | ||
|
|
||
| import com.intellij.openapi.diagnostic.Logger; | ||
| import com.intellij.openapi.project.Project; | ||
| import com.intellij.openapi.vfs.VirtualFile; | ||
| import com.intellij.psi.search.FilenameIndex; | ||
| import com.intellij.psi.search.GlobalSearchScope; | ||
| import com.intellij.util.PathUtil; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Generic fallback implementation for finding files in external libraries. | ||
| * <p> | ||
| * This lookup searches across all project scopes (including libraries) using filename | ||
| * and relative path matching. It runs after language-specific lookups (order="last") | ||
| * and supports any file type (Python, Ruby, JavaScript, etc.). | ||
| * <p> | ||
| * Uses {@link FilenameIndex} for performance and filters by relative path suffix to | ||
| * minimize false matches. | ||
| */ | ||
| public class AppMapGenericFileLookup implements AppMapFileLookup { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic implemented here is basically the same as the |
||
| private static final Logger LOG = Logger.getInstance(AppMapGenericFileLookup.class); | ||
|
|
||
| @Override | ||
| public @Nullable VirtualFile findFile(@NotNull Project project, @NotNull Data data) { | ||
| // Search by filename across all scopes (project + libraries) | ||
| String filename = PathUtil.getFileName(data.relativePath()); // e.g., "Filter.java" | ||
| Collection<VirtualFile> candidates = FilenameIndex.getVirtualFilesByName( | ||
| filename, true, GlobalSearchScope.allScope(project) | ||
| ); | ||
|
|
||
| // Filter to only files whose full path ends with the relative path | ||
| // This is specific enough to avoid most false matches | ||
| List<VirtualFile> matches = candidates.stream() | ||
| .filter(file -> file.getPath().endsWith(data.relativePath())) | ||
| .toList(); | ||
|
Comment on lines
+36
to
+40
|
||
|
|
||
| if (matches.isEmpty()) return null; | ||
|
|
||
| if (matches.size() > 1) { | ||
| LOG.warn("Multiple candidates found for " + data.relativePath() + | ||
| ", using first match from: " + matches); | ||
| } | ||
|
|
||
| return matches.get(0); | ||
|
Comment on lines
+44
to
+49
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import appland.index.AppMapSearchScopes; | ||||||||||||||||||||||||||||||||||
| import com.google.common.collect.Lists; | ||||||||||||||||||||||||||||||||||
| import com.intellij.openapi.diagnostic.Logger; | ||||||||||||||||||||||||||||||||||
| import com.intellij.openapi.project.DumbService; | ||||||||||||||||||||||||||||||||||
| import com.intellij.openapi.project.Project; | ||||||||||||||||||||||||||||||||||
| import com.intellij.openapi.util.SystemInfo; | ||||||||||||||||||||||||||||||||||
|
|
@@ -29,6 +30,8 @@ | |||||||||||||||||||||||||||||||||
| * This class tries to locate the best matching file in the project. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| public class FileLookup { | ||||||||||||||||||||||||||||||||||
| private static final Logger LOG = Logger.getInstance(FileLookup.class); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * @param project Current project | ||||||||||||||||||||||||||||||||||
| * @param base The base file or directory. This usually is the currently opened appmap file. {@code null} indicates that no context is available. | ||||||||||||||||||||||||||||||||||
|
|
@@ -98,6 +101,18 @@ public class FileLookup { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Fallback: try extension points for finding files in external libraries | ||||||||||||||||||||||||||||||||||
| // Language-specific extensions (e.g., Java) run first, followed by generic lookup | ||||||||||||||||||||||||||||||||||
| LOG.debug("File not found in project content, trying extension lookups: " + relativePath); | ||||||||||||||||||||||||||||||||||
| var data = new AppMapFileLookup.Data(relativePath, null); | ||||||||||||||||||||||||||||||||||
| for (var lookup : AppMapFileLookup.EP_NAME.getExtensionList()) { | ||||||||||||||||||||||||||||||||||
| var file = lookup.findFile(project, data); | ||||||||||||||||||||||||||||||||||
| if (file != null) { | ||||||||||||||||||||||||||||||||||
| LOG.debug("File found by extension " + lookup + ": " + file.getPath()); | ||||||||||||||||||||||||||||||||||
| return file; | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+112
|
||||||||||||||||||||||||||||||||||
| LOG.debug("File not found in project content, trying extension lookups: " + relativePath); | |
| var data = new AppMapFileLookup.Data(relativePath, null); | |
| for (var lookup : AppMapFileLookup.EP_NAME.getExtensionList()) { | |
| var file = lookup.findFile(project, data); | |
| if (file != null) { | |
| LOG.debug("File found by extension " + lookup + ": " + file.getPath()); | |
| return file; | |
| if (searchScope == null) { | |
| LOG.debug("File not found in project content, trying extension lookups: " + relativePath); | |
| var data = new AppMapFileLookup.Data(relativePath, null); | |
| for (var lookup : AppMapFileLookup.EP_NAME.getExtensionList()) { | |
| var file = lookup.findFile(project, data); | |
| if (file != null) { | |
| LOG.debug("File found by extension " + lookup + ": " + file.getPath()); | |
| return file; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |||||||||
|
|
||||||||||
| import java.nio.file.Files; | ||||||||||
| import java.nio.file.Path; | ||||||||||
| import java.nio.file.attribute.PosixFilePermission; | ||||||||||
| import java.util.List; | ||||||||||
|
|
||||||||||
| public class CliToolsTest extends AppMapBaseTest { | ||||||||||
|
|
@@ -94,6 +95,39 @@ public void bundledBinaryIsPreferred() throws Exception { | |||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| @Test | ||||||||||
| public void testBundledBinaryPermissionsFixed() throws Exception { | ||||||||||
| if (!SystemInfo.isUnix) { | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
Comment on lines
+100
to
+102
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: WIth an assumption it would show as an ignored test on Windows.
Suggested change
|
||||||||||
|
|
||||||||||
| // Mock binary name that matches the current platform/arch | ||||||||||
| var binaryName = String.format("appmap-%s-%s-v1000.0.0", CliTools.currentPlatform(), CliTools.currentArch()); | ||||||||||
| var mockBinary = createMockBinary(binaryName); | ||||||||||
|
|
||||||||||
| AppMapDeploymentTestUtils.withBundledBinaries(List.of(mockBinary), () -> { | ||||||||||
| // Find the file that was just copied | ||||||||||
| var bundledPath = AppMapDeploymentSettingsService.bundledBinarySearchPath().get(0).resolve(binaryName); | ||||||||||
|
|
||||||||||
| // Force it to be non-executable first | ||||||||||
| if (Files.isExecutable(bundledPath)) { | ||||||||||
| var perms = Files.getPosixFilePermissions(bundledPath); | ||||||||||
| perms.remove(PosixFilePermission.OWNER_EXECUTE); | ||||||||||
| perms.remove(PosixFilePermission.GROUP_EXECUTE); | ||||||||||
| perms.remove(PosixFilePermission.OTHERS_EXECUTE); | ||||||||||
| Files.setPosixFilePermissions(bundledPath, perms); | ||||||||||
| } | ||||||||||
| assertFalse(Files.isExecutable(bundledPath)); | ||||||||||
|
|
||||||||||
| // Now call the method under test | ||||||||||
| var binaryPath = CliTools.getBinaryPath(CliTool.AppMap); | ||||||||||
|
|
||||||||||
| assertNotNull(binaryPath); | ||||||||||
| assertEquals(binaryName, binaryPath.getFileName().toString()); | ||||||||||
| assertTrue("Bundled binary must be made executable", Files.isExecutable(binaryPath)); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private @NotNull Path createMockBinary(String filename) { | ||||||||||
| return myFixture.getTempDirFixture().createFile(filename).toNioPath(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||||||||
| package appland.java; | ||||||||||||
|
|
||||||||||||
| import appland.files.AppMapFileLookup; | ||||||||||||
| import com.intellij.openapi.project.Project; | ||||||||||||
| import com.intellij.openapi.vfs.VirtualFile; | ||||||||||||
| import com.intellij.psi.JavaPsiFacade; | ||||||||||||
| import com.intellij.psi.PsiClass; | ||||||||||||
| import com.intellij.psi.search.GlobalSearchScope; | ||||||||||||
| import org.jetbrains.annotations.NotNull; | ||||||||||||
| import org.jetbrains.annotations.Nullable; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Java-specific implementation for finding classes in external libraries (JARs). | ||||||||||||
| * <p> | ||||||||||||
| * Converts file paths to fully qualified class names and uses {@link JavaPsiFacade} | ||||||||||||
| * to locate classes. This automatically: | ||||||||||||
| * <ul> | ||||||||||||
| * <li>Resolves to source attachments when available</li> | ||||||||||||
| * <li>Falls back to decompiled view if no source is attached</li> | ||||||||||||
| * <li>Handles inner classes and classpath resolution correctly</li> | ||||||||||||
| * </ul> | ||||||||||||
| * This is the same mechanism IntelliJ's "Go to Class" uses internally. | ||||||||||||
| */ | ||||||||||||
| public class AppMapJavaFileLookup implements AppMapFileLookup { | ||||||||||||
| @Override | ||||||||||||
| public @Nullable VirtualFile findFile(@NotNull Project project, @NotNull Data data) { | ||||||||||||
| String relativePath = data.relativePath(); | ||||||||||||
| if (!relativePath.endsWith(".java")) { | ||||||||||||
| return null; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Convert path to FQN: org/springframework/web/filter/Filter.java -> org.springframework.web.filter.Filter | ||||||||||||
| String fqn = relativePath.substring(0, relativePath.length() - ".java".length()) | ||||||||||||
| .replace('/', '.') | ||||||||||||
| .replace('\\', '.'); | ||||||||||||
|
|
||||||||||||
| PsiClass psiClass = JavaPsiFacade.getInstance(project).findClass(fqn, GlobalSearchScope.allScope(project)); | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs a read access and also smart mode because of the index access.
Suggested change
|
||||||||||||
| if (psiClass == null) { | ||||||||||||
| return null; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return psiClass.getContainingFile().getVirtualFile(); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package appland.java; | ||
|
|
||
| import appland.files.AppMapFileLookup; | ||
| import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; | ||
|
|
||
| public class AppMapJavaFileLookupTest extends LightJavaCodeInsightFixtureTestCase { | ||
| public void testJavaLookup() { | ||
| myFixture.addClass("package org.example; public class Lib {}"); | ||
|
|
||
| var lookup = new AppMapJavaFileLookup(); | ||
| var data = new AppMapFileLookup.Data("org/example/Lib.java", null); | ||
|
|
||
| var found = lookup.findFile(getProject(), data); | ||
| assertNotNull(found); | ||
| assertEquals("Lib.java", found.getName()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unclear from the documentation if
relativePathis a /-delimited path or not.I think it should be added here that it's a /-delimited path (because it's passed down from
FileLookup)