Skip to content

Commit

Permalink
feat: Qute: add support for template records
Browse files Browse the repository at this point in the history
See redhat-developer/quarkus-ls#956

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Jul 11, 2024
1 parent 5fc6859 commit 6eec921
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class QuteJavaConstants {

public static final String TEMPLATE_CLASS = "io.quarkus.qute.Template";

public static final String TEMPLATE_INSTANCE_INTERFACE = "io.quarkus.qute.TemplateInstance";

public static final String ENGINE_BUILDER_CLASS = "io.quarkus.qute.EngineBuilder";

public static final String VALUE_ANNOTATION_NAME = "value";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ public AbstractQuteTemplateLinkCollector(PsiFile typeRoot, IPsiUtils utils, Prog
this.relativeTemplatesBaseDir = PsiQuteProjectUtils.getRelativeTemplateBaseDir(utils.getModule(), resourcesDir);
}

/**
* Support for "Template Fields"
*
* <p>
* private Template items;
* </p>
*
* @see <a href=
* "https://quarkus.io/guides/qute-reference#quarkus_integration">Quarkus
* Integration</a>
*/
@Override
public void visitField(PsiField node) {
PsiType type = node.getType();
Expand Down Expand Up @@ -130,6 +141,27 @@ private AnnotationLocationSupport getAnnotationLocationSupport() {

@Override
public void visitClass(PsiClass node) {
if (node.isRecord()) {
visitRecordType(node);
} else {
visitClassType(node);
}
}

/**
* Support for "TypeSafe Templates"
*
* <p>
*
* @CheckedTemplate public static class Templates { public static native
* TemplateInstance book(Book book);
* </p>
*
* @see <a href=
* "https://quarkus.io/guides/qute-reference#typesafe_templates">TypeSafe
* Templates</a>
*/
private void visitClassType(PsiClass node) {
levelTypeDecl++;
for (PsiAnnotation annotation : node.getAnnotations()) {
if (AnnotationUtils.isMatchAnnotation(annotation, CHECKED_TEMPLATE_ANNOTATION)
Expand All @@ -148,11 +180,22 @@ public void visitClass(PsiClass node) {
levelTypeDecl--;
}

/**
* Support for "Template Records"
*
* @see <a href=
* "https://quarkus.io/guides/qute-reference#template-records">Template
* Records</a>
*/
private void visitRecordType(PsiClass node) {
String recordName = node.getName();
collectTemplateLink(null, node, null, node.getContainingClass(), null, recordName, false);
}

private static PsiClass getTypeDeclaration(PsiElement node) {
return PsiTreeUtil.getParentOfType(node, PsiClass.class);
}


private void collectTemplateLink(String basePath, PsiMethod methodDeclaration, PsiClass type, boolean ignoreFragments) {
String className = null;
boolean innerClass = levelTypeDecl > 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*******************************************************************************
* Copyright (c) 2024 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package com.redhat.devtools.intellij.qute.psi.internal.template.datamodel;

import static com.redhat.devtools.intellij.qute.psi.internal.QuteJavaConstants.TEMPLATE_INSTANCE_INTERFACE;
import static com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils.getTemplatePath;

import java.util.ArrayList;
import java.util.List;

import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiRecordComponent;
import com.redhat.devtools.intellij.qute.psi.internal.template.TemplateDataSupport;
import com.redhat.devtools.intellij.qute.psi.template.datamodel.AbstractTypeWhichImplementsInterfaceDataModelProvider;
import com.redhat.devtools.intellij.qute.psi.template.datamodel.SearchContext;
import com.redhat.devtools.intellij.qute.psi.utils.PsiTypeUtils;

import com.redhat.qute.commons.datamodel.DataModelParameter;
import com.redhat.qute.commons.datamodel.DataModelTemplate;

/**
* Template Records support for template files:
*
* <code>
* public class HelloResource {
* <p>
* record Hello(String name) implements TemplateInstance {}
* <p>
* ...
* <p>
* <p>
* &#64;GET
* &#64;Produces(MediaType.TEXT_PLAIN)
* public TemplateInstance get(@QueryParam("name") String name) {
* return new Hello(name).data("bar", 100);
* }
* </code>
*
* @author Angelo ZERR
* @see <a href=
* "https://quarkus.io/guides/qute-reference#template-records">Template
* Records</a>
*/
public class TemplateRecordsSupport extends AbstractTypeWhichImplementsInterfaceDataModelProvider {

private static final String[] INTERFACE_NAMES = {TEMPLATE_INSTANCE_INTERFACE};

@Override
protected String[] getInterfaceNames() {
return INTERFACE_NAMES;
}

@Override
protected void processType(PsiClass type, SearchContext context, ProgressIndicator monitor) {
if (!type.isRecord()) {
return;
}
collectDataModelTemplateForTemplateRecord(type, context.getDataModelProject().getTemplates(),
monitor);
}

private static void collectDataModelTemplateForTemplateRecord(PsiClass type,
List<DataModelTemplate<DataModelParameter>> templates, ProgressIndicator monitor) {
DataModelTemplate<DataModelParameter> template = createTemplateDataModel(type, monitor);
templates.add(template);
}

private static DataModelTemplate<DataModelParameter> createTemplateDataModel(PsiClass type,
ProgressIndicator monitor) {

String recordName = type.getName();
// src/main/resources/templates/${recordName}.qute.html
String templateUri = getTemplatePath(context.getRelativeTemplateBaseDir(), null, null, recordName, true).getTemplateUri();

// Create template data model with:
// - template uri : Qute template file which must be bind with data model.
// - source type : the record class which defines Templates
// -
DataModelTemplate<DataModelParameter> template = new DataModelTemplate<DataModelParameter>();
template.setParameters(new ArrayList<>());
template.setTemplateUri(templateUri);
template.setSourceType(type.getQualifiedName());

// Collect data parameters from the record fields
for (PsiRecordComponent field : type.getRecordComponents()) {
DataModelParameter parameter = new DataModelParameter();
parameter.setKey(field.getName());
parameter.setSourceType(PsiTypeUtils.resolveSignature(field.getType(), field.isVarArgs()));
template.getParameters().add(parameter);
}

// Collect data parameters for the given template
TemplateDataSupport.collectParametersFromDataMethodInvocation(type, template, monitor);
return template;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.searches.AnnotatedElementsSearch;
import com.intellij.psi.search.searches.DefinitionsScopedSearch;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.util.EmptyQuery;
import com.intellij.util.MergeQuery;
Expand Down Expand Up @@ -125,6 +126,22 @@ protected static Query<PsiReference> createFieldDeclarationTypeReferenceSearchPa
}
}

/**
* Create a search pattern for the given <code>interfaceName</code> interface.
*
* @param interfaceName the interface name to search.
* @return a search pattern for the given <code>interfaceName</code> interface.
*/
protected static Query<PsiElement> createTypeWhichImplementsInterfaceSearchPattern(SearchContext context,
String interfaceName) {
PsiClass interfaceClass = context.getUtils().findClass(context.getJavaProject(), interfaceName);
if (interfaceClass != null) {
return DefinitionsScopedSearch.search(interfaceClass, context.getJavaProject().getModuleWithDependenciesAndLibrariesScope(false), false);
} else {
return new EmptyQuery<>();
}
}

@Override
public void endSearch(SearchContext context, ProgressIndicator monitor) {
NamespaceResolverInfo info = getNamespaceResolverInfo();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*******************************************************************************
* Copyright (c) 2024 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package com.redhat.devtools.intellij.qute.psi.template.datamodel;

import java.util.logging.Level;
import java.util.logging.Logger;

import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.psi.PsiClass;
import com.intellij.util.Query;
import org.jetbrains.annotations.NotNull;

/**
* Abstract class for data model provider based on class type search which
* implements some interfaces.
*
* @author Angelo ZERR
*
*/
public abstract class AbstractTypeWhichImplementsInterfaceDataModelProvider extends AbstractDataModelProvider {

private static final Logger LOGGER = Logger
.getLogger(AbstractTypeWhichImplementsInterfaceDataModelProvider.class.getName());

@Override
protected String[] getPatterns() {
return getInterfaceNames();
}

/**
* Returns the interface names to search.
*
* @return the interface names to search.
*/
protected abstract String[] getInterfaceNames();

@Override
protected Query<? extends Object> createSearchPattern(SearchContext context, String interfaceName) {
return createTypeWhichImplementsInterfaceSearchPattern(context, interfaceName);
}

@Override
public void collectDataModel(Object match, SearchContext context, ProgressIndicator monitor) {
Object element = match;
if (element instanceof PsiClass type) {
try {
if (isApplicable(type)) {
processType(type, context, monitor);
}
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE,
"Cannot collect Qute data model for the type '" + type.getQualifiedName() + "'.", e);
}
}
}
}

private boolean isApplicable(PsiClass type) {
PsiClass @NotNull [] superInterfaceNames = type.getInterfaces();
if (superInterfaceNames == null || superInterfaceNames.length == 0) {
return false;
}
for (String interfaceName : getInterfaceNames()) {
for (PsiClass superInterfaceName : superInterfaceNames) {
if (interfaceName.equals(superInterfaceName.getQualifiedName())) {
return true;
}
}
}
return false;
}

protected abstract void processType(PsiClass recordElement, SearchContext context, ProgressIndicator monitor);

}
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,8 @@
implementation="com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.reactivemessaging.java.MicroProfileReactiveMessagingASTValidator"/>

<!-- Qute -->
<qute.dataModelProvider
implementationClass="com.redhat.devtools.intellij.qute.psi.internal.template.datamodel.TemplateRecordsSupport"/>
<qute.dataModelProvider
implementationClass="com.redhat.devtools.intellij.qute.psi.internal.template.datamodel.CheckedTemplateSupport"/>
<qute.dataModelProvider
Expand Down

0 comments on commit 6eec921

Please sign in to comment.