Skip to content

Commit

Permalink
feat: Check if project is a MicroProfile, Qute, etc project to map file
Browse files Browse the repository at this point in the history
with a language server

Fixes #1185

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Oct 3, 2023
1 parent 2be23ca commit af5c507
Show file tree
Hide file tree
Showing 20 changed files with 750 additions and 265 deletions.
Original file line number Diff line number Diff line change
@@ -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<Boolean> {

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<Boolean> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Language, LanguageServersRegistry.LanguageServerDefinition> {
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 <R> CompletableFuture<Boolean> matchAsync(VirtualFile file, Project project) {
return documentMatcher.matchAsync(file, project);
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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<Boolean> 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.
* <p>
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<DocumentMatcher> {

private static final DocumentMatcher DEFAULT_DOCUMENT_MATCHER = (file,project) -> true;

public class LanguageMappingExtensionPointBean extends AbstractExtensionPointBean {
public static final ExtensionPointName<LanguageMappingExtensionPointBean> EP_NAME = ExtensionPointName.create("com.redhat.devtools.intellij.quarkus.languageMapping");

@Attribute("id")
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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()));
}
}

Expand All @@ -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$
}
Expand All @@ -223,24 +224,21 @@ public Icon getServerIcon(String serverId) {
* @return the {@link LanguageServerDefinition}s <strong>directly</strong> associated to the given content-type.
* This does <strong>not</strong> include the one that match transitively as per content-type hierarchy
*/
List<ContentTypeToLanguageServerDefinition> findProviderFor(final @NonNull Language contentType) {
List<ContentTypeToLanguageServerDefinition> 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<ContentTypeToLanguageServerDefinition> getContentTypeToLSPExtensions() {
return this.connections.stream().filter(mapping -> mapping.getValue() instanceof ExtensionLanguageServerDefinition).collect(Collectors.toList());
connections.add(new ContentTypeToLanguageServerDefinition(language, serverDefinition, mapping.getDocumentMatcher()));
}

public @Nullable
Expand All @@ -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<LanguageServerDefinition> getAvailableLSFor(Language language) {
Set<LanguageServerDefinition> 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<LanguageServerDefinition> getAllDefinitions() {
Expand Down
Loading

0 comments on commit af5c507

Please sign in to comment.