diff --git a/docs/img/code_template_proposal.png b/docs/img/code_template_proposal.png
new file mode 100644
index 000000000..59e3673e5
Binary files /dev/null and b/docs/img/code_template_proposal.png differ
diff --git a/docs/img/templates_preferences.png b/docs/img/templates_preferences.png
new file mode 100644
index 000000000..fe4b0942c
Binary files /dev/null and b/docs/img/templates_preferences.png differ
diff --git a/docs/user-guide.md b/docs/user-guide.md
index 815b9a147..a70b172e2 100644
--- a/docs/user-guide.md
+++ b/docs/user-guide.md
@@ -57,7 +57,26 @@ Language-configuration files enable additional editor behavior for a language. T
These behaviors are applied on top of whatever the underlying editor already provides.
TM4E can add these behaviors to simple text editors, or refine them when an editor or language server already offers partial support.
-### 3) Diagnostic tools (token hover)
+### 3) Custom code templates and code proposals
+
+TM4E offers support for defining and proposing custom code templates for the languages available through a TextMate grammar.
+Users can specify their own code templates using the `TextMate > Templates` preferences page.
+
+Each template is registered to a context type, i.e. a language or grammar (technically a TextMate scope).
+In addition to available grammars, TM4E offers two special context types for comments.
+Templates registered for these context types can be used in all languages having such comments in their grammar.
+
+For example, users can register custom C/C++ or JavaScript statements, but also generally usable comment texts like a Copyright notice, a license, or a TODO comment with the user's name.
+Having a TM4E-based editor open, they'll get that code snippets suggested via code completion triggered by Ctrl + Space.
+
+The two generic context types for comments are:
+
+- Comment (any comment that is not a documentation comment, i.e. line comments and standard block comments like code between `/*` and `*/` in Java / C++)
+- Documentation comment (a comment that is used for code documentation, e.g. javadoc comments in Java, i.e. code between `/**` and `*/`)
+
+
+
+### 4) Diagnostic tools (token hover)
For advanced users, some editors expose a TextMate token hover that shows the token scopes and partition information at the caret location.
@@ -102,6 +121,9 @@ Most user-facing configuration lives under the `TextMate` section in the Eclipse
1. `TextMate > Task Tags` lets you define tags in comments (such as `TODO` or `FIXME`) that should be treated as tasks or problems, and configure how they are marked in the workspace.\

+1. `TextMate > Templates` lets you specify custom code templates for available TextMate grammars (languages). These will be used in code proposals triggered by Ctrl + Space.\
+ 
+
1. `TextMate > Themes` lets you choose between built-in Light and Dark themes and any additional themes contributed by installed plugins, as well as import extra theme files and set the default theme for light and dark modes.
You can also switch themes from the editor's context menu under `TextMate`.\

diff --git a/org.eclipse.tm4e.ui/META-INF/MANIFEST.MF b/org.eclipse.tm4e.ui/META-INF/MANIFEST.MF
index 2821ff4cf..c715a73ae 100644
--- a/org.eclipse.tm4e.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.tm4e.ui/META-INF/MANIFEST.MF
@@ -29,6 +29,7 @@ Export-Package: org.eclipse.tm4e.ui,
org.eclipse.tm4e.ui.internal.widgets;x-friends:="org.eclipse.tm4e.languageconfiguration",
org.eclipse.tm4e.ui.model,
org.eclipse.tm4e.ui.samples,
+ org.eclipse.tm4e.ui.templates,
org.eclipse.tm4e.ui.text,
org.eclipse.tm4e.ui.themes,
org.eclipse.tm4e.ui.themes.css
diff --git a/org.eclipse.tm4e.ui/icons/full/obj16/template_obj.svg b/org.eclipse.tm4e.ui/icons/full/obj16/template_obj.svg
new file mode 100644
index 000000000..377d93141
Binary files /dev/null and b/org.eclipse.tm4e.ui/icons/full/obj16/template_obj.svg differ
diff --git a/org.eclipse.tm4e.ui/plugin.properties b/org.eclipse.tm4e.ui/plugin.properties
index 17e97ed0b..ffe7f1210 100644
--- a/org.eclipse.tm4e.ui/plugin.properties
+++ b/org.eclipse.tm4e.ui/plugin.properties
@@ -29,6 +29,7 @@ TextMatePreferencePage.name=TextMate
GrammarPreferencePage.name=Grammar
TaskTagsPreferencePage.name=Task Tags
ThemePreferencePage.name=Theme
+TemplatesPreferencePage.name=Templates
# Wizards
TextMateWizard.category=TextMate
diff --git a/org.eclipse.tm4e.ui/plugin.xml b/org.eclipse.tm4e.ui/plugin.xml
index fc7ffbb05..7764df5c2 100644
--- a/org.eclipse.tm4e.ui/plugin.xml
+++ b/org.eclipse.tm4e.ui/plugin.xml
@@ -71,6 +71,11 @@
class="org.eclipse.tm4e.ui.internal.preferences.ThemePreferencePage"
id="org.eclipse.tm4e.ui.preferences.ThemePreferencePage"
category="org.eclipse.tm4e.ui.preferences.TextMatePreferencePage" />
+
+
@@ -155,4 +160,39 @@
class="org.eclipse.tm4e.ui.internal.hover.TMTokenTextHover"
contentType="org.eclipse.core.runtime.text" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMImages.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMImages.java
new file mode 100644
index 000000000..caae3b459
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMImages.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui;
+
+import java.net.URL;
+
+import org.eclipse.core.runtime.FileLocator;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.resource.ImageRegistry;
+import org.eclipse.swt.graphics.Image;
+import org.osgi.framework.Bundle;
+
+public final class TMImages {
+
+ private static final String ICONS_PATH = "$nl$/icons/full/"; //$NON-NLS-1$
+ private static final String OBJECT = ICONS_PATH + "obj16/"; // basic colors - size 16x16 //$NON-NLS-1$
+
+ public static final String IMG_TEMPLATE = "IMG_TEMPALTE"; //$NON-NLS-1$
+
+ private TMImages() {
+ // no instantiation desired
+ }
+
+ private static @Nullable ImageRegistry imageRegistry;
+
+ public static void initalize(final ImageRegistry registry) {
+ imageRegistry = registry;
+
+ registerImage(IMG_TEMPLATE, OBJECT + "template_obj.svg"); //$NON-NLS-1$
+ }
+
+ private static void registerImage(final String key, final String path) {
+ ImageDescriptor desc = ImageDescriptor.getMissingImageDescriptor();
+ final Bundle bundle = Platform.getBundle(TMUIPlugin.PLUGIN_ID);
+ final ImageRegistry imageRegistry = getImageRegistry();
+ URL url = null;
+ if (bundle != null) {
+ url = FileLocator.find(bundle, new Path(path), null);
+ if (url != null) {
+ desc = ImageDescriptor.createFromURL(url);
+ }
+ }
+ if (imageRegistry != null) {
+ imageRegistry.put(key, desc);
+ }
+ }
+
+ /**
+ * Returns the {@link Image} identified by the given key, or null if it does not exist.
+ */
+ public static @Nullable Image getImage(final String key) {
+ final ImageRegistry imageRegistry = getImageRegistry();
+ if (imageRegistry == null) {
+ return null;
+ }
+ return imageRegistry.get(key);
+ }
+
+ /**
+ * Returns the {@link ImageDescriptor} identified by the given key, or null if it does not exist.
+ */
+ public static @Nullable ImageDescriptor getImageDescriptor(final String key) {
+ final ImageRegistry imageRegistry = getImageRegistry();
+ if (imageRegistry == null) {
+ return null;
+ }
+ return imageRegistry.getDescriptor(key);
+ }
+
+ public static @Nullable ImageRegistry getImageRegistry() {
+ if (imageRegistry == null) {
+ final TMUIPlugin plugin = TMUIPlugin.getDefault();
+ if (plugin == null) {
+ return null;
+ }
+ imageRegistry = plugin.getImageRegistry();
+ }
+ return imageRegistry;
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java
index 1b20bbca6..18837c9d0 100644
--- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java
@@ -8,9 +8,12 @@
*
* Contributors:
* Angelo Zerr - initial API and implementation
+ * Dietrich Travkin (SOLUNAR GmbH) - Additions for custom code templates
*/
package org.eclipse.tm4e.ui;
+import java.io.IOException;
+import java.util.Iterator;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
@@ -20,13 +23,26 @@
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.templates.TemplateContextType;
+import org.eclipse.jface.text.templates.persistence.TemplateStore;
+import org.eclipse.text.templates.ContextTypeRegistry;
+import org.eclipse.tm4e.registry.IGrammarDefinition;
+import org.eclipse.tm4e.registry.ITMScope;
+import org.eclipse.tm4e.registry.TMEclipseRegistryPlugin;
import org.eclipse.tm4e.ui.internal.model.TMModelManager;
import org.eclipse.tm4e.ui.internal.samples.SampleManager;
import org.eclipse.tm4e.ui.internal.themes.ThemeManager;
+import org.eclipse.tm4e.ui.internal.utils.CodeTemplateContextTypeUtils;
import org.eclipse.tm4e.ui.model.ITMModelManager;
import org.eclipse.tm4e.ui.samples.ISampleManager;
+import org.eclipse.tm4e.ui.templates.CommentTemplateContextType;
+import org.eclipse.tm4e.ui.templates.DefaultTMTemplateContextType;
+import org.eclipse.tm4e.ui.templates.DocumentationCommentTemplateContextType;
+import org.eclipse.tm4e.ui.templates.TMLanguageTemplateContextType;
import org.eclipse.tm4e.ui.themes.ColorManager;
import org.eclipse.tm4e.ui.themes.IThemeManager;
+import org.eclipse.ui.editors.text.templates.ContributionContextTypeRegistry;
+import org.eclipse.ui.editors.text.templates.ContributionTemplateStore;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
@@ -39,9 +55,17 @@ public class TMUIPlugin extends AbstractUIPlugin {
public static final String PLUGIN_ID = "org.eclipse.tm4e.ui"; //$NON-NLS-1$
private static final String TRACE_ID = PLUGIN_ID + "/trace"; //$NON-NLS-1$
+ // IDs for custom code templates
+ private static final String CUSTOM_TEMPLATES_KEY = PLUGIN_ID + ".text.templates.custom"; //$NON-NLS-1$
+ private static final String TEMPLATES_REGISTRY_ID = PLUGIN_ID + ".templates"; //$NON-NLS-1$
+
// The shared instance
private static volatile @Nullable TMUIPlugin plugin;
+ // registry and store for custom code templates
+ private @Nullable ContributionContextTypeRegistry contextTypeRegistry = null;
+ private @Nullable TemplateStore templateStore = null;
+
/**
* Returns the shared instance
*
@@ -146,12 +170,100 @@ public void close() throws SecurityException {
}
});
}
+
+ TMImages.initalize(getImageRegistry());
}
@Override
public void stop(final BundleContext context) throws Exception {
+ if (templateStore != null) {
+ templateStore.stopListeningForPreferenceChanges();
+ }
ColorManager.getInstance().dispose();
plugin = null;
super.stop(context);
}
+
+ public ContextTypeRegistry getTemplateContextRegistry() {
+ final var contextTypeRegistry = this.contextTypeRegistry;
+ if (contextTypeRegistry != null) {
+ return contextTypeRegistry;
+ }
+
+ final var newContextTypeRegistry = new ContributionContextTypeRegistry(TEMPLATES_REGISTRY_ID);
+ this.contextTypeRegistry = newContextTypeRegistry;
+
+ newContextTypeRegistry.addContextType(DefaultTMTemplateContextType.CONTEXT_ID);
+ newContextTypeRegistry.addContextType(CommentTemplateContextType.CONTEXT_ID);
+ newContextTypeRegistry.addContextType(DocumentationCommentTemplateContextType.CONTEXT_ID);
+
+ // Add language-specific context types
+ // TODO Skip certain grammars? Some grammars have no name or are only used for highlighting code snippets, e.g. in Markdown
+ final IGrammarDefinition[] grammarDefinitions = TMEclipseRegistryPlugin.getGrammarRegistryManager().getDefinitions();
+ for (final IGrammarDefinition definition : grammarDefinitions) {
+ final ITMScope languageScope = definition.getScope();
+
+ // TODO It seems TemplatePreferencePage.EditTemplateDialog requires the context type names to be unique. Can we shorten the names somehow?
+ final String contextTypeName = CodeTemplateContextTypeUtils.toContextTypeName(languageScope);
+
+ final TMLanguageTemplateContextType languageContextType = new TMLanguageTemplateContextType(
+ contextTypeName, languageScope);
+ newContextTypeRegistry.addContextType(languageContextType);
+ }
+
+ return newContextTypeRegistry;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static class ContextTypeRegistryWrapper extends org.eclipse.jface.text.templates.ContextTypeRegistry {
+
+ private final ContextTypeRegistry delegate;
+
+ public ContextTypeRegistryWrapper(final ContextTypeRegistry registry) {
+ this.delegate = registry;
+ }
+
+ @Override
+ public Iterator contextTypes() {
+ return delegate.contextTypes();
+ }
+
+ @Override
+ public void addContextType(final TemplateContextType contextType) {
+ delegate.addContextType(contextType);
+ }
+
+ @Override
+ public @Nullable TemplateContextType getContextType(final String id) {
+ return delegate.getContextType(id);
+ }
+
+ }
+
+ public static ContextTypeRegistryWrapper from(final ContextTypeRegistry registry) {
+ return new ContextTypeRegistryWrapper(registry);
+ }
+
+ public TemplateStore getTemplateStore() {
+ final var templateStore = this.templateStore;
+
+ if (templateStore != null) {
+ return templateStore;
+ }
+
+ final TemplateStore newTemplateStore = new ContributionTemplateStore(
+ from(getTemplateContextRegistry()), getPreferenceStore(), CUSTOM_TEMPLATES_KEY);
+ this.templateStore = newTemplateStore;
+
+ try {
+ newTemplateStore.load();
+ } catch (final IOException e) {
+ Platform.getLog(this.getClass()).error(e.getMessage(), e);
+ }
+
+ newTemplateStore.startListeningForPreferenceChanges();
+
+ return newTemplateStore;
+ }
+
}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/CustomCodeTemplatePreferencePage.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/CustomCodeTemplatePreferencePage.java
new file mode 100644
index 000000000..76e8352b2
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/CustomCodeTemplatePreferencePage.java
@@ -0,0 +1,338 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.internal.preferences;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.Document;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.TextPresentation;
+import org.eclipse.jface.text.contentassist.ContentAssistant;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContentAssistant;
+import org.eclipse.jface.text.presentation.IPresentationReconciler;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.jface.text.source.SourceViewerConfiguration;
+import org.eclipse.jface.text.templates.Template;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.text.templates.TemplatePersistenceData;
+import org.eclipse.tm4e.core.grammar.IGrammar;
+import org.eclipse.tm4e.registry.ITMScope;
+import org.eclipse.tm4e.registry.TMEclipseRegistryPlugin;
+import org.eclipse.tm4e.ui.TMUIPlugin;
+import org.eclipse.tm4e.ui.internal.utils.CodeTemplateContextTypeUtils;
+import org.eclipse.tm4e.ui.text.ITMPresentationReconcilerListener;
+import org.eclipse.tm4e.ui.text.TMPresentationReconciler;
+import org.eclipse.ui.texteditor.templates.TemplatePreferencePage;
+
+public class CustomCodeTemplatePreferencePage extends TemplatePreferencePage {
+
+ private @Nullable TMPresentationReconciler previewReconsiler;
+
+ public CustomCodeTemplatePreferencePage() {
+ final TMUIPlugin plugin = TMUIPlugin.getDefault();
+ if (plugin == null) {
+ return;
+ }
+
+ setPreferenceStore(plugin.getPreferenceStore());
+ setTemplateStore(plugin.getTemplateStore());
+ setContextTypeRegistry(TMUIPlugin.from(plugin.getTemplateContextRegistry()));
+ }
+
+ private static class TMTemplateVariableHighlighter {
+
+ // pattern for code template variables like ${variable_name} or ${horizontal:link(FILL, BEGINNING, CENTER, END)}
+ private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[^\\r\\n\\}]*\\}");
+ private static final Color VAR_COLOR_FOREGROUND = new Color(181, 123, 21);
+
+ private @Nullable IDocument document;
+
+ public void setDocument(@Nullable final IDocument document) {
+ this.document = document;
+ }
+
+ public void createPresentation(final @Nullable TextPresentation presentation, final @Nullable IRegion damage) {
+ final var document = this.document;
+ if (presentation == null || damage == null || document == null) {
+ return;
+ }
+
+ try {
+ final String code = document.get(damage.getOffset(), damage.getLength());
+ final Matcher varMatcher = VARIABLE_PATTERN.matcher(code);
+ while (varMatcher.find()) {
+ final int offset = damage.getOffset() + varMatcher.start();
+ final int length = varMatcher.end() - varMatcher.start();
+ presentation.mergeStyleRange(new StyleRange(offset, length, VAR_COLOR_FOREGROUND, null, SWT.ITALIC));
+ }
+ } catch (final BadLocationException e) {
+ // do nothing
+ }
+ }
+
+ }
+
+ private static class TMCodeTemplatePresentationReconsiler extends TMPresentationReconciler {
+
+ private final TMTemplateVariableHighlighter templateVariableDamagerRepairer = new TMTemplateVariableHighlighter();
+ private @Nullable ITextViewer viewer;
+
+ @Override
+ public void install(final ITextViewer viewer) {
+ super.install(viewer);
+
+ this.viewer = viewer;
+ }
+
+ @Override
+ public void setGrammar(@Nullable final IGrammar newGrammar) {
+ super.setGrammar(newGrammar);
+
+ this.addListener(new ITMPresentationReconcilerListener() {
+
+ @Override
+ public void onUninstalled() {
+ }
+
+ @Override
+ public void onInstalled(final ITextViewer viewer, final IDocument document) {
+ }
+
+ @Override
+ public void onColorized(final TextPresentation presentation, @Nullable final Throwable error) {
+ final var viewer = TMCodeTemplatePresentationReconsiler.this.viewer;
+ if (viewer != null && viewer.getDocument() != null) {
+ final IDocument document = viewer.getDocument();
+ templateVariableDamagerRepairer.setDocument(document);
+
+ // highlight code template variables
+ final IRegion region = presentation.getExtent();
+ templateVariableDamagerRepairer.createPresentation(presentation, region);
+
+ viewer.changeTextPresentation(presentation, false);
+ }
+ }
+ });
+
+ final var viewer = this.viewer;
+ final var document = viewer != null ? viewer.getDocument() : null;
+ if (newGrammar == null && viewer != null && document != null) {
+ templateVariableDamagerRepairer.setDocument(document);
+
+ // create default style range for whole document
+ final IRegion region = new Region(0, document.getLength());
+ final TextPresentation presentation = new TextPresentation(region, 100);
+ presentation.setDefaultStyleRange(new StyleRange(region.getOffset(), region.getLength(), null, null));
+
+ // highlight code template variables
+ templateVariableDamagerRepairer.createPresentation(presentation, region);
+
+ viewer.changeTextPresentation(presentation, false);
+ }
+ }
+
+ }
+
+ private static class TMEditTemplateDialog extends TemplatePreferencePage.EditTemplateDialog {
+
+ private @Nullable TMPresentationReconciler editTemplatePresentationReconsiler = null;
+ private @Nullable Combo contextTypeDropDownList = null;
+
+ public TMEditTemplateDialog(final Shell shell, final Template template, final boolean edit, final boolean isNameModifiable,
+ @SuppressWarnings("deprecation") final org.eclipse.jface.text.templates.ContextTypeRegistry contextTypeRegistry) {
+ super(shell, template, edit, isNameModifiable, contextTypeRegistry);
+ }
+
+ @Override
+ protected Control createDialogArea(final Composite ancestor) {
+ final Control result = super.createDialogArea(ancestor);
+
+ findContextTypeComboAndSetMinNameFieldWidth(ancestor);
+
+ return result;
+ }
+
+ private void findContextTypeComboAndSetMinNameFieldWidth(final Composite ancestor) {
+ // work-around 1: find the name text field and set a minimum width
+ // work-around 2: find the combo box (drop-down list) and register a modify listener for updating
+ // the reconsiler's grammar to the current context type
+ boolean foundNameField = false;
+ boolean foundDropDownList = false;
+ for (final Control childControlLvl1 : ancestor.getChildren()) {
+ if (childControlLvl1 instanceof final Composite childL1) {
+ for (final Control childControlLvl2 : childL1.getChildren()) {
+ if (childControlLvl2 instanceof final Composite childL2) {
+ for (final Control childControlLvl3 : childL2.getChildren()) {
+ if (childControlLvl3 instanceof final Text nameTextField
+ && nameTextField.getLayoutData() instanceof final GridData gridData) {
+ gridData.minimumWidth = 50;
+ foundNameField = true;
+ } else if (childControlLvl3 instanceof final Combo dropDownList) {
+ foundDropDownList = true;
+
+ contextTypeDropDownList = dropDownList;
+
+ updateSyntaxHighlighting();
+
+ dropDownList.addModifyListener(e -> {
+ updateSyntaxHighlighting();
+ });
+ }
+
+ if (foundNameField && foundDropDownList) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void updateSyntaxHighlighting() {
+ final var editTemplatePresentationReconsiler = this.editTemplatePresentationReconsiler;
+ final var contextTypeDropDownList = this.contextTypeDropDownList;
+ if (editTemplatePresentationReconsiler == null || contextTypeDropDownList == null
+ || contextTypeDropDownList.isDisposed()) {
+ return;
+ }
+
+ final String activeContextTypeName = contextTypeDropDownList.getText();
+ final IGrammar grammar = CodeTemplateContextTypeUtils.toGrammar(activeContextTypeName);
+ editTemplatePresentationReconsiler.setGrammar(grammar);
+ }
+
+ @Override
+ protected SourceViewer createViewer(final @Nullable Composite parent) {
+ final SourceViewer originalViewer = super.createViewer(parent);
+
+ // work-around 3: re-use source viewer from parent class, but add our reconsiler for syntax highlighting
+ // by un-configuring and re-configuring the source viewer with a modified SourceViewerConfiguration
+ final IContentAssistProcessor templateVariableProcessor = getTemplateProcessor();
+
+ originalViewer.unconfigure();
+
+ final SourceViewerConfiguration configuration = new SourceViewerConfiguration() {
+ @Override
+ public @Nullable IContentAssistant getContentAssistant(@Nullable final ISourceViewer sourceViewer) {
+
+ final ContentAssistant assistant = new ContentAssistant();
+ assistant.enableAutoActivation(true);
+ assistant.enableAutoInsert(true);
+ assistant.setContentAssistProcessor(templateVariableProcessor, IDocument.DEFAULT_CONTENT_TYPE);
+ return assistant;
+ }
+
+ @Override
+ public IPresentationReconciler getPresentationReconciler(@Nullable final ISourceViewer sourceViewer) {
+ editTemplatePresentationReconsiler = new TMCodeTemplatePresentationReconsiler();
+ return editTemplatePresentationReconsiler;
+ }
+ };
+
+ originalViewer.configure(configuration);
+
+ originalViewer.addTextListener(event -> updateSyntaxHighlighting());
+
+ return originalViewer;
+ }
+ }
+
+ @Override
+ protected boolean isShowFormatterSetting() {
+ // Do not show a checkbox for formatting template code, since TM4E can't do that
+ return false;
+ }
+
+ @Override
+ protected @Nullable Template editTemplate(final @Nullable Template template, final boolean edit, final boolean isNameModifiable) {
+ if (template == null) {
+ return null;
+ }
+
+ final TMEditTemplateDialog dialog = new TMEditTemplateDialog(getShell(), template, edit, isNameModifiable,
+ getContextTypeRegistry());
+ if (dialog.open() == Window.OK) {
+ return dialog.getTemplate();
+ }
+ return null;
+ }
+
+ @Override
+ protected SourceViewer createViewer(final @Nullable Composite parent) {
+ final SourceViewer viewer = new SourceViewer(parent, null, null, false, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ final SourceViewerConfiguration configuration = new SourceViewerConfiguration() {
+ @Override
+ public IPresentationReconciler getPresentationReconciler(@Nullable final ISourceViewer sourceViewer) {
+ previewReconsiler = new TMCodeTemplatePresentationReconsiler();
+ return previewReconsiler;
+ }
+ };
+ viewer.configure(configuration);
+ final IDocument document = new Document();
+ viewer.setDocument(document);
+ getTableViewer().addSelectionChangedListener(e -> selectedCodeTemplateChanged());
+ return viewer;
+ }
+
+ private void selectedCodeTemplateChanged() {
+ final Template selectedTemplate = getSelectedTemplate();
+
+ if (selectedTemplate != null) {
+
+ final String id = selectedTemplate.getContextTypeId();
+ final ITMScope scope = CodeTemplateContextTypeUtils.findScopeFor(id);
+
+ if (scope != null) {
+ final IGrammar languageGrammar = TMEclipseRegistryPlugin.getGrammarRegistryManager().getGrammarForScope(scope);
+ if (languageGrammar != null && previewReconsiler != null) {
+ previewReconsiler.setGrammar(languageGrammar);
+ return;
+ }
+ }
+ }
+ if (previewReconsiler != null) {
+ previewReconsiler.setGrammar(null);
+ }
+ }
+
+ private @Nullable Template getSelectedTemplate() {
+ final IStructuredSelection selection = getTableViewer().getStructuredSelection();
+
+ if (selection != null && selection.size() == 1) {
+ final TemplatePersistenceData data = (TemplatePersistenceData) selection.getFirstElement();
+ if (data != null) {
+ return data.getTemplate();
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/templates/TMTemplateCompletionProcessor.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/templates/TMTemplateCompletionProcessor.java
new file mode 100644
index 000000000..38d7dc8b9
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/templates/TMTemplateCompletionProcessor.java
@@ -0,0 +1,197 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.internal.templates;
+
+import java.util.List;
+
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.templates.Template;
+import org.eclipse.jface.text.templates.TemplateCompletionProcessor;
+import org.eclipse.jface.text.templates.TemplateContextType;
+import org.eclipse.jface.text.templates.persistence.TemplateStore;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.text.templates.ContextTypeRegistry;
+import org.eclipse.tm4e.core.model.TMToken;
+import org.eclipse.tm4e.ui.TMImages;
+import org.eclipse.tm4e.ui.TMUIPlugin;
+import org.eclipse.tm4e.ui.internal.utils.CodeTemplateContextTypeUtils;
+import org.eclipse.tm4e.ui.internal.utils.UI;
+import org.eclipse.tm4e.ui.model.ITMDocumentModel;
+import org.eclipse.tm4e.ui.templates.CommentTemplateContextType;
+import org.eclipse.tm4e.ui.templates.DefaultTMTemplateContextType;
+import org.eclipse.tm4e.ui.templates.DocumentationCommentTemplateContextType;
+
+public class TMTemplateCompletionProcessor extends TemplateCompletionProcessor {
+
+ private static final Template[] NO_TEMPLATES = {};
+
+ @Override
+ public ICompletionProposal[] computeCompletionProposals(final ITextViewer viewer, final int offset) {
+ // TODO Check why Invalid thread access exception occurs here without UI.runSync()
+ return UI.runSync(() -> TMTemplateCompletionProcessor.super.computeCompletionProposals(viewer, offset));
+ }
+
+ private static class TmTokenRegion implements IRegion {
+
+ private final TMToken token;
+ private final int offset;
+ private final int length;
+
+ public TmTokenRegion(final TMToken token, final int offset, final int length) {
+ this.token = token;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ @Override
+ public int getLength() {
+ return this.length;
+ }
+
+ @Override
+ public int getOffset() {
+ return this.offset;
+ }
+
+ public TMToken getToken() {
+ return this.token;
+ }
+ }
+
+ private @Nullable TmTokenRegion retrieveTmTokenFor(final IDocument document, final int offset) {
+ final ITMDocumentModel model = TMUIPlugin.getTMModelManager().connect(document);
+
+ int lineIndex;
+ int lineStartOffset;
+ int lineLength;
+ String lineDelimiter;
+ try {
+ lineIndex = document.getLineOfOffset(offset);
+ lineStartOffset = document.getLineOffset(lineIndex);
+ lineLength = document.getLineLength(lineIndex);
+ lineDelimiter = document.getLineDelimiter(lineIndex);
+ } catch (final BadLocationException e) {
+ Platform.getLog(getClass()).error(e.getMessage(), e);
+ return null;
+ }
+
+ final List lineTokens = model.getLineTokens(lineIndex);
+ if (lineTokens == null) {
+ return null;
+ }
+
+ TMToken tokenAtOffset = null;
+ TMToken nextToken = null;
+ for (final TMToken token : lineTokens) {
+ if (token.startIndex <= offset - lineStartOffset) {
+ tokenAtOffset = token;
+ } else {
+ nextToken = token;
+ break;
+ }
+ }
+
+ if (tokenAtOffset == null) {
+ return null;
+ }
+
+ int length;
+ if (nextToken != null) {
+ length = nextToken.startIndex - tokenAtOffset.startIndex;
+ } else {
+ length = lineLength - tokenAtOffset.startIndex;
+ if (lineDelimiter != null) {
+ length -= lineDelimiter.length();
+ }
+ }
+
+ return new TmTokenRegion(tokenAtOffset, offset, length);
+ }
+
+ private @Nullable TemplateContextType retrieveTemplateContextType(final TMToken textMateToken) {
+ final TMUIPlugin plugin = TMUIPlugin.getDefault();
+ if (plugin == null) {
+ return null;
+ }
+
+ // check language-agnostic context types (comment context types)
+ final ContextTypeRegistry contextTypeRegistry = plugin.getTemplateContextRegistry();
+ if (textMateToken.type.contains("comment")) {
+ TemplateContextType contextType;
+ if (textMateToken.type.contains("documentation")) {
+ contextType = contextTypeRegistry.getContextType(DocumentationCommentTemplateContextType.CONTEXT_ID);
+ } else {
+ contextType = contextTypeRegistry.getContextType(CommentTemplateContextType.CONTEXT_ID);
+ }
+
+ if (contextType != null) {
+ return contextType;
+ }
+ }
+
+ // Check language-specific context types
+ final TemplateContextType contextType = plugin.getTemplateContextRegistry().getContextType(
+ CodeTemplateContextTypeUtils.toContextTypeId(textMateToken));
+ if (contextType != null) {
+ return contextType;
+ }
+
+ // last option
+ return contextTypeRegistry.getContextType(DefaultTMTemplateContextType.CONTEXT_ID);
+ }
+
+ @Override
+ protected @Nullable TemplateContextType getContextType(final ITextViewer viewer, final IRegion region) {
+ final var plugin = TMUIPlugin.getDefault();
+ if (plugin == null) {
+ return null;
+ }
+
+ final var document = viewer.getDocument();
+ if (document != null) {
+ final TmTokenRegion tokenRegion = retrieveTmTokenFor(document, region.getOffset());
+ if (tokenRegion != null) {
+ return retrieveTemplateContextType(tokenRegion.getToken());
+ }
+ }
+
+ return plugin.getTemplateContextRegistry().getContextType(DefaultTMTemplateContextType.CONTEXT_ID);
+ }
+
+ @Override
+ protected @Nullable Image getImage(final Template template) {
+ return TMImages.getImage(TMImages.IMG_TEMPLATE);
+ }
+
+ @Override
+ protected Template[] getTemplates(final String contextTypeId) {
+ final TMUIPlugin plugin = TMUIPlugin.getDefault();
+ if (plugin == null) {
+ return NO_TEMPLATES;
+ }
+
+ final TemplateStore templateStore = plugin.getTemplateStore();
+ final Template[] customTemplates = templateStore.getTemplates(contextTypeId);
+
+ if (customTemplates.length == 0) {
+ return NO_TEMPLATES;
+ }
+ return customTemplates;
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/templates/package-info.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/templates/package-info.java
new file mode 100644
index 000000000..f2eee715c
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/templates/package-info.java
@@ -0,0 +1,6 @@
+@NonNullByDefault({ ARRAY_CONTENTS, PARAMETER, RETURN_TYPE, FIELD, TYPE_BOUND, TYPE_ARGUMENT })
+package org.eclipse.tm4e.ui.internal.templates;
+
+import static org.eclipse.jdt.annotation.DefaultLocation.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/CodeTemplateContextTypeUtils.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/CodeTemplateContextTypeUtils.java
new file mode 100644
index 000000000..5c98343d5
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/CodeTemplateContextTypeUtils.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.internal.utils;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tm4e.core.grammar.IGrammar;
+import org.eclipse.tm4e.core.model.TMToken;
+import org.eclipse.tm4e.registry.IGrammarDefinition;
+import org.eclipse.tm4e.registry.ITMScope;
+import org.eclipse.tm4e.registry.TMEclipseRegistryPlugin;
+import org.eclipse.tm4e.ui.TMUIPlugin;
+
+public class CodeTemplateContextTypeUtils {
+
+ private static final String CONTEXT_TYPE_ID_PREFIX = TMUIPlugin.PLUGIN_ID + ".templates.context."; //$NON-NLS-1$
+
+ private CodeTemplateContextTypeUtils() {
+ // no instantiation desired
+ }
+
+ public static String toContextTypeId(final ITMScope languageScope) {
+ return CONTEXT_TYPE_ID_PREFIX + languageScope.getQualifiedName();
+ }
+
+ public static String toContextTypeId(final TMToken textMateToken) {
+ return CONTEXT_TYPE_ID_PREFIX + textMateToken.grammarScope;
+ }
+
+ public static @Nullable ITMScope findScopeFor(final String contextTypeId) {
+ final IGrammarDefinition[] grammarDefinitions = TMEclipseRegistryPlugin.getGrammarRegistryManager().getDefinitions();
+
+ return Arrays.stream(grammarDefinitions)
+ .map(IGrammarDefinition::getScope)
+ .filter(scope -> contextTypeId.equals(CodeTemplateContextTypeUtils.toContextTypeId(scope)))
+ .findFirst().orElse(null);
+ }
+
+ public static @Nullable String getContentTypeName(final ITMScope languageScope) {
+ @Nullable
+ final Collection contentTypes = TMEclipseRegistryPlugin.getGrammarRegistryManager()
+ .getContentTypesForScope(languageScope);
+
+ if (contentTypes != null && contentTypes.size() > 0) {
+ // we only consider the first content type
+ return contentTypes.iterator().next().getName();
+ }
+ return null;
+ }
+
+ public static String toContextTypeName(final ITMScope languageScope) {
+ final String contentTypeName = getContentTypeName(languageScope);
+
+ String name = "";
+ if (contentTypeName != null) {
+ name = contentTypeName;
+ }
+
+ name += " (" + languageScope.getQualifiedName() + ")";
+ return name;
+ }
+
+ public static @Nullable IGrammar toGrammar(final String contextTypeName) {
+ final IGrammarDefinition[] grammarDefinitions = TMEclipseRegistryPlugin.getGrammarRegistryManager().getDefinitions();
+
+ return Arrays.stream(grammarDefinitions)
+ .map(IGrammarDefinition::getScope)
+ .filter(scope -> contextTypeName.equals(toContextTypeName(scope)))
+ .map(scope -> TMEclipseRegistryPlugin.getGrammarRegistryManager().getGrammarForScope(scope))
+ .findFirst().orElse(null);
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/AbstractTMTemplateContextType.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/AbstractTMTemplateContextType.java
new file mode 100644
index 000000000..1a21a6c50
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/AbstractTMTemplateContextType.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.templates;
+
+import org.eclipse.jface.text.templates.GlobalTemplateVariables;
+import org.eclipse.jface.text.templates.TemplateContextType;
+import org.eclipse.jface.text.templates.TemplateVariableResolver;
+
+/**
+ * Default implementation for new, language-specific code template context types.
+ */
+public abstract class AbstractTMTemplateContextType extends TemplateContextType {
+
+ public AbstractTMTemplateContextType(final String contextTypeId, final String contextTypeName) {
+ super(contextTypeId, contextTypeName);
+ addTemplateVariableResolvers();
+ }
+
+ /**
+ * Adds {@link TemplateVariableResolver}s.
+ * Subclasses may override this method, but should call super.addTemplateVariableResolvers().
+ * The default implementation adds some global template variables like ${user},
+ * ${date}, and ${cursor} (see {@link #addGlobalTemplateVariableResolvers()}).
+ */
+ protected void addTemplateVariableResolvers() {
+ addGlobalTemplateVariableResolvers();
+ }
+
+ /**
+ * Adds {@link TemplateVariableResolver}s for some global template variables like
+ * ${user} (user name),
+ * ${date}, ${time}, ${year}, ${cursor},
+ * ${lineselection}, and ${wordselection}.
+ */
+ protected final void addGlobalTemplateVariableResolvers() {
+ addResolver(new GlobalTemplateVariables.User());
+
+ addResolver(new GlobalTemplateVariables.Date());
+ addResolver(new GlobalTemplateVariables.Time());
+ addResolver(new GlobalTemplateVariables.Year());
+
+ addResolver(new GlobalTemplateVariables.Cursor());
+ addResolver(new GlobalTemplateVariables.LineSelection());
+ addResolver(new GlobalTemplateVariables.WordSelection());
+
+ addResolver(new GlobalTemplateVariables.Dollar());
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/CommentTemplateContextType.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/CommentTemplateContextType.java
new file mode 100644
index 000000000..44ffe859b
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/CommentTemplateContextType.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.templates;
+
+import org.eclipse.tm4e.ui.TMUIPlugin;
+
+/**
+ * Language-independent code template context type for comments.
+ */
+public class CommentTemplateContextType extends AbstractTMTemplateContextType {
+
+ public static final String CONTEXT_ID = TMUIPlugin.PLUGIN_ID + ".templates.context.comment"; //$NON-NLS-1$
+
+ public CommentTemplateContextType() {
+ super(CONTEXT_ID, "Comment");
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/DefaultTMTemplateContextType.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/DefaultTMTemplateContextType.java
new file mode 100644
index 000000000..83a84c829
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/DefaultTMTemplateContextType.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.templates;
+
+import org.eclipse.tm4e.ui.TMUIPlugin;
+
+/**
+ * Language-independent default code template context type.
+ * It is used by TM4E as a fallback if case no other applicable context type can be found.
+ */
+public class DefaultTMTemplateContextType extends AbstractTMTemplateContextType {
+
+ public static final String CONTEXT_ID = TMUIPlugin.PLUGIN_ID + ".templates.context"; //$NON-NLS-1$
+
+ public DefaultTMTemplateContextType() {
+ super(CONTEXT_ID, "Default context");
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/DocumentationCommentTemplateContextType.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/DocumentationCommentTemplateContextType.java
new file mode 100644
index 000000000..896933a26
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/DocumentationCommentTemplateContextType.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.templates;
+
+import org.eclipse.tm4e.ui.TMUIPlugin;
+
+/**
+ * Language-independent code template context type for documentation comments, e.g. for javadoc comments.
+ */
+public class DocumentationCommentTemplateContextType extends AbstractTMTemplateContextType {
+
+ public static final String CONTEXT_ID = TMUIPlugin.PLUGIN_ID + ".templates.context.comment.doc"; //$NON-NLS-1$
+
+ public DocumentationCommentTemplateContextType() {
+ super(CONTEXT_ID, "Documentation Comment");
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/TMLanguageTemplateContextType.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/TMLanguageTemplateContextType.java
new file mode 100644
index 000000000..2b83c2de7
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/TMLanguageTemplateContextType.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Advantest Europe GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Dietrich Travkin (SOLUNAR GmbH) - initial implementation
+ *******************************************************************************/
+package org.eclipse.tm4e.ui.templates;
+
+import org.eclipse.tm4e.registry.ITMScope;
+import org.eclipse.tm4e.ui.internal.utils.CodeTemplateContextTypeUtils;
+
+/**
+ * Language-specific default code template context type. It is created for each language with a registered TM4E grammar.
+ */
+public class TMLanguageTemplateContextType extends AbstractTMTemplateContextType {
+
+ // TODO What could be a good approach for adding language-specific code template variables (resolvers)?
+ public TMLanguageTemplateContextType(final String contextTypeName, final ITMScope languageScope) {
+ super(CodeTemplateContextTypeUtils.toContextTypeId(languageScope), contextTypeName);
+ }
+
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/package-info.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/package-info.java
new file mode 100644
index 000000000..f28f526f9
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/package-info.java
@@ -0,0 +1,6 @@
+@NonNullByDefault({ ARRAY_CONTENTS, PARAMETER, RETURN_TYPE, FIELD, TYPE_BOUND, TYPE_ARGUMENT })
+package org.eclipse.tm4e.ui.templates;
+
+import static org.eclipse.jdt.annotation.DefaultLocation.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
diff --git a/org.eclipse.tm4e.ui/src/main/resources/templates/templates.properties b/org.eclipse.tm4e.ui/src/main/resources/templates/templates.properties
new file mode 100644
index 000000000..11a503c86
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/resources/templates/templates.properties
@@ -0,0 +1,4 @@
+# template content
+
+author.description=Author
+author.pattern=author ${user}${cursor}
diff --git a/org.eclipse.tm4e.ui/src/main/resources/templates/templates.xml b/org.eclipse.tm4e.ui/src/main/resources/templates/templates.xml
new file mode 100644
index 000000000..b77cff10b
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/resources/templates/templates.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ %author.pattern
+
+
\ No newline at end of file