From af5c507b7e91569b2ab9125e569c922b99b9d164 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 | 53 ++++ ...ContentTypeToLanguageServerDefinition.java | 28 +- .../intellij/lsp4ij/DocumentMatcher.java | 63 ++++ .../LanguageMappingExtensionPointBean.java | 25 +- .../lsp4ij/LanguageServersRegistry.java | 61 +--- .../lsp4ij/LanguageServiceAccessor.java | 273 +++++++++++++----- .../lsp4ij/client/LSPCompletableFuture.java | 134 +-------- .../internal/PromiseToCompletableFuture.java | 174 +++++++++++ .../intellij/quarkus/QuarkusModuleUtil.java | 11 +- .../lsp/AbstractQuarkusDocumentMatcher.java | 30 ++ .../QuarkusDocumentMatcherForJavaFile.java | 18 ++ ...arkusDocumentMatcherForPropertiesFile.java | 34 +++ .../qute/lang/QuteLanguageSubstitutor.java | 4 +- .../qute/lsp/AbstractQuteDocumentMatcher.java | 30 ++ .../lsp/QuteDocumentMatcherForJavaFile.java | 17 ++ .../QuteDocumentMatcherForTemplateFile.java | 31 ++ .../qute/psi/utils/PsiQuteProjectUtils.java | 7 + .../resources/META-INF/lsp4ij-quarkus.xml | 12 +- src/main/resources/META-INF/lsp4ij-qute.xml | 8 +- ...enApplicationPropertiesCompletionTest.java | 2 + 20 files changed, 750 insertions(+), 265 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/internal/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..eca72e645 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractDocumentMatcher.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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; + +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.internal.PromiseToCompletableFuture; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +/** + * Abstract document matcher which prevent the execute of the match in an read action and when IJ is not indexing. + */ +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()); + init(); + } + } + + @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().isUnitTestMode()) { + return false; + } + 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..798ad1c05 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/DocumentMatcher.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * 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; + +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; + +/** + * When a file is opened, the LSP support connect all available language server which matches the language of the file. + *

+ * DocumentMatcher provides the capability to add advanced filter like check the file name, check that project have some Java classes in the classpath. + */ +public interface DocumentMatcher { + + /** + * Returns true if the given file matches a mapping with a language server and false otherwise. + * + * @param file teh file to check. + * @param project the file project. + * @return true if the given file matches a mapping with a language server and false otherwise. + */ + boolean match(@NotNull VirtualFile file, @NotNull Project project); + + /** + * Returns true if the given file matches a mapping with a language server and false otherwise. + *

+ * In this case,the match is done in async mode. A typical usecase is when the matcher need to check that a given Java class belongs to the project or the file belongs to a source folder. + * To evaluate this match, the read action mode is required and it can create a non blocking read action to evaluate the match. + * + * @param file + * @param project + * @return true if the given file matches a mapping with a language server and false otherwise. + * @see AbstractDocumentMatcher + */ + default @NotNull CompletableFuture matchAsync(@NotNull VirtualFile file, @NotNull Project project) { + return CompletableFuture.completedFuture(match(file, project)); + } + + /** + * Returns true if the match must be done asynchronously and false otherwise. + *

+ * A typical usecase is when IJ is indexing or read action is not allowed,this method should return true, to execute match in a non blocking read action. + * + * @param project the project. + * @return true if the match must be done asynchronously and false otherwise. + * @see AbstractDocumentMatcher + */ + 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..1ebe97209 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,24 +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)); - } - - public List getContentTypeToLSPExtensions() { - return this.connections.stream().filter(mapping -> mapping.getValue() instanceof ExtensionLanguageServerDefinition).collect(Collectors.toList()); + connections.add(new ContentTypeToLanguageServerDefinition(language, serverDefinition, mapping.getDocumentMatcher())); } public @Nullable @@ -258,52 +256,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; } - } - - /** - * @param file - * @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); - } - - - private Set getAvailableLSFor(Language language) { - Set res = new HashSet<>(); - if (language != null) { - for (ContentTypeToLanguageServerDefinition mapping : this.connections) { - if (language.isKindOf(mapping.getKey())) { - res.add(mapping.getValue()); - } - } + public DocumentMatcher getDocumentMatcher() { + return documentMatcher; } - return res; } public Set getAllDefinitions() { 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..e5068cd79 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java @@ -11,6 +11,8 @@ package com.redhat.devtools.intellij.lsp4ij; import com.intellij.lang.Language; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; @@ -51,29 +53,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 +119,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 +159,184 @@ 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()); + } + 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; } - URI path = uri; + } + + /** + * 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; - } - 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; + 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<>(); } - 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 (match(file, fileProject, mapping)) { + 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 static boolean match(VirtualFile file, Project fileProject, ContentTypeToLanguageServerDefinition mapping) { + if (ApplicationManager.getApplication().isUnitTestMode()) { + return ReadAction.compute(() -> mapping.match(file, fileProject)); + } + return mapping.match(file, fileProject); } private LanguageServerWrapper getLSWrapperForConnection(VirtualFile file, @@ -242,18 +375,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..fc27c091f 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 @@ -13,148 +13,26 @@ *******************************************************************************/ package com.redhat.devtools.intellij.lsp4ij.client; -import com.intellij.openapi.application.ReadAction; -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.util.concurrency.AppExecutorUtil; -import com.redhat.devtools.intellij.lsp4ij.LanguageServersRegistry; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.concurrency.CancellablePromise; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.redhat.devtools.intellij.lsp4ij.internal.PromiseToCompletableFuture; -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 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/internal/PromiseToCompletableFuture.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/PromiseToCompletableFuture.java new file mode 100644 index 000000000..cc293b508 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/PromiseToCompletableFuture.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * 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.internal; + +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 com.redhat.devtools.intellij.lsp4ij.client.ExecutionAttemptLimitReachedException; +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/QuarkusModuleUtil.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java index ad0af318d..82edd8c72 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java @@ -228,20 +228,23 @@ public static Set getModulesURIs(Project project) { public static boolean isQuarkusPropertiesFile(VirtualFile file, Project project) { if (APPLICATION_PROPERTIES.matcher(file.getName()).matches() || MICROPROFILE_CONFIG_PROPERTIES.matcher(file.getName()).matches()) { - Module module = ModuleUtilCore.findModuleForFile(file, project); - return module != null && FacetManager.getInstance(module).getFacetByType(QuarkusFacet.FACET_TYPE_ID) != null; + return isQuarkusModule(file, project); } return false; } public static boolean isQuarkusYAMLFile(VirtualFile file, Project project) { if (APPLICATION_YAML.matcher(file.getName()).matches()) { - Module module = ModuleUtilCore.findModuleForFile(file, project); - return module != null && FacetManager.getInstance(module).getFacetByType(QuarkusFacet.FACET_TYPE_ID) != null; + return isQuarkusModule(file, project); } return false; } + private static boolean isQuarkusModule(VirtualFile file, Project project) { + Module module = ModuleUtilCore.findModuleForFile(file, project); + return module != null && (FacetManager.getInstance(module).getFacetByType(QuarkusFacet.FACET_TYPE_ID) != null || QuarkusModuleUtil.isQuarkusModule(module)); + } + public static VirtualFile getModuleDirPath(Module module) { ModuleRootManager manager = ModuleRootManager.getInstance(module); VirtualFile[] roots = manager.getContentRoots(); 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..d5488c11c --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/AbstractQuarkusDocumentMatcher.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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.quarkus.lsp; + +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.LSPIJUtils; +import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; + +/** + * Base class for Quarkus document matcher which checks that the file belongs to a Quarkus project. + */ +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..012ecb369 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForJavaFile.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * 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.quarkus.lsp; + +/** + * Quarkus document matcher for Java file which checks that a Java file belongs to a Quarkus project. + */ +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..baf011c24 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusDocumentMatcherForPropertiesFile.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * 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.quarkus.lsp; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; + +/** + * Quarkus document matcher for application.properties, microprofile-config.properties + * file which checks that the properties file belongs to a Quarkus project. + */ +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..f3f81c519 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/AbstractQuteDocumentMatcher.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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.qute.lsp; + +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.LSPIJUtils; +import com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils; + +/** + * Base class for Qute document matcher which checks that the file belongs to a Qute project. + */ +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..eac6cd227 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForJavaFile.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * 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.qute.lsp; + +/** + * Qute document matcher for Java file which checks that a Java file belongs to a Qute project. + */ +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..84c77f0ac --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteDocumentMatcherForTemplateFile.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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.qute.lsp; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; + +import static com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils.isQuteTemplate; + +/** + * Qute document matcher for html, yaml, txt, json files which checks that the file belongs to a Qute project and it is hosted in the template folder (ex : src/main/resources/templates) + */ +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 @@ ]]> - - + + diff --git a/src/test/java/com/redhat/devtools/intellij/quarkus/completion/MavenApplicationPropertiesCompletionTest.java b/src/test/java/com/redhat/devtools/intellij/quarkus/completion/MavenApplicationPropertiesCompletionTest.java index 76e4403e8..1845fa673 100644 --- a/src/test/java/com/redhat/devtools/intellij/quarkus/completion/MavenApplicationPropertiesCompletionTest.java +++ b/src/test/java/com/redhat/devtools/intellij/quarkus/completion/MavenApplicationPropertiesCompletionTest.java @@ -17,6 +17,7 @@ import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.redhat.devtools.intellij.MavenEditorTest; +import com.redhat.devtools.intellij.lsp4ij.ConnectDocumentToLanguageServerSetupParticipant; import org.junit.Test; import java.io.File; @@ -30,6 +31,7 @@ public class MavenApplicationPropertiesCompletionTest extends MavenEditorTest { @Test public void testBooleanCompletion() throws Exception { + Module module = createMavenModule(new File("projects/quarkus/projects/maven/config-quickstart")); VirtualFile propertiesFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module) + "/src/main/resources/application.properties"); codeInsightTestFixture.configureFromExistingVirtualFile(propertiesFile);