diff --git a/docs/adopter-guide.md b/docs/adopter-guide.md
index 420ea6c38..a267374e0 100644
--- a/docs/adopter-guide.md
+++ b/docs/adopter-guide.md
@@ -134,17 +134,22 @@ TM4E also defines commands for toggling line comments and adding or removing blo
## Contributing Themes
-TM4E ships with built-in Light and Dark themes that are linked to the Eclipse appearance themes, but plugins can contribute additional CSS-based themes through the `org.eclipse.tm4e.ui.themes` extension point.
+TM4E ships with built-in Light and Dark themes that are linked to the Eclipse appearance themes, but plugins can contribute additional themes through the `org.eclipse.tm4e.ui.themes` extension point.
```xml
-
```
+The `path` attribute can point to:
+
+- a CSS theme file (`*.css`), or
+- a TextMate theme file (for example `*.tmTheme`, `*.plist`, `*.json`, `*.yaml`, `*.yml`).
+
Themes can be flagged as more suitable for light or dark appearances and can be associated with specific grammar scopes so that, for example, a dedicated theme applies whenever a particular language is active.
You declare one or more `` elements and then add `themeAssociation` elements that link themes to one or more scopes and optional dark/light variants.
The exact attributes and options are described in the `themes` extension point schema.
diff --git a/org.eclipse.tm4e.ui.tests/build.properties b/org.eclipse.tm4e.ui.tests/build.properties
index 3fd0f106d..a97d1aeeb 100644
--- a/org.eclipse.tm4e.ui.tests/build.properties
+++ b/org.eclipse.tm4e.ui.tests/build.properties
@@ -6,3 +6,6 @@ bin.includes = META-INF/,\
grammars/,\
plugin.xml,\
about.html
+
+# JDT Null Analysis for Eclipse
+additional.bundles = org.eclipse.jdt.annotation,assertj-core
diff --git a/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/internal/themes/CSSThemeTokenProviderTest.java b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/internal/themes/CSSThemeTokenProviderTest.java
new file mode 100644
index 000000000..52945b0f5
--- /dev/null
+++ b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/internal/themes/CSSThemeTokenProviderTest.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2025 Vegard IT 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:
+ * Sebastian Thomschke (Vegard IT GmbH) - initial implementation
+ */
+package org.eclipse.tm4e.ui.tests.internal.themes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jface.text.TextAttribute;
+import org.eclipse.swt.SWT;
+import org.eclipse.tm4e.core.theme.RGB;
+import org.eclipse.tm4e.ui.themes.ColorManager;
+import org.eclipse.tm4e.ui.themes.css.CSSTokenProvider;
+import org.junit.jupiter.api.Test;
+
+class CSSThemeTokenProviderTest {
+
+ private final ColorManager colors = ColorManager.getInstance();
+
+ private TextAttribute getTextAttribute(final CSSTokenProvider provider, final String tokenType) {
+ final var tokenData = provider.getToken(tokenType).getData();
+ assertThat(tokenData).isInstanceOf(TextAttribute.class);
+ return (TextAttribute) tokenData;
+ }
+
+ @Test
+ void testBuiltInDarkCssTheme() throws Exception {
+ try (var in = Files.newInputStream(Path.of("../org.eclipse.tm4e.ui/themes/Dark.css"))) {
+ final var provider = new CSSTokenProvider(in);
+
+ assertThat(provider.getEditorForeground()).isEqualTo(colors.getColor(new RGB(212, 212, 212)));
+ assertThat(provider.getEditorBackground()).isEqualTo(colors.getColor(new RGB(30, 30, 30)));
+ assertThat(provider.getEditorCurrentLineHighlight()).isEqualTo(colors.getColor(new RGB(40, 40, 40)));
+
+ assertThat(getTextAttribute(provider, "entity.other.attribute-name").getForeground())
+ .isEqualTo(colors.getColor(new RGB(156, 220, 254)));
+
+ assertThat(getTextAttribute(provider, "storage.type.java").getForeground())
+ .isEqualTo(colors.getColor(new RGB(78, 201, 176)));
+
+ assertThat(provider.getToken("this.selector.does.not.exist").getData()).isNull();
+ }
+ }
+
+ @Test
+ void testBuiltInMonokaiCssTheme() throws Exception {
+ try (var in = Files.newInputStream(Path.of("../org.eclipse.tm4e.ui/themes/Monokai.css"))) {
+ final var provider = new CSSTokenProvider(in);
+
+ assertThat(provider.getEditorForeground()).isEqualTo(colors.getColor(new RGB(248, 248, 242)));
+ assertThat(provider.getEditorBackground()).isEqualTo(colors.getColor(new RGB(39, 40, 34)));
+
+ assertThat(getTextAttribute(provider, "keyword").getForeground())
+ .isEqualTo(colors.getColor(new RGB(249, 38, 114)));
+
+ final var storageTypeAttrs = getTextAttribute(provider, "storage.type");
+ assertThat(storageTypeAttrs.getForeground()).isEqualTo(colors.getColor(new RGB(102, 217, 239)));
+ assertThat(storageTypeAttrs.getStyle() & SWT.ITALIC).isEqualTo(SWT.ITALIC);
+
+ final var inheritedClassAttrs = getTextAttribute(provider, "entity.other.inherited-class.java");
+ assertThat(inheritedClassAttrs.getForeground()).isEqualTo(colors.getColor(new RGB(166, 226, 46)));
+ assertThat(inheritedClassAttrs.getStyle() & SWT.ITALIC).isEqualTo(SWT.ITALIC);
+ assertThat(inheritedClassAttrs.getStyle() & TextAttribute.UNDERLINE).isEqualTo(TextAttribute.UNDERLINE);
+ }
+ }
+}
diff --git a/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/internal/themes/TMTokenProviderTest.java b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/internal/themes/TMTokenProviderTest.java
index d63392b69..325a2679b 100644
--- a/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/internal/themes/TMTokenProviderTest.java
+++ b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/internal/themes/TMTokenProviderTest.java
@@ -16,9 +16,11 @@
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.swt.SWT;
+import org.eclipse.tm4e.core.model.TMToken;
import org.eclipse.tm4e.core.registry.IThemeSource.ContentType;
import org.eclipse.tm4e.core.theme.RGB;
import org.eclipse.tm4e.ui.internal.themes.TMThemeTokenProvider;
@@ -157,4 +159,43 @@ void testVSCodeJsonTheme() throws Exception {
assertThat(attrs.getForeground()).isEqualTo(colors.getColor(RGB.fromHex("#00FF00")));
}
}
+
+ @Test
+ void testTMThemeMatchesAgainstScopesStack() throws Exception {
+ try (var in = new ByteArrayInputStream("""
+ {
+ "name": "Scopes test",
+ "tokenColors": [
+ {
+ "settings": {
+ "foreground": "#000000"
+ }
+ },
+ {
+ "scope": "entity.other",
+ "settings": {
+ "foreground": "#D197D9"
+ }
+ }
+ ]
+ }
+ """.getBytes())) {
+ final var theme = new TMThemeTokenProvider(ContentType.JSON, in);
+
+ final var token = new TMToken(
+ 0,
+ "meta.java.other.definition.class.entity.inherited.classes.inherited-class",
+ List.of(
+ "source.java@org.eclipse.tm4e.language_pack",
+ "meta.class.java",
+ "meta.definition.class.inherited.classes.java",
+ "entity.other.inherited-class.java"),
+ null);
+
+ final var data = theme.getToken(token).getData();
+ assertThat(data).isInstanceOf(TextAttribute.class);
+ final var attrs = (TextAttribute) data;
+ assertThat(attrs.getForeground()).isEqualTo(colors.getColor(RGB.fromHex("#D197D9")));
+ }
+ }
}
diff --git a/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/support/StyleRangesCollector.java b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/support/StyleRangesCollector.java
index 6c47b7828..7d1286fda 100644
--- a/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/support/StyleRangesCollector.java
+++ b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/support/StyleRangesCollector.java
@@ -45,7 +45,12 @@ public void onUninstalled() {
public void onColorized(final TextPresentation presentation, final Throwable error) {
add(presentation);
if (waitForToLineNumber != null) {
- final int offset = presentation.getExtent().getOffset() + presentation.getExtent().getLength();
+ final int endOffsetExclusive = presentation.getExtent().getOffset() + presentation.getExtent().getLength();
+ // The extent end offset is exclusive. If it points to the start of the next line,
+ // document.getLineOfOffset(endOffsetExclusive) would already return that next line
+ // even though the presentation does not actually cover it.
+ // We therefore check the line number of the last included character.
+ final int offset = presentation.getExtent().getLength() > 0 ? endOffsetExclusive - 1 : endOffsetExclusive;
try {
if (waitForToLineNumber != document.getLineOfOffset(offset)) {
return;
diff --git a/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/themes/CSSThemeColorizationTest.java b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/themes/CSSThemeColorizationTest.java
new file mode 100644
index 000000000..6e385c19f
--- /dev/null
+++ b/org.eclipse.tm4e.ui.tests/src/main/java/org/eclipse/tm4e/ui/tests/themes/CSSThemeColorizationTest.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2025 Vegard IT 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:
+ * Sebastian Thomschke (Vegard IT GmbH) - initial implementation
+ */
+package org.eclipse.tm4e.ui.tests.themes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.tm4e.core.grammar.IGrammar;
+import org.eclipse.tm4e.core.registry.IGrammarSource;
+import org.eclipse.tm4e.core.registry.Registry;
+import org.eclipse.tm4e.ui.tests.support.TMEditor;
+import org.eclipse.tm4e.ui.tests.support.TestUtils;
+import org.eclipse.tm4e.ui.themes.css.CSSTokenProvider;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class CSSThemeColorizationTest {
+
+ private static final String SAMPLE_TEXT = "let a = '';\nlet b = 10;\nlet c = true;";
+
+ private IGrammar grammar;
+ private TMEditor editor;
+
+ @BeforeEach
+ void setup() throws Exception {
+ TestUtils.assertNoTM4EThreadsRunning();
+ grammar = new Registry().addGrammar(IGrammarSource.fromResource(getClass(), "/grammars/TypeScript.tmLanguage.json"));
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if (editor != null) {
+ editor.dispose();
+ editor = null;
+ }
+ TestUtils.assertNoTM4EThreadsRunning();
+ }
+
+ private static void assertStyleRange(
+ final String styleRanges,
+ final int offset,
+ final int length,
+ final String expectedFontStyle,
+ final String expectedForegroundColor) {
+ assertThat(styleRanges).contains(
+ "StyleRange {" + offset + ", " + length + ", fontStyle=" + expectedFontStyle + ", foreground=Color {"
+ + expectedForegroundColor + ", 255}}");
+ }
+
+ @Test
+ void darkCssThemeColorsAreApplied() throws Exception {
+ try (var in = Files.newInputStream(Path.of("../org.eclipse.tm4e.ui/themes/Dark.css"))) {
+ final var theme = new CSSTokenProvider(in);
+ editor = new TMEditor(grammar, theme, SAMPLE_TEXT);
+
+ final var commands = editor.execute();
+ assertThat(commands).hasSize(1);
+ final String ranges = commands.get(0).getStyleRanges();
+
+ // let -> storage (matches .storage / .storage.type)
+ assertStyleRange(ranges, 0, 3, "normal", "86, 156, 214");
+ // '' -> string
+ assertStyleRange(ranges, 8, 2, "normal", "206, 145, 120");
+ // 10 -> constant.numeric
+ assertStyleRange(ranges, 20, 2, "normal", "181, 206, 168");
+ // true -> constant.language
+ assertStyleRange(ranges, 32, 4, "normal", "86, 156, 214");
+ }
+ }
+
+ @Test
+ void monokaiCssThemeColorsAreApplied() throws Exception {
+ try (var in = Files.newInputStream(Path.of("../org.eclipse.tm4e.ui/themes/Monokai.css"))) {
+ final var theme = new CSSTokenProvider(in);
+ editor = new TMEditor(grammar, theme, SAMPLE_TEXT);
+
+ final var commands = editor.execute();
+ assertThat(commands).hasSize(1);
+ final String ranges = commands.get(0).getStyleRanges();
+
+ // let -> storage.type
+ assertStyleRange(ranges, 0, 3, "italic", "102, 217, 239");
+ // '' -> string
+ assertStyleRange(ranges, 8, 2, "normal", "230, 219, 116");
+ // 10 -> constant.numeric
+ assertStyleRange(ranges, 20, 2, "normal", "174, 129, 255");
+ // true -> constant.language
+ assertStyleRange(ranges, 32, 4, "normal", "174, 129, 255");
+ }
+ }
+}
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/TMThemeTokenProvider.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/TMThemeTokenProvider.java
index 75c88d8c5..f1642aa61 100644
--- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/TMThemeTokenProvider.java
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/TMThemeTokenProvider.java
@@ -14,8 +14,11 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.rules.IToken;
import org.eclipse.swt.graphics.Color;
import org.eclipse.tm4e.core.internal.grammar.ScopeStack;
import org.eclipse.tm4e.core.internal.theme.FontStyle;
@@ -23,6 +26,8 @@
import org.eclipse.tm4e.core.internal.theme.StyleAttributes;
import org.eclipse.tm4e.core.internal.theme.Theme;
import org.eclipse.tm4e.core.internal.theme.raw.RawThemeReader;
+import org.eclipse.tm4e.core.internal.utils.ScopeNames;
+import org.eclipse.tm4e.core.model.TMToken;
import org.eclipse.tm4e.core.registry.IThemeSource;
import org.eclipse.tm4e.core.theme.IStyle;
import org.eclipse.tm4e.core.theme.RGB;
@@ -35,6 +40,7 @@ public class TMThemeTokenProvider extends AbstractTokenProvider {
private final Theme theme;
private final List colors;
+ private final ConcurrentMap tokenCacheByScopeStack = new ConcurrentHashMap<>();
public TMThemeTokenProvider(final IThemeSource.ContentType contentType, final InputStream in) throws Exception {
final var rawTheme = RawThemeReader
@@ -43,6 +49,41 @@ public TMThemeTokenProvider(final IThemeSource.ContentType contentType, final In
colors = theme.getColorMap();
}
+ @Override
+ public IToken getToken(final TMToken token) {
+ if (token.scopes.isEmpty())
+ return DEFAULT_TOKEN;
+
+ final String[] scopeNames = new String[token.scopes.size()];
+ final var keyBuilder = new StringBuilder(token.scopes.size() * 24);
+ for (int i = 0, size = token.scopes.size(); i < size; i++) {
+ final var scope = ScopeNames.withoutContributor(token.scopes.get(i));
+ scopeNames[i] = scope;
+ if (i > 0) {
+ keyBuilder.append('\n');
+ }
+ keyBuilder.append(scope);
+ }
+ final var cacheKey = keyBuilder.toString();
+ final var cachedToken = tokenCacheByScopeStack.get(cacheKey);
+ if (cachedToken != null)
+ return cachedToken;
+
+ final IToken computedToken = getTokenUncached(scopeNames);
+ final var existingToken = tokenCacheByScopeStack.putIfAbsent(cacheKey, computedToken);
+ return existingToken == null ? computedToken : existingToken;
+ }
+
+ private IToken getTokenUncached(final String[] scopeNames) {
+ final var scopePath = ScopeStack.from(scopeNames);
+ final var styleAttrs = theme.match(scopePath);
+ if (styleAttrs == null || styleAttrs.equals(StyleAttributes.NO_STYLE))
+ return DEFAULT_TOKEN;
+
+ final var style = toStyle(styleAttrs);
+ return style == null ? DEFAULT_TOKEN : getJFaceTextToken(style);
+ }
+
@Override
protected @Nullable IStyle getBestStyle(String textMateTokenType) {
StyleAttributes styleAttrs = null;
@@ -57,27 +98,31 @@ public TMThemeTokenProvider(final IThemeSource.ContentType contentType, final In
// this is a workaround because org.eclipse.tm4e.core.model.TMTokenizationSupport.decodeTextMateToken(DecodeMap, List)
// simply concatenates scopes into one and here we don't know how to separate them, e.g. "meta.package.java" + "keyword.other" = "meta.package.java.keyword.other"
// this results in style definitions for "keyword.other" are not returned by Theme#match() which only matches like "^keyword\.other.*"
- // -> time to upgrade to IGrammar.tokenizeLine2?
+ // Prefer calling getToken(TMToken), which matches against the real TextMate scopes stack.
textMateTokenType = textMateTokenType.substring(dotIdx + 1);
}
}
- if (styleAttrs != null) {
- final var style = new Style();
- if (styleAttrs.foregroundId > 0)
- style.setColor(RGB.fromHex(colors.get(styleAttrs.foregroundId)));
- if (styleAttrs.backgroundId > 0)
- style.setBackgroundColor(RGB.fromHex(colors.get(styleAttrs.backgroundId)));
-
- if (styleAttrs.fontStyle > 0) {
- style.setBold(FontStyle.isBold(styleAttrs.fontStyle));
- style.setItalic(FontStyle.isItalic(styleAttrs.fontStyle));
- style.setUnderline(FontStyle.isUnderline(styleAttrs.fontStyle));
- style.setStrikeThrough(FontStyle.isStrikethrough(styleAttrs.fontStyle));
- }
- return style;
+ return toStyle(styleAttrs);
+ }
+
+ private @Nullable IStyle toStyle(final @Nullable StyleAttributes styleAttrs) {
+ if (styleAttrs == null)
+ return null;
+
+ final var style = new Style();
+ if (styleAttrs.foregroundId > 0)
+ style.setColor(RGB.fromHex(colors.get(styleAttrs.foregroundId)));
+ if (styleAttrs.backgroundId > 0)
+ style.setBackgroundColor(RGB.fromHex(colors.get(styleAttrs.backgroundId)));
+
+ if (styleAttrs.fontStyle > 0) {
+ style.setBold(FontStyle.isBold(styleAttrs.fontStyle));
+ style.setItalic(FontStyle.isItalic(styleAttrs.fontStyle));
+ style.setUnderline(FontStyle.isUnderline(styleAttrs.fontStyle));
+ style.setStrikeThrough(FontStyle.isStrikethrough(styleAttrs.fontStyle));
}
- return null;
+ return style;
}
protected @Nullable Color getEditorColor(final String... names) {
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/Theme.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/Theme.java
index 153798d57..d9a5517fa 100644
--- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/Theme.java
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/themes/Theme.java
@@ -21,6 +21,7 @@
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
+import org.eclipse.tm4e.core.model.TMToken;
import org.eclipse.tm4e.core.internal.utils.StringUtils;
import org.eclipse.tm4e.core.registry.IThemeSource.ContentType;
import org.eclipse.tm4e.registry.TMResource;
@@ -98,6 +99,12 @@ public IToken getToken(final String textMateTokenType) {
return provider == null ? ITokenProvider.DEFAULT_TOKEN : provider.getToken(textMateTokenType);
}
+ @Override
+ public IToken getToken(final TMToken token) {
+ final ITokenProvider provider = getTokenProvider();
+ return provider == null ? ITokenProvider.DEFAULT_TOKEN : provider.getToken(token);
+ }
+
private @Nullable Color getPriorityColor(final @Nullable Color themeColor, final String prefStoreKey) {
// if the theme is light but Eclipse is in dark mode (or vice versa) we cannot use the pref settings and always use the theme colors
return UI.isDarkEclipseTheme() == isDark()
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/text/Colorizer.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/text/Colorizer.java
index c83bcf262..1bab4c889 100644
--- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/text/Colorizer.java
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/text/Colorizer.java
@@ -136,7 +136,7 @@ void colorize(final IRegion damageRegion, final ITMDocumentModel tmModel) throws
tokenStartIndex = damageRegion.getOffset() - startLineOffset;
} else {
tokenStartIndex = damageRegion.getOffset() - startLineOffset;
- final IToken token = theme == null ? ITokenProvider.DEFAULT_TOKEN : theme.getToken(currentToken.type);
+ final IToken token = theme == null ? ITokenProvider.DEFAULT_TOKEN : theme.getToken(currentToken);
lastAttribute = getTokenTextAttribute(token);
length += getTokenLength(tokenStartIndex, nextToken, lineLength);
firstToken = false;
@@ -151,7 +151,7 @@ else if (isTokenAfterRegion(currentToken, startLineOffset, damageRegion)) {
break;
}
- final IToken token = theme == null ? ITokenProvider.DEFAULT_TOKEN : theme.getToken(currentToken.type);
+ final IToken token = theme == null ? ITokenProvider.DEFAULT_TOKEN : theme.getToken(currentToken);
final TextAttribute attribute = getTokenTextAttribute(token);
if (lastAttribute.equals(attribute)) {
length += getTokenLength(tokenStartIndex, nextToken, lineLength);
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/ITokenProvider.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/ITokenProvider.java
index 94f84714a..522d3c329 100644
--- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/ITokenProvider.java
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/ITokenProvider.java
@@ -15,6 +15,7 @@
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.swt.graphics.Color;
+import org.eclipse.tm4e.core.model.TMToken;
/**
* Provider to retrieve Eclipse JFace Text {@link IToken}s for TextMate token types.
@@ -28,6 +29,14 @@ public interface ITokenProvider {
*/
IToken getToken(String tmTokenType);
+ /**
+ * Allows token providers to compute styles using the token's full TextMate scopes stack.
+ *
+ * @return the Eclipse JFace Text {@link IToken} for the given TexMate token or {@link #DEFAULT_TOKEN} if {@link TMToken#scopes} is
+ * empty.
+ */
+ IToken getToken(final TMToken token);
+
@Nullable
Color getEditorBackground();
diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/css/CSSTokenProvider.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/css/CSSTokenProvider.java
index ecc89e3ab..98d39f46f 100644
--- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/css/CSSTokenProvider.java
+++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/themes/css/CSSTokenProvider.java
@@ -17,8 +17,10 @@
import java.util.List;
import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.rules.IToken;
import org.eclipse.swt.graphics.Color;
import org.eclipse.tm4e.core.internal.utils.StringUtils;
+import org.eclipse.tm4e.core.model.TMToken;
import org.eclipse.tm4e.core.theme.IStyle;
import org.eclipse.tm4e.core.theme.css.CSSParser;
import org.eclipse.tm4e.ui.TMUIPlugin;
@@ -93,4 +95,9 @@ public CSSTokenProvider(final InputStream in) {
public @Nullable Color getEditorCurrentLineHighlight() {
return getColor(false, "editor", "lineHighlight");
}
+
+ @Override
+ public IToken getToken(final TMToken token) {
+ return getToken(token.type);
+ }
}
diff --git a/target-platforms/staging.target b/target-platforms/staging.target
index 1be65107d..e014d9d59 100644
--- a/target-platforms/staging.target
+++ b/target-platforms/staging.target
@@ -10,7 +10,7 @@
-
+
@@ -38,17 +38,18 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+