lenses) {
+ CodeLens codeLens = new CodeLens(QutePositionUtility.ZERO_RANGE);
+ Command command = new Command("[Alternative]",
+ applicationPropertiesOrDotQuteFileUri != null ? QuteClientCommandConstants.COMMAND_OPEN_URI : "");
+ if (applicationPropertiesOrDotQuteFileUri != null) {
+ command.setArguments(List.of(applicationPropertiesOrDotQuteFileUri));
+ }
+ codeLens.setCommand(command);
+ lenses.add(codeLens);
+ }
+
}
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/DotQuteFile.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/DotQuteFile.java
new file mode 100644
index 000000000..790bd6407
--- /dev/null
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/DotQuteFile.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+* Copyright (c) 2026 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.qute.project.extensions.config;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import com.redhat.qute.commons.TemplateRootPath;
+
+/**
+ * Represents a .qute configuration file marking a template root directory.
+ *
+ *
+ * A .qute file in a template root directory indicates configuration for all templates
+ * in that directory and subdirectories, such as whether to use alternative expression
+ * syntax via the {@code alt-expr-syntax} property.
+ *
+ *
+ * @author Angelo ZERR
+ */
+public class DotQuteFile extends PropertiesFile {
+
+ private final TemplateRootPath templateRootPath;
+
+ /**
+ * Creates a new .qute file and loads its content.
+ *
+ * @param quteFile the path to the .qute file
+ * @param templateRootPath the template root path associated with this .qute file
+ * @param propertiesFileName the file name info
+ * @throws IOException if an error occurs reading the file
+ */
+ public DotQuteFile(Path quteFile, TemplateRootPath templateRootPath, PropertiesFileName propertiesFileName) throws IOException {
+ super(quteFile, propertiesFileName);
+ this.templateRootPath = templateRootPath;
+ }
+
+ /**
+ * Returns the template root path associated with this .qute file.
+ *
+ * @return the template root path
+ */
+ public TemplateRootPath getTemplateRootPath() {
+ return templateRootPath;
+ }
+
+ /**
+ * Gets the alt-expr-syntax configuration value.
+ *
+ * @return true if alternative expression syntax is enabled
+ */
+ public boolean isAltExprSyntax() {
+ String value = getProperty("alt-expr-syntax");
+ return "true".equals(value);
+ }
+
+}
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/DotQuteFileRegistry.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/DotQuteFileRegistry.java
new file mode 100644
index 000000000..452b08370
--- /dev/null
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/DotQuteFileRegistry.java
@@ -0,0 +1,142 @@
+/*******************************************************************************
+* Copyright (c) 2026 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.qute.project.extensions.config;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.redhat.qute.commons.TemplateRootPath;
+import com.redhat.qute.project.extensions.config.PropertiesFile.PropertiesFileName;
+
+/**
+ * Registry for .qute configuration files associated with template root paths.
+ *
+ *
+ * Manages .qute files marking template root directories and their configurations.
+ *
+ */
+public class DotQuteFileRegistry {
+
+ private static final Logger LOGGER = Logger.getLogger(DotQuteFileRegistry.class.getName());
+
+ private final Map dotQuteFiles;
+
+ public DotQuteFileRegistry() {
+ this.dotQuteFiles = new HashMap<>();
+ }
+
+ /**
+ * Loads .qute files from template root paths and updates their altExprSyntax property.
+ *
+ * @param templateRootPaths the template root paths to scan
+ */
+ public void load(List templateRootPaths) {
+ if (templateRootPaths == null) {
+ return;
+ }
+ for (TemplateRootPath templateRootPath : templateRootPaths) {
+ loadDotQuteFile(templateRootPath);
+ }
+ }
+
+ /**
+ * Loads a .qute file from a template root path if it exists and updates the template root path's altExprSyntax property.
+ *
+ * @param templateRootPath the template root path
+ */
+ private void loadDotQuteFile(TemplateRootPath templateRootPath) {
+ Path basePath = templateRootPath.getBasePath();
+ if (basePath == null) {
+ return;
+ }
+ Path quteFile = basePath.resolve(".qute");
+ if (Files.exists(quteFile)) {
+ try {
+ PropertiesFileName propertiesFileName = new PropertiesFileName(".qute", null);
+ DotQuteFile dotQuteFile = new DotQuteFile(quteFile, templateRootPath, propertiesFileName);
+ dotQuteFiles.put(templateRootPath, dotQuteFile);
+ // Update the template root path's altExprSyntax property
+ templateRootPath.setAltExprSyntax(dotQuteFile.isAltExprSyntax());
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Error loading .qute file: " + quteFile, e);
+ }
+ } else {
+ // No .qute file, set to null to delegate to application.properties
+ templateRootPath.setAltExprSyntax(null);
+ }
+ }
+
+ /**
+ * Finds the .qute file for a given template root path.
+ *
+ * @param templateRootPath the template root path
+ * @return the .qute file, or null if none found
+ */
+ public DotQuteFile findDotQuteFileFor(TemplateRootPath templateRootPath) {
+ return dotQuteFiles.get(templateRootPath);
+ }
+
+ /**
+ * Handles file system changes for .qute files and updates the template root path's altExprSyntax property.
+ *
+ * @param filePath the changed file path
+ * @param templateRootPaths the template root paths
+ * @return true if a .qute file was affected
+ */
+ public boolean didChangeWatchedFile(Path filePath, List templateRootPaths) {
+ String fileName = filePath.getName(filePath.getNameCount() - 1).toString();
+ if (!".qute".equals(fileName)) {
+ return false;
+ }
+
+ // Find which template root path contains this .qute file
+ for (TemplateRootPath templateRootPath : templateRootPaths) {
+ Path basePath = templateRootPath.getBasePath();
+ if (basePath != null && filePath.equals(basePath.resolve(".qute"))) {
+ try {
+ if (Files.exists(filePath)) {
+ // Reload the .qute file
+ PropertiesFileName propertiesFileName = new PropertiesFileName(".qute", null);
+ DotQuteFile dotQuteFile = new DotQuteFile(filePath, templateRootPath, propertiesFileName);
+ dotQuteFiles.put(templateRootPath, dotQuteFile);
+ // Update the template root path's altExprSyntax property
+ templateRootPath.setAltExprSyntax(dotQuteFile.isAltExprSyntax());
+ } else {
+ // Remove the .qute file, set to null to delegate to application.properties
+ dotQuteFiles.remove(templateRootPath);
+ templateRootPath.setAltExprSyntax(null);
+ }
+ return true;
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "Error handling .qute file change: " + filePath, e);
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void clearFiles() {
+ dotQuteFiles.clear();
+ }
+
+ public boolean hasFiles() {
+ return !dotQuteFiles.isEmpty();
+ }
+
+}
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/PropertiesFile.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/PropertiesFile.java
index e1b95a440..2ede1bb6e 100644
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/PropertiesFile.java
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/PropertiesFile.java
@@ -11,35 +11,136 @@
*******************************************************************************/
package com.redhat.qute.project.extensions.config;
+import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
/**
- * Properties file.
+ * Represents a properties file with its parsed content.
+ *
+ *
+ * Manages loading and reloading of Java properties file.
+ *
+ *
+ * @author Angelo ZERR
*/
-public class PropertiesFile {
+public class PropertiesFile implements Comparable {
- private final Path file;
+ /**
+ * Holds properties file name information including locale.
+ */
+ public static class PropertiesFileName {
+
+ private final String fileName;
+ private final String locale;
+
+ public PropertiesFileName(String fileName, String locale) {
+ this.fileName = fileName;
+ this.locale = locale;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+ }
private final Properties properties;
+ private final Path propertiesFile;
+ private final PropertiesFileName propertiesFileName;
+
+ private boolean defaultFile;
- public PropertiesFile(Path file) {
- this.file = file;
+ /**
+ * Creates a new messages file info and loads its content.
+ *
+ * @param messagesFile the path to the messages.properties file
+ * @param propertiesFileName
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException if an error occurs reading the file
+ */
+ public PropertiesFile(Path messagesFile, PropertiesFileName propertiesFileName) throws IOException {
+ this.propertiesFile = messagesFile;
+ this.propertiesFileName = propertiesFileName;
properties = new Properties();
- if (Files.exists(file)) {
- try (InputStream input = Files.newInputStream(file)) {
- properties.load(input);
- } catch (IOException e) {
- // throw new RuntimeException("Failed to load properties file: " + file, e);
- }
- }
+ reload();
+ }
+
+ /**
+ * Checks if a message key exists in this properties file.
+ *
+ * @param messageKey the message key to check (e.g., "main.login")
+ * @return true if the key exists
+ */
+ public boolean hasMessage(String messageKey) {
+ return properties.containsKey(messageKey);
+ }
+
+ /**
+ * Returns the path to the.properties file.
+ *
+ * @return the file path
+ */
+ public Path getPropertiesFile() {
+ return propertiesFile;
}
public String getProperty(String key) {
return properties.getProperty(key);
}
-}
+ public String getLocale() {
+ return propertiesFileName.getLocale();
+ }
+
+ /**
+ * Reloads the properties from the file.
+ *
+ *
+ * Called when the file is modified to refresh the in-memory content.
+ *
+ *
+ * @throws IOException if an error occurs reading the file
+ */
+ public void reload() throws IOException {
+ properties.clear();
+ try (Reader reader = Files.newBufferedReader(propertiesFile, StandardCharsets.UTF_8)) {
+ properties.load(reader);
+ }
+ }
+
+ public boolean isDefaultFile() {
+ return defaultFile;
+ }
+
+ public void setDefaultFile(boolean defaultFile) {
+ this.defaultFile = defaultFile;
+ }
+
+ public Properties getProperties() {
+ return properties;
+ }
+
+ @Override
+ public int compareTo(PropertiesFile other) {
+ // Default file (no locale) always comes first
+ if (this.isDefaultFile() && !other.isDefaultFile()) {
+ return -1;
+ }
+ if (!this.isDefaultFile() && other.isDefaultFile()) {
+ return 1;
+ }
+ // Then sort alphabetically by locale
+ String thisLocale = this.getLocale() != null ? this.getLocale() : "";
+ String otherLocale = other.getLocale() != null ? other.getLocale() : "";
+ return thisLocale.compareTo(otherLocale);
+ }
+
+}
\ No newline at end of file
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/PropertiesFileRegistry.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/PropertiesFileRegistry.java
new file mode 100644
index 000000000..23aa9ff37
--- /dev/null
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/config/PropertiesFileRegistry.java
@@ -0,0 +1,212 @@
+/*******************************************************************************
+* Copyright (c) 2026 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.qute.project.extensions.config;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+
+import org.eclipse.lsp4j.FileChangeType;
+
+import com.redhat.qute.commons.config.PropertyConfig;
+import com.redhat.qute.project.extensions.config.PropertiesFile.PropertiesFileName;
+
+/**
+ * Base registry for managing properties files.
+ *
+ *
+ * Handles loading, reloading, and querying of properties files with file
+ * watching support.
+ *
+ *
+ * @param the properties file type
+ */
+public abstract class PropertiesFileRegistry {
+
+ private static final Logger LOGGER = Logger.getLogger(PropertiesFileRegistry.class.getName());
+
+ private final List propertiesFiles;
+
+ public PropertiesFileRegistry() {
+ propertiesFiles = new ArrayList<>();
+ }
+
+ /**
+ * Scans source folders for properties.properties files.
+ *
+ * @param sourcePaths the source paths to scan
+ */
+ public void load(Set sourcePaths) {
+ for (Path sourcePath : sourcePaths) {
+ if (Files.exists(sourcePath)) {
+ try (Stream stream = Files.list(sourcePath)) {
+ stream.forEach(this::loadPropertiesFile);
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Error scanning source folder: " + sourcePath, e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads a properties file if it matches the naming pattern.
+ *
+ * @param filePath the file path to check and load
+ */
+ private void loadPropertiesFile(Path filePath) {
+ PropertiesFileName propertiesFileName = getPropertiesFileName(filePath);
+ if (propertiesFileName != null) {
+ try {
+ T propertiesFile = createPropertiesFile(filePath, propertiesFileName);
+ // TODO: improve default by reading 'quarkus.default-locale=en' from
+ // application.properties
+ propertiesFile.setDefaultFile(propertiesFile.getLocale() == null);
+ propertiesFiles.add(propertiesFile);
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Error loading properties file: " + filePath, e);
+ }
+ }
+ }
+
+ /**
+ * Gets a configuration property value.
+ *
+ * @param property the property config
+ * @return the property value, or the default value if not found
+ */
+ public String getConfig(PropertyConfig property) {
+ for (PropertiesFile propertiesFile : propertiesFiles) {
+ String value = propertiesFile.getProperty(property.getName());
+ if (value != null) {
+ return value;
+ }
+ }
+ return property.getDefaultValue();
+ }
+
+ /**
+ * Handles file system changes for properties files.
+ *
+ * @param filePath the changed file path
+ * @param sourcePaths the source paths to check
+ * @param changeTypes the types of changes
+ * @return true if a properties file was affected
+ */
+ public boolean didChangeWatchedFile(Path filePath, Set sourcePaths, Set changeTypes) {
+ PropertiesFileName propertiesFileName = getPropertiesFileName(filePath, sourcePaths);
+ if (propertiesFileName == null) {
+ return false;
+ }
+
+ PropertiesFile file = findPropertiesFile(filePath);
+ boolean fileDeleted = changeTypes.contains(FileChangeType.Deleted);
+
+ try {
+ if (file != null) {
+ if (fileDeleted) {
+ propertiesFiles.remove(file);
+ } else {
+ file.reload();
+ }
+ } else if (!fileDeleted) {
+ propertiesFiles.add(createPropertiesFile(filePath, propertiesFileName));
+ }
+ return true;
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Error handling file change: " + filePath, e);
+ return false;
+ }
+ }
+
+ /**
+ * Finds a properties file info by filename.
+ *
+ * @param name the filename to search for
+ * @return the properties file info, or null if not found
+ */
+ private PropertiesFile findPropertiesFile(Path filePath) {
+ return propertiesFiles.stream().filter(info -> {
+ Path path = info.getPropertiesFile();
+ // String fileName = path.getName(path.getNameCount() - 1).toString();
+ // return name.equals(fileName);
+ return filePath.equals(path);
+ }).findFirst().orElse(null);
+ }
+
+ /**
+ * Gets properties file name info if the file is in source paths.
+ *
+ * @param filePath the file path
+ * @param sourcePaths the source paths to check
+ * @return the properties file name info, or null if not in source paths
+ */
+ public PropertiesFileName getPropertiesFileName(Path filePath, Set sourcePaths) {
+ if (sourcePaths == null || sourcePaths.isEmpty()) {
+ return null;
+ }
+
+ PropertiesFileName fileName = getPropertiesFileName(filePath);
+ if (fileName == null) {
+ return null;
+ }
+ // Check if file is stored in src/main/resources
+ Path baseDir = filePath.getParent();
+ for (Path sourcePath : sourcePaths) {
+ if (sourcePath.equals(baseDir)) {
+ return fileName;
+ }
+ }
+ return null;
+ }
+
+ public void clearFiles() {
+ propertiesFiles.clear();
+ }
+
+ public boolean hasFiles() {
+ return !propertiesFiles.isEmpty();
+ }
+
+ public void sortFiles(Comparator comparator) {
+ propertiesFiles.sort(comparator);
+ }
+
+ public List getPropertiesFiles() {
+ return propertiesFiles;
+ }
+
+ /**
+ * Extracts properties file name info from a file path.
+ *
+ * @param filePath the file path
+ * @return the properties file name info, or null if not a valid properties file
+ */
+ protected abstract PropertiesFileName getPropertiesFileName(Path filePath);
+
+ /**
+ * Creates a properties file instance.
+ *
+ * @param messagesFile the file path
+ * @param propertiesFileName the file name info
+ * @return the properties file instance
+ * @throws IOException if an error occurs reading the file
+ */
+ protected abstract T createPropertiesFile(Path messagesFile, PropertiesFileName propertiesFileName)
+ throws IOException;
+}
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/flags/FlagsProjectExtension.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/flags/FlagsProjectExtension.java
index 3cb8c3b77..b20b6c072 100644
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/flags/FlagsProjectExtension.java
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/flags/FlagsProjectExtension.java
@@ -20,6 +20,7 @@
import com.redhat.qute.project.datamodel.ExtendedDataModelProject;
import com.redhat.qute.project.datamodel.resolvers.MethodValueResolver;
import com.redhat.qute.project.extensions.AbstractProjectExtension;
+import com.redhat.qute.project.extensions.ProjectExtensionContext;
/**
* Qute project extension for Flags integration.
@@ -46,7 +47,8 @@ public FlagsProjectExtension() {
}
@Override
- protected void init(ExtendedDataModelProject dataModelProject, boolean enabled) {
+ protected void initialize(ExtendedDataModelProject dataModelProject, boolean onLoad, boolean enabled,
+ ProjectExtensionContext context) {
if (enabled) {
// register flag: namespace support
dataModelProject.registerNamespaceResolver(FLAG_NAMESPACE);
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFile.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFile.java
new file mode 100644
index 000000000..ac3a27207
--- /dev/null
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFile.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+* Copyright (c) 2026 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.qute.project.extensions.renarde;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import com.redhat.qute.project.extensions.config.PropertiesFile;
+
+/**
+ * Represents a messages.properties file with its parsed content.
+ *
+ *
+ * Manages loading and reloading of Java properties files used for
+ * internationalization (i18n) in Renarde applications.
+ *
+ *
+ * @author Angelo ZERR
+ */
+public class MessagesFile extends PropertiesFile {
+
+ /**
+ * Creates a new messages file info and loads its content.
+ *
+ * @param messagesFile the path to the messages.properties file
+ * @param messagesFileName
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException if an error occurs reading the file
+ */
+ public MessagesFile(Path messagesFile, PropertiesFileName messagesFileName) throws IOException {
+ super(messagesFile, messagesFileName);
+ }
+
+}
\ No newline at end of file
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFileInfo.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFileInfo.java
deleted file mode 100644
index 502091e22..000000000
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFileInfo.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*******************************************************************************
-* Copyright (c) 2026 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.qute.project.extensions.renarde;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.Reader;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Properties;
-import java.util.Set;
-
-/**
- * Represents a messages.properties file with its parsed content.
- *
- *
- * Manages loading and reloading of Java properties files used for
- * internationalization (i18n) in Renarde applications.
- *
- *
- * @author Angelo ZERR
- */
-public class MessagesFileInfo implements Comparable {
-
- public static class MessagesFileName {
-
- private final String fileName;
- private final String locale;
-
- public MessagesFileName(String fileName, String locale) {
- this.fileName = fileName;
- this.locale = locale;
- }
-
- public String getFileName() {
- return fileName;
- }
-
- public String getLocale() {
- return locale;
- }
- }
-
- private final Properties properties;
- private final Path messagesFile;
- private final MessagesFileName messagesFileName;
-
- private boolean defaultFile;
-
- /**
- * Creates a new messages file info and loads its content.
- *
- * @param messagesFile the path to the messages.properties file
- * @param messagesFileName
- * @throws FileNotFoundException if the file does not exist
- * @throws IOException if an error occurs reading the file
- */
- public MessagesFileInfo(Path messagesFile, MessagesFileName messagesFileName) throws IOException {
- this.messagesFile = messagesFile;
- this.messagesFileName = messagesFileName;
- properties = new Properties();
- reload();
- }
-
- /**
- * Checks if a message key exists in this properties file.
- *
- * @param messageKey the message key to check (e.g., "main.login")
- * @return true if the key exists
- */
- public boolean hasMessage(String messageKey) {
- return properties.containsKey(messageKey);
- }
-
- /**
- * Returns the path to the messages.properties file.
- *
- * @return the file path
- */
- public Path getMessagesFile() {
- return messagesFile;
- }
-
- /**
- * Returns the properties loaded from this file.
- *
- * @return the properties
- */
- public Properties getProperties() {
- return properties;
- }
-
- public String getLocale() {
- return messagesFileName.getLocale();
- }
-
- /**
- * Reloads the properties from the file.
- *
- *
- * Called when the file is modified to refresh the in-memory content.
- *
- *
- * @throws IOException if an error occurs reading the file
- */
- public void reload() throws IOException {
- properties.clear();
- try (Reader reader = Files.newBufferedReader(messagesFile, StandardCharsets.UTF_8)) {
- properties.load(reader);
- }
- }
-
- public boolean isDefaultFile() {
- return defaultFile;
- }
-
- public void setDefaultFile(boolean defaultFile) {
- this.defaultFile = defaultFile;
- }
-
- public static MessagesFileName getMessagesFileName(Path filePath, Set sourcePaths) {
- if (sourcePaths == null || sourcePaths.isEmpty()) {
- return null;
- }
-
- MessagesFileName fileName = getMessagesFileName(filePath);
- if (fileName == null) {
- return null;
- }
- // Check if file is stored in src/main/resources
- Path baseDir = filePath.getParent();
- for (Path sourcePath : sourcePaths) {
- if (sourcePath.equals(baseDir)) {
- return fileName;
- }
- }
- return null;
- }
-
- public static MessagesFileName getMessagesFileName(Path filePath) {
- String fileName = filePath.getName(filePath.getNameCount() - 1).toString();
- if (fileName.equals("messages.properties")) {
- return new MessagesFileName(fileName, null);
- } else {
- if (fileName.startsWith("messages_")) {
- int start = 9;
- int end = fileName.indexOf(".properties", start);
- if (end != -1) {
- String locale = fileName.substring(start, end);
- return new MessagesFileName(fileName, locale);
- }
- }
- }
- return null;
- }
-
- @Override
- public int compareTo(MessagesFileInfo other) {
- // Default file (no locale) always comes first
- if (this.isDefaultFile() && !other.isDefaultFile()) {
- return -1;
- }
- if (!this.isDefaultFile() && other.isDefaultFile()) {
- return 1;
- }
- // Then sort alphabetically by locale
- String thisLocale = this.getLocale() != null ? this.getLocale() : "";
- String otherLocale = other.getLocale() != null ? other.getLocale() : "";
- return thisLocale.compareTo(otherLocale);
- }
-
-}
\ No newline at end of file
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFileRegistry.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFileRegistry.java
new file mode 100644
index 000000000..71e56638b
--- /dev/null
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/MessagesFileRegistry.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+* Copyright (c) 2026 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.qute.project.extensions.renarde;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import com.redhat.qute.project.extensions.config.PropertiesFile.PropertiesFileName;
+import com.redhat.qute.project.extensions.config.PropertiesFileRegistry;
+
+/**
+ * Registry for messages.properties files.
+ *
+ * Manages messages.properties and messages_{locale}.properties files for Renarde i18n.
+ */
+public class MessagesFileRegistry extends PropertiesFileRegistry {
+
+ @Override
+ protected PropertiesFileName getPropertiesFileName(Path filePath) {
+ String fileName = filePath.getName(filePath.getNameCount() - 1).toString();
+ if (fileName.equals("messages.properties")) {
+ return new PropertiesFileName(fileName, null);
+ } else {
+ if (fileName.startsWith("messages_")) {
+ int start = 9;
+ int end = fileName.indexOf(".properties", start);
+ if (end != -1) {
+ String locale = fileName.substring(start, end);
+ return new PropertiesFileName(fileName, locale);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected MessagesFile createPropertiesFile(Path propertiesFile, PropertiesFileName propertiesFileName)
+ throws IOException {
+ return new MessagesFile(propertiesFile, propertiesFileName);
+ }
+
+}
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/RenardeProjectExtension.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/RenardeProjectExtension.java
index 0f13ca20c..dd1dcd72d 100644
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/RenardeProjectExtension.java
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/renarde/RenardeProjectExtension.java
@@ -13,14 +13,11 @@
import static com.redhat.qute.services.diagnostics.DiagnosticDataFactory.createDiagnostic;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.stream.Stream;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionItemKind;
@@ -56,7 +53,7 @@
import com.redhat.qute.project.extensions.DidChangeWatchedFilesParticipant;
import com.redhat.qute.project.extensions.HoverParticipant;
import com.redhat.qute.project.extensions.InlayHintParticipant;
-import com.redhat.qute.project.extensions.renarde.MessagesFileInfo.MessagesFileName;
+import com.redhat.qute.project.extensions.ProjectExtensionContext;
import com.redhat.qute.services.ResolvingJavaTypeContext;
import com.redhat.qute.services.completions.CompletionRequest;
import com.redhat.qute.settings.QuteCompletionSettings;
@@ -102,63 +99,29 @@ public class RenardeProjectExtension extends AbstractProjectExtension
private static final Logger LOGGER = Logger.getLogger(RenardeProjectExtension.class.getName());
- private final List messagesFileInfos;
+ private final MessagesFileRegistry messagesFileRegistry;
private Set sourcePaths;
public RenardeProjectExtension() {
super(RenardeConfig.PROJECT_FEATURE);
- this.messagesFileInfos = new ArrayList<>();
+ this.messagesFileRegistry = new MessagesFileRegistry();
}
@Override
- protected void init(ExtendedDataModelProject dataModelProject, boolean enabled) {
+ protected void initialize(ExtendedDataModelProject dataModelProject, boolean onLoad, boolean enabled,
+ ProjectExtensionContext context) {
// Check if the m: namespace resolver exists in the project
sourcePaths = dataModelProject.getSourcePaths();
if (!enabled) {
- messagesFileInfos.clear();
+ messagesFileRegistry.clearFiles();
return;
}
// Scan project source folders for messages.properties files
- if (messagesFileInfos.isEmpty()) {
- scanMessagesFiles(dataModelProject.getSourcePaths());
- messagesFileInfos.sort(null);
- }
- }
-
- /**
- * Scans source folders for messages.properties files.
- *
- * @param sourcePaths the source paths to scan
- */
- private void scanMessagesFiles(Set sourcePaths) {
- for (Path sourcePath : sourcePaths) {
- try (Stream stream = Files.list(sourcePath)) {
- stream.forEach(this::loadMessagesFile);
- } catch (Exception e) {
- LOGGER.log(Level.SEVERE, "Error scanning source folder: " + sourcePath, e);
- }
- }
- }
-
- /**
- * Loads a messages file if it matches the naming pattern.
- *
- * @param filePath the file path to check and load
- */
- private void loadMessagesFile(Path filePath) {
- MessagesFileName messagesFileName = MessagesFileInfo.getMessagesFileName(filePath);
- if (messagesFileName != null) {
- try {
- MessagesFileInfo messagesFile = new MessagesFileInfo(filePath, messagesFileName);
- // TODO: improve default by reading 'quarkus.default-locale=en' from
- // application.properties
- messagesFile.setDefaultFile(messagesFile.getLocale() == null);
- messagesFileInfos.add(messagesFile);
- } catch (Exception e) {
- LOGGER.log(Level.SEVERE, "Error loading messages file: " + filePath, e);
- }
+ if (!messagesFileRegistry.hasFiles()) {
+ messagesFileRegistry.load(dataModelProject.getSourcePaths());
+ messagesFileRegistry.sortFiles(null);
}
}
@@ -169,7 +132,9 @@ private void loadMessagesFile(Path filePath) {
* @return true if the key exists
*/
public boolean hasMessage(String messageKey) {
- return messagesFileInfos.stream().anyMatch(info -> info.hasMessage(messageKey));
+ return messagesFileRegistry.getPropertiesFiles() //
+ .stream() //
+ .anyMatch(propertiesFile -> propertiesFile.hasMessage(messageKey));
}
/**
@@ -177,8 +142,8 @@ public boolean hasMessage(String messageKey) {
*
* @return the list of messages file info
*/
- public List getMessagesFileInfos() {
- return messagesFileInfos;
+ public List getMessagesFiles() {
+ return messagesFileRegistry.getPropertiesFiles();
}
/**
@@ -186,22 +151,12 @@ public List getMessagesFileInfos() {
*
* @return the default messages file info, or null if not found
*/
- private MessagesFileInfo getDefaultMessagesFileInfo() {
- return messagesFileInfos.stream().filter(MessagesFileInfo::isDefaultFile).findFirst().orElse(null);
- }
-
- /**
- * Finds a messages file info by filename.
- *
- * @param name the filename to search for
- * @return the messages file info, or null if not found
- */
- private MessagesFileInfo find(String name) {
- return messagesFileInfos.stream().filter(info -> {
- Path path = info.getMessagesFile();
- String fileName = path.getName(path.getNameCount() - 1).toString();
- return name.equals(fileName);
- }).findFirst().orElse(null);
+ private MessagesFile getDefaultMessagesFile() {
+ return messagesFileRegistry.getPropertiesFiles() //
+ .stream()//
+ .filter(MessagesFile::isDefaultFile)//
+ .findFirst()//
+ .orElse(null);
}
/**
@@ -241,30 +196,9 @@ private static String getMessageKey(Parts parts) {
// ========================
@Override
- public boolean didChangeWatchedFile(Path filePath, Set changeTypes) {
- MessagesFileName messagesFileName = MessagesFileInfo.getMessagesFileName(filePath, sourcePaths);
- if (messagesFileName == null) {
- return false;
- }
-
- MessagesFileInfo file = find(messagesFileName.getFileName());
- boolean fileDeleted = changeTypes.contains(FileChangeType.Deleted);
-
- try {
- if (file != null) {
- if (fileDeleted) {
- messagesFileInfos.remove(file);
- } else {
- file.reload();
- }
- } else if (!fileDeleted) {
- messagesFileInfos.add(new MessagesFileInfo(filePath, messagesFileName));
- }
- return true;
- } catch (Exception e) {
- LOGGER.log(Level.SEVERE, "Error handling file change: " + filePath, e);
- return false;
- }
+ public boolean didChangeWatchedFile(Path filePath, Set changeTypes,
+ ProjectExtensionContext context) {
+ return messagesFileRegistry.didChangeWatchedFile(filePath, sourcePaths, changeTypes);
}
// ======================== DefinitionParticipant ========================
@@ -275,8 +209,8 @@ public void definition(Part part, List locationLinks, CancelChecke
String messageKey = getMessageKey(parts);
if (messageKey != null && hasMessage(messageKey)) {
- for (MessagesFileInfo info : messagesFileInfos) {
- String messagesFileUri = info.getMessagesFile().toUri().toASCIIString();
+ for (MessagesFile info : messagesFileRegistry.getPropertiesFiles()) {
+ String messagesFileUri = info.getPropertiesFile().toUri().toASCIIString();
Range originRange = QutePositionUtility.createRange(parts);
// TODO: Parse .properties file to find exact line number of the key
@@ -324,8 +258,8 @@ public void doHover(Part part, List hovers, CancelChecker cancelChecker)
boolean hasValue = false;
// Collect message values from all messages files (different locales)
- for (MessagesFileInfo messagesFileInfo : messagesFileInfos) {
- String value = (String) messagesFileInfo.getProperties().get(messageKey);
+ for (MessagesFile messagesFileInfo : messagesFileRegistry.getPropertiesFiles()) {
+ String value = (String) messagesFileInfo.getProperty(messageKey);
if (value != null) {
if (hasValue) {
doc.append("\n");
@@ -356,7 +290,7 @@ public void doHover(Part part, List hovers, CancelChecker cancelChecker)
* @param doc the string builder to append to
* @param messagesFileInfo the messages file info
*/
- private void appendLocaleInfo(StringBuilder doc, MessagesFileInfo messagesFileInfo) {
+ private void appendLocaleInfo(StringBuilder doc, MessagesFile messagesFileInfo) {
if (messagesFileInfo.isDefaultFile()) {
doc.append("default");
} else {
@@ -376,12 +310,12 @@ public void collectInlayHints(Expression node, List inlayHints, Cance
return;
}
- MessagesFileInfo defaultMessagesFileInfo = getDefaultMessagesFileInfo();
- if (defaultMessagesFileInfo == null) {
+ MessagesFile defaultMessagesFile = getDefaultMessagesFile();
+ if (defaultMessagesFile == null) {
return;
}
- String value = (String) defaultMessagesFileInfo.getProperties().get(messageKey);
+ String value = defaultMessagesFile.getProperty(messageKey);
if (value == null || value.isEmpty()) {
return;
}
@@ -413,8 +347,8 @@ public void doComplete(CompletionRequest completionRequest, Part part, Parts par
Range range = createRange(completionRequest, part, parts);
- for (MessagesFileInfo messagesFileInfo : messagesFileInfos) {
- messagesFileInfo.getProperties().forEach((k, v) -> {
+ for (MessagesFile messagesFile : messagesFileRegistry.getPropertiesFiles()) {
+ messagesFile.getProperties().forEach((k, v) -> {
String key = k.toString();
String value = v.toString();
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/RoqProjectExtension.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/RoqProjectExtension.java
index fc7ffdf09..99c0062b2 100644
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/RoqProjectExtension.java
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/RoqProjectExtension.java
@@ -51,6 +51,7 @@
import com.redhat.qute.project.extensions.DidChangeWatchedFilesParticipant;
import com.redhat.qute.project.extensions.MemberResolutionParticipant;
import com.redhat.qute.project.extensions.ProjectExtension;
+import com.redhat.qute.project.extensions.ProjectExtensionContext;
import com.redhat.qute.project.extensions.TemplateLanguageInjectionParticipant;
import com.redhat.qute.project.extensions.roq.data.DataLoader;
import com.redhat.qute.project.extensions.roq.data.RoqDataFile;
@@ -226,12 +227,13 @@ public RoqProjectExtension() {
* @param dataModelProject The project data model to extend
*/
@Override
- protected void init(ExtendedDataModelProject dataModelProject, boolean enabled) {
+ protected void initialize(ExtendedDataModelProject dataModelProject, boolean onLoad, boolean enabled,
+ ProjectExtensionContext context) {
if (enabled) {
scanDataDir(dataModelProject);
contentDir = dataModelProject.getConfigAsPath(RoqConfig.SITE_CONTENT_DIR);
- if (availableThemes == null) {
+ if (onLoad) {
// Find available themes
availableThemes = findAvailableThemes(dataModelProject.getBinaryDocuments());
@@ -240,20 +242,21 @@ protected void init(ExtendedDataModelProject dataModelProject, boolean enabled)
// not initialized and
// template are parsed with INJECTOR_DETECTORS.
// We need to reparse all binary and opened templates.
- reparseTemplates(dataModelProject);
+ reparseTemplates(dataModelProject, context);
}
} else {
// Roq not enabled - clear data directory
dataDir = null;
}
+
}
- private static void reparseTemplates(ExtendedDataModelProject dataModelProject) {
+ private static void reparseTemplates(ExtendedDataModelProject dataModelProject, ProjectExtensionContext context) {
// Reparse opened source document
for (QuteTextDocument document : dataModelProject.getSourceDocuments()) {
if (!document.isUserTag() && document.isOpened()) {
- document.reparseTemplate();
+ context.reparseTemplate(document);
}
}
// Reparse binary document
@@ -430,7 +433,8 @@ private void registerRoqDataFile(Path file, ExtendedDataModelProject dataModelPr
* @return true if this extension handled the file, false otherwise
*/
@Override
- public boolean didChangeWatchedFile(Path filePath, Set changeTypes) {
+ public boolean didChangeWatchedFile(Path filePath, Set changeTypes,
+ ProjectExtensionContext context) {
// Check if the file is in our data directory
if (dataDir != null && filePath.startsWith(dataDir)) {
var dataModelProject = getDataModelProject();
@@ -669,7 +673,8 @@ public TemplatePath getImagePath(Path filePath, String imageFilePath) {
// ======================== MemberResolutionParticipant ========================
@Override
- public List getAdditionalTypes(ResolvedJavaTypeInfo baseType, Part previousPart, Part part, Template template) {
+ public List getAdditionalTypes(ResolvedJavaTypeInfo baseType, Part previousPart, Part part,
+ Template template) {
return yamlFrontMatterSupport.getAdditionalTypes(baseType, previousPart, part, template);
}
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/webbundler/WebBundlerProjectExtension.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/webbundler/WebBundlerProjectExtension.java
index 0808eb10a..d9e6265b7 100644
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/webbundler/WebBundlerProjectExtension.java
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/webbundler/WebBundlerProjectExtension.java
@@ -14,6 +14,7 @@
import com.redhat.qute.commons.config.webbundler.WebBundlerConfig;
import com.redhat.qute.project.datamodel.ExtendedDataModelProject;
import com.redhat.qute.project.extensions.AbstractProjectExtension;
+import com.redhat.qute.project.extensions.ProjectExtensionContext;
/**
* Qute project extension for Web Bundler integration.
@@ -28,7 +29,8 @@ public WebBundlerProjectExtension() {
}
@Override
- protected void init(ExtendedDataModelProject dataModelProject, boolean enabled) {
+ protected void initialize(ExtendedDataModelProject dataModelProject, boolean onLoad, boolean enabled,
+ ProjectExtensionContext context) {
}
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/QuteCompletions.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/QuteCompletions.java
index 992049001..91aacc62a 100644
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/QuteCompletions.java
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/QuteCompletions.java
@@ -184,8 +184,8 @@ public CompletableFuture doComplete(Template template, Position
// {#| --> valid section
// {{#| --> invalid section
// {{{#| --> valid section
-
- if (nbBrackets % 2 != 0) {
+ boolean altExprSyntax = template.getExpressionCommand() != null;
+ if (!altExprSyntax && nbBrackets % 2 != 0) {
// The completion is triggered in text node after bracket '{' character
return completionForExpression.doCompleteExpression(completionRequest, null, node, template, offset,
completionSettings, formattingSettings, nativeImagesSettings, cancelChecker);
diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/settings/capabilities/QuteCapabilityManager.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/settings/capabilities/QuteCapabilityManager.java
index ebfcabaa9..38289f16f 100644
--- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/settings/capabilities/QuteCapabilityManager.java
+++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/settings/capabilities/QuteCapabilityManager.java
@@ -165,9 +165,15 @@ private void registerWatchedFiles() {
watchers.add(new FileSystemWatcher(Either.forLeft("**/*.yaml")));
watchers.add(new FileSystemWatcher(Either.forLeft("**/*.yml")));
watchers.add(new FileSystemWatcher(Either.forLeft("**/*.txt")));
+ // application.properties
+ watchers.add(new FileSystemWatcher(Either.forLeft("**/application.properties")));
+ watchers.add(new FileSystemWatcher(Either.forLeft("**/application-*.properties")));
+ // .qute file used by template root to mark templates as alt expression syntaxe
+ // (ex: {=foo})
+ watchers.add(new FileSystemWatcher(Either.forLeft("**/.qute")));
// Renarde
watchers.add(new FileSystemWatcher(Either.forLeft("**/messages.properties")));
- watchers.add(new FileSystemWatcher(Either.forLeft("**/messages*.properties")));
+ watchers.add(new FileSystemWatcher(Either.forLeft("**/messages_*.properties")));
DidChangeWatchedFilesRegistrationOptions options = new DidChangeWatchedFilesRegistrationOptions(watchers);
registerCapability(WORKSPACE_WATCHED_FILES_ID, WORKSPACE_WATCHED_FILES, options);
}
diff --git a/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/parser/template/TemplateParserAlternativeSyntaxTest.java b/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/parser/template/TemplateParserAlternativeSyntaxTest.java
new file mode 100644
index 000000000..7e45b8a30
--- /dev/null
+++ b/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/parser/template/TemplateParserAlternativeSyntaxTest.java
@@ -0,0 +1,451 @@
+/*******************************************************************************
+* Copyright (c) 2021 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.qute.parser.template;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import com.redhat.qute.parser.expression.MethodPart;
+import com.redhat.qute.parser.expression.Part;
+import com.redhat.qute.parser.expression.Parts;
+import com.redhat.qute.parser.expression.Parts.PartKind;
+
+/**
+ * Test with template parser which builds a Template AST with alternative syntax
+ * expression command (e.g., {=foo} instead of {foo}).
+ *
+ * This syntax is activated via quarkus.qute.alt-...=true configuration.
+ *
+ * @author Angelo ZERR
+ *
+ */
+public class TemplateParserAlternativeSyntaxTest {
+
+ private static final Character EXPRESSION_COMMAND = '=';
+
+ @Test
+ public void simpleExpression() {
+ String content = "{=name}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Expression, first.getKind());
+ Expression expression = (Expression) first;
+
+ assertEquals(0, expression.getStart());
+ assertEquals(7, expression.getEnd());
+ assertEquals("name", expression.getContent());
+ }
+
+ @Test
+ public void expressionWithProperty() {
+ String content = "{=item.name}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Expression, first.getKind());
+ Expression expression = (Expression) first;
+
+ assertEquals(0, expression.getStart());
+ assertEquals(12, expression.getEnd());
+ assertEquals("item.name", expression.getContent());
+
+ // Check expression parts
+ List exprContent = expression.getExpressionContent();
+ assertEquals(1, exprContent.size());
+ Parts parts = (Parts) exprContent.get(0);
+ assertEquals(2, parts.getChildCount());
+
+ // ObjectPart --> item
+ Part itemPart = (Part) parts.getChild(0);
+ assertEquals(PartKind.Object, itemPart.getPartKind());
+ assertEquals("item", itemPart.getPartName());
+
+ // PropertyPart --> name
+ Part namePart = (Part) parts.getChild(1);
+ assertEquals(PartKind.Property, namePart.getPartKind());
+ assertEquals("name", namePart.getPartName());
+ }
+
+ @Test
+ public void expressionWithMethodCall() {
+ String content = "{=item.getPrice()}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Expression, first.getKind());
+ Expression expression = (Expression) first;
+
+ assertEquals(0, expression.getStart());
+ assertEquals(18, expression.getEnd());
+ assertEquals("item.getPrice()", expression.getContent());
+
+ // Check expression parts
+ List exprContent = expression.getExpressionContent();
+ assertEquals(1, exprContent.size());
+ Parts parts = (Parts) exprContent.get(0);
+ assertEquals(2, parts.getChildCount());
+
+ // ObjectPart --> item
+ Part itemPart = (Part) parts.getChild(0);
+ assertEquals(PartKind.Object, itemPart.getPartKind());
+ assertEquals("item", itemPart.getPartName());
+
+ // MethodPart --> getPrice()
+ Part methodPart = (Part) parts.getChild(1);
+ assertEquals(PartKind.Method, methodPart.getPartKind());
+ assertEquals("getPrice", methodPart.getPartName());
+ }
+
+ @Test
+ public void mixedTextAndExpression() {
+ String content = "Hello {=name}!";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(3, template.getChildCount());
+
+ // Text node: "Hello "
+ Node text1 = template.getChild(0);
+ assertEquals(NodeKind.Text, text1.getKind());
+
+ // Expression: {=name}
+ Node expr = template.getChild(1);
+ assertEquals(NodeKind.Expression, expr.getKind());
+ Expression expression = (Expression) expr;
+ assertEquals(6, expression.getStart());
+ assertEquals(13, expression.getEnd());
+ assertEquals("name", expression.getContent());
+
+ // Text node: "!"
+ Node text2 = template.getChild(2);
+ assertEquals(NodeKind.Text, text2.getKind());
+ }
+
+ @Test
+ public void expressionInSection() {
+ String content = "{#if item.active}\n" +
+ " {=item.name}\n" +
+ "{/if}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ // {#if item.active}
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Section, first.getKind());
+ Section section = (Section) first;
+ assertEquals(SectionKind.IF, section.getSectionKind());
+ assertTrue(section.isClosed());
+
+ // Check section has 3 children: text, expression, text
+ assertEquals(3, section.getChildCount());
+
+ // Text node: "\n "
+ Node text1 = section.getChild(0);
+ assertEquals(NodeKind.Text, text1.getKind());
+
+ // Expression: {=item.name}
+ Node expr = section.getChild(1);
+ assertEquals(NodeKind.Expression, expr.getKind());
+ Expression expression = (Expression) expr;
+ assertEquals("item.name", expression.getContent());
+
+ // Text node: "\n"
+ Node text2 = section.getChild(2);
+ assertEquals(NodeKind.Text, text2.getKind());
+ }
+
+ @Test
+ public void letSectionWithAlternativeExpression() {
+ String content = "{#let greeting='Hello'}\n" +
+ " {=greeting} {=name}\n" +
+ "{/let}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ // {#let greeting='Hello'}
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Section, first.getKind());
+ Section section = (Section) first;
+ assertEquals(SectionKind.LET, section.getSectionKind());
+ assertTrue(section.isClosed());
+
+ // Check parameter
+ Parameter parameter = section.getParameters().get(0);
+ assertEquals("greeting", parameter.getName());
+
+ // Check section children
+ assertEquals(5, section.getChildCount());
+
+ // Text: "\n "
+ assertEquals(NodeKind.Text, section.getChild(0).getKind());
+
+ // Expression: {=greeting}
+ Node expr1 = section.getChild(1);
+ assertEquals(NodeKind.Expression, expr1.getKind());
+ assertEquals("greeting", ((Expression) expr1).getContent());
+
+ // Text: " "
+ assertEquals(NodeKind.Text, section.getChild(2).getKind());
+
+ // Expression: {=name}
+ Node expr2 = section.getChild(3);
+ assertEquals(NodeKind.Expression, expr2.getKind());
+ assertEquals("name", ((Expression) expr2).getContent());
+
+ // Text: "\n"
+ assertEquals(NodeKind.Text, section.getChild(4).getKind());
+ }
+
+ @Test
+ public void forSectionWithAlternativeExpression() {
+ String content = "{#for item in items}\n" +
+ " {=item.name}: {=item.price}\n" +
+ "{/for}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ // {#for item in items}
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Section, first.getKind());
+ Section section = (Section) first;
+ assertEquals(SectionKind.FOR, section.getSectionKind());
+ assertTrue(section.isClosed());
+
+ // Check section children: text, expr, text, expr, text
+ assertEquals(5, section.getChildCount());
+
+ // Expression: {=item.name}
+ Node expr1 = section.getChild(1);
+ assertEquals(NodeKind.Expression, expr1.getKind());
+ assertEquals("item.name", ((Expression) expr1).getContent());
+
+ // Expression: {=item.price}
+ Node expr2 = section.getChild(3);
+ assertEquals(NodeKind.Expression, expr2.getKind());
+ assertEquals("item.price", ((Expression) expr2).getContent());
+ }
+
+ @Test
+ public void noExpressionWithoutCommand() {
+ // Without '=' command, {name} should be treated as text
+ String content = "{name}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Text, first.getKind());
+ }
+
+ @Test
+ public void complexTemplate() {
+ String content = "{@java.util.List items}\n" +
+ "{#for item in items}\n" +
+ " {! Item details !}\n" +
+ " Name: {=item.name}\n" +
+ " Price: {=item.price}\n" +
+ "{/for}";
+
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(3, template.getChildCount());
+
+ // {@java.util.List items}
+ Node paramDecl = template.getChild(0);
+ assertEquals(NodeKind.ParameterDeclaration, paramDecl.getKind());
+
+ // {#for item in items}
+ Node forSection = template.getChild(2);
+ assertEquals(NodeKind.Section, forSection.getKind());
+ Section section = (Section) forSection;
+ assertEquals(SectionKind.FOR, section.getSectionKind());
+ }
+
+ @Test
+ public void expressionWithMethodChain() {
+ String content = "{=item.getName().toUpperCase()}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Expression, first.getKind());
+ Expression expression = (Expression) first;
+
+ assertEquals("item.getName().toUpperCase()", expression.getContent());
+
+ // Check expression parts
+ List exprContent = expression.getExpressionContent();
+ assertEquals(1, exprContent.size());
+ Parts parts = (Parts) exprContent.get(0);
+ assertEquals(3, parts.getChildCount());
+
+ // ObjectPart --> item
+ Part itemPart = (Part) parts.getChild(0);
+ assertEquals(PartKind.Object, itemPart.getPartKind());
+ assertEquals("item", itemPart.getPartName());
+
+ // MethodPart --> getName()
+ Part getNamePart = (Part) parts.getChild(1);
+ assertEquals(PartKind.Method, getNamePart.getPartKind());
+ assertEquals("getName", getNamePart.getPartName());
+
+ // MethodPart --> toUpperCase()
+ Part toUpperCasePart = (Part) parts.getChild(2);
+ assertEquals(PartKind.Method, toUpperCasePart.getPartKind());
+ assertEquals("toUpperCase", toUpperCasePart.getPartName());
+ }
+
+ @Test
+ public void expressionWithInfixNotation() {
+ String content = "{#let result=(a ?: b)}{=result}{/let}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Section, first.getKind());
+ Section section = (Section) first;
+ assertEquals(SectionKind.LET, section.getSectionKind());
+
+ // Check parameter with infix notation
+ Parameter parameter = section.getParameters().get(0);
+ assertEquals("result", parameter.getName());
+ assertNotNull(parameter.getJavaTypeExpression());
+
+ Expression paramExpr = parameter.getJavaTypeExpression();
+ assertEquals("a ?: b", paramExpr.getContent());
+ assertTrue(paramExpr.canSupportInfixNotation());
+
+ // Check expression in section body
+ assertEquals(1, section.getChildCount());
+ Node expr = section.getChild(0);
+ assertEquals(NodeKind.Expression, expr.getKind());
+ assertEquals("result", ((Expression) expr).getContent());
+ }
+
+ @Test
+ public void multipleExpressionsWithDifferentPatterns() {
+ String content = "{=name} and {=item.property} and {=obj.method()}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(5, template.getChildCount());
+
+ // Expression: {=name}
+ Node expr1 = template.getChild(0);
+ assertEquals(NodeKind.Expression, expr1.getKind());
+ assertEquals("name", ((Expression) expr1).getContent());
+
+ // Text: " and "
+ assertEquals(NodeKind.Text, template.getChild(1).getKind());
+
+ // Expression: {=item.property}
+ Node expr2 = template.getChild(2);
+ assertEquals(NodeKind.Expression, expr2.getKind());
+ assertEquals("item.property", ((Expression) expr2).getContent());
+
+ // Text: " and "
+ assertEquals(NodeKind.Text, template.getChild(3).getKind());
+
+ // Expression: {=obj.method()}
+ Node expr3 = template.getChild(4);
+ assertEquals(NodeKind.Expression, expr3.getKind());
+ assertEquals("obj.method()", ((Expression) expr3).getContent());
+ }
+
+ @Test
+ public void expressionWithStringParameter() {
+ String content = "{=item.format('Hello')}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ Node first = template.getChild(0);
+ assertEquals(NodeKind.Expression, first.getKind());
+ Expression expression = (Expression) first;
+
+ assertEquals("item.format('Hello')", expression.getContent());
+
+ // Check expression parts
+ List exprContent = expression.getExpressionContent();
+ assertEquals(1, exprContent.size());
+ Parts parts = (Parts) exprContent.get(0);
+ assertEquals(2, parts.getChildCount());
+
+ // MethodPart --> format('Hello')
+ Part methodPart = (Part) parts.getChild(1);
+ assertEquals(PartKind.Method, methodPart.getPartKind());
+ assertEquals("format", methodPart.getPartName());
+
+ MethodPart method = (MethodPart) methodPart;
+ List params = method.getParameters();
+ assertEquals(1, params.size());
+
+ Parameter param = params.get(0);
+ Expression paramExpr = param.getJavaTypeExpression();
+ assertNotNull(paramExpr);
+ assertEquals("'Hello'", paramExpr.getContent());
+ }
+
+ @Test
+ public void nestedSectionsWithAlternativeExpressions() {
+ String content = "{#if active}\n" +
+ " {#for item in items}\n" +
+ " {=item.name}\n" +
+ " {/for}\n" +
+ "{/if}";
+
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(1, template.getChildCount());
+
+ // Outer {#if active}
+ Node ifSection = template.getChild(0);
+ assertEquals(NodeKind.Section, ifSection.getKind());
+ Section ifSec = (Section) ifSection;
+ assertEquals(SectionKind.IF, ifSec.getSectionKind());
+
+ // Should have text, for section, text
+ assertEquals(3, ifSec.getChildCount());
+
+ // Inner {#for item in items}
+ Node forSection = ifSec.getChild(1);
+ assertEquals(NodeKind.Section, forSection.getKind());
+ Section forSec = (Section) forSection;
+ assertEquals(SectionKind.FOR, forSec.getSectionKind());
+
+ // For section should have text, expression, text
+ assertEquals(3, forSec.getChildCount());
+
+ // Expression: {=item.name}
+ Node expr = forSec.getChild(1);
+ assertEquals(NodeKind.Expression, expr.getKind());
+ assertEquals("item.name", ((Expression) expr).getContent());
+ }
+
+ @Test
+ public void commentStillWorks() {
+ String content = "{! This is a comment !}{=name}";
+ Template template = TemplateParser.parse(content, "test.qute", EXPRESSION_COMMAND);
+ assertEquals(2, template.getChildCount());
+
+ // Comment
+ Node comment = template.getChild(0);
+ assertEquals(NodeKind.Comment, comment.getKind());
+
+ // Expression
+ Node expr = template.getChild(1);
+ assertEquals(NodeKind.Expression, expr.getKind());
+ assertEquals("name", ((Expression) expr).getContent());
+ }
+
+}
diff --git a/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/parser/template/scanner/TemplateScannerAlternativeSyntaxTest.java b/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/parser/template/scanner/TemplateScannerAlternativeSyntaxTest.java
new file mode 100644
index 000000000..703fe7091
--- /dev/null
+++ b/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/parser/template/scanner/TemplateScannerAlternativeSyntaxTest.java
@@ -0,0 +1,180 @@
+/*******************************************************************************
+* Copyright (c) 2021 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.qute.parser.template.scanner;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import com.redhat.qute.parser.scanner.Scanner;
+
+/**
+ * Tests for template scanner {@link TemplateScanner} with alternative syntax
+ * expression command (e.g., {=foo} instead of {foo}).
+ *
+ * This syntax is activated via quarkus.qute.alt-...=true configuration.
+ *
+ * @author Angelo ZERR
+ *
+ */
+public class TemplateScannerAlternativeSyntaxTest {
+
+ private static final Character EXPRESSION_COMMAND = '=';
+
+ private Scanner scanner;
+
+ @Test
+ public void testExpression() {
+ scanner = TemplateScanner.createScanner("{=abcd}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(6, TokenType.EndExpression, "}");
+ assertOffsetAndToken(7, TokenType.EOS, "");
+ }
+
+ @Test
+ public void testExpressionWithString() {
+ scanner = TemplateScanner.createScanner("{=abc'}'d}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(5, TokenType.StartString, "'");
+ assertOffsetAndToken(6, TokenType.String, "}");
+ assertOffsetAndToken(7, TokenType.EndString, "'");
+ assertOffsetAndToken(9, TokenType.EndExpression, "}");
+ assertOffsetAndToken(10, TokenType.EOS, "");
+ }
+
+ @Test
+ public void testExpressionWithProperty() {
+ scanner = TemplateScanner.createScanner("{=item.name}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(11, TokenType.EndExpression, "}");
+ assertOffsetAndToken(12, TokenType.EOS, "");
+ }
+
+ @Test
+ public void testExpressionWithMethod() {
+ scanner = TemplateScanner.createScanner("{=item.getPrice()}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(17, TokenType.EndExpression, "}");
+ assertOffsetAndToken(18, TokenType.EOS, "");
+ }
+
+ @Test
+ public void noExpressionWithoutCommand() {
+ // Without the '=' command, {abcd} should be treated as content, not an expression
+ scanner = TemplateScanner.createScanner("{abcd}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.Content, "{abcd}");
+ assertOffsetAndToken(6, TokenType.EOS, "");
+ }
+
+ @Test
+ public void noExpressionWithSpace() {
+ scanner = TemplateScanner.createScanner("{= abcd}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(2, TokenType.Whitespace, " ");
+ assertOffsetAndToken(7, TokenType.EndExpression, "}");
+ assertOffsetAndToken(8, TokenType.EOS, "");
+ }
+
+ @Test
+ public void mixedExpressionAndText() {
+ scanner = TemplateScanner.createScanner("Hello {=name}!", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.Content, "Hello ");
+ assertOffsetAndToken(6, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(12, TokenType.EndExpression, "}");
+ assertOffsetAndToken(13, TokenType.Content, "!");
+ assertOffsetAndToken(14, TokenType.EOS, "");
+ }
+
+ @Test
+ public void multipleExpressions() {
+ scanner = TemplateScanner.createScanner("{=foo} and {=bar}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(5, TokenType.EndExpression, "}");
+ assertOffsetAndToken(6, TokenType.Content, " and ");
+ assertOffsetAndToken(11, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(16, TokenType.EndExpression, "}");
+ assertOffsetAndToken(17, TokenType.EOS, "");
+ }
+
+ @Test
+ public void commentStillWorks() {
+ // Comments should still work normally with {! !}
+ scanner = TemplateScanner.createScanner("{! This is a comment !}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartComment, "{!");
+ assertOffsetAndToken(2, TokenType.Comment, " This is a comment ");
+ assertOffsetAndToken(21, TokenType.EndComment, "!}");
+ assertOffsetAndToken(23, TokenType.EOS, "");
+ }
+
+ @Test
+ public void cdataStillWorks() {
+ // CDATA should still work normally with {| |}
+ scanner = TemplateScanner.createScanner("{||}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.CDATATagOpen, "{|");
+ assertOffsetAndToken(2, TokenType.CDATAContent, "");
+ assertOffsetAndToken(33, TokenType.CDATATagClose, "|}");
+ assertOffsetAndToken(35, TokenType.EOS, "");
+ }
+
+ @Test
+ public void sectionTagStillWorks() {
+ // Section tags should still work normally with {#tag}
+ scanner = TemplateScanner.createScanner("{#if item.active}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartTagOpen, "{#");
+ assertOffsetAndToken(2, TokenType.StartTag, "if");
+ assertOffsetAndToken(4, TokenType.Whitespace, " ");
+ assertOffsetAndToken(5, TokenType.ParameterTag, "item.active");
+ assertOffsetAndToken(16, TokenType.StartTagClose, "}");
+ assertOffsetAndToken(17, TokenType.EOS, "");
+ }
+
+ @Test
+ public void endTagStillWorks() {
+ // End tags should still work normally with {/tag}
+ scanner = TemplateScanner.createScanner("{/if}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.EndTagOpen, "{/");
+ assertOffsetAndToken(2, TokenType.EndTag, "if");
+ assertOffsetAndToken(4, TokenType.EndTagClose, "}");
+ assertOffsetAndToken(5, TokenType.EOS, "");
+ }
+
+ @Test
+ public void expressionWithNestedBraces() {
+ scanner = TemplateScanner.createScanner("{=items.size}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(12, TokenType.EndExpression, "}");
+ assertOffsetAndToken(13, TokenType.EOS, "");
+ }
+
+ @Test
+ public void emptyExpression() {
+ scanner = TemplateScanner.createScanner("{=}", EXPRESSION_COMMAND, Collections.emptyList());
+ assertOffsetAndToken(0, TokenType.StartExpression, "{=");
+ assertOffsetAndToken(2, TokenType.EndExpression, "}");
+ assertOffsetAndToken(3, TokenType.EOS, "");
+ }
+
+ public void assertOffsetAndToken(int tokenOffset, TokenType tokenType) {
+ TokenType token = scanner.scan();
+ assertEquals(tokenOffset, scanner.getTokenOffset());
+ assertEquals(tokenType, token);
+ }
+
+ public void assertOffsetAndToken(int tokenOffset, TokenType tokenType, String tokenText) {
+ TokenType token = scanner.scan();
+ assertEquals(tokenOffset, scanner.getTokenOffset());
+ assertEquals(tokenType, token);
+ assertEquals(tokenText, scanner.getTokenText());
+ }
+}
diff --git a/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/project/MockQuteTextDocument.java b/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/project/MockQuteTextDocument.java
index 585366b7c..f7f36ac8e 100644
--- a/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/project/MockQuteTextDocument.java
+++ b/qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/project/MockQuteTextDocument.java
@@ -19,6 +19,7 @@
import java.util.stream.Collectors;
import com.redhat.qute.commons.ProjectInfo;
+import com.redhat.qute.commons.TemplateRootPath;
import com.redhat.qute.parser.injection.InjectionDetector;
import com.redhat.qute.parser.template.Parameter;
import com.redhat.qute.parser.template.Template;
@@ -203,4 +204,13 @@ public void putUserData(Key key, T data) {
}
+ @Override
+ public Character getExpressionCommand() {
+ return null;
+ }
+
+ @Override
+ public TemplateRootPath getTemplateRootPath() {
+ return null;
+ }
}