Theme rules are lexically sorted before insertion into the theme trie.
Matching rules are later sorted by _cmpBySpecificity. When that comparator returns 0, the stable sort preserves the earlier lexical order. Theme.match() then selects the first matching rule.
As a result, lexical spelling can decide precedence even though it is not part of TextMate selector semantics.
Incorrect behavior
The initial ordering compares parent-scope arrays before consulting source index:
r = strArrCmp(a.parentScopes, b.parentScopes);
if (r !== 0) {
return r;
}
return a.index - b.index;
Later, the specificity comparator can return 0. The first matching rule is then selected:
const effectiveRule = matchingTrieElements.find((rule) =>
_scopePathMatchesParentScopes(scopePath.parent, rule.parentScopes)
);
The lexical order introduced for trie construction has therefore become an undocumented precedence rule.
Expected behavior
Selector precedence should be determined by semantic specificity against the actual scope stack.
A parent match nearer the terminal scope should rank above an otherwise equivalent match farther away. If two selectors have equal semantic rank, later theme source order should resolve the tie.
Lexical comparison of scope names should not affect the result.
Reproduction
This test can be added to src/tests/themes.test.ts:
test('lexical parent ordering does not decide precedence', () => {
const theme = Theme.createFromRawTheme({
settings: [
{
scope: 'meta.aaa punctuation.definition.test',
settings: { foreground: '#FF0000' },
},
{
scope: 'meta.zzz punctuation.definition.test',
settings: { foreground: '#00FF00' },
},
],
});
const result = theme.match(ScopeStack.from(
'source.test',
'meta.aaa.test',
'meta.zzz.test',
'punctuation.definition.test',
));
assert.strictEqual(
theme.getColorMap()[result!.foregroundId],
'#00FF00',
);
});
Actual result: #FF0000
Expected result: #00FF00
Both parent selectors contain two atoms and eight characters, so _cmpBySpecificity returns a tie.
meta.aaa sorts before meta.zzz, so the first rule wins. However, meta.zzz matches the nearer ancestor and therefore has the higher semantic rank. It is also the later rule in the theme.
Renaming the scopes without changing their structure can reverse the current result.
Theme rules are lexically sorted before insertion into the theme trie.
Matching rules are later sorted by
_cmpBySpecificity. When that comparator returns0, the stable sort preserves the earlier lexical order.Theme.match()then selects the first matching rule.As a result, lexical spelling can decide precedence even though it is not part of TextMate selector semantics.
Incorrect behavior
The initial ordering compares parent-scope arrays before consulting source index:
Later, the specificity comparator can return
0. The first matching rule is then selected:The lexical order introduced for trie construction has therefore become an undocumented precedence rule.
Expected behavior
Selector precedence should be determined by semantic specificity against the actual scope stack.
A parent match nearer the terminal scope should rank above an otherwise equivalent match farther away. If two selectors have equal semantic rank, later theme source order should resolve the tie.
Lexical comparison of scope names should not affect the result.
Reproduction
This test can be added to
src/tests/themes.test.ts:Actual result: #FF0000
Expected result: #00FF00
Both parent selectors contain two atoms and eight characters, so
_cmpBySpecificityreturns a tie.meta.aaasorts beforemeta.zzz, so the first rule wins. However,meta.zzzmatches the nearer ancestor and therefore has the higher semantic rank. It is also the later rule in the theme.Renaming the scopes without changing their structure can reverse the current result.