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..2ffccfedb 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;
@@ -19,14 +22,29 @@
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.annotation.NonNull;
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.core.grammar.IGrammar;
+import org.eclipse.tm4e.core.internal.utils.NullSafetyHelper;
+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.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 +57,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 +172,109 @@ 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() {
+ @NonNull
+ ContributionContextTypeRegistry result;
+
+ if (contextTypeRegistry == null) {
+ result = new ContributionContextTypeRegistry(TEMPLATES_REGISTRY_ID);
+ contextTypeRegistry = result;
+
+ result.addContextType(DefaultTMTemplateContextType.CONTEXT_ID);
+ result.addContextType(CommentTemplateContextType.CONTEXT_ID);
+ result.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();
+ final IGrammar languageGrammar = TMEclipseRegistryPlugin.getGrammarRegistryManager().getGrammarForScope(languageScope);
+ if (languageGrammar != null) {
+ String name = languageGrammar.getName();
+ if (name == null) {
+ name = "";
+ }
+ name += " (" + languageScope.getName() + ")";
+ final TMLanguageTemplateContextType languageContextType = new TMLanguageTemplateContextType(
+ name, languageScope);
+ result.addContextType(languageContextType);
+ }
+ }
+
+ } else {
+ result = NullSafetyHelper.castNonNull(contextTypeRegistry);
+ }
+ return result;
+ }
+
+ @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;
+ }
+
+ // TODO How can this null-safety check be handled correctly?
+ @SuppressWarnings("null")
+ @Override
+ public Iterator<@Nullable TemplateContextType> contextTypes() {
+ return delegate.contextTypes();
+ }
+
+ @Override
+ public void addContextType(final @Nullable TemplateContextType contextType) {
+ delegate.addContextType(contextType);
+ }
+
+ @Override
+ public @Nullable TemplateContextType getContextType(final @Nullable String id) {
+ return delegate.getContextType(id);
+ }
+
+ }
+
+ public static ContextTypeRegistryWrapper from(final ContextTypeRegistry registry) {
+ return new ContextTypeRegistryWrapper(registry);
+ }
+
+ public TemplateStore getTemplateStore() {
+ @NonNull
+ TemplateStore result;
+
+ if (templateStore == null) {
+ result = new ContributionTemplateStore(from(getTemplateContextRegistry()), getPreferenceStore(),
+ CUSTOM_TEMPLATES_KEY);
+ templateStore = result;
+
+ try {
+ result.load();
+ } catch (final IOException e) {
+ Platform.getLog(this.getClass()).error(e.getMessage(), e);
+ }
+
+ result.startListeningForPreferenceChanges();
+ } else {
+ result = NullSafetyHelper.castNonNull(templateStore);
+ }
+
+ return result;
+ }
+
}
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..aac2ffe01
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/CustomCodeTemplatePreferencePage.java
@@ -0,0 +1,135 @@
+/*******************************************************************************
+ * 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 org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.Document;
+import org.eclipse.jface.text.IDocument;
+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.widgets.Composite;
+import org.eclipse.swt.widgets.Shell;
+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.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 TMEditTemplateDialog extends TemplatePreferencePage.EditTemplateDialog {
+
+ 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 SourceViewer createViewer(final @Nullable Composite parent) {
+ // TODO Can we adapt the SourceViewerConfiguration from super.createViewer and add our own presentation reconsiler that adapts to the selected context type?
+ return super.createViewer(parent);
+ }
+ }
+
+ @Override
+ protected String getFormatterPreferenceKey() {
+ return PreferenceConstants.TEMPLATES_USE_CODEFORMATTER;
+ }
+
+ @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) {
+ // TODO check if we need special config for highlighting template variables
+ previewReconsiler = new TMPresentationReconciler();
+ 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/preferences/PreferenceConstants.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceConstants.java
index 79164b536..8c36dbace 100644
--- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceConstants.java
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceConstants.java
@@ -27,6 +27,8 @@ public final class PreferenceConstants {
public static final String TMTOKEN_HOVER_ENABLED = "org.eclipse.tm4e.ui.tmScopeHoverEnabled";
+ public static final String TEMPLATES_USE_CODEFORMATTER = "org.eclipse.tm4e.ui.templates.useCodeFormatter";
+
private PreferenceConstants() {
}
}
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..1b342333c
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/templates/TMTemplateCompletionProcessor.java
@@ -0,0 +1,242 @@
+/*******************************************************************************
+ * 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.ArrayList;
+import java.util.Collections;
+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.CompletionProposal;
+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.swt.graphics.Point;
+import org.eclipse.text.templates.ContextTypeRegistry;
+import org.eclipse.tm4e.core.internal.utils.NullSafetyHelper;
+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.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 ICompletionProposal[] NO_PROPOSALS = {};
+ 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 syncExec()
+ final ArrayList templateProposals = new ArrayList<>();
+ UI.getDisplay().syncExec(() -> {
+ final ICompletionProposal[] proposalsFromParent = TMTemplateCompletionProcessor.super.computeCompletionProposals(
+ viewer, offset);
+ Collections.addAll(templateProposals, proposalsFromParent);
+ });
+
+ if (templateProposals.size() > 0) {
+ final ICompletionProposal[] proposals = templateProposals
+ .toArray(new ICompletionProposal[templateProposals.size()]);
+ return proposals;
+ }
+
+ if (viewer == null || viewer.getDocument() == null) {
+ return NO_PROPOSALS;
+ }
+
+ // add dummy proposal if nothing else applies
+ final List selectionRangeList = new ArrayList<>(1);
+ // avoid illegal thread access exception:
+ UI.getDisplay().syncExec(() -> {
+ final Point selectionRange = viewer.getSelectedRange();
+ selectionRangeList.add(selectionRange);
+ });
+
+ final int replacementOffset = selectionRangeList.get(0).x;
+ final int replacementLength = selectionRangeList.get(0).y;
+
+ final String replacementText = "test completion"; //$NON-NLS-1$
+ return new ICompletionProposal[] {
+ new CompletionProposal(replacementText, replacementOffset, replacementLength, replacementText.length(), getImage(null),
+ "Dummy code proposal", null,
+ replacementText) };
+ }
+
+ 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;
+ }
+
+ 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 String id = TMUIPlugin.PLUGIN_ID + ".templates.context." + textMateToken.grammarScope;
+ final TemplateContextType contextType = plugin.getTemplateContextRegistry().getContextType(id);
+ if (contextType != null) {
+ return contextType;
+ }
+
+ // TODO Also check language-specific context types from extensions?
+
+ // last option
+ return contextTypeRegistry.getContextType(DefaultTMTemplateContextType.CONTEXT_ID);
+ }
+
+ @Override
+ public @Nullable String getErrorMessage() {
+ // TODO add error message if applicable
+ return null;
+ }
+
+ @Override
+ protected @Nullable TemplateContextType getContextType(final @Nullable ITextViewer viewer, final @Nullable IRegion region) {
+ if (viewer != null && region != null && viewer.getDocument() != null) {
+ final TmTokenRegion tokenRegion = retrieveTmTokenFor(
+ NullSafetyHelper.castNonNull(viewer.getDocument()), region.getOffset());
+ if (tokenRegion != null) {
+ return retrieveTemplateContextType(tokenRegion.getToken());
+ }
+ }
+
+ final TMUIPlugin plugin = TMUIPlugin.getDefault();
+ if (plugin == null) {
+ return null;
+ }
+
+ return plugin.getTemplateContextRegistry()
+ .getContextType(DefaultTMTemplateContextType.CONTEXT_ID);
+ }
+
+ @Override
+ protected @Nullable Image getImage(final @Nullable Template template) {
+ return TMImages.getImage(TMImages.IMG_TEMPLATE);
+ }
+
+ @Override
+ protected Template[] getTemplates(final @Nullable String contextTypeId) {
+ final TMUIPlugin plugin = TMUIPlugin.getDefault();
+ if (contextTypeId == null || plugin == null) {
+ return NO_TEMPLATES;
+ }
+
+ final TemplateStore templateStore = plugin.getTemplateStore();
+ final Template[] customTemplates = templateStore.getTemplates(contextTypeId);
+
+ if (customTemplates == null || 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..e170cccf5
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/CodeTemplateContextTypeUtils.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * 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 org.eclipse.jdt.annotation.Nullable;
+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) {
+ final String contextTypeIdSuffix = languageScope.getQualifiedName();
+
+ return CONTEXT_TYPE_ID_PREFIX + contextTypeIdSuffix;
+ }
+
+ 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);
+ }
+
+}
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..64bb5271a
--- /dev/null
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/templates/TMLanguageTemplateContextType.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * 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 {
+
+ 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