Skip to content

Commit ac03143

Browse files
authored
feat: Add support for custom code templates (fix #987) (#988)
This PR fixes issue #987 and adds - a preferences dialog for editing custom code templates for languages available through registered TextMate grammars - first language-independent code templates - support for adding new custom code templates - code proposals based on available custom code templates - syntax highlighting in the code templates preview based an the settings for the registered language grammars
1 parent f90c835 commit ac03143

21 files changed

Lines changed: 1086 additions & 1 deletion
13.3 KB
Loading

docs/img/templates_preferences.png

94.7 KB
Loading

docs/user-guide.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,26 @@ Language-configuration files enable additional editor behavior for a language. T
5757
These behaviors are applied on top of whatever the underlying editor already provides.
5858
TM4E can add these behaviors to simple text editors, or refine them when an editor or language server already offers partial support.
5959

60-
### 3) Diagnostic tools (token hover)
60+
### 3) Custom code templates and code proposals
61+
62+
TM4E offers support for defining and proposing custom code templates for the languages available through a TextMate grammar.
63+
Users can specify their own code templates using the `TextMate > Templates` preferences page.
64+
65+
Each template is registered to a context type, i.e. a language or grammar (technically a TextMate scope).
66+
In addition to available grammars, TM4E offers two special context types for comments.
67+
Templates registered for these context types can be used in all languages having such comments in their grammar.
68+
69+
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.
70+
Having a TM4E-based editor open, they'll get that code snippets suggested via code completion triggered by Ctrl + Space.
71+
72+
The two generic context types for comments are:
73+
74+
- Comment (any comment that is not a documentation comment, i.e. line comments and standard block comments like code between `/*` and `*/` in Java / C++)
75+
- Documentation comment (a comment that is used for code documentation, e.g. javadoc comments in Java, i.e. code between `/**` and `*/`)
76+
77+
![Custom Code Proposal](img/code_template_proposal.png)
78+
79+
### 4) Diagnostic tools (token hover)
6180

6281
For advanced users, some editors expose a TextMate token hover that shows the token scopes and partition information at the caret location.
6382

@@ -102,6 +121,9 @@ Most user-facing configuration lives under the `TextMate` section in the Eclipse
102121
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.\
103122
![Task Tags Preferences](img/task_tags_preferences.png)
104123

124+
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.\
125+
![Templates Preferences](img/templates_preferences.png)
126+
105127
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.
106128
You can also switch themes from the editor's context menu under `TextMate`.\
107129
![Themes Preferences](img/themes_preferences.png)

org.eclipse.tm4e.ui/META-INF/MANIFEST.MF

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Export-Package: org.eclipse.tm4e.ui,
2929
org.eclipse.tm4e.ui.internal.widgets;x-friends:="org.eclipse.tm4e.languageconfiguration",
3030
org.eclipse.tm4e.ui.model,
3131
org.eclipse.tm4e.ui.samples,
32+
org.eclipse.tm4e.ui.templates,
3233
org.eclipse.tm4e.ui.text,
3334
org.eclipse.tm4e.ui.themes,
3435
org.eclipse.tm4e.ui.themes.css
387 Bytes
Loading

org.eclipse.tm4e.ui/plugin.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ TextMatePreferencePage.name=TextMate
2929
GrammarPreferencePage.name=Grammar
3030
TaskTagsPreferencePage.name=Task Tags
3131
ThemePreferencePage.name=Theme
32+
TemplatesPreferencePage.name=Templates
3233

3334
# Wizards
3435
TextMateWizard.category=TextMate

org.eclipse.tm4e.ui/plugin.xml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
class="org.eclipse.tm4e.ui.internal.preferences.ThemePreferencePage"
7272
id="org.eclipse.tm4e.ui.preferences.ThemePreferencePage"
7373
category="org.eclipse.tm4e.ui.preferences.TextMatePreferencePage" />
74+
<page name="%TemplatesPreferencePage.name"
75+
class="org.eclipse.tm4e.ui.internal.preferences.CustomCodeTemplatePreferencePage"
76+
id="org.eclipse.tm4e.ui.preferences.CustomCodeTemplatePreferencePage"
77+
category="org.eclipse.tm4e.ui.preferences.TextMatePreferencePage">
78+
</page>
7479
</extension>
7580

7681
<!-- Wizards -->
@@ -155,4 +160,39 @@
155160
class="org.eclipse.tm4e.ui.internal.hover.TMTokenTextHover"
156161
contentType="org.eclipse.core.runtime.text" />
157162
</extension>
163+
164+
<extension point="org.eclipse.ui.editors.templates">
165+
<contextType
166+
class="org.eclipse.tm4e.ui.templates.DefaultTMTemplateContextType"
167+
id="org.eclipse.tm4e.ui.templates.context"
168+
name="Default context"
169+
registryId="org.eclipse.tm4e.ui.templates">
170+
</contextType>
171+
<contextType
172+
class="org.eclipse.tm4e.ui.templates.CommentTemplateContextType"
173+
id="org.eclipse.tm4e.ui.templates.context.comment"
174+
name="Comment"
175+
registryId="org.eclipse.tm4e.ui.templates">
176+
</contextType>
177+
<contextType
178+
class="org.eclipse.tm4e.ui.templates.DocumentationCommentTemplateContextType"
179+
id="org.eclipse.tm4e.ui.templates.context.comment.doc"
180+
name="Documentation Comment"
181+
registryId="org.eclipse.tm4e.ui.templates">
182+
</contextType>
183+
<include
184+
file="src/main/resources/templates/templates.xml"
185+
translations="src/main/resources/templates/templates.properties">
186+
</include>
187+
<contextTypeRegistry
188+
id="org.eclipse.tm4e.ui.templates">
189+
</contextTypeRegistry>
190+
</extension>
191+
192+
<extension point="org.eclipse.ui.genericeditor.contentAssistProcessors">
193+
<contentAssistProcessor
194+
class="org.eclipse.tm4e.ui.internal.templates.TMTemplateCompletionProcessor"
195+
contentType="org.eclipse.core.runtime.text" />
196+
</extension>
197+
158198
</plugin>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Advantest Europe GmbH and others.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Dietrich Travkin (SOLUNAR GmbH) - initial implementation
11+
*******************************************************************************/
12+
package org.eclipse.tm4e.ui;
13+
14+
import java.net.URL;
15+
16+
import org.eclipse.core.runtime.FileLocator;
17+
import org.eclipse.core.runtime.Path;
18+
import org.eclipse.core.runtime.Platform;
19+
import org.eclipse.jdt.annotation.Nullable;
20+
import org.eclipse.jface.resource.ImageDescriptor;
21+
import org.eclipse.jface.resource.ImageRegistry;
22+
import org.eclipse.swt.graphics.Image;
23+
import org.osgi.framework.Bundle;
24+
25+
public final class TMImages {
26+
27+
private static final String ICONS_PATH = "$nl$/icons/full/"; //$NON-NLS-1$
28+
private static final String OBJECT = ICONS_PATH + "obj16/"; // basic colors - size 16x16 //$NON-NLS-1$
29+
30+
public static final String IMG_TEMPLATE = "IMG_TEMPALTE"; //$NON-NLS-1$
31+
32+
private TMImages() {
33+
// no instantiation desired
34+
}
35+
36+
private static @Nullable ImageRegistry imageRegistry;
37+
38+
public static void initalize(final ImageRegistry registry) {
39+
imageRegistry = registry;
40+
41+
registerImage(IMG_TEMPLATE, OBJECT + "template_obj.svg"); //$NON-NLS-1$
42+
}
43+
44+
private static void registerImage(final String key, final String path) {
45+
ImageDescriptor desc = ImageDescriptor.getMissingImageDescriptor();
46+
final Bundle bundle = Platform.getBundle(TMUIPlugin.PLUGIN_ID);
47+
final ImageRegistry imageRegistry = getImageRegistry();
48+
URL url = null;
49+
if (bundle != null) {
50+
url = FileLocator.find(bundle, new Path(path), null);
51+
if (url != null) {
52+
desc = ImageDescriptor.createFromURL(url);
53+
}
54+
}
55+
if (imageRegistry != null) {
56+
imageRegistry.put(key, desc);
57+
}
58+
}
59+
60+
/**
61+
* Returns the {@link Image} identified by the given key, or <code>null</code> if it does not exist.
62+
*/
63+
public static @Nullable Image getImage(final String key) {
64+
final ImageRegistry imageRegistry = getImageRegistry();
65+
if (imageRegistry == null) {
66+
return null;
67+
}
68+
return imageRegistry.get(key);
69+
}
70+
71+
/**
72+
* Returns the {@link ImageDescriptor} identified by the given key, or <code>null</code> if it does not exist.
73+
*/
74+
public static @Nullable ImageDescriptor getImageDescriptor(final String key) {
75+
final ImageRegistry imageRegistry = getImageRegistry();
76+
if (imageRegistry == null) {
77+
return null;
78+
}
79+
return imageRegistry.getDescriptor(key);
80+
}
81+
82+
public static @Nullable ImageRegistry getImageRegistry() {
83+
if (imageRegistry == null) {
84+
final TMUIPlugin plugin = TMUIPlugin.getDefault();
85+
if (plugin == null) {
86+
return null;
87+
}
88+
imageRegistry = plugin.getImageRegistry();
89+
}
90+
return imageRegistry;
91+
}
92+
93+
}

org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
*
99
* Contributors:
1010
* Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
11+
* Dietrich Travkin (SOLUNAR GmbH) - Additions for custom code templates
1112
*/
1213
package org.eclipse.tm4e.ui;
1314

15+
import java.io.IOException;
16+
import java.util.Iterator;
1417
import java.util.logging.Handler;
1518
import java.util.logging.Level;
1619
import java.util.logging.LogRecord;
@@ -20,13 +23,26 @@
2023
import org.eclipse.core.runtime.Platform;
2124
import org.eclipse.core.runtime.Status;
2225
import org.eclipse.jdt.annotation.Nullable;
26+
import org.eclipse.jface.text.templates.TemplateContextType;
27+
import org.eclipse.jface.text.templates.persistence.TemplateStore;
28+
import org.eclipse.text.templates.ContextTypeRegistry;
29+
import org.eclipse.tm4e.registry.IGrammarDefinition;
30+
import org.eclipse.tm4e.registry.ITMScope;
31+
import org.eclipse.tm4e.registry.TMEclipseRegistryPlugin;
2332
import org.eclipse.tm4e.ui.internal.model.TMModelManager;
2433
import org.eclipse.tm4e.ui.internal.samples.SampleManager;
2534
import org.eclipse.tm4e.ui.internal.themes.ThemeManager;
35+
import org.eclipse.tm4e.ui.internal.utils.CodeTemplateContextTypeUtils;
2636
import org.eclipse.tm4e.ui.model.ITMModelManager;
2737
import org.eclipse.tm4e.ui.samples.ISampleManager;
38+
import org.eclipse.tm4e.ui.templates.CommentTemplateContextType;
39+
import org.eclipse.tm4e.ui.templates.DefaultTMTemplateContextType;
40+
import org.eclipse.tm4e.ui.templates.DocumentationCommentTemplateContextType;
41+
import org.eclipse.tm4e.ui.templates.TMLanguageTemplateContextType;
2842
import org.eclipse.tm4e.ui.themes.ColorManager;
2943
import org.eclipse.tm4e.ui.themes.IThemeManager;
44+
import org.eclipse.ui.editors.text.templates.ContributionContextTypeRegistry;
45+
import org.eclipse.ui.editors.text.templates.ContributionTemplateStore;
3046
import org.eclipse.ui.plugin.AbstractUIPlugin;
3147
import org.osgi.framework.BundleContext;
3248

@@ -39,9 +55,17 @@ public class TMUIPlugin extends AbstractUIPlugin {
3955
public static final String PLUGIN_ID = "org.eclipse.tm4e.ui"; //$NON-NLS-1$
4056
private static final String TRACE_ID = PLUGIN_ID + "/trace"; //$NON-NLS-1$
4157

58+
// IDs for custom code templates
59+
private static final String CUSTOM_TEMPLATES_KEY = PLUGIN_ID + ".text.templates.custom"; //$NON-NLS-1$
60+
private static final String TEMPLATES_REGISTRY_ID = PLUGIN_ID + ".templates"; //$NON-NLS-1$
61+
4262
// The shared instance
4363
private static volatile @Nullable TMUIPlugin plugin;
4464

65+
// registry and store for custom code templates
66+
private @Nullable ContributionContextTypeRegistry contextTypeRegistry = null;
67+
private @Nullable TemplateStore templateStore = null;
68+
4569
/**
4670
* Returns the shared instance
4771
*
@@ -146,12 +170,100 @@ public void close() throws SecurityException {
146170
}
147171
});
148172
}
173+
174+
TMImages.initalize(getImageRegistry());
149175
}
150176

151177
@Override
152178
public void stop(final BundleContext context) throws Exception {
179+
if (templateStore != null) {
180+
templateStore.stopListeningForPreferenceChanges();
181+
}
153182
ColorManager.getInstance().dispose();
154183
plugin = null;
155184
super.stop(context);
156185
}
186+
187+
public ContextTypeRegistry getTemplateContextRegistry() {
188+
final var contextTypeRegistry = this.contextTypeRegistry;
189+
if (contextTypeRegistry != null) {
190+
return contextTypeRegistry;
191+
}
192+
193+
final var newContextTypeRegistry = new ContributionContextTypeRegistry(TEMPLATES_REGISTRY_ID);
194+
this.contextTypeRegistry = newContextTypeRegistry;
195+
196+
newContextTypeRegistry.addContextType(DefaultTMTemplateContextType.CONTEXT_ID);
197+
newContextTypeRegistry.addContextType(CommentTemplateContextType.CONTEXT_ID);
198+
newContextTypeRegistry.addContextType(DocumentationCommentTemplateContextType.CONTEXT_ID);
199+
200+
// Add language-specific context types
201+
// TODO Skip certain grammars? Some grammars have no name or are only used for highlighting code snippets, e.g. in Markdown
202+
final IGrammarDefinition[] grammarDefinitions = TMEclipseRegistryPlugin.getGrammarRegistryManager().getDefinitions();
203+
for (final IGrammarDefinition definition : grammarDefinitions) {
204+
final ITMScope languageScope = definition.getScope();
205+
206+
// TODO It seems TemplatePreferencePage.EditTemplateDialog requires the context type names to be unique. Can we shorten the names somehow?
207+
final String contextTypeName = CodeTemplateContextTypeUtils.toContextTypeName(languageScope);
208+
209+
final TMLanguageTemplateContextType languageContextType = new TMLanguageTemplateContextType(
210+
contextTypeName, languageScope);
211+
newContextTypeRegistry.addContextType(languageContextType);
212+
}
213+
214+
return newContextTypeRegistry;
215+
}
216+
217+
@SuppressWarnings("deprecation")
218+
private static class ContextTypeRegistryWrapper extends org.eclipse.jface.text.templates.ContextTypeRegistry {
219+
220+
private final ContextTypeRegistry delegate;
221+
222+
public ContextTypeRegistryWrapper(final ContextTypeRegistry registry) {
223+
this.delegate = registry;
224+
}
225+
226+
@Override
227+
public Iterator<TemplateContextType> contextTypes() {
228+
return delegate.contextTypes();
229+
}
230+
231+
@Override
232+
public void addContextType(final TemplateContextType contextType) {
233+
delegate.addContextType(contextType);
234+
}
235+
236+
@Override
237+
public @Nullable TemplateContextType getContextType(final String id) {
238+
return delegate.getContextType(id);
239+
}
240+
241+
}
242+
243+
public static ContextTypeRegistryWrapper from(final ContextTypeRegistry registry) {
244+
return new ContextTypeRegistryWrapper(registry);
245+
}
246+
247+
public TemplateStore getTemplateStore() {
248+
final var templateStore = this.templateStore;
249+
250+
if (templateStore != null) {
251+
return templateStore;
252+
}
253+
254+
final TemplateStore newTemplateStore = new ContributionTemplateStore(
255+
from(getTemplateContextRegistry()), getPreferenceStore(), CUSTOM_TEMPLATES_KEY);
256+
this.templateStore = newTemplateStore;
257+
258+
try {
259+
newTemplateStore.load();
260+
} catch (final IOException e) {
261+
Platform.getLog(this.getClass()).error(e.getMessage(), e);
262+
}
263+
264+
newTemplateStore.startListeningForPreferenceChanges();
265+
266+
return newTemplateStore;
267+
}
268+
157269
}

0 commit comments

Comments
 (0)