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 @@ + + + + + + + \ No newline at end of file