-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathFileLookup.java
More file actions
151 lines (133 loc) · 6.19 KB
/
Copy pathFileLookup.java
File metadata and controls
151 lines (133 loc) · 6.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package appland.files;
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;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* This class locates files by relative paths.
* <p>
* AppMaps contain relative paths. Although these paths are usually based on the "project root path" the root path isn't
* clearly defined.
* <p>
* 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.
* @param relativePath A relative path, it has to use / as delimiter.
* @return The target file, if it was found
*/
@RequiresReadLock
public static @Nullable VirtualFile findRelativeFile(@NotNull Project project,
@Nullable VirtualFile base,
@NotNull String relativePath) {
return findRelativeFile(project, null, base, relativePath);
}
/**
* @param project Current project
* @param searchScope Search scope to restrict the search if it's a relative path
* @param base The base file or directory. This usually is the currently opened appmap file. {@code null} indicates that no context is available.
* @param relativePath A relative path, it has to use / as delimiter.
* @return The target file, if it was found
*/
@RequiresReadLock
public static @Nullable VirtualFile findRelativeFile(@NotNull Project project,
@Nullable GlobalSearchScope searchScope,
@Nullable VirtualFile base,
@NotNull String relativePath) {
var appliedSearchScope = AppMapSearchScopes.projectFilesWithExcluded(project);
if (searchScope != null) {
appliedSearchScope = appliedSearchScope.intersectWith(searchScope);
}
// support the rare case of absolute paths
if (isAbsolutePath(relativePath)) {
return LocalFileSystem.getInstance().findFileByPath(relativePath);
}
// 1st, try to locate relative to the base path
if (base != null) {
var baseDir = base.isDirectory() ? base : base.getParent();
var file = baseDir.findFileByRelativePath(relativePath);
if (file != null) {
return file;
}
}
if (DumbService.isDumb(project)) {
// no further lookup possible without an index
return null;
}
// Candidates need to be sorted by length to make sure "a/searched/file" is returned and not "a/a/searched/file".
var candidates = FilenameIndex.getVirtualFilesByName(filename(relativePath), true, appliedSearchScope)
.stream()
.sorted(Comparator.comparingInt(o -> o.getPath().length()));
for (var candidate : candidates.collect(Collectors.toList())) {
var parent = candidate.getParent();
for (String expectedParentName : parentsReversed(relativePath)) {
if (parent == null || !FileUtil.namesEqual(expectedParentName, parent.getName())) {
parent = null;
break;
}
parent = parent.getParent();
}
if (parent != null) {
// the candidate is matching all parent directories in the hierarchy
return candidate;
}
}
// 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;
}
}
// no match :(
return null;
}
public static boolean isAbsolutePath(@NotNull String relativePath) {
return isAbsolutePath(relativePath, SystemInfo.isWindows);
}
public static boolean isAbsolutePath(@NotNull String relativePath, boolean checkWindowsPath) {
// handle C:/dir/file.txt or C:\dir\file.txt
// /-based paths are generated by Ruby on Windows, for example
if (checkWindowsPath && relativePath.length() >= 3 && relativePath.charAt(1) == ':') {
var thirdChar = relativePath.charAt(2);
return thirdChar == '/' || thirdChar == '\\';
}
return relativePath.startsWith("/");
}
static String filename(@NotNull String path) {
int index = path.lastIndexOf('/');
if (index == -1) {
return path;
}
return path.substring(index + 1);
}
static List<String> parentsReversed(@NotNull String path) {
int index = path.lastIndexOf('/');
if (index == -1) {
return Collections.emptyList();
}
return Lists.reverse(StringUtil.split(path.substring(0, index), "/"));
}
}