From e457326ca2de3c9f2e9578c53aca4f2c45d0dbb7 Mon Sep 17 00:00:00 2001 From: azerr Date: Sun, 1 Oct 2023 16:36:39 +0200 Subject: [PATCH] feat: Check if project is a MicroProfile, Qute, etc project to map file with a language server Fixes #1185 Signed-off-by: azerr --- .../lsp4ij/AbstractDocumentMatcher.java | 36 +++ ...ContentTypeToLanguageServerDefinition.java | 28 +- .../intellij/lsp4ij/DocumentMatcher.java | 21 ++ .../LanguageMappingExtensionPointBean.java | 25 +- .../lsp4ij/LanguageServersRegistry.java | 53 ++-- .../lsp4ij/LanguageServiceAccessor.java | 264 +++++++++++++----- .../lsp4ij/client/LSPCompletableFuture.java | 120 +------- .../client/PromiseToCompletableFuture.java | 176 ++++++++++++ .../lsp/AbstractQuarkusDocumentMatcher.java | 19 ++ .../QuarkusDocumentMatcherForJavaFile.java | 12 + ...arkusDocumentMatcherForPropertiesFile.java | 21 ++ .../qute/lang/QuteLanguageSubstitutor.java | 4 +- .../qute/lsp/AbstractQuteDocumentMatcher.java | 19 ++ .../lsp/QuteDocumentMatcherForJavaFile.java | 12 + .../QuteDocumentMatcherForTemplateFile.java | 20 ++ .../qute/psi/utils/PsiQuteProjectUtils.java | 7 + .../resources/META-INF/lsp4ij-quarkus.xml | 12 +- src/main/resources/META-INF/lsp4ij-qute.xml | 8 +- 18 files changed, 632 insertions(+), 225 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractDocumentMatcher.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/DocumentMatcher.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/client/PromiseToCompletableFuture.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp/AbstractQuarkusDocumentMatcher.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForJavaFile.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForPropertiesFile.java create mode 100644 src/main/java/com/redhat/devtools/intellij/qute/lsp/AbstractQuteDocumentMatcher.java create mode 100644 src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForJavaFile.java create mode 100644 src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForTemplateFile.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractDocumentMatcher.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractDocumentMatcher.java new file mode 100644 index 000000000..e0ddf2c27 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractDocumentMatcher.java @@ -0,0 +1,36 @@ +package com.redhat.devtools.intellij.lsp4ij; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.lsp4ij.client.PromiseToCompletableFuture; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public abstract class AbstractDocumentMatcher implements DocumentMatcher { + + private class CompletableFutureWrapper extends PromiseToCompletableFuture { + + public CompletableFutureWrapper(@NotNull VirtualFile file, @NotNull Project project) { + super(indicator -> { + return AbstractDocumentMatcher.this.match(file, project); + }, "Match with " + AbstractDocumentMatcher.this.getClass().getName(), + project, null, AbstractDocumentMatcher.class, file.getUrl()); + } + } + + @Override + public @NotNull CompletableFuture matchAsync(@NotNull VirtualFile file, @NotNull Project project) { + return new CompletableFutureWrapper(file, project); + } + + @Override + public boolean shouldBeMatchedAsynchronously(@NotNull Project project) { + if (!ApplicationManager.getApplication().isReadAccessAllowed()) { + return true; + } + return DumbService.getInstance(project).isDumb(); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/ContentTypeToLanguageServerDefinition.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/ContentTypeToLanguageServerDefinition.java index 703cefb03..020318c42 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/ContentTypeToLanguageServerDefinition.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/ContentTypeToLanguageServerDefinition.java @@ -1,17 +1,37 @@ package com.redhat.devtools.intellij.lsp4ij; import com.intellij.lang.Language; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; -import javax.annotation.Nonnull; import java.util.AbstractMap; +import java.util.concurrent.CompletableFuture; public class ContentTypeToLanguageServerDefinition extends AbstractMap.SimpleEntry { - public ContentTypeToLanguageServerDefinition(@Nonnull Language language, - @Nonnull LanguageServersRegistry.LanguageServerDefinition provider) { + + private final DocumentMatcher documentMatcher; + + public ContentTypeToLanguageServerDefinition(@NotNull Language language, + @NotNull LanguageServersRegistry.LanguageServerDefinition provider, + @NotNull DocumentMatcher documentMatcher) { super(language, provider); + this.documentMatcher = documentMatcher; + } + + public boolean match(VirtualFile file, Project project) { + return documentMatcher.match(file, project); + } + + public boolean shouldBeMatchedAsynchronously(Project project) { + return documentMatcher.shouldBeMatchedAsynchronously(project); } public boolean isEnabled() { - return true; + return getValue().isEnabled(); + } + + public @NotNull CompletableFuture matchAsync(VirtualFile file, Project project) { + return documentMatcher.matchAsync(file, project); } } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/DocumentMatcher.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/DocumentMatcher.java new file mode 100644 index 000000000..9384951ce --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/DocumentMatcher.java @@ -0,0 +1,21 @@ +package com.redhat.devtools.intellij.lsp4ij; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.CancellablePromise; + +import java.util.concurrent.CompletableFuture; + +public interface DocumentMatcher { + + boolean match(@NotNull VirtualFile file, @NotNull Project project); + + default @NotNull CompletableFuture matchAsync(@NotNull VirtualFile file, @NotNull Project project) { + return CompletableFuture.completedFuture(match(file,project)); + } + + default boolean shouldBeMatchedAsynchronously(@NotNull Project project) { + return false; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageMappingExtensionPointBean.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageMappingExtensionPointBean.java index 7bfedcf8a..ba1d74778 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageMappingExtensionPointBean.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageMappingExtensionPointBean.java @@ -2,9 +2,15 @@ import com.intellij.openapi.extensions.AbstractExtensionPointBean; import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.serviceContainer.BaseKeyedLazyInstance; import com.intellij.util.xmlb.annotations.Attribute; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class LanguageMappingExtensionPointBean extends BaseKeyedLazyInstance { + + private static final DocumentMatcher DEFAULT_DOCUMENT_MATCHER = (file,project) -> true; -public class LanguageMappingExtensionPointBean extends AbstractExtensionPointBean { public static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.redhat.devtools.intellij.quarkus.languageMapping"); @Attribute("id") @@ -15,4 +21,21 @@ public class LanguageMappingExtensionPointBean extends AbstractExtensionPointBea @Attribute("serverId") public String serverId; + + @Attribute("documentMatcher") + public String documentMatcher; + + public @NotNull DocumentMatcher getDocumentMatcher() { + try { + return super.getInstance(); + } + catch(Exception e) { + return DEFAULT_DOCUMENT_MATCHER; + } + } + + @Override + protected @Nullable String getImplementationClassName() { + return documentMatcher; + } } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java index 882255e1a..80d0c2574 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java @@ -20,6 +20,7 @@ import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.eclipse.lsp4j.services.LanguageServer; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -194,7 +195,7 @@ private void initialize() { for (LanguageMappingExtensionPointBean extension : LanguageMappingExtensionPointBean.EP_NAME.getExtensions()) { Language language = Language.findLanguageByID(extension.language); if (language != null) { - languageMappings.add(new LanguageMapping(language, extension.id, extension.serverId)); + languageMappings.add(new LanguageMapping(language, extension.id, extension.serverId, extension.getDocumentMatcher())); } } @@ -205,7 +206,7 @@ private void initialize() { for (LanguageMapping mapping : languageMappings) { LanguageServerDefinition lsDefinition = servers.get(mapping.languageId); if (lsDefinition != null) { - registerAssociation(mapping.language, lsDefinition, mapping.languageId); + registerAssociation(lsDefinition, mapping); } else { LOGGER.warn("server '" + mapping.id + "' not available"); //$NON-NLS-1$ //$NON-NLS-2$ } @@ -223,20 +224,21 @@ public Icon getServerIcon(String serverId) { * @return the {@link LanguageServerDefinition}s directly associated to the given content-type. * This does not include the one that match transitively as per content-type hierarchy */ - List findProviderFor(final @NonNull Language contentType) { + List findProviderFor(final @NotNull Language contentType) { return connections.stream() .filter(entry -> contentType.isKindOf(entry.getKey())) .collect(Collectors.toList()); } - public void registerAssociation(@Nonnull Language language, - @Nonnull LanguageServerDefinition serverDefinition, @Nullable String languageId) { + public void registerAssociation(@NotNull LanguageServerDefinition serverDefinition, @Nullable LanguageMapping mapping) { + @NotNull Language language = mapping.language; + @Nullable String languageId = mapping.languageId; if (languageId != null) { serverDefinition.registerAssociation(language, languageId); } - connections.add(new ContentTypeToLanguageServerDefinition(language, serverDefinition)); + connections.add(new ContentTypeToLanguageServerDefinition(language, serverDefinition, mapping.getDocumentMatcher())); } public List getContentTypeToLSPExtensions() { @@ -258,19 +260,25 @@ LanguageServerDefinition getDefinition(@NonNull String languageServerId) { */ private static class LanguageMapping { - @Nonnull + @NotNull public final String id; - @Nonnull + @NotNull public final Language language; @Nullable public final String languageId; - public LanguageMapping(@Nonnull Language language, @Nonnull String id, @Nullable String languageId) { + private final DocumentMatcher documentMatcher; + + public LanguageMapping(@NotNull Language language, @Nullable String id, @Nullable String languageId, @NotNull DocumentMatcher documentMatcher) { this.language = language; this.id = id; this.languageId = languageId; + this.documentMatcher = documentMatcher; } + public DocumentMatcher getDocumentMatcher() { + return documentMatcher; + } } /** @@ -278,19 +286,20 @@ public LanguageMapping(@Nonnull Language language, @Nonnull String id, @Nullable * @param serverDefinition * @return whether the given serverDefinition is suitable for the file */ - public boolean matches(@Nonnull VirtualFile file, @NonNull LanguageServerDefinition serverDefinition, - Project project) { - return getAvailableLSFor(LSPIJUtils.getFileLanguage(file, project)).contains(serverDefinition); - } - - /** - * @param document - * @param serverDefinition - * @return whether the given serverDefinition is suitable for the file - */ - public boolean matches(@Nonnull Document document, @Nonnull LanguageServerDefinition serverDefinition, - Project project) { - return getAvailableLSFor(LSPIJUtils.getDocumentLanguage(document, project)).contains(serverDefinition); + public boolean matches(VirtualFile file, + Project project, @Nonnull LanguageServerDefinition serverDefinition) { + Language language = LSPIJUtils.getFileLanguage(file, project); + if (language == null) { + return false; + } + for (ContentTypeToLanguageServerDefinition mapping : this.connections) { + if (language.isKindOf(mapping.getKey()) && serverDefinition.equals(mapping.getValue())) { + if (mapping.match(file, project)) { + return true; + } + } + } + return false; } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java index 3f2720ecf..428ea030b 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java @@ -51,29 +51,41 @@ private LanguageServiceAccessor(Project project) { @NotNull public CompletableFuture> getLanguageServers(@NotNull VirtualFile file, - Predicate filter) { + Predicate filter) { URI uri = LSPIJUtils.toUri(file); if (uri == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } + + // Collect started (or not) language servers which matches the given file. + CompletableFuture> matchedServers = getMatchedLanguageServersWrappers(file); + if (matchedServers.isDone() && matchedServers.getNow(Collections.emptyList()).isEmpty()) { + // None language servers matches the given file + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + // Returns the language servers which match the given file, start them and connect the file to each matched language server final List servers = Collections.synchronizedList(new ArrayList<>()); try { - return CompletableFuture.allOf(getLSWrappers(file).stream().map(wrapper -> - wrapper.getInitializedServer() - .thenComposeAsync(server -> { - if (server != null && wrapper.isEnabled() && (filter == null || filter.test(wrapper.getServerCapabilities()))) { - try { - return wrapper.connect(file); - } catch (IOException ex) { - LOGGER.warn(ex.getLocalizedMessage(), ex); - } - } - return CompletableFuture.completedFuture(null); - }).thenAccept(server -> { - if (server != null) { - servers.add(new LanguageServerItem(server, wrapper)); - } - })).toArray(CompletableFuture[]::new)) + return matchedServers + .thenComposeAsync(result -> CompletableFuture.allOf(result + .stream() + .map(wrapper -> + wrapper.getInitializedServer() + .thenComposeAsync(server -> { + if (server != null && wrapper.isEnabled() && (filter == null || filter.test(wrapper.getServerCapabilities()))) { + try { + return wrapper.connect(file); + } catch (IOException ex) { + LOGGER.warn(ex.getLocalizedMessage(), ex); + } + } + return CompletableFuture.completedFuture(null); + }).thenAccept(server -> { + if (server != null) { + servers.add(new LanguageServerItem(server, wrapper)); + } + })).toArray(CompletableFuture[]::new))) .thenApply(theVoid -> servers); } catch (final ProcessCanceledException cancellation) { throw cancellation; @@ -105,8 +117,8 @@ public void projectClosing(Project project) { * Get the requested language server instance for the given file. Starts the * language server if not already started. * - * @param file the file for which the initialized LanguageServer shall be returned - * @param lsDefinition the language server definition + * @param file the file for which the initialized LanguageServer shall be returned + * @param lsDefinition the language server definition * @param capabilitiesPredicate a predicate to check capabilities * @return a LanguageServer for the given file, which is defined with provided * server ID and conforms to specified request. If @@ -145,65 +157,177 @@ private static boolean capabilitiesComply(LanguageServerWrapper wrapper, } @NotNull - private Collection getLSWrappers(@NotNull VirtualFile file) { - LinkedHashSet res = new LinkedHashSet<>(); - URI uri = LSPIJUtils.toUri(file); - if (uri == null) { - return Collections.emptyList(); + private CompletableFuture> getMatchedLanguageServersWrappers(@NotNull VirtualFile file) { + final Project fileProject = LSPIJUtils.getProject(file); + if (fileProject == null) { + return CompletableFuture.completedFuture(Collections.emptyList()); } - URI path = uri; + MatchedLanguageServerDefinitions mappings = getMatchedLanguageServerDefinitions(file, fileProject); + if (mappings == MatchedLanguageServerDefinitions.NO_MATCH) { + // There are no mapping for the given file + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + LinkedHashSet matchedServers = new LinkedHashSet<>(); + + // Collect sync server definitions + var serverDefinitions = mappings.getMatched(); + collectLanguageServersFromDefinition(file, fileProject, serverDefinitions, matchedServers); + + CompletableFuture> async = mappings.getAsyncMatched(); + if (async != null) { + // Collect async server definitions + return async + .thenApply(asyncServerDefinitions -> { + collectLanguageServersFromDefinition(file, fileProject, asyncServerDefinitions, matchedServers); + return matchedServers; + }); + } + return CompletableFuture.completedFuture(matchedServers); + } + + /** + * Get or create a language server wrapper for the given server definitions and add then to the given matched servers. + * + * @param file the file. + * @param fileProject the file project. + * @param serverDefinitions the server definitions. + * @param matchedServers the list to update with get/created language server. + */ + private void collectLanguageServersFromDefinition(@NotNull VirtualFile file, @NotNull Project fileProject, @NotNull Set serverDefinitions, @NotNull Set matchedServers) { + synchronized (startedServers) { + for (var serverDefinition : serverDefinitions) { + boolean useExistingServer = false; + // Loop for started language servers + for (var startedServer : startedServers) { + if (startedServer.serverDefinition.equals(serverDefinition) + && startedServer.canOperate(file)) { + // A started language server match the file, use it + matchedServers.add(startedServer); + useExistingServer = true; + break; + } + } + if (!useExistingServer) { + // There are none started servers which matches the file, create and add it. + LanguageServerWrapper wrapper = new LanguageServerWrapper(fileProject, serverDefinition); + startedServers.add(wrapper); + matchedServers.add(wrapper); + } + } + } + } + + /** + * Store the matched language server definitions for a given file. + */ + private static class MatchedLanguageServerDefinitions { + + public static final MatchedLanguageServerDefinitions NO_MATCH = new MatchedLanguageServerDefinitions(Collections.emptySet(), null); + + private final Set matched; + + private final CompletableFuture> asyncMatched; + + public MatchedLanguageServerDefinitions(@NotNull Set matchedLanguageServersDefinition, CompletableFuture> async) { + this.matched = matchedLanguageServersDefinition; + this.asyncMatched = async; + } + + /** + * Return the matched server definitions get synchronously. + * + * @return the matched server definitions get synchronously. + */ + public @NotNull Set getMatched() { + return matched; + } + + /** + * Return the matched server definitions get asynchronously or null otherwise. + * + * @return the matched server definitions get asynchronously or null otherwise. + */ + public CompletableFuture> getAsyncMatched() { + return asyncMatched; + } + } + + /** + * Returns the matched language server definitions for the given file. + * + * @param file the file. + * @param fileProject the file project. + * @return the matched language server definitions for the given file. + */ + private MatchedLanguageServerDefinitions getMatchedLanguageServerDefinitions(@NotNull VirtualFile file, @NotNull Project fileProject) { + + Set syncMatchedDefinitions = null; + Set asyncMatchedDefinitions = null; // look for running language servers via content-type Queue contentTypes = new LinkedList<>(); Set processedContentTypes = new HashSet<>(); contentTypes.add(LSPIJUtils.getFileLanguage(file, project)); - synchronized (startedServers) { - // already started compatible servers that fit request - res.addAll(startedServers.stream() - .filter(wrapper -> { - try { - return wrapper.isEnabled() && (wrapper.isConnectedTo(path) || LanguageServersRegistry.getInstance().matches(file, wrapper.serverDefinition, project)); - } catch (ProcessCanceledException cancellation) { - throw cancellation; - } catch (Exception e) { - LOGGER.warn(e.getLocalizedMessage(), e); - return false; - } - }) - .filter(wrapper -> wrapper.canOperate(file)) - .collect(Collectors.toList())); - - while (!contentTypes.isEmpty()) { - Language contentType = contentTypes.poll(); - if (contentType == null || processedContentTypes.contains(contentType)) { + while (!contentTypes.isEmpty()) { + Language contentType = contentTypes.poll(); + if (contentType == null || processedContentTypes.contains(contentType)) { + continue; + } + // Loop for server/language mapping + for (ContentTypeToLanguageServerDefinition mapping : LanguageServersRegistry.getInstance() + .findProviderFor(contentType)) { + if (mapping == null || !mapping.isEnabled() || (syncMatchedDefinitions != null && syncMatchedDefinitions.contains(mapping.getValue()))) { + // the mapping is disabled + // or the server definition has been already added continue; } - for (ContentTypeToLanguageServerDefinition mapping : LanguageServersRegistry.getInstance() - .findProviderFor(contentType)) { - if (mapping == null || !mapping.isEnabled()) { - continue; + if (mapping.shouldBeMatchedAsynchronously(fileProject)) { + // Async mapping + // Mapping must be done asynchronously because the match of DocumentMatcher of the mapping need to be done asynchronously + // This usecase comes from for instance when custom match need to collect classes from the Java project and requires read only action. + if (asyncMatchedDefinitions == null) { + asyncMatchedDefinitions = new HashSet<>(); } - LanguageServersRegistry.LanguageServerDefinition serverDefinition = mapping.getValue(); - if (serverDefinition == null) { - continue; - } - if (startedServers.stream().anyMatch(wrapper -> wrapper.serverDefinition.equals(serverDefinition) - && wrapper.canOperate(file))) { - // we already checked a compatible LS with this definition - continue; - } - final Project fileProject = file != null ? LSPIJUtils.getProject(file) : null; - if (fileProject != null) { - LanguageServerWrapper wrapper = new LanguageServerWrapper(fileProject, serverDefinition); - startedServers.add(wrapper); - res.add(wrapper); + asyncMatchedDefinitions.add(mapping); + } else { + // Sync mapping + if (mapping.match(file, fileProject)) { + if (syncMatchedDefinitions == null) { + syncMatchedDefinitions = new HashSet<>(); + } + syncMatchedDefinitions.add(mapping.getValue()); } } - processedContentTypes.add(contentType); } - return res; } + if (syncMatchedDefinitions != null || asyncMatchedDefinitions != null) { + // Some match... + CompletableFuture> async = null; + if (asyncMatchedDefinitions != null) { + // Async match, compute a future which process all matchAsync and return a list of server definitions + final Set serverDefinitions = Collections.synchronizedSet(new HashSet<>()); + async = CompletableFuture.allOf(asyncMatchedDefinitions + .stream() + .map(mapping -> { + return mapping + .matchAsync(file, fileProject) + .thenApply(result -> { + if (result) { + serverDefinitions.add(mapping.getValue()); + } + return null; + }); + } + ) + .toArray(CompletableFuture[]::new)) + .thenApply(theVoid -> serverDefinitions); + } + return new MatchedLanguageServerDefinitions(syncMatchedDefinitions != null ? syncMatchedDefinitions : Collections.emptySet(), async); + } + // No match... + return MatchedLanguageServerDefinitions.NO_MATCH; } private LanguageServerWrapper getLSWrapperForConnection(VirtualFile file, @@ -242,18 +366,6 @@ private List getStartedLSWrappers(Predicate getMatchingStartedWrappers(@NotNull VirtualFile file, - @Nullable Predicate request) { - synchronized (startedServers) { - return startedServers.stream().filter(wrapper -> wrapper.isConnectedTo(LSPIJUtils.toUri(file)) - || (LanguageServersRegistry.getInstance().matches(file, wrapper.serverDefinition, project) - && wrapper.canOperate(LSPIJUtils.getProject(file)))).filter(wrapper -> request == null - || (wrapper.getServerCapabilities() == null || request.test(wrapper.getServerCapabilities()))) - .collect(Collectors.toList()); - } - } - /** * Gets list of running LS satisfying a capability predicate. This does not * start any matching language servers, it returns the already running ones. 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 ca63cb868..5e15034b1 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 @@ -33,128 +33,18 @@ /** * LSP completable future which execute a given function code in a non blocking reading action promise. */ -public class LSPCompletableFuture extends CompletableFuture { - - private static final Logger LOGGER = LoggerFactory.getLogger(LSPCompletableFuture.class); - - private class ResultOrError { - - public final R result; - - public final Exception error; - - public ResultOrError(R result, Exception error) { - this.result = result; - this.error = error; - } - } - - private static final int MAX_ATTEMPT = 5; - private final Function code; +public class LSPCompletableFuture extends PromiseToCompletableFuture { 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, Object coalesceBy) { - this.code = code; - this.progressTitle = progressTitle; + super(code, progressTitle, languageClient.getProject(), languageClient, coalesceBy); 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(); - var promise = nonBlockingReadActionPromise(executeInSmartMode); - bind(promise); - } - - /** - * Bind the given promise with the completable future. - * - * @param promise the promise which will execute the function code in a non blocking read action context - */ - private void bind(CancellablePromise> promise) { - this.nonBlockingReadActionPromise = promise; - // On error... - promise.onError(ex -> { - if (ex instanceof ProcessCanceledException || ex instanceof CancellationException) { - // Case 2: cancel the completable future - this.cancel(true); - } else { - // Other case..., mark the completable future as error - this.completeExceptionally(ex); - } - }); - // On success... - promise.onSuccess(value -> { - if (value.error != null) { - Exception ex = value.error; - // There were an error with IndexNotReadyException or ReadAction.CannotReadException - // Case 1: Attempt to retry the start of the promise - if (nbAttempt.incrementAndGet() >= MAX_ATTEMPT) { - // 1.1 Maximum number reached, mark the completable future as error - LOGGER.warn("Maximum number (" + MAX_ATTEMPT + ")" + " of attempts to start non blocking read action for '" + progressTitle + "' has been reached", ex); - this.completeExceptionally(new ExecutionAttemptLimitReachedException(progressTitle, MAX_ATTEMPT, ex)); - } else { - // Retry ... - // 1.2 Index are not ready or the read action cannot be done, retry in smart mode... - LOGGER.warn("Restart non blocking read action for '" + progressTitle + "' with attempt " + nbAttempt.get() + "/" + MAX_ATTEMPT + ".", ex); - var newPromise = nonBlockingReadActionPromise(true); - bind(newPromise); - } - } else { - this.complete(value.result); - } - }); - } - - /** - * Create a non blocking read action promise. - * - * @param executeInSmartMode true if the promise must be executed in smart mode and false otherwise. - * @return a non blocking read action promise - */ - @NotNull - private CancellablePromise> nonBlockingReadActionPromise(boolean executeInSmartMode) { - var project = languageClient.getProject(); - - var indicator = new LSPProgressIndicator(languageClient); - indicator.setText(progressTitle); - var action = ReadAction.nonBlocking(() -> - { - try { - R result = code.apply(indicator); - return new ResultOrError(result, null); - } catch (IndexNotReadyException | ReadAction.CannotReadException e) { - // When there is any exception, AsyncPromise report a log error. - // As we retry to execute the function code 5 times, we don't want to log this error - // To do that we catch the error and recreate a new promise on the promise.onSuccess - return new ResultOrError(null, e); - } - }) - .wrapProgress(indicator) - .expireWith(languageClient); // promise is canceled when language client is stopped - if (executeInSmartMode) { - action = action.inSmartMode(project); - } - if (coalesceBy != null) { - action = action.coalesceBy(coalesceBy); - } - return action - .submit(AppExecutorUtil.getAppExecutorService()); + init(); } @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (nonBlockingReadActionPromise != null) { - // cancel the current promise - if (!nonBlockingReadActionPromise.isDone()) { - nonBlockingReadActionPromise.cancel(mayInterruptIfRunning); - } - } - return super.cancel(mayInterruptIfRunning); + protected ProgressIndicator createProgressIndicator() { + return new LSPProgressIndicator(languageClient); } } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/PromiseToCompletableFuture.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/PromiseToCompletableFuture.java new file mode 100644 index 000000000..b7206c80f --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/PromiseToCompletableFuture.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.client; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.progress.EmptyProgressIndicator; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.IndexNotReadyException; +import com.intellij.openapi.project.Project; +import com.intellij.util.concurrency.AppExecutorUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.CancellablePromise; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +/** + * LSP completable future which execute a given function code in a non blocking reading action promise. + */ +public class PromiseToCompletableFuture extends CompletableFuture { + + private static final Logger LOGGER = LoggerFactory.getLogger(PromiseToCompletableFuture.class); + + private class ResultOrError { + + public final R result; + + public final Exception error; + + public ResultOrError(R result, Exception error) { + this.result = result; + this.error = error; + } + } + + private static final int MAX_ATTEMPT = 5; + + private final Function code; + + private final String progressTitle; + + private final Project project; + private final Disposable parentDisposable; + + private final AtomicInteger nbAttempt; + + private final Object[] coalesceBy; + private CancellablePromise> nonBlockingReadActionPromise; + + public PromiseToCompletableFuture(@NotNull Function code, @NotNull String progressTitle, @NotNull Project project, @Nullable Disposable parentDisposable, Object... coalesceBy) { + this.code = code; + this.progressTitle = progressTitle; + this.project = project; + this.parentDisposable = parentDisposable; + this.coalesceBy = coalesceBy; + this.nbAttempt = new AtomicInteger(0); + } + + protected void init() { + // if indexation is processing, we need to execute the promise in smart mode + var executeInSmartMode = DumbService.getInstance(project).isDumb(); + var promise = nonBlockingReadActionPromise(executeInSmartMode); + bind(promise); + } + + /** + * Bind the given promise with the completable future. + * + * @param promise the promise which will execute the function code in a non blocking read action context + */ + private void bind(CancellablePromise> promise) { + this.nonBlockingReadActionPromise = promise; + // On error... + promise.onError(ex -> { + if (ex instanceof ProcessCanceledException || ex instanceof CancellationException) { + // Case 2: cancel the completable future + this.cancel(true); + } else { + // Other case..., mark the completable future as error + this.completeExceptionally(ex); + } + }); + // On success... + promise.onSuccess(value -> { + if (value.error != null) { + Exception ex = value.error; + // There were an error with IndexNotReadyException or ReadAction.CannotReadException + // Case 1: Attempt to retry the start of the promise + if (nbAttempt.incrementAndGet() >= MAX_ATTEMPT) { + // 1.1 Maximum number reached, mark the completable future as error + LOGGER.warn("Maximum number (" + MAX_ATTEMPT + ")" + " of attempts to start non blocking read action for '" + progressTitle + "' has been reached", ex); + this.completeExceptionally(new ExecutionAttemptLimitReachedException(progressTitle, MAX_ATTEMPT, ex)); + } else { + // Retry ... + // 1.2 Index are not ready or the read action cannot be done, retry in smart mode... + LOGGER.warn("Restart non blocking read action for '" + progressTitle + "' with attempt " + nbAttempt.get() + "/" + MAX_ATTEMPT + ".", ex); + var newPromise = nonBlockingReadActionPromise(true); + bind(newPromise); + } + } else { + this.complete(value.result); + } + }); + } + + /** + * Create a non blocking read action promise. + * + * @param executeInSmartMode true if the promise must be executed in smart mode and false otherwise. + * @return a non blocking read action promise + */ + @NotNull + private CancellablePromise> nonBlockingReadActionPromise(boolean executeInSmartMode) { + var indicator = createProgressIndicator(); + indicator.setText(progressTitle); + var action = ReadAction.nonBlocking(() -> + { + try { + R result = code.apply(indicator); + return new ResultOrError(result, null); + } catch (IndexNotReadyException | ReadAction.CannotReadException e) { + // When there is any exception, AsyncPromise report a log error. + // As we retry to execute the function code 5 times, we don't want to log this error + // To do that we catch the error and recreate a new promise on the promise.onSuccess + return new ResultOrError(null, e); + } + }) + .wrapProgress(indicator); + if (parentDisposable != null) { + action = action.expireWith(parentDisposable); // ex: promise is canceled when language client is stopped + } + if (executeInSmartMode) { + action = action.inSmartMode(project); + } + if (coalesceBy != null) { + action = action.coalesceBy(coalesceBy); + } + return action + .submit(AppExecutorUtil.getAppExecutorService()); + } + + protected ProgressIndicator createProgressIndicator() { + return new EmptyProgressIndicator(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (nonBlockingReadActionPromise != null) { + // cancel the current promise + if (!nonBlockingReadActionPromise.isDone()) { + nonBlockingReadActionPromise.cancel(mayInterruptIfRunning); + } + } + return super.cancel(mayInterruptIfRunning); + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/AbstractQuarkusDocumentMatcher.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/AbstractQuarkusDocumentMatcher.java new file mode 100644 index 000000000..f727c5c58 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/AbstractQuarkusDocumentMatcher.java @@ -0,0 +1,19 @@ +package com.redhat.devtools.intellij.quarkus.lsp; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.lsp4ij.AbstractDocumentMatcher; +import com.redhat.devtools.intellij.lsp4ij.DocumentMatcher; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; +import com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils; + +public class AbstractQuarkusDocumentMatcher extends AbstractDocumentMatcher { + @Override + public boolean match(VirtualFile file, Project fileProject) { + Module module = LSPIJUtils.getModule(file); + return module != null && QuarkusModuleUtil.isQuarkusModule(module); + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForJavaFile.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForJavaFile.java new file mode 100644 index 000000000..fc224cc52 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForJavaFile.java @@ -0,0 +1,12 @@ +package com.redhat.devtools.intellij.quarkus.lsp; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; + +public class QuarkusDocumentMatcherForJavaFile extends AbstractQuarkusDocumentMatcher { + +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForPropertiesFile.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForPropertiesFile.java new file mode 100644 index 000000000..65936bcf7 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForPropertiesFile.java @@ -0,0 +1,21 @@ +package com.redhat.devtools.intellij.quarkus.lsp; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; + +public class QuarkusDocumentMatcherForPropertiesFile extends AbstractQuarkusDocumentMatcher { + + @Override + public boolean match(VirtualFile file, Project fileProject) { + if (!matchFile(file, fileProject)) { + return false; + } + return super.match(file, fileProject); + } + + private boolean matchFile(VirtualFile file, Project fileProject) { + return QuarkusModuleUtil.isQuarkusPropertiesFile(file, fileProject) || QuarkusModuleUtil.isQuarkusYAMLFile(file, fileProject); + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lang/QuteLanguageSubstitutor.java b/src/main/java/com/redhat/devtools/intellij/qute/lang/QuteLanguageSubstitutor.java index 788e0131e..ed4bb119f 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/lang/QuteLanguageSubstitutor.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/lang/QuteLanguageSubstitutor.java @@ -25,6 +25,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils.isQuteTemplate; + /** * Qute language substitutor to force some language file (ex:HTML, YAML, etc) to "_Qute" language when: *
    @@ -34,7 +36,7 @@ */ public class QuteLanguageSubstitutor extends LanguageSubstitutor { protected boolean isTemplate(VirtualFile file, Module module) { - return file.getPath().contains("templates") && + return isQuteTemplate(file, module) && ModuleRootManager.getInstance(module).getFileIndex().isInSourceContent(file); } diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lsp/AbstractQuteDocumentMatcher.java b/src/main/java/com/redhat/devtools/intellij/qute/lsp/AbstractQuteDocumentMatcher.java new file mode 100644 index 000000000..dbf053d69 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/AbstractQuteDocumentMatcher.java @@ -0,0 +1,19 @@ +package com.redhat.devtools.intellij.qute.lsp; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.lsp4ij.AbstractDocumentMatcher; +import com.redhat.devtools.intellij.lsp4ij.DocumentMatcher; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils; + +public class AbstractQuteDocumentMatcher extends AbstractDocumentMatcher { + + @Override + public boolean match(VirtualFile file, Project fileProject) { + Module module = LSPIJUtils.getModule(file); + return module != null && PsiQuteProjectUtils.hasQuteSupport(module); + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForJavaFile.java b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForJavaFile.java new file mode 100644 index 000000000..b3dafa861 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForJavaFile.java @@ -0,0 +1,12 @@ +package com.redhat.devtools.intellij.qute.lsp; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.lsp4ij.DocumentMatcher; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; + +public class QuteDocumentMatcherForJavaFile extends AbstractQuteDocumentMatcher { +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForTemplateFile.java b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForTemplateFile.java new file mode 100644 index 000000000..bc88fe1f5 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForTemplateFile.java @@ -0,0 +1,20 @@ +package com.redhat.devtools.intellij.qute.lsp; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils; + +import static com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils.isQuteTemplate; + +public class QuteDocumentMatcherForTemplateFile extends AbstractQuteDocumentMatcher { + + @Override + public boolean match(VirtualFile file, Project fileProject) { + if (!super.match(file, fileProject)) { + return false; + } + return isQuteTemplate(file, LSPIJUtils.getModule(file)); + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/qute/psi/utils/PsiQuteProjectUtils.java b/src/main/java/com/redhat/devtools/intellij/qute/psi/utils/PsiQuteProjectUtils.java index 65d3681f2..27bbcbfbf 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/psi/utils/PsiQuteProjectUtils.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/psi/utils/PsiQuteProjectUtils.java @@ -14,6 +14,8 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.vfs.VirtualFile; import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.qute.psi.internal.QuteJavaConstants; @@ -121,4 +123,9 @@ public static void appendAndSlash(@NotNull StringBuilder path, @NotNull String s path.append('/'); } } + + public static boolean isQuteTemplate(VirtualFile file, Module module) { + return file.getPath().contains("templates") && + ModuleRootManager.getInstance(module).getFileIndex().isInSourceContent(file); + } } diff --git a/src/main/resources/META-INF/lsp4ij-quarkus.xml b/src/main/resources/META-INF/lsp4ij-quarkus.xml index 7394280e0..2be18868a 100644 --- a/src/main/resources/META-INF/lsp4ij-quarkus.xml +++ b/src/main/resources/META-INF/lsp4ij-quarkus.xml @@ -1,7 +1,7 @@ - - - - + + + diff --git a/src/main/resources/META-INF/lsp4ij-qute.xml b/src/main/resources/META-INF/lsp4ij-qute.xml index dd65d62f3..b1a26b051 100644 --- a/src/main/resources/META-INF/lsp4ij-qute.xml +++ b/src/main/resources/META-INF/lsp4ij-qute.xml @@ -17,8 +17,12 @@ ]]> - - + +