Skip to content

Commit dd8fcef

Browse files
committed
fix: Read access is allowed from inside read-action only in
LSPUsageSearcher Fixes #1605 Signed-off-by: azerr <azerr@redhat.com>
1 parent 2b6a905 commit dd8fcef

1 file changed

Lines changed: 90 additions & 82 deletions

File tree

src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java

Lines changed: 90 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import com.intellij.find.findUsages.CustomUsageSearcher;
1414
import com.intellij.find.findUsages.FindUsagesOptions;
15+
import com.intellij.openapi.application.ApplicationManager;
1516
import com.intellij.openapi.editor.Document;
1617
import com.intellij.openapi.progress.ProcessCanceledException;
1718
import com.intellij.openapi.project.Project;
@@ -26,7 +27,6 @@
2627
import com.intellij.util.Processor;
2728
import com.intellij.util.containers.ContainerUtil;
2829
import com.redhat.devtools.lsp4ij.LSPIJUtils;
29-
import com.redhat.devtools.lsp4ij.LanguageServiceAccessor;
3030
import org.eclipse.lsp4j.Position;
3131
import org.jetbrains.annotations.NotNull;
3232
import org.jetbrains.annotations.Nullable;
@@ -77,6 +77,9 @@ public class LSPUsageSearcher extends CustomUsageSearcher {
7777

7878
private static final Logger LOGGER = LoggerFactory.getLogger(LSPUsageSearcher.class);
7979

80+
record ElementContext(Project project, Position position, PsiFile file) {
81+
}
82+
8083
/**
8184
* Processes all usages of the given element by querying the language server.
8285
* <p>
@@ -103,100 +106,105 @@ public class LSPUsageSearcher extends CustomUsageSearcher {
103106
* separate results.
104107
* </p>
105108
*
106-
* @param element the PSI element to find usages for (typically an {@link LSPUsageTriggeredPsiElement}).
109+
* @param element the PSI element to find usages for (typically an {@link LSPUsageTriggeredPsiElement}).
107110
* @param processor the processor to handle each found usage.
108-
* @param options the Find Usages options, including the search scope.
111+
* @param options the Find Usages options, including the search scope.
109112
*/
110113
@Override
111114
public void processElementUsages(@NotNull PsiElement element, @NotNull Processor<? super Usage> processor, @NotNull FindUsagesOptions options) {
112-
// 1. Collect only what we need from the element inside a narrow ReadAction
113-
Project project = element.getProject();
114-
Position position = runCancellableReadAction(() -> getPosition(element, element.getContainingFile()), project);
115-
PsiFile file = runCancellableReadAction(() -> element.getContainingFile(), project);
116-
117-
if (position == null || file == null) {
118-
return;
119-
}
120-
if (!isUsageSupportedByLanguageServer(file)) {
121-
return;
122-
}
115+
// 1. Collect only what we need from the element inside a narrow ReadAction
116+
ElementContext ctx = runCancellableReadAction(() -> {
117+
PsiFile file = element.getContainingFile();
118+
return new ElementContext(file.getProject(), getPosition(element, file), file);
119+
}, ApplicationManager.getApplication());
120+
Project project = ctx.project();
121+
Position position = ctx.position();
122+
PsiFile file = ctx.file();
123+
124+
125+
if (position == null || file == null) {
126+
return;
127+
}
128+
if (!isUsageSupportedByLanguageServer(file)) {
129+
return;
130+
}
123131

124-
SearchScope searchScope = options.searchScope;
125-
126-
// Fast path: return cached references if available (avoids redundant LSP requests)
127-
if (element instanceof LSPUsageTriggeredPsiElement elt) {
128-
if (elt.getLSPReferences() != null) {
129-
elt.getLSPReferences()
130-
.forEach(ref -> {
131-
runCancellableReadAction(() -> {
132-
var psiElement = LSPUsagesManager.toPsiElement(ref.location(), ref.languageServer().getClientFeatures(), LSPUsagePsiElement.UsageKind.references, project);
133-
if (psiElement != null) {
134-
VirtualFile psiElementFile = LSPIJUtils.getFile(psiElement);
135-
if (psiElementFile != null) {
136-
processor.process(new UsageInfo2UsageAdapter(new UsageInfo(psiElement)));
137-
}
132+
SearchScope searchScope = options.searchScope;
133+
134+
// Fast path: return cached references if available (avoids redundant LSP requests)
135+
if (element instanceof LSPUsageTriggeredPsiElement elt) {
136+
if (elt.getLSPReferences() != null) {
137+
elt.getLSPReferences()
138+
.forEach(ref -> {
139+
runCancellableReadAction(() -> {
140+
var psiElement = LSPUsagesManager.toPsiElement(ref.location(), ref.languageServer().getClientFeatures(), LSPUsagePsiElement.UsageKind.references, project);
141+
if (psiElement != null) {
142+
VirtualFile psiElementFile = LSPIJUtils.getFile(psiElement);
143+
if (psiElementFile != null) {
144+
processor.process(new UsageInfo2UsageAdapter(new UsageInfo(psiElement)));
138145
}
139-
}, project);
140-
});
141-
return;
142-
}
146+
}
147+
}, project);
148+
});
149+
return;
143150
}
151+
}
144152

145-
// 2. Dispatch the async LSP request OUTSIDE of the ReadAction
146-
LSPUsageSupport usageSupport = new LSPUsageSupport(file);
147-
LSPUsageSupport.LSPUsageSupportParams params = new LSPUsageSupport.LSPUsageSupportParams(position);
148-
CompletableFuture<List<LSPUsagePsiElement>> usagesFuture = usageSupport.getFeatureData(params);
149-
try {
150-
// 3. Wait for the future OUTSIDE of the ReadAction
151-
// This allows the ForkJoinPool background threads to freely acquire ReadActions
152-
// to resolve the PSI elements during mapping!
153-
waitUntilDone(usagesFuture);
154-
if (usagesFuture.isDone()) {
155-
List<LSPUsagePsiElement> usages = usagesFuture.getNow(null);
156-
if (usages != null && !usages.isEmpty()) {
157-
// 4. Process the results (requires ReadAction again for VFS/PSI mapping)
158-
runCancellableReadAction(() -> {
159-
List<LSPUsagePsiElement> filteredUsages = new ArrayList<>(usages);
160-
// Remove invalid usages and deduplicate overlapping ranges
161-
filteredUsages.removeIf(usage -> ContainerUtil.exists(usages, otherUsage -> {
162-
VirtualFile usageFile = LSPIJUtils.getFile(usage);
163-
if (usageFile == null) {
164-
return true;
165-
}
166-
// TODO: respect search scope - currently disabled
167-
// if (!searchScope.contains(usageFile)) return true;
168-
169-
// Remove usages that fully contain other usages (keep the more specific one)
170-
if (usage != otherUsage) {
171-
TextRange textRange = usage.getTextRange();
172-
TextRange otherTextRange = otherUsage.getTextRange();
173-
return (textRange != null) &&
174-
(otherTextRange != null) &&
175-
textRange.contains(otherTextRange) &&
176-
!textRange.equals(otherTextRange);
177-
}
153+
// 2. Dispatch the async LSP request OUTSIDE of the ReadAction
154+
LSPUsageSupport usageSupport = new LSPUsageSupport(file);
155+
LSPUsageSupport.LSPUsageSupportParams params = new LSPUsageSupport.LSPUsageSupportParams(position);
156+
CompletableFuture<List<LSPUsagePsiElement>> usagesFuture = usageSupport.getFeatureData(params);
157+
try {
158+
// 3. Wait for the future OUTSIDE of the ReadAction
159+
// This allows the ForkJoinPool background threads to freely acquire ReadActions
160+
// to resolve the PSI elements during mapping!
161+
waitUntilDone(usagesFuture);
162+
if (usagesFuture.isDone()) {
163+
List<LSPUsagePsiElement> usages = usagesFuture.getNow(null);
164+
if (usages != null && !usages.isEmpty()) {
165+
// 4. Process the results (requires ReadAction again for VFS/PSI mapping)
166+
runCancellableReadAction(() -> {
167+
List<LSPUsagePsiElement> filteredUsages = new ArrayList<>(usages);
168+
// Remove invalid usages and deduplicate overlapping ranges
169+
filteredUsages.removeIf(usage -> ContainerUtil.exists(usages, otherUsage -> {
170+
VirtualFile usageFile = LSPIJUtils.getFile(usage);
171+
if (usageFile == null) {
172+
return true;
173+
}
174+
// TODO: respect search scope - currently disabled
175+
// if (!searchScope.contains(usageFile)) return true;
176+
177+
// Remove usages that fully contain other usages (keep the more specific one)
178+
if (usage != otherUsage) {
179+
TextRange textRange = usage.getTextRange();
180+
TextRange otherTextRange = otherUsage.getTextRange();
181+
return (textRange != null) &&
182+
(otherTextRange != null) &&
183+
textRange.contains(otherTextRange) &&
184+
!textRange.equals(otherTextRange);
185+
}
178186

179-
return false;
180-
}));
187+
return false;
188+
}));
181189

182-
for (LSPUsagePsiElement usage : filteredUsages) {
183-
processor.process(new UsageInfo2UsageAdapter(new UsageInfo(usage)));
184-
}
185-
}, project);
186-
}
190+
for (LSPUsagePsiElement usage : filteredUsages) {
191+
processor.process(new UsageInfo2UsageAdapter(new UsageInfo(usage)));
192+
}
193+
}, project);
187194
}
188-
} catch (ProcessCanceledException pce) {
189-
throw pce;
190-
} catch (Exception e) {
191-
LOGGER.error("Error while collection LSP Usages", e);
192195
}
196+
} catch (ProcessCanceledException pce) {
197+
throw pce;
198+
} catch (Exception e) {
199+
LOGGER.error("Error while collection LSP Usages", e);
200+
}
193201

194-
LSPExternalReferencesFinder.processExternalReferences(
195-
file,
196-
element.getTextOffset(),
197-
searchScope,
198-
reference -> processor.process(new UsageInfo2UsageAdapter(new UsageInfo(reference)))
199-
);
202+
LSPExternalReferencesFinder.processExternalReferences(
203+
file,
204+
element.getTextOffset(),
205+
searchScope,
206+
reference -> processor.process(new UsageInfo2UsageAdapter(new UsageInfo(reference)))
207+
);
200208
}
201209

202210
/**

0 commit comments

Comments
 (0)