Skip to content

Commit 2a5d587

Browse files
Merge pull request #836 from savetheclocktower/fix-scope-resolver-config-cache
[tree-sitter] Share config caches between `ScopeResolver`s
2 parents 3cc30ce + 6cae381 commit 2a5d587

4 files changed

Lines changed: 121 additions & 17 deletions

File tree

packages/language-clojure/grammars/ts/highlights.scm

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@
3434
((list_lit
3535
"(" @punctuation.section.expression.begin (#is-not? test.descendantOfNodeWithData "clojure.dismissTag")
3636
.
37-
(sym_lit) @meta.definition.global @keyword.control (#eq? @meta.definition.global "ns")
37+
(sym_lit) @meta.definition.global @keyword.control
38+
(#eq? @meta.definition.global "ns")
3839
.
39-
(sym_lit) @meta.definition.global @entity.global
40+
; We need to distinguish this `@meta.definition.global` from the one above or
41+
; else this query will fail.
42+
(sym_lit) @meta.definition.global.__TEXT__ @entity.global
4043
")" @punctuation.section.expression.end)
4144
@meta.namespace.clojure
4245
(#set! isNamespace true))

packages/language-clojure/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "lib/main",
66
"engines": {
77
"atom": "*",
8-
"node": "*"
8+
"node": ">=12"
99
},
1010
"repository": "https://github.com/pulsar-edit/pulsar",
1111
"license": "MIT",

spec/scope-resolver-spec.js

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,9 @@ const jsRegexGrammarPath = resolve(
3232
);
3333
let jsRegexConfig = CSON.readFileSync(jsRegexGrammarPath);
3434

35-
async function getAllCaptures(grammar, languageMode, layer = null) {
35+
async function getAllCapturesWithScopeResolver(grammar, languageMode, scopeResolver, layer = null) {
3636
let query = await grammar.getQuery('highlightsQuery');
3737
layer = layer ?? languageMode.rootLanguageLayer;
38-
let scopeResolver = new ScopeResolver(
39-
layer,
40-
(name) => languageMode.idForScope(name),
41-
);
4238
let { start, end } = languageMode.buffer.getRange();
4339
let { tree } = layer;
4440
return {
@@ -47,6 +43,32 @@ async function getAllCaptures(grammar, languageMode, layer = null) {
4743
};
4844
}
4945

46+
function makeScopeResolver(languageMode, layer) {
47+
layer = layer ?? languageMode.rootLanguageLayer;
48+
return new ScopeResolver(
49+
layer,
50+
(name) => languageMode.idForScope(name),
51+
);
52+
}
53+
54+
async function getAllCaptures(grammar, languageMode, layer = null) {
55+
layer = layer ?? languageMode.rootLanguageLayer;
56+
let scopeResolver = makeScopeResolver(languageMode, layer);
57+
return getAllCapturesWithScopeResolver(grammar, languageMode, scopeResolver, layer);
58+
}
59+
60+
async function getAllMatchesWithScopeResolver(...args) {
61+
let { captures, scopeResolver } = await getAllCapturesWithScopeResolver(...args);
62+
let matches = [];
63+
for (let capture of captures) {
64+
let range = scopeResolver.store(capture);
65+
if (range) {
66+
matches.push(capture);
67+
}
68+
}
69+
return matches;
70+
}
71+
5072
async function getAllMatches(...args) {
5173
let { captures, scopeResolver } = await getAllCaptures(...args);
5274
let matches = [];
@@ -1136,12 +1158,33 @@ describe('ScopeResolver', () => {
11361158
`);
11371159
await languageMode.ready;
11381160

1139-
let matched = await getAllMatches(grammar, languageMode);
1161+
let scopeResolver = makeScopeResolver(languageMode);
1162+
1163+
let matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
11401164
expect(matched.length).toBe(4);
11411165

11421166
atom.config.set('core.careAboutBooleans', "something-else");
11431167

1144-
matched = await getAllMatches(grammar, languageMode);
1168+
matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
1169+
expect(matched.length).toBe(0);
1170+
1171+
atom.config.set(
1172+
'core.careAboutBooleans',
1173+
'something',
1174+
{ scope: [grammar.scopeName] }
1175+
);
1176+
1177+
matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
1178+
expect(matched.length).toBe(4);
1179+
1180+
atom.config.set('core.careAboutBooleans', "something");
1181+
1182+
atom.config.set(
1183+
'core.careAboutBooleans',
1184+
'something-else',
1185+
{ scope: [grammar.scopeName] }
1186+
);
1187+
matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
11451188
expect(matched.length).toBe(0);
11461189
});
11471190

src/scope-resolver.js

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const { CompositeDisposable } = require('event-kit');
12
const { Point } = require('text-buffer');
23
const ScopeDescriptor = require('./scope-descriptor');
34

@@ -16,7 +17,7 @@ function comparePoints(a, b) {
1617
}
1718
}
1819

19-
function rangeSpecToString (range) {
20+
function rangeSpecToString(range) {
2021
let [sp, ep] = [range.startPosition, range.endPosition];
2122
return `(${sp.row}, ${sp.column}) - (${ep.row}, ${ep.column})`;
2223
}
@@ -63,6 +64,67 @@ function interpretPossibleKeyValuePair(rawValue, coerceValue = false) {
6364
return [key, value];
6465
}
6566

67+
// `ScopeResolver`s can share a config cache if they have the same grammar.
68+
// There are many such `ScopeResolver`s (because there are many such
69+
// `LanguageLayer`s), and this consolidation cuts down on the number of
70+
// `onDidChange` handlers (which are costly when observing all config values).
71+
class ConfigCache {
72+
constructor(config) {
73+
this.subscriptions = new CompositeDisposable();
74+
this.cachesByGrammar = new Map();
75+
this.config = config;
76+
77+
this.subscriptions.add(
78+
this.config.onDidChange(() => this.clearAll()),
79+
atom.grammars.onDidAddGrammar(() => this.clearAll()),
80+
atom.grammars.onDidUpdateGrammar(() => this.clearAll())
81+
);
82+
}
83+
84+
dispose() {
85+
this.subscriptions.dispose();
86+
}
87+
88+
clearAll() {
89+
for (let cache of this.cachesByGrammar.values()) {
90+
cache.clear();
91+
}
92+
}
93+
94+
getCacheForGrammar(grammar) {
95+
let { scopeName } = grammar;
96+
// We key on the scope name rather than the grammar instance. We need to be
97+
// able to iterate over grammars, so we can't use a `WeakSet` here, and we
98+
// have no lifecycle event to keep the map from getting stale.
99+
//
100+
// To prevent two different incarnations of the same grammar (after
101+
// disabling and reenabling) from incorrectly sharing a cache, we clear all
102+
// caches whenever grammars are added or updated.
103+
let cache = this.cachesByGrammar.get(scopeName);
104+
if (!cache) {
105+
cache = new Map();
106+
this.cachesByGrammar.set(scopeName, cache);
107+
}
108+
return cache;
109+
}
110+
}
111+
112+
// We can technically have more than one configuration object, though in
113+
// practice this will only point to `atom.config`. We do it this way for ease
114+
// of testing (e.g., if a test mocks a config object) and to avoid silly hacks.
115+
ConfigCache.CACHES_FOR_CONFIG_OBJECTS = new Map();
116+
117+
ConfigCache.forConfig = (config) => {
118+
let { CACHES_FOR_CONFIG_OBJECTS } = ConfigCache;
119+
let configCache = CACHES_FOR_CONFIG_OBJECTS.get(config);
120+
if (!configCache) {
121+
configCache = new ConfigCache(config);
122+
CACHES_FOR_CONFIG_OBJECTS.set(config, configCache);
123+
}
124+
return configCache;
125+
};
126+
127+
66128
// A data structure for storing scope information while processing capture
67129
// data. The data is reset in between each task.
68130
//
@@ -86,10 +148,8 @@ class ScopeResolver {
86148
this.rangeData = new Map;
87149
this.pointKeyCache = new Map;
88150
this.patternCache = new Map;
89-
this.configCache = new Map;
90-
this.configSubscription = this.config.onDidChange(() => {
91-
this.configCache.clear();
92-
});
151+
this.configCache = ConfigCache.forConfig(this.config)
152+
.getCacheForGrammar(this.grammar);
93153
}
94154

95155
getOrCompilePattern(pattern) {
@@ -477,8 +537,6 @@ class ScopeResolver {
477537
this.reset();
478538
this.patternCache.clear();
479539
this.pointKeyCache.clear();
480-
this.configCache.clear();
481-
this.configSubscription.dispose();
482540
}
483541

484542
*[Symbol.iterator]() {

0 commit comments

Comments
 (0)