From 882e4759c579b0217a0a37552ac0e2d76ff2a9a0 Mon Sep 17 00:00:00 2001 From: azerr Date: Thu, 10 Aug 2023 17:46:19 +0200 Subject: [PATCH] fix: Too many non-blocking read actions submitted at once in LSPDiagnosticHandler Fixes #1089 Signed-off-by: azerr --- .../intellij/lsp4ij/client/CoalesceByKey.java | 44 +++ .../client/IndexAwareLanguageClient.java | 14 +- .../lsp4ij/client/LSPCompletableFuture.java | 8 +- .../diagnostics/LSPDiagnosticHandler.java | 19 +- .../ClasspathResourceChangedNotifier.java | 26 +- .../jaxrs/java/DefaultJaxRsInfoProvider.java | 5 + .../quarkus/lsp/QuarkusLanguageClient.java | 42 ++- .../intellij/qute/lsp/QuteLanguageClient.java | 264 ++++++++++-------- 8 files changed, 276 insertions(+), 146 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/client/CoalesceByKey.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/CoalesceByKey.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/CoalesceByKey.java new file mode 100644 index 000000000..3409331db --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/CoalesceByKey.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2023 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 https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.client; + +import java.util.Arrays; +import java.util.Objects; + +/** + * A CoalesceBy key which defines a type (like LSP request) and a value which should be identified the request. + */ +public class CoalesceByKey { + + private final String type; + + private final Object[] value; + + public CoalesceByKey(String type, Object... value) { + this.type = type; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CoalesceByKey that = (CoalesceByKey) o; + return Objects.equals(type, that.type) && Arrays.equals(value, that.value); + } + + @Override + public int hashCode() { + int result = Objects.hash(type); + result = 31 * result + Arrays.hashCode(value); + return result; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/IndexAwareLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/IndexAwareLanguageClient.java index 7b77b1ffa..ce239c687 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/IndexAwareLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/IndexAwareLanguageClient.java @@ -39,7 +39,19 @@ public IndexAwareLanguageClient(Project project) { * @return the output of the function code */ protected CompletableFuture runAsBackground(String progressTitle, Function code) { - return new LSPCompletableFuture<>(code, progressTitle, IndexAwareLanguageClient.this); + return runAsBackground(progressTitle, code, null); + } + + /** + * Run the given function as a background task, wrapped in a read action + * + * @param progressTitle the progress title of the action being run + * @param code the function code to execute in the background + * @param the return type + * @return the output of the function code + */ + protected CompletableFuture runAsBackground(String progressTitle, Function code, Object coalesceBy) { + return new LSPCompletableFuture<>(code, progressTitle, IndexAwareLanguageClient.this, coalesceBy); } /** diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java index 9b7141d3a..ca63cb868 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java @@ -54,12 +54,15 @@ public ResultOrError(R result, Exception error) { private final IndexAwareLanguageClient languageClient; private final String progressTitle; private final AtomicInteger nbAttempt; + + private final Object coalesceBy; private CancellablePromise> nonBlockingReadActionPromise; - public LSPCompletableFuture(Function code, String progressTitle, IndexAwareLanguageClient languageClient) { + public LSPCompletableFuture(Function code, String progressTitle, IndexAwareLanguageClient languageClient, Object coalesceBy) { this.code = code; this.progressTitle = progressTitle; this.languageClient = languageClient; + this.coalesceBy = coalesceBy; this.nbAttempt = new AtomicInteger(0); // if indexation is processing, we need to execute the promise in smart mode var executeInSmartMode = DumbService.getInstance(languageClient.getProject()).isDumb(); @@ -136,6 +139,9 @@ private CancellablePromise> nonBlockingReadActionPromise(boolea if (executeInSmartMode) { action = action.inSmartMode(project); } + if (coalesceBy != null) { + action = action.coalesceBy(coalesceBy); + } return action .submit(AppExecutorUtil.getAppExecutorService()); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/diagnostics/LSPDiagnosticHandler.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/diagnostics/LSPDiagnosticHandler.java index 67f271dfc..acb9966d9 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/diagnostics/LSPDiagnosticHandler.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/diagnostics/LSPDiagnosticHandler.java @@ -17,6 +17,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; @@ -25,6 +26,7 @@ import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4ij.LSPVirtualFileWrapper; import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.intellij.lsp4ij.client.CoalesceByKey; import org.eclipse.lsp4j.PublishDiagnosticsParams; import java.util.function.Consumer; @@ -45,12 +47,23 @@ public LSPDiagnosticHandler(LanguageServerWrapper languageServerWrapper) { @Override public void accept(PublishDiagnosticsParams params) { + Project project = languageServerWrapper.getProject(); + if(project.isDisposed()) { + return; + } if (ApplicationManager.getApplication().isReadAccessAllowed()) { updateDiagnostics(params); } else { - ReadAction.nonBlocking(() -> updateDiagnostics(params)) - .submit(AppExecutorUtil.getAppExecutorService()); - + // Cancel if needed the previous "textDocument/publishDiagnostics" for a given uri. + var coalesceBy = new CoalesceByKey("textDocument/publishDiagnostics", params.getUri()); + var executeInSmartMode = DumbService.getInstance(languageServerWrapper.getProject()).isDumb(); + var action = ReadAction.nonBlocking(() -> updateDiagnostics(params)) + .expireWith(languageServerWrapper) + .coalesceBy(coalesceBy); + if (executeInSmartMode) { + action.inSmartMode(project); + } + action.submit(AppExecutorUtil.getAppExecutorService()); } } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java index 35fb93c36..a2cac68c1 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/classpath/ClasspathResourceChangedNotifier.java @@ -35,7 +35,7 @@ /** * Source file change notifier with a debounce mode. */ -public class ClasspathResourceChangedNotifier implements Disposable { +public class ClasspathResourceChangedNotifier implements Disposable { private static final long DEBOUNCE_DELAY = 1000; @@ -46,7 +46,8 @@ public class ClasspathResourceChangedNotifier implements Disposable { private final Set> sourceFiles; private boolean librariesChanged; -private final List processBeforeLibrariesChanged; + private final List processBeforeLibrariesChanged; + private boolean disposed; public ClasspathResourceChangedNotifier(Project project, List preprocessors) { this.project = project; @@ -74,6 +75,9 @@ public synchronized void addSourceFile(Pair pair) { } private void asyncNotifyChanges() { + if (isDisposed()) { + return; + } if (ApplicationManager.getApplication().isUnitTestMode()) { notifyChanges(); } else { @@ -93,6 +97,9 @@ public void run() { } private void notifyChanges() { + if (isDisposed()) { + return; + } synchronized (sourceFiles) { // Java, config sources files has changed project.getMessageBus().syncPublisher(ClasspathResourceChangedManager.TOPIC).sourceFilesChanged(sourceFiles); @@ -121,8 +128,7 @@ public void run(@NotNull ProgressIndicator progressIndicator) { for (var runnable : processBeforeLibrariesChanged) { runnable.run(progressIndicator); } - } - finally { + } finally { // Send the libraries changed event project.getMessageBus().syncPublisher(ClasspathResourceChangedManager.TOPIC).librariesChanged(); librariesChanged = false; @@ -134,13 +140,23 @@ public void run(@NotNull ProgressIndicator progressIndicator) { } } + public boolean isDisposed() { + return disposed; + } + @Override public void dispose() { + if (isDisposed()) { + return; + } + this.disposed = true; if (debounceTask != null) { debounceTask.cancel(); + debounceTask =null; } - if(debounceTimer != null) { + if (debounceTimer != null) { debounceTimer.cancel(); + debounceTimer = null; } } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/jaxrs/java/DefaultJaxRsInfoProvider.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/jaxrs/java/DefaultJaxRsInfoProvider.java index 94f24433d..3eabafcda 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/jaxrs/java/DefaultJaxRsInfoProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/jaxrs/java/DefaultJaxRsInfoProvider.java @@ -14,7 +14,9 @@ package com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.jaxrs.java; import com.intellij.openapi.module.Module; +import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.IndexNotReadyException; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; @@ -28,6 +30,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.logging.Level; import java.util.logging.Logger; @@ -64,6 +67,8 @@ public List getJaxRsMethodInfo(PsiFile typeRoot, JaxRsContext j List methodInfos = new ArrayList<>(); try { collectJaxRsMethodInfo(typeRoot.getChildren(), null, methodInfos, jaxrsContext, utils, monitor); + } catch (IndexNotReadyException | ProcessCanceledException | CancellationException e) { + throw e; } catch (Exception e) { LOGGER.log(Level.SEVERE, "while collecting JAX-RS method info using the default method", e); } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java index 7219484b3..5e27be2bc 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java @@ -15,6 +15,7 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.lsp4ij.client.CoalesceByKey; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.ProjectLabelManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.PropertiesManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.PropertiesManagerForJava; @@ -112,65 +113,81 @@ private boolean isConfigSource(VirtualFile file) { @Override public CompletableFuture getProjectInfo(MicroProfileProjectInfoParams params) { + var coalesceBy = new CoalesceByKey("microprofile/projectInfo", params.getUri(), params.getScopes()); String filePath = getFilePath(params.getUri()); return runAsBackground("Computing MicroProfile properties for '" + filePath + "'.", monitor -> PropertiesManager.getInstance().getMicroProfileProjectInfo(params, PsiUtilsLSImpl.getInstance(getProject()), monitor) - ); + , coalesceBy); } @Override public CompletableFuture getJavaHover(MicroProfileJavaHoverParams javaParams) { - return runAsBackground("Computing MicroProfile Java hover", monitor -> PropertiesManagerForJava.getInstance().hover(javaParams, PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/java/hover", javaParams.getUri(), javaParams.getPosition()); + return runAsBackground("Computing MicroProfile Java hover", monitor -> PropertiesManagerForJava.getInstance().hover(javaParams, PsiUtilsLSImpl.getInstance(getProject())), coalesceBy); } @Override public CompletableFuture> getJavaDiagnostics(MicroProfileJavaDiagnosticsParams javaParams) { - return runAsBackground("Computing MicroProfile Java diagnostics", monitor -> PropertiesManagerForJava.getInstance().diagnostics(javaParams, PsiUtilsLSImpl.getInstance(getProject()))); + // When project is indexing and user types a lot of characters in the Java editor, the MicroProfile language server + // validates the Java document and consumes a 'microprofile/java/diagnostics' for each typed character. + // The response of 'microprofile/java/diagnostics' are blocked (or takes some times) and we have + // "Too many non-blocking read actions submitted at once in". To avoid having this error, we create a coalesceBy key + // managed by IJ ReadAction.nonBlocking() to cancel the previous request. + var coalesceBy = new CoalesceByKey("microprofile/java/diagnostics", javaParams.getUris()); + return runAsBackground("Computing MicroProfile Java diagnostics", monitor -> PropertiesManagerForJava.getInstance().diagnostics(javaParams, PsiUtilsLSImpl.getInstance(getProject())), coalesceBy); } @Override public CompletableFuture getPropertyDefinition(MicroProfilePropertyDefinitionParams params) { - return runAsBackground("Computing property definition", monitor -> PropertiesManager.getInstance().findPropertyLocation(params, PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/propertyDefinition", params.getUri(), params.getSourceType(), params.getSourceField(), params.getSourceMethod()); + return runAsBackground("Computing property definition", monitor -> PropertiesManager.getInstance().findPropertyLocation(params, PsiUtilsLSImpl.getInstance(getProject())), coalesceBy); } @Override public CompletableFuture getJavaProjectLabels(MicroProfileJavaProjectLabelsParams javaParams) { - return runAsBackground("Computing Java projects labels", monitor -> ProjectLabelManager.getInstance().getProjectLabelInfo(javaParams, PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/java/projectLabels", javaParams.getUri(), javaParams.getTypes()); + return runAsBackground("Computing Java projects labels", monitor -> ProjectLabelManager.getInstance().getProjectLabelInfo(javaParams, PsiUtilsLSImpl.getInstance(getProject())), coalesceBy); } @Override public CompletableFuture> getAllJavaProjectLabels() { - return runAsBackground("Computing All Java projects labels", monitor -> ProjectLabelManager.getInstance().getProjectLabelInfo(PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/java/workspaceLabels"); + return runAsBackground("Computing All Java projects labels", monitor -> ProjectLabelManager.getInstance().getProjectLabelInfo(PsiUtilsLSImpl.getInstance(getProject())),coalesceBy); } @Override public CompletableFuture getJavaFileInfo(MicroProfileJavaFileInfoParams javaParams) { - return runAsBackground("Computing Java file info", monitor -> PropertiesManagerForJava.getInstance().fileInfo(javaParams, PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/java/fileInfo", javaParams.getUri()); + return runAsBackground("Computing Java file info", monitor -> PropertiesManagerForJava.getInstance().fileInfo(javaParams, PsiUtilsLSImpl.getInstance(getProject())), coalesceBy); } @Override public CompletableFuture> getJavaDefinition(MicroProfileJavaDefinitionParams javaParams) { - return runAsBackground("Computing Java definitions", monitor -> PropertiesManagerForJava.getInstance().definition(javaParams, PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/java/definition", javaParams.getUri(),javaParams.getPosition()); + return runAsBackground("Computing Java definitions", monitor -> PropertiesManagerForJava.getInstance().definition(javaParams, PsiUtilsLSImpl.getInstance(getProject())), coalesceBy); } @Override public CompletableFuture getJavaCompletion(MicroProfileJavaCompletionParams javaParams) { + var coalesceBy = new CoalesceByKey("microprofile/java/completion", javaParams.getUri(),javaParams.getPosition()); return runAsBackground("Computing Java completion", monitor -> { IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); CompletionList completionList = PropertiesManagerForJava.getInstance().completion(javaParams, utils); JavaCursorContextResult cursorContext = PropertiesManagerForJava.getInstance().javaCursorContext(javaParams, utils); return new MicroProfileJavaCompletionResult(completionList, cursorContext); - }); + }, coalesceBy); } @Override public CompletableFuture> getJavaCodelens(MicroProfileJavaCodeLensParams javaParams) { - return runAsBackground("Computing Java codelens", monitor -> PropertiesManagerForJava.getInstance().codeLens(javaParams, PsiUtilsLSImpl.getInstance(getProject()), monitor)); + var coalesceBy = new CoalesceByKey("microprofile/java/codeLens", javaParams.getUri()); + return runAsBackground("Computing Java codelens", monitor -> PropertiesManagerForJava.getInstance().codeLens(javaParams, PsiUtilsLSImpl.getInstance(getProject()), monitor), coalesceBy); } @Override public CompletableFuture> getJavaCodeAction(MicroProfileJavaCodeActionParams javaParams) { - return runAsBackground("Computing Java code actions", monitor -> (List) PropertiesManagerForJava.getInstance().codeAction(javaParams, PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/java/codeAction", javaParams.getUri()); + return runAsBackground("Computing Java code actions", monitor -> (List) PropertiesManagerForJava.getInstance().codeAction(javaParams, PsiUtilsLSImpl.getInstance(getProject())),coalesceBy); } @Override @@ -184,7 +201,8 @@ public CompletableFuture resolveCodeAction(CodeAction unresolved) { @Override public CompletableFuture getJavaCursorContext(MicroProfileJavaCompletionParams params) { - return runAsBackground("Computing Java Cursor context", monitor -> PropertiesManagerForJava.getInstance().javaCursorContext(params, PsiUtilsLSImpl.getInstance(getProject()))); + var coalesceBy = new CoalesceByKey("microprofile/java/javaCursorContext", params.getUri(), params.getPosition()); + return runAsBackground("Computing Java Cursor context", monitor -> PropertiesManagerForJava.getInstance().javaCursorContext(params, PsiUtilsLSImpl.getInstance(getProject())), coalesceBy); } @Override diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java index c77f7ca4e..741067acb 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java @@ -15,6 +15,7 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.lsp4ij.client.CoalesceByKey; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.core.ls.PsiUtilsLSImpl; import com.redhat.devtools.intellij.lsp4ij.client.IndexAwareLanguageClient; @@ -57,129 +58,144 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +/** + * Qute language client. + */ public class QuteLanguageClient extends IndexAwareLanguageClient implements QuteLanguageClientAPI, ClasspathResourceChangedManager.Listener { - private static final Logger LOGGER = LoggerFactory.getLogger(QuteLanguageClient.class); - - private final MessageBusConnection connection; - - public QuteLanguageClient(Project project) { - super(project); - connection = project.getMessageBus().connect(project); - connection.subscribe(ClasspathResourceChangedManager.TOPIC, this); - } - - @Override - public void dispose() { - super.dispose(); - connection.disconnect(); - } - - /** - * Send the notification qute/dataModelChanged with the project Uris to - * refresh data model used in Qute Template. - * - * @param uris the project uris where the data model must be refreshed. - */ - private void notifyQuteDataModelChanged(Set uris) { - QuteLanguageServerAPI server = (QuteLanguageServerAPI) getLanguageServer(); - if (server != null) { - JavaDataModelChangeEvent event = new JavaDataModelChangeEvent(); - event.setProjectURIs(uris); - server.dataModelChanged(event); - } - } - - @Override - public void librariesChanged() { - if (isDisposed()) { - // The language client has been disposed, ignore changes in libraries - return; - } - Set uris = new HashSet<>(); - uris.add(PsiQuteProjectUtils.getProjectURI(getProject())); - notifyQuteDataModelChanged(uris); - } - - @Override - public void sourceFilesChanged(Set> sources) { - if (isDisposed()) { - // The language client has been disposed, ignore changes in Java source files - return; - } - Set uris = sources.stream() - // qute/dataModelChanged must be sent only if there are some Java files which are changed - .filter(pair -> PsiMicroProfileProjectManager.isJavaFile(pair.getFirst())) - .map(pair -> pair.getSecond()) - .map(module -> PsiUtilsLSImpl.getProjectURI(module)) - .collect(Collectors.toSet()); - if (!uris.isEmpty()) { - notifyQuteDataModelChanged(uris); - } - } - - @Override - public CompletableFuture getProjectInfo(QuteProjectParams params) { - return runAsBackground("getProjectInfo", monitor -> QuteSupportForTemplate.getInstance().getProjectInfo(params, PsiUtilsLSImpl.getInstance(getProject()), monitor)); - } - - @Override - public CompletableFuture>> getDataModelProject( - QuteDataModelProjectParams params) { - return runAsBackground("getDataModel", monitor -> QuteSupportForTemplate.getInstance().getDataModelProject(params, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture> getJavaTypes(QuteJavaTypesParams params) { - return runAsBackground("getJavaTypes", monitor -> QuteSupportForTemplate.getInstance().getJavaTypes(params, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture getResolvedJavaType(QuteResolvedJavaTypeParams params) { - return runAsBackground("getResolvedJavaType", monitor -> QuteSupportForTemplate.getInstance().getResolvedJavaType(params, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture getJavaDefinition(QuteJavaDefinitionParams params) { - return runAsBackground("getJavaDefinition", monitor -> QuteSupportForTemplate.getInstance().getJavaDefinition(params, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture> getJavaCodelens(QuteJavaCodeLensParams javaParams) { - return runAsBackground("getJavaCodelens", monitor -> QuteSupportForJava.getInstance().codeLens(javaParams, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture> getJavaDiagnostics(QuteJavaDiagnosticsParams javaParams) { - return runAsBackground("getJavaDiagnostics", monitor -> QuteSupportForJava.getInstance().diagnostics(javaParams, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture> getJavaDocumentLink(QuteJavaDocumentLinkParams javaParams) { - return runAsBackground("getJavaDocumentLink", monitor -> QuteSupportForJava.getInstance().documentLink(javaParams, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture> getUserTags(QuteUserTagParams params) { - return runAsBackground("getUserTags", monitor -> QuteSupportForTemplate.getInstance().getUserTags(params, PsiUtilsLSImpl.getInstance(getProject()), - monitor)); - } - - @Override - public CompletableFuture generateMissingJavaMember(GenerateMissingJavaMemberParams params) { - return runAsBackground("generateMissingJavaMember", monitor -> QuteSupportForTemplate.getInstance() - .generateMissingJavaMember(params, PsiUtilsLSImpl.getInstance(getProject()), monitor)); - } - - @Override - public CompletableFuture getJavadoc(QuteJavadocParams params) { - return runAsBackground("getJavadoc", monitor -> QuteSupportForTemplate.getInstance() - .getJavadoc(params, PsiUtilsLSImpl.getInstance(getProject()), monitor)); - } + private final MessageBusConnection connection; + + public QuteLanguageClient(Project project) { + super(project); + connection = project.getMessageBus().connect(project); + connection.subscribe(ClasspathResourceChangedManager.TOPIC, this); + } + + @Override + public void dispose() { + super.dispose(); + connection.disconnect(); + } + + /** + * Send the notification qute/dataModelChanged with the project Uris to + * refresh data model used in Qute Template. + * + * @param uris the project uris where the data model must be refreshed. + */ + private void notifyQuteDataModelChanged(Set uris) { + QuteLanguageServerAPI server = (QuteLanguageServerAPI) getLanguageServer(); + if (server != null) { + JavaDataModelChangeEvent event = new JavaDataModelChangeEvent(); + event.setProjectURIs(uris); + server.dataModelChanged(event); + } + } + + @Override + public void librariesChanged() { + if (isDisposed()) { + // The language client has been disposed, ignore changes in libraries + return; + } + Set uris = new HashSet<>(); + uris.add(PsiQuteProjectUtils.getProjectURI(getProject())); + notifyQuteDataModelChanged(uris); + } + + @Override + public void sourceFilesChanged(Set> sources) { + if (isDisposed()) { + // The language client has been disposed, ignore changes in Java source files + return; + } + Set uris = sources.stream() + // qute/dataModelChanged must be sent only if there are some Java files which are changed + .filter(pair -> PsiMicroProfileProjectManager.isJavaFile(pair.getFirst())) + .map(pair -> pair.getSecond()) + .map(module -> PsiUtilsLSImpl.getProjectURI(module)) + .collect(Collectors.toSet()); + if (!uris.isEmpty()) { + notifyQuteDataModelChanged(uris); + } + } + + @Override + public CompletableFuture getProjectInfo(QuteProjectParams params) { + var coalesceBy = new CoalesceByKey("qute/template/project", params.getTemplateFileUri()); + return runAsBackground("getProjectInfo", monitor -> QuteSupportForTemplate.getInstance().getProjectInfo(params, PsiUtilsLSImpl.getInstance(getProject()), monitor), coalesceBy); + } + + @Override + public CompletableFuture>> getDataModelProject( + QuteDataModelProjectParams params) { + var coalesceBy = new CoalesceByKey("qute/template/projectDataModel", params.getProjectUri()); + return runAsBackground("getDataModel", monitor -> QuteSupportForTemplate.getInstance().getDataModelProject(params, PsiUtilsLSImpl.getInstance(getProject()), + monitor), coalesceBy); + } + + @Override + public CompletableFuture> getJavaTypes(QuteJavaTypesParams params) { + var coalesceBy = new CoalesceByKey("qute/template/javaTypes", params.getProjectUri(), params.getPattern()); + return runAsBackground("getJavaTypes", monitor -> QuteSupportForTemplate.getInstance().getJavaTypes(params, PsiUtilsLSImpl.getInstance(getProject()), + monitor), coalesceBy); + } + + @Override + public CompletableFuture getResolvedJavaType(QuteResolvedJavaTypeParams params) { + var coalesceBy = new CoalesceByKey("qute/template/resolvedJavaType", params.getProjectUri(), params.getClassName()); + return runAsBackground("getResolvedJavaType", monitor -> QuteSupportForTemplate.getInstance().getResolvedJavaType(params, PsiUtilsLSImpl.getInstance(getProject()), + monitor), coalesceBy); + } + + @Override + public CompletableFuture getJavaDefinition(QuteJavaDefinitionParams params) { + var coalesceBy = new CoalesceByKey("qute/java/definition", params); + return runAsBackground("getJavaDefinition", monitor -> QuteSupportForTemplate.getInstance().getJavaDefinition(params, PsiUtilsLSImpl.getInstance(getProject()), + monitor), coalesceBy); + } + + @Override + public CompletableFuture> getJavaCodelens(QuteJavaCodeLensParams javaParams) { + var coalesceBy = new CoalesceByKey("qute/java/codeLens", javaParams.getUri()); + return runAsBackground("getJavaCodelens", monitor -> QuteSupportForJava.getInstance().codeLens(javaParams, PsiUtilsLSImpl.getInstance(getProject()), + monitor), coalesceBy); + } + + @Override + public CompletableFuture> getJavaDiagnostics(QuteJavaDiagnosticsParams javaParams) { + // When project is indexing and user types a lot of characters in the Java editor, the MicroProfile language server + // validates the Java document and consumes a 'microprofile/java/diagnostics' for each typed character. + // The response of 'microprofile/java/diagnostics' are blocked (or takes some times) and we have + // "Too many non-blocking read actions submitted at once in". To avoid having this error, we create a coalesceBy key + // managed by IJ ReadAction.nonBlocking() to cancel the previous request. + var coalesceBy = new CoalesceByKey("qute/java/diagnostics", javaParams.getUris()); + return runAsBackground("getJavaDiagnostics", monitor -> QuteSupportForJava.getInstance().diagnostics(javaParams, PsiUtilsLSImpl.getInstance(getProject()), monitor), coalesceBy); + } + + @Override + public CompletableFuture> getJavaDocumentLink(QuteJavaDocumentLinkParams javaParams) { + var coalesceBy = new CoalesceByKey("qute/java/documentLink", javaParams.getUri()); + return runAsBackground("getJavaDocumentLink", monitor -> QuteSupportForJava.getInstance().documentLink(javaParams, PsiUtilsLSImpl.getInstance(getProject()), + monitor), coalesceBy); + } + + @Override + public CompletableFuture> getUserTags(QuteUserTagParams params) { + var coalesceBy = new CoalesceByKey("qute/template/userTag", params.getProjectUri()); + return runAsBackground("getUserTags", monitor -> QuteSupportForTemplate.getInstance().getUserTags(params, PsiUtilsLSImpl.getInstance(getProject()), + monitor), coalesceBy); + } + + @Override + public CompletableFuture generateMissingJavaMember(GenerateMissingJavaMemberParams params) { + return runAsBackground("generateMissingJavaMember", monitor -> QuteSupportForTemplate.getInstance() + .generateMissingJavaMember(params, PsiUtilsLSImpl.getInstance(getProject()), monitor)); + } + + @Override + public CompletableFuture getJavadoc(QuteJavadocParams params) { + var coalesceBy = new CoalesceByKey("qute/template/javadoc", params.getProjectUri(), params.getSourceType(), params.getMemberName(), params.getSignature()); + return runAsBackground("getJavadoc", monitor -> QuteSupportForTemplate.getInstance() + .getJavadoc(params, PsiUtilsLSImpl.getInstance(getProject()), monitor), coalesceBy); + } }