diff --git a/src/tests/themes.test.ts b/src/tests/themes.test.ts index 31a27a3c..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"]] ); } @@ -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, 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)), }) }) }) @@ -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,