From 1394d8f5c3a2625b0b68e4c93433d16e84264861 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Sun, 13 Sep 2026 09:26:00 +0000 Subject: [PATCH 1/2] fix: don't count child combinators as selector specificity A selector's parentScopes array mixes '>' child-combinator tokens with parent scope names. The specificity comparator skipped '>' while comparing individual parent scopes, but still counted them in the final array-length comparison, so selectors with more combinators won ties they should not have. Count only real parent scope names for that comparison, and fall back to the rule's source index when two selectors are equally specific, so that the later rule in the theme wins - matching how equal-specificity ties are already resolved for merged selectors. Fixes #292 --- src/tests/themes.test.ts | 80 ++++++++++++++++++++++++++-------------- src/theme.ts | 41 ++++++++++++++++---- 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/src/tests/themes.test.ts b/src/tests/themes.test.ts index 31a27a3c..066ef92b 100644 --- a/src/tests/themes.test.ts +++ b/src/tests/themes.test.ts @@ -426,7 +426,7 @@ test('Theme resolving always has defaults', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) ); assertThemeEqual(actual, expected); }); @@ -443,7 +443,7 @@ test('Theme resolving respects incoming defaults 1', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) ); assertThemeEqual(actual, expected); }); @@ -460,7 +460,7 @@ test('Theme resolving respects incoming defaults 2', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) ); assertThemeEqual(actual, expected); }); @@ -477,7 +477,7 @@ test('Theme resolving respects incoming defaults 3', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.Bold, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) ); assertThemeEqual(actual, expected); }); @@ -494,7 +494,7 @@ test('Theme resolving respects incoming defaults 4', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) ); assertThemeEqual(actual, expected); }); @@ -511,7 +511,7 @@ test('Theme resolving respects incoming defaults 5', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) ); assertThemeEqual(actual, expected); }); @@ -530,7 +530,7 @@ test('Theme resolving can merge incoming defaults', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.Bold, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) ); assertThemeEqual(actual, expected); }); @@ -549,8 +549,8 @@ test('Theme resolving defaults are inherited', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, FontStyle.NotSet, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.NotSet, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) }) ); assertThemeEqual(actual, expected); @@ -571,8 +571,8 @@ test('Theme resolving same rules get merged', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, FontStyle.Bold, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.Bold, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) }) ); assertThemeEqual(actual, expected); @@ -594,9 +594,9 @@ test('Theme resolving rules are inherited 1', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, FontStyle.Bold, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'identifier': new ThemeTrieElement(new ThemeTrieElementRule(2, null, FontStyle.Bold, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.Bold, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'identifier': new ThemeTrieElement(new ThemeTrieElementRule(2, null, -1, FontStyle.Bold, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) }) }) ); @@ -627,15 +627,15 @@ test('Theme resolving rules are inherited 2', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, FontStyle.Bold, _F, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'identifier': new ThemeTrieElement(new ThemeTrieElementRule(2, null, FontStyle.Bold, _G, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.Bold, _F, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'identifier': new ThemeTrieElement(new ThemeTrieElementRule(2, null, -1, FontStyle.Bold, _G, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) }), - 'constant': new ThemeTrieElement(new ThemeTrieElementRule(1, null, FontStyle.Italic, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'numeric': new ThemeTrieElement(new ThemeTrieElementRule(2, null, FontStyle.Italic, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'hex': new ThemeTrieElement(new ThemeTrieElementRule(3, null, FontStyle.Bold, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), - 'oct': new ThemeTrieElement(new ThemeTrieElementRule(3, null, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), - 'dec': new ThemeTrieElement(new ThemeTrieElementRule(3, null, FontStyle.None, _E, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), + 'constant': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.Italic, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'numeric': new ThemeTrieElement(new ThemeTrieElementRule(2, null, -1, FontStyle.Italic, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'hex': new ThemeTrieElement(new ThemeTrieElementRule(3, null, -1, FontStyle.Bold, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), + 'oct': new ThemeTrieElement(new ThemeTrieElementRule(3, null, -1, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), + 'dec': new ThemeTrieElement(new ThemeTrieElementRule(3, null, -1, FontStyle.None, _E, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), }) }) }) @@ -662,14 +662,14 @@ test('Theme resolving rules with parent scopes', () => { let expected = new Theme( colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { 'var': new ThemeTrieElement( - new ThemeTrieElementRule(1, null, FontStyle.Bold, _C, 0, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - [new ThemeTrieElementRule(1, ['source.css'], FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)], + new ThemeTrieElementRule(1, null, -1, FontStyle.Bold, _C, 0, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), + [new ThemeTrieElementRule(1, ['source.css'], 2, FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)], { 'identifier': new ThemeTrieElement( - new ThemeTrieElementRule(2, null, FontStyle.Bold, _E, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), - [new ThemeTrieElementRule(1, ['source.css'], FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)] + new ThemeTrieElementRule(2, null, -1, FontStyle.Bold, _E, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), + [new ThemeTrieElementRule(1, ['source.css'], 2, FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)] ) } ) @@ -825,3 +825,29 @@ test('Theme resolving issue #35: Trailing comma in a tmTheme scope selector', () assert.deepStrictEqual(actual, expected); }); + +test('Theme resolving: child combinators do not add specificity (#292)', () => { + const theme = Theme.createFromRawTheme({ + settings: [ + { + scope: 'source.ini > string > punctuation.definition.string.begin.ini', + settings: { foreground: '#FF0000' }, + }, + { + scope: 'source.ini string punctuation.definition.string.begin.ini', + settings: { foreground: '#00FF00' }, + }, + ], + }); + + const result = theme.match(ScopeStack.from( + 'source.ini', + 'string.quoted.single.ini', + 'punctuation.definition.string.begin.ini', + )); + + assert.strictEqual( + theme.getColorMap()[result!.foregroundId], + '#00FF00', + ); +}); diff --git a/src/theme.ts b/src/theme.ts index 037ff0e8..66a2ae73 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -213,6 +213,16 @@ function _matchesScope(scopeName: ScopeName, scopePattern: ScopeName): boolean { return scopePattern === scopeName || (scopeName.startsWith(scopePattern) && scopeName[scopePattern.length] === '.'); } +function parentScopeCount(parentScopes: readonly ScopeName[]): number { + let count = 0; + for (let i = 0, len = parentScopes.length; i < len; i++) { + if (parentScopes[i] !== '>') { + count++; + } + } + return count; +} + export class StyleAttributes { constructor( public readonly fontStyle: OrMask, @@ -443,13 +453,14 @@ function resolveParsedThemeRules(parsedThemeRules: ParsedThemeRule[], _colorMap: defaultLineHeight ); - let root = new ThemeTrieElement(new ThemeTrieElementRule(0, null, FontStyle.NotSet, 0, 0, defaultFontFamily, defaultFontSize, defaultLineHeight), []); + let root = new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, 0, 0, defaultFontFamily, defaultFontSize, defaultLineHeight), []); for (let i = 0, len = parsedThemeRules.length; i < len; i++) { let rule = parsedThemeRules[i]; root.insert( 0, rule.scope, rule.parentScopes, + rule.index, rule.fontStyle, colorMap.getId(rule.foreground), colorMap.getId(rule.background), @@ -513,6 +524,7 @@ export class ThemeTrieElementRule { scopeDepth: number; parentScopes: readonly ScopeName[]; + index: number; fontStyle: number; foreground: number; background: number; @@ -523,6 +535,7 @@ export class ThemeTrieElementRule { constructor( scopeDepth: number, parentScopes: readonly ScopeName[] | null, + index: number, fontStyle: number, foreground: number, background: number, @@ -532,6 +545,7 @@ export class ThemeTrieElementRule { ) { this.scopeDepth = scopeDepth; this.parentScopes = parentScopes || emptyParentScopes; + this.index = index; this.fontStyle = fontStyle; this.foreground = foreground; this.background = background; @@ -544,6 +558,7 @@ export class ThemeTrieElementRule { return new ThemeTrieElementRule( this.scopeDepth, this.parentScopes, + this.index, this.fontStyle, this.foreground, this.background, @@ -563,6 +578,7 @@ export class ThemeTrieElementRule { public acceptOverwrite( scopeDepth: number, + index: number, fontStyle: number, foreground: number, background: number, @@ -575,6 +591,7 @@ export class ThemeTrieElementRule { } else { this.scopeDepth = scopeDepth; } + this.index = index; // console.log('TODO -> my depth: ' + this.scopeDepth + ', overwriting depth: ' + scopeDepth); if (fontStyle !== FontStyle.NotSet) { this.fontStyle = fontStyle; @@ -658,8 +675,15 @@ export class ThemeTrieElement { } // If a depth-first, scope-by-scope comparison resulted in a tie, the rule with - // more parent scopes is considered more specific. - return b.parentScopes.length - a.parentScopes.length; + // more parent scopes is considered more specific. Child combinators only + // constrain matching, so they are not counted as parent scopes. + const parentScopeCountDiff = parentScopeCount(b.parentScopes) - parentScopeCount(a.parentScopes); + if (parentScopeCountDiff !== 0) { + return parentScopeCountDiff; + } + + // The rules are equally specific, so the later rule wins. + return b.index - a.index; } public match(scope: ScopeName): ThemeTrieElementRule[] { @@ -689,6 +713,7 @@ export class ThemeTrieElement { scopeDepth: number, scope: ScopeName, parentScopes: ScopeName[] | null, + index: number, fontStyle: number, foreground: number, background: number, @@ -697,7 +722,7 @@ export class ThemeTrieElement { lineHeight: number, ): void { if (scope === '') { - this._doInsertHere(scopeDepth, parentScopes, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); + this._doInsertHere(scopeDepth, parentScopes, index, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); return; } @@ -720,12 +745,13 @@ export class ThemeTrieElement { this._children[head] = child; } - child.insert(scopeDepth + 1, tail, parentScopes, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); + child.insert(scopeDepth + 1, tail, parentScopes, index, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); } private _doInsertHere( scopeDepth: number, parentScopes: ScopeName[] | null, + index: number, fontStyle: number, foreground: number, background: number, @@ -736,7 +762,7 @@ export class ThemeTrieElement { if (parentScopes === null) { // Merge into the main rule - this._mainRule.acceptOverwrite(scopeDepth, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); + this._mainRule.acceptOverwrite(scopeDepth, index, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); return; } @@ -746,7 +772,7 @@ export class ThemeTrieElement { if (strArrCmp(rule.parentScopes, parentScopes) === 0) { // bingo! => we get to merge this into an existing one - rule.acceptOverwrite(scopeDepth, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); + rule.acceptOverwrite(scopeDepth, index, fontStyle, foreground, background, fontFamily, fontSize, lineHeight); return; } } @@ -776,6 +802,7 @@ export class ThemeTrieElement { this._rulesWithParentScopes.push(new ThemeTrieElementRule( scopeDepth, parentScopes, + index, fontStyle, foreground, background, From 11730a3983fd00a0984aa1163eac52d8a6945667 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Sun, 13 Sep 2026 21:47:56 +0100 Subject: [PATCH 2/2] fix: record actual rule indices in theme trie fixtures assertThemeEqual compared each Theme's root to itself, so expected index values for the combinator-specificity change were never checked. Compare against the expected tree and store the indices produced by merging. --- src/tests/themes.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tests/themes.test.ts b/src/tests/themes.test.ts index 066ef92b..fada40a9 100644 --- a/src/tests/themes.test.ts +++ b/src/tests/themes.test.ts @@ -412,7 +412,7 @@ function assertThemeEqual(actual: Theme, expected: Theme): void { // Don't compare cache objects assert.deepStrictEqual( [actual["_colorMap"], actual["_defaults"], actual["_root"]], - [expected["_colorMap"], actual["_defaults"], actual["_root"]] + [expected["_colorMap"], expected["_defaults"], expected["_root"]] ); } @@ -572,7 +572,7 @@ test('Theme resolving same rules get merged', () => { colorMap, new StyleAttributes(FontStyle.None, _A, _B, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), new ThemeTrieElement(new ThemeTrieElementRule(0, null, -1, FontStyle.NotSet, _NUMBER_NOT_SET, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.Bold, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) + 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, 1, FontStyle.Bold, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) }) ); assertThemeEqual(actual, expected); @@ -631,11 +631,11 @@ test('Theme resolving rules are inherited 2', () => { 'var': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.Bold, _F, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { 'identifier': new ThemeTrieElement(new ThemeTrieElementRule(2, null, -1, FontStyle.Bold, _G, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)) }), - 'constant': new ThemeTrieElement(new ThemeTrieElementRule(1, null, -1, FontStyle.Italic, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'numeric': new ThemeTrieElement(new ThemeTrieElementRule(2, null, -1, FontStyle.Italic, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { - 'hex': new ThemeTrieElement(new ThemeTrieElementRule(3, null, -1, FontStyle.Bold, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), - 'oct': new ThemeTrieElement(new ThemeTrieElementRule(3, null, -1, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), - 'dec': new ThemeTrieElement(new ThemeTrieElementRule(3, null, -1, FontStyle.None, _E, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), + 'constant': new ThemeTrieElement(new ThemeTrieElementRule(1, null, 4, FontStyle.Italic, _C, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'numeric': new ThemeTrieElement(new ThemeTrieElementRule(2, null, 5, FontStyle.Italic, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET), [], { + 'hex': new ThemeTrieElement(new ThemeTrieElementRule(3, null, 6, FontStyle.Bold, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), + 'oct': new ThemeTrieElement(new ThemeTrieElementRule(3, null, 7, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, _D, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), + 'dec': new ThemeTrieElement(new ThemeTrieElementRule(3, null, 8, FontStyle.None, _E, _NUMBER_NOT_SET, _STRING_NOT_SET, _NUMBER_NOT_SET, _NUMBER_NOT_SET)), }) }) })