@codemirror/language 0.17.4 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/index.cjs +424 -223
- package/dist/index.d.ts +417 -7
- package/dist/index.js +434 -234
- package/package.json +9 -14
- package/dist/index.js.map +0 -1
- package/tsconfig.local.json +0 -19
package/dist/index.cjs
CHANGED
|
@@ -7,37 +7,52 @@ var text = require('@codemirror/text');
|
|
|
7
7
|
var state = require('@codemirror/state');
|
|
8
8
|
var view = require('@codemirror/view');
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
Node prop stored in a grammar's top syntax node to provide the
|
|
12
|
+
facet that stores language data for that language.
|
|
13
|
+
*/
|
|
12
14
|
const languageDataProp = new lezerTree.NodeProp();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
/**
|
|
16
|
+
Helper function to define a facet (to be added to the top syntax
|
|
17
|
+
node(s) for a language via
|
|
18
|
+
[`languageDataProp`](https://codemirror.net/6/docs/ref/#language.languageDataProp)), that will be
|
|
19
|
+
used to associate language data with the language. You
|
|
20
|
+
probably only need this when subclassing
|
|
21
|
+
[`Language`](https://codemirror.net/6/docs/ref/#language.Language).
|
|
22
|
+
*/
|
|
19
23
|
function defineLanguageFacet(baseData) {
|
|
20
24
|
return state.Facet.define({
|
|
21
25
|
combine: baseData ? values => values.concat(baseData) : undefined
|
|
22
26
|
});
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
/**
|
|
29
|
+
A language object manages parsing and per-language
|
|
30
|
+
[metadata](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt). Parse data is
|
|
31
|
+
managed as a [Lezer](https://lezer.codemirror.net) tree. You'll
|
|
32
|
+
want to subclass this class for custom parsers, or use the
|
|
33
|
+
[`LezerLanguage`](https://codemirror.net/6/docs/ref/#language.LezerLanguage) or
|
|
34
|
+
[`StreamLanguage`](https://codemirror.net/6/docs/ref/#stream-parser.StreamLanguage) abstractions for
|
|
35
|
+
[Lezer](https://lezer.codemirror.net/) or stream parsers.
|
|
36
|
+
*/
|
|
31
37
|
class Language {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
/**
|
|
39
|
+
Construct a language object. You usually don't need to invoke
|
|
40
|
+
this directly. But when you do, make sure you use
|
|
41
|
+
[`defineLanguageFacet`](https://codemirror.net/6/docs/ref/#language.defineLanguageFacet) to create
|
|
42
|
+
the first argument.
|
|
43
|
+
*/
|
|
36
44
|
constructor(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
/**
|
|
46
|
+
The [language data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt) data
|
|
47
|
+
facet used for this language.
|
|
48
|
+
*/
|
|
49
|
+
data, parser,
|
|
50
|
+
/**
|
|
51
|
+
The node type of the top node of trees produced by this parser.
|
|
52
|
+
*/
|
|
53
|
+
topNode, extraExtensions = []) {
|
|
40
54
|
this.data = data;
|
|
55
|
+
this.topNode = topNode;
|
|
41
56
|
// Kludge to define EditorState.tree as a debugging helper,
|
|
42
57
|
// without the EditorState package actually knowing about
|
|
43
58
|
// languages and lezer trees.
|
|
@@ -49,13 +64,17 @@ class Language {
|
|
|
49
64
|
state.EditorState.languageData.of((state, pos) => state.facet(languageDataFacetAt(state, pos)))
|
|
50
65
|
].concat(extraExtensions);
|
|
51
66
|
}
|
|
52
|
-
|
|
67
|
+
/**
|
|
68
|
+
Query whether this language is active at the given position.
|
|
69
|
+
*/
|
|
53
70
|
isActiveAt(state, pos) {
|
|
54
71
|
return languageDataFacetAt(state, pos) == this.data;
|
|
55
72
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
/**
|
|
74
|
+
Find the document regions that were parsed using this language.
|
|
75
|
+
The returned regions will _include_ any nested languages rooted
|
|
76
|
+
in this language, when those exist.
|
|
77
|
+
*/
|
|
59
78
|
findRegions(state) {
|
|
60
79
|
let lang = state.facet(language);
|
|
61
80
|
if ((lang === null || lang === void 0 ? void 0 : lang.data) == this.data)
|
|
@@ -74,19 +93,25 @@ class Language {
|
|
|
74
93
|
});
|
|
75
94
|
return result;
|
|
76
95
|
}
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
/**
|
|
97
|
+
Indicates whether this language allows nested languages. The
|
|
98
|
+
default implementation returns true.
|
|
99
|
+
*/
|
|
79
100
|
get allowsNesting() { return true; }
|
|
80
|
-
|
|
101
|
+
/**
|
|
102
|
+
Use this language to parse the given string into a tree.
|
|
103
|
+
*/
|
|
81
104
|
parseString(code) {
|
|
82
105
|
let doc = text.Text.of(code.split("\n"));
|
|
83
|
-
let parse = this.parser.startParse(new DocInput(doc), 0, new EditorParseContext(this.parser, state.EditorState.create({ doc }), [], lezerTree.Tree.empty, { from: 0, to: code.length }, []));
|
|
106
|
+
let parse = this.parser.startParse(new DocInput(doc), 0, new EditorParseContext(this.parser, state.EditorState.create({ doc }), [], lezerTree.Tree.empty, { from: 0, to: code.length }, [], null));
|
|
84
107
|
let tree;
|
|
85
108
|
while (!(tree = parse.advance())) { }
|
|
86
109
|
return tree;
|
|
87
110
|
}
|
|
88
111
|
}
|
|
89
|
-
|
|
112
|
+
/**
|
|
113
|
+
@internal
|
|
114
|
+
*/
|
|
90
115
|
Language.setState = state.StateEffect.define();
|
|
91
116
|
function languageDataFacetAt(state, pos) {
|
|
92
117
|
let topLang = state.facet(language);
|
|
@@ -104,38 +129,48 @@ function languageDataFacetAt(state, pos) {
|
|
|
104
129
|
}
|
|
105
130
|
return topLang.data;
|
|
106
131
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
/**
|
|
133
|
+
A subclass of [`Language`](https://codemirror.net/6/docs/ref/#language.Language) for use with
|
|
134
|
+
[Lezer](https://lezer.codemirror.net/docs/ref#lezer.Parser)
|
|
135
|
+
parsers.
|
|
136
|
+
*/
|
|
110
137
|
class LezerLanguage extends Language {
|
|
111
138
|
constructor(data, parser) {
|
|
112
|
-
super(data, parser);
|
|
139
|
+
super(data, parser, parser.topNode);
|
|
113
140
|
this.parser = parser;
|
|
114
141
|
}
|
|
115
|
-
|
|
142
|
+
/**
|
|
143
|
+
Define a language from a parser.
|
|
144
|
+
*/
|
|
116
145
|
static define(spec) {
|
|
117
146
|
let data = defineLanguageFacet(spec.languageData);
|
|
118
147
|
return new LezerLanguage(data, spec.parser.configure({
|
|
119
148
|
props: [languageDataProp.add(type => type.isTop ? data : undefined)]
|
|
120
149
|
}));
|
|
121
150
|
}
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
/**
|
|
152
|
+
Create a new instance of this language with a reconfigured
|
|
153
|
+
version of its parser.
|
|
154
|
+
*/
|
|
124
155
|
configure(options) {
|
|
125
156
|
return new LezerLanguage(this.data, this.parser.configure(options));
|
|
126
157
|
}
|
|
127
158
|
get allowsNesting() { return this.parser.hasNested; }
|
|
128
159
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
/**
|
|
161
|
+
Get the syntax tree for a state, which is the current (possibly
|
|
162
|
+
incomplete) parse tree of active [language](https://codemirror.net/6/docs/ref/#language.Language),
|
|
163
|
+
or the empty tree if there is no language available.
|
|
164
|
+
*/
|
|
132
165
|
function syntaxTree(state) {
|
|
133
166
|
let field = state.field(Language.state, false);
|
|
134
167
|
return field ? field.tree : lezerTree.Tree.empty;
|
|
135
168
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
169
|
+
/**
|
|
170
|
+
Try to get a parse tree that spans at least up to `upto`. The
|
|
171
|
+
method will do at most `timeout` milliseconds of work to parse
|
|
172
|
+
up to that point if the tree isn't already available.
|
|
173
|
+
*/
|
|
139
174
|
function ensureSyntaxTree(state, upto, timeout = 50) {
|
|
140
175
|
var _a;
|
|
141
176
|
let parse = (_a = state.field(Language.state, false)) === null || _a === void 0 ? void 0 : _a.context;
|
|
@@ -178,7 +213,7 @@ class DocInput {
|
|
|
178
213
|
let stringStart = this.cursorPos - this.string.length;
|
|
179
214
|
if (pos < stringStart || pos >= this.cursorPos)
|
|
180
215
|
stringStart = this.syncTo(pos);
|
|
181
|
-
return this.cursor.lineBreak ? "" : this.string.slice(pos - stringStart);
|
|
216
|
+
return this.cursor.lineBreak ? "" : this.string.slice(pos - stringStart, Math.min(this.length - stringStart, this.string.length));
|
|
182
217
|
}
|
|
183
218
|
read(from, to) {
|
|
184
219
|
let stringStart = this.cursorPos - this.string.length;
|
|
@@ -191,36 +226,61 @@ class DocInput {
|
|
|
191
226
|
return new DocInput(this.doc, at);
|
|
192
227
|
}
|
|
193
228
|
}
|
|
194
|
-
|
|
229
|
+
/**
|
|
230
|
+
A parse context provided to parsers working on the editor content.
|
|
231
|
+
*/
|
|
195
232
|
class EditorParseContext {
|
|
196
|
-
|
|
233
|
+
/**
|
|
234
|
+
@internal
|
|
235
|
+
*/
|
|
197
236
|
constructor(parser,
|
|
198
|
-
|
|
237
|
+
/**
|
|
238
|
+
The current editor state.
|
|
239
|
+
*/
|
|
199
240
|
state,
|
|
200
|
-
|
|
241
|
+
/**
|
|
242
|
+
Tree fragments that can be reused by incremental re-parses.
|
|
243
|
+
*/
|
|
201
244
|
fragments = [],
|
|
202
|
-
|
|
245
|
+
/**
|
|
246
|
+
@internal
|
|
247
|
+
*/
|
|
203
248
|
tree,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
249
|
+
/**
|
|
250
|
+
The current editor viewport (or some overapproximation
|
|
251
|
+
thereof). Intended to be used for opportunistically avoiding
|
|
252
|
+
work (in which case
|
|
253
|
+
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.EditorParseContext.skipUntilInView)
|
|
254
|
+
should be called to make sure the parser is restarted when the
|
|
255
|
+
skipped region becomes visible).
|
|
256
|
+
*/
|
|
210
257
|
viewport,
|
|
211
|
-
|
|
212
|
-
|
|
258
|
+
/**
|
|
259
|
+
@internal
|
|
260
|
+
*/
|
|
261
|
+
skipped,
|
|
262
|
+
/**
|
|
263
|
+
This is where skipping parsers can register a promise that,
|
|
264
|
+
when resolved, will schedule a new parse. It is cleared when
|
|
265
|
+
the parse worker picks up the promise. @internal
|
|
266
|
+
*/
|
|
267
|
+
scheduleOn) {
|
|
213
268
|
this.parser = parser;
|
|
214
269
|
this.state = state;
|
|
215
270
|
this.fragments = fragments;
|
|
216
271
|
this.tree = tree;
|
|
217
272
|
this.viewport = viewport;
|
|
218
273
|
this.skipped = skipped;
|
|
274
|
+
this.scheduleOn = scheduleOn;
|
|
219
275
|
this.parse = null;
|
|
220
|
-
|
|
276
|
+
/**
|
|
277
|
+
@internal
|
|
278
|
+
*/
|
|
221
279
|
this.tempSkipped = [];
|
|
222
280
|
}
|
|
223
|
-
|
|
281
|
+
/**
|
|
282
|
+
@internal
|
|
283
|
+
*/
|
|
224
284
|
work(time, upto) {
|
|
225
285
|
if (this.tree != lezerTree.Tree.empty && (upto == null ? this.tree.length == this.state.doc.length : this.tree.length >= upto)) {
|
|
226
286
|
this.takeTree();
|
|
@@ -245,7 +305,9 @@ class EditorParseContext {
|
|
|
245
305
|
return false;
|
|
246
306
|
}
|
|
247
307
|
}
|
|
248
|
-
|
|
308
|
+
/**
|
|
309
|
+
@internal
|
|
310
|
+
*/
|
|
249
311
|
takeTree() {
|
|
250
312
|
if (this.parse && this.parse.pos > this.tree.length) {
|
|
251
313
|
this.tree = this.parse.forceFinish();
|
|
@@ -257,7 +319,9 @@ class EditorParseContext {
|
|
|
257
319
|
fragments = cutFragments(fragments, r.from, r.to);
|
|
258
320
|
return fragments;
|
|
259
321
|
}
|
|
260
|
-
|
|
322
|
+
/**
|
|
323
|
+
@internal
|
|
324
|
+
*/
|
|
261
325
|
changes(changes, newState) {
|
|
262
326
|
let { fragments, tree, viewport, skipped } = this;
|
|
263
327
|
this.takeTree();
|
|
@@ -276,9 +340,11 @@ class EditorParseContext {
|
|
|
276
340
|
}
|
|
277
341
|
}
|
|
278
342
|
}
|
|
279
|
-
return new EditorParseContext(this.parser, newState, fragments, tree, viewport, skipped);
|
|
343
|
+
return new EditorParseContext(this.parser, newState, fragments, tree, viewport, skipped, this.scheduleOn);
|
|
280
344
|
}
|
|
281
|
-
|
|
345
|
+
/**
|
|
346
|
+
@internal
|
|
347
|
+
*/
|
|
282
348
|
updateViewport(viewport) {
|
|
283
349
|
this.viewport = viewport;
|
|
284
350
|
let startLen = this.skipped.length;
|
|
@@ -291,40 +357,61 @@ class EditorParseContext {
|
|
|
291
357
|
}
|
|
292
358
|
return this.skipped.length < startLen;
|
|
293
359
|
}
|
|
294
|
-
|
|
360
|
+
/**
|
|
361
|
+
@internal
|
|
362
|
+
*/
|
|
295
363
|
reset() {
|
|
296
364
|
if (this.parse) {
|
|
297
365
|
this.takeTree();
|
|
298
366
|
this.parse = null;
|
|
299
367
|
}
|
|
300
368
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
369
|
+
/**
|
|
370
|
+
Notify the parse scheduler that the given region was skipped
|
|
371
|
+
because it wasn't in view, and the parse should be restarted
|
|
372
|
+
when it comes into view.
|
|
373
|
+
*/
|
|
304
374
|
skipUntilInView(from, to) {
|
|
305
375
|
this.skipped.push({ from, to });
|
|
306
376
|
}
|
|
307
|
-
|
|
377
|
+
/**
|
|
378
|
+
Returns a parser intended to be used as placeholder when
|
|
379
|
+
asynchronously loading a nested parser. It'll skip its input and
|
|
380
|
+
mark it as not-really-parsed, so that the next update will parse
|
|
381
|
+
it again.
|
|
382
|
+
|
|
383
|
+
When `until` is given, a reparse will be scheduled when that
|
|
384
|
+
promise resolves.
|
|
385
|
+
*/
|
|
386
|
+
static getSkippingParser(until) {
|
|
387
|
+
return {
|
|
388
|
+
startParse(input, startPos, context) {
|
|
389
|
+
return {
|
|
390
|
+
pos: startPos,
|
|
391
|
+
advance() {
|
|
392
|
+
let ecx = context;
|
|
393
|
+
ecx.tempSkipped.push({ from: startPos, to: input.length });
|
|
394
|
+
if (until)
|
|
395
|
+
ecx.scheduleOn = ecx.scheduleOn ? Promise.all([ecx.scheduleOn, until]) : until;
|
|
396
|
+
this.pos = input.length;
|
|
397
|
+
return new lezerTree.Tree(lezerTree.NodeType.none, [], [], input.length - startPos);
|
|
398
|
+
},
|
|
399
|
+
forceFinish() { return this.advance(); }
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
@internal
|
|
406
|
+
*/
|
|
308
407
|
movedPast(pos) {
|
|
309
408
|
return this.tree.length < pos && this.parse && this.parse.pos >= pos;
|
|
310
409
|
}
|
|
311
410
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
EditorParseContext.skippingParser =
|
|
316
|
-
startParse(input, startPos, context) {
|
|
317
|
-
return {
|
|
318
|
-
pos: startPos,
|
|
319
|
-
advance() {
|
|
320
|
-
context.tempSkipped.push({ from: startPos, to: input.length });
|
|
321
|
-
this.pos = input.length;
|
|
322
|
-
return new lezerTree.Tree(lezerTree.NodeType.none, [], [], input.length - startPos);
|
|
323
|
-
},
|
|
324
|
-
forceFinish() { return this.advance(); }
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
};
|
|
411
|
+
/**
|
|
412
|
+
FIXME backwards compatible shim, remove on next major @internal
|
|
413
|
+
*/
|
|
414
|
+
EditorParseContext.skippingParser = EditorParseContext.getSkippingParser();
|
|
328
415
|
function cutFragments(fragments, from, to) {
|
|
329
416
|
return lezerTree.TreeFragment.applyChanges(fragments, [{ fromA: from, toA: to, fromB: from, toB: to }]);
|
|
330
417
|
}
|
|
@@ -350,7 +437,7 @@ class LanguageState {
|
|
|
350
437
|
return new LanguageState(newCx);
|
|
351
438
|
}
|
|
352
439
|
static init(state) {
|
|
353
|
-
let parseState = new EditorParseContext(state.facet(language).parser, state, [], lezerTree.Tree.empty, { from: 0, to: state.doc.length }, []);
|
|
440
|
+
let parseState = new EditorParseContext(state.facet(language).parser, state, [], lezerTree.Tree.empty, { from: 0, to: state.doc.length }, [], null);
|
|
354
441
|
if (!parseState.work(25 /* Apply */))
|
|
355
442
|
parseState.takeTree();
|
|
356
443
|
return new LanguageState(parseState);
|
|
@@ -382,8 +469,8 @@ const parseWorker = view.ViewPlugin.fromClass(class ParseWorker {
|
|
|
382
469
|
this.scheduleWork();
|
|
383
470
|
}
|
|
384
471
|
update(update) {
|
|
472
|
+
let cx = this.view.state.field(Language.state).context;
|
|
385
473
|
if (update.viewportChanged) {
|
|
386
|
-
let cx = this.view.state.field(Language.state).context;
|
|
387
474
|
if (cx.updateViewport(update.view.viewport))
|
|
388
475
|
cx.reset();
|
|
389
476
|
if (this.view.viewport.to > cx.tree.length)
|
|
@@ -394,12 +481,13 @@ const parseWorker = view.ViewPlugin.fromClass(class ParseWorker {
|
|
|
394
481
|
this.chunkBudget += 50 /* ChangeBonus */;
|
|
395
482
|
this.scheduleWork();
|
|
396
483
|
}
|
|
484
|
+
this.checkAsyncSchedule(cx);
|
|
397
485
|
}
|
|
398
486
|
scheduleWork() {
|
|
399
487
|
if (this.working > -1)
|
|
400
488
|
return;
|
|
401
|
-
let { state } = this.view, field = state.field(Language.state);
|
|
402
|
-
if (field.tree.length >= state.doc.length)
|
|
489
|
+
let { state } = this.view, field = state.field(Language.state), frags = field.context.fragments;
|
|
490
|
+
if (field.tree.length >= state.doc.length && frags.length && frags[0].from == 0 && frags[0].to >= state.doc.length)
|
|
403
491
|
return;
|
|
404
492
|
this.working = requestIdle(this.work, { timeout: 500 /* Pause */ });
|
|
405
493
|
}
|
|
@@ -424,6 +512,13 @@ const parseWorker = view.ViewPlugin.fromClass(class ParseWorker {
|
|
|
424
512
|
}
|
|
425
513
|
if (!done && this.chunkBudget > 0)
|
|
426
514
|
this.scheduleWork();
|
|
515
|
+
this.checkAsyncSchedule(field.context);
|
|
516
|
+
}
|
|
517
|
+
checkAsyncSchedule(cx) {
|
|
518
|
+
if (cx.scheduleOn) {
|
|
519
|
+
cx.scheduleOn.then(() => this.scheduleWork());
|
|
520
|
+
cx.scheduleOn = null;
|
|
521
|
+
}
|
|
427
522
|
}
|
|
428
523
|
destroy() {
|
|
429
524
|
if (this.working >= 0)
|
|
@@ -432,70 +527,98 @@ const parseWorker = view.ViewPlugin.fromClass(class ParseWorker {
|
|
|
432
527
|
}, {
|
|
433
528
|
eventHandlers: { focus() { this.scheduleWork(); } }
|
|
434
529
|
});
|
|
435
|
-
|
|
530
|
+
/**
|
|
531
|
+
The facet used to associate a language with an editor state.
|
|
532
|
+
*/
|
|
436
533
|
const language = state.Facet.define({
|
|
437
534
|
combine(languages) { return languages.length ? languages[0] : null; },
|
|
438
535
|
enables: [Language.state, parseWorker]
|
|
439
536
|
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
537
|
+
/**
|
|
538
|
+
This class bundles a [language object](https://codemirror.net/6/docs/ref/#language.Language) with an
|
|
539
|
+
optional set of supporting extensions. Language packages are
|
|
540
|
+
encouraged to export a function that optionally takes a
|
|
541
|
+
configuration object and returns a `LanguageSupport` instance, as
|
|
542
|
+
the main way for client code to use the package.
|
|
543
|
+
*/
|
|
445
544
|
class LanguageSupport {
|
|
446
|
-
|
|
545
|
+
/**
|
|
546
|
+
Create a support object.
|
|
547
|
+
*/
|
|
447
548
|
constructor(
|
|
448
|
-
|
|
549
|
+
/**
|
|
550
|
+
The language object.
|
|
551
|
+
*/
|
|
449
552
|
language,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
553
|
+
/**
|
|
554
|
+
An optional set of supporting extensions. When nesting a
|
|
555
|
+
language in another language, the outer language is encouraged
|
|
556
|
+
to include the supporting extensions for its inner languages
|
|
557
|
+
in its own set of support extensions.
|
|
558
|
+
*/
|
|
454
559
|
support = []) {
|
|
455
560
|
this.language = language;
|
|
456
561
|
this.support = support;
|
|
457
562
|
this.extension = [language, support];
|
|
458
563
|
}
|
|
459
564
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
565
|
+
/**
|
|
566
|
+
Language descriptions are used to store metadata about languages
|
|
567
|
+
and to dynamically load them. Their main role is finding the
|
|
568
|
+
appropriate language for a filename or dynamically loading nested
|
|
569
|
+
parsers.
|
|
570
|
+
*/
|
|
464
571
|
class LanguageDescription {
|
|
465
572
|
constructor(
|
|
466
|
-
|
|
573
|
+
/**
|
|
574
|
+
The name of this language.
|
|
575
|
+
*/
|
|
467
576
|
name,
|
|
468
|
-
|
|
577
|
+
/**
|
|
578
|
+
Alternative names for the mode (lowercased, includes `this.name`).
|
|
579
|
+
*/
|
|
469
580
|
alias,
|
|
470
|
-
|
|
581
|
+
/**
|
|
582
|
+
File extensions associated with this language.
|
|
583
|
+
*/
|
|
471
584
|
extensions,
|
|
472
|
-
|
|
473
|
-
|
|
585
|
+
/**
|
|
586
|
+
Optional filename pattern that should be associated with this
|
|
587
|
+
language.
|
|
588
|
+
*/
|
|
474
589
|
filename, loadFunc) {
|
|
475
590
|
this.name = name;
|
|
476
591
|
this.alias = alias;
|
|
477
592
|
this.extensions = extensions;
|
|
478
593
|
this.filename = filename;
|
|
479
594
|
this.loadFunc = loadFunc;
|
|
480
|
-
|
|
595
|
+
/**
|
|
596
|
+
If the language has been loaded, this will hold its value.
|
|
597
|
+
*/
|
|
481
598
|
this.support = undefined;
|
|
482
599
|
this.loading = null;
|
|
483
600
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
601
|
+
/**
|
|
602
|
+
Start loading the the language. Will return a promise that
|
|
603
|
+
resolves to a [`LanguageSupport`](https://codemirror.net/6/docs/ref/#language.LanguageSupport)
|
|
604
|
+
object when the language successfully loads.
|
|
605
|
+
*/
|
|
487
606
|
load() {
|
|
488
607
|
return this.loading || (this.loading = this.loadFunc().then(support => this.support = support, err => { this.loading = null; throw err; }));
|
|
489
608
|
}
|
|
490
|
-
|
|
609
|
+
/**
|
|
610
|
+
Create a language description.
|
|
611
|
+
*/
|
|
491
612
|
static of(spec) {
|
|
492
613
|
return new LanguageDescription(spec.name, (spec.alias || []).concat(spec.name).map(s => s.toLowerCase()), spec.extensions || [], spec.filename, spec.load);
|
|
493
614
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
615
|
+
/**
|
|
616
|
+
Look for a language in the given array of descriptions that
|
|
617
|
+
matches the filename. Will first match
|
|
618
|
+
[`filename`](https://codemirror.net/6/docs/ref/#language.LanguageDescription.filename) patterns,
|
|
619
|
+
and then [extensions](https://codemirror.net/6/docs/ref/#language.LanguageDescription.extensions),
|
|
620
|
+
and return the first language that matches.
|
|
621
|
+
*/
|
|
499
622
|
static matchFilename(descs, filename) {
|
|
500
623
|
for (let d of descs)
|
|
501
624
|
if (d.filename && d.filename.test(filename))
|
|
@@ -507,11 +630,13 @@ class LanguageDescription {
|
|
|
507
630
|
return d;
|
|
508
631
|
return null;
|
|
509
632
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
633
|
+
/**
|
|
634
|
+
Look for a language whose name or alias matches the the given
|
|
635
|
+
name (case-insensitively). If `fuzzy` is true, and no direct
|
|
636
|
+
matchs is found, this'll also search for a language whose name
|
|
637
|
+
or alias occurs in the string (for names shorter than three
|
|
638
|
+
characters, only when surrounded by non-word characters).
|
|
639
|
+
*/
|
|
515
640
|
static matchLanguageName(descs, name, fuzzy = true) {
|
|
516
641
|
name = name.toLowerCase();
|
|
517
642
|
for (let d of descs)
|
|
@@ -528,13 +653,17 @@ class LanguageDescription {
|
|
|
528
653
|
}
|
|
529
654
|
}
|
|
530
655
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
656
|
+
/**
|
|
657
|
+
Facet that defines a way to provide a function that computes the
|
|
658
|
+
appropriate indentation depth at the start of a given line, or
|
|
659
|
+
`null` to indicate no appropriate indentation could be determined.
|
|
660
|
+
*/
|
|
534
661
|
const indentService = state.Facet.define();
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
662
|
+
/**
|
|
663
|
+
Facet for overriding the unit by which indentation happens.
|
|
664
|
+
Should be a string consisting either entirely of spaces or
|
|
665
|
+
entirely of tabs. When not set, this defaults to 2 spaces.
|
|
666
|
+
*/
|
|
538
667
|
const indentUnit = state.Facet.define({
|
|
539
668
|
combine: values => {
|
|
540
669
|
if (!values.length)
|
|
@@ -544,18 +673,22 @@ const indentUnit = state.Facet.define({
|
|
|
544
673
|
return values[0];
|
|
545
674
|
}
|
|
546
675
|
});
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
676
|
+
/**
|
|
677
|
+
Return the _column width_ of an indent unit in the state.
|
|
678
|
+
Determined by the [`indentUnit`](https://codemirror.net/6/docs/ref/#language.indentUnit)
|
|
679
|
+
facet, and [`tabSize`](https://codemirror.net/6/docs/ref/#state.EditorState^tabSize) when that
|
|
680
|
+
contains tabs.
|
|
681
|
+
*/
|
|
551
682
|
function getIndentUnit(state) {
|
|
552
683
|
let unit = state.facet(indentUnit);
|
|
553
684
|
return unit.charCodeAt(0) == 9 ? state.tabSize * unit.length : unit.length;
|
|
554
685
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
686
|
+
/**
|
|
687
|
+
Create an indentation string that covers columns 0 to `cols`.
|
|
688
|
+
Will use tabs for as much of the columns as possible when the
|
|
689
|
+
[`indentUnit`](https://codemirror.net/6/docs/ref/#language.indentUnit) facet contains
|
|
690
|
+
tabs.
|
|
691
|
+
*/
|
|
559
692
|
function indentString(state, cols) {
|
|
560
693
|
let result = "", ts = state.tabSize;
|
|
561
694
|
if (state.facet(indentUnit).charCodeAt(0) == 9)
|
|
@@ -567,12 +700,14 @@ function indentString(state, cols) {
|
|
|
567
700
|
result += " ";
|
|
568
701
|
return result;
|
|
569
702
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
703
|
+
/**
|
|
704
|
+
Get the indentation at the given position. Will first consult any
|
|
705
|
+
[indent services](https://codemirror.net/6/docs/ref/#language.indentService) that are registered,
|
|
706
|
+
and if none of those return an indentation, this will check the
|
|
707
|
+
syntax tree for the [indent node prop](https://codemirror.net/6/docs/ref/#language.indentNodeProp)
|
|
708
|
+
and use that if found. Returns a number when an indentation could
|
|
709
|
+
be determined, and null otherwise.
|
|
710
|
+
*/
|
|
576
711
|
function getIndentation(context, pos) {
|
|
577
712
|
if (context instanceof state.EditorState)
|
|
578
713
|
context = new IndentContext(context);
|
|
@@ -584,23 +719,33 @@ function getIndentation(context, pos) {
|
|
|
584
719
|
let tree = syntaxTree(context.state);
|
|
585
720
|
return tree ? syntaxIndentation(context, tree, pos) : null;
|
|
586
721
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
722
|
+
/**
|
|
723
|
+
Indentation contexts are used when calling [indentation
|
|
724
|
+
services](https://codemirror.net/6/docs/ref/#language.indentService). They provide helper utilities
|
|
725
|
+
useful in indentation logic, and can selectively override the
|
|
726
|
+
indentation reported for some lines.
|
|
727
|
+
*/
|
|
591
728
|
class IndentContext {
|
|
592
|
-
|
|
729
|
+
/**
|
|
730
|
+
Create an indent context.
|
|
731
|
+
*/
|
|
593
732
|
constructor(
|
|
594
|
-
|
|
733
|
+
/**
|
|
734
|
+
The editor state.
|
|
735
|
+
*/
|
|
595
736
|
state,
|
|
596
|
-
|
|
737
|
+
/**
|
|
738
|
+
@internal
|
|
739
|
+
*/
|
|
597
740
|
options = {}) {
|
|
598
741
|
this.state = state;
|
|
599
742
|
this.options = options;
|
|
600
743
|
this.unit = getIndentUnit(state);
|
|
601
744
|
}
|
|
602
|
-
|
|
603
|
-
|
|
745
|
+
/**
|
|
746
|
+
Get the text directly after `pos`, either the entire line
|
|
747
|
+
or the next 100 characters, whichever is shorter.
|
|
748
|
+
*/
|
|
604
749
|
textAfterPos(pos) {
|
|
605
750
|
var _a, _b;
|
|
606
751
|
let sim = (_a = this.options) === null || _a === void 0 ? void 0 : _a.simulateBreak;
|
|
@@ -608,7 +753,9 @@ class IndentContext {
|
|
|
608
753
|
return "";
|
|
609
754
|
return this.state.sliceDoc(pos, Math.min(pos + 100, sim != null && sim > pos ? sim : 1e9, this.state.doc.lineAt(pos).to));
|
|
610
755
|
}
|
|
611
|
-
|
|
756
|
+
/**
|
|
757
|
+
Find the column for the given position.
|
|
758
|
+
*/
|
|
612
759
|
column(pos) {
|
|
613
760
|
var _a;
|
|
614
761
|
let line = this.state.doc.lineAt(pos), text = line.text.slice(0, pos - line.from);
|
|
@@ -618,12 +765,16 @@ class IndentContext {
|
|
|
618
765
|
result += override - this.countColumn(text, text.search(/\S/));
|
|
619
766
|
return result;
|
|
620
767
|
}
|
|
621
|
-
|
|
622
|
-
|
|
768
|
+
/**
|
|
769
|
+
find the column position (taking tabs into account) of the given
|
|
770
|
+
position in the given string.
|
|
771
|
+
*/
|
|
623
772
|
countColumn(line, pos) {
|
|
624
773
|
return text.countColumn(pos < 0 ? line : line.slice(0, pos), 0, this.state.tabSize);
|
|
625
774
|
}
|
|
626
|
-
|
|
775
|
+
/**
|
|
776
|
+
Find the indentation column of the given document line.
|
|
777
|
+
*/
|
|
627
778
|
lineIndent(line) {
|
|
628
779
|
var _a;
|
|
629
780
|
let override = (_a = this.options) === null || _a === void 0 ? void 0 : _a.overrideIndentation;
|
|
@@ -635,10 +786,12 @@ class IndentContext {
|
|
|
635
786
|
return this.countColumn(line.text, line.text.search(/\S/));
|
|
636
787
|
}
|
|
637
788
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
789
|
+
/**
|
|
790
|
+
A syntax tree node prop used to associate indentation strategies
|
|
791
|
+
with node types. Such a strategy is a function from an indentation
|
|
792
|
+
context to a column number or null, where null indicates that no
|
|
793
|
+
definitive indentation can be determined.
|
|
794
|
+
*/
|
|
642
795
|
const indentNodeProp = new lezerTree.NodeProp();
|
|
643
796
|
// Compute the indentation for a given position from the syntax tree.
|
|
644
797
|
function syntaxIndentation(cx, ast, pos) {
|
|
@@ -659,12 +812,7 @@ function syntaxIndentation(cx, ast, pos) {
|
|
|
659
812
|
scanPos = scan.to + 1;
|
|
660
813
|
}
|
|
661
814
|
}
|
|
662
|
-
|
|
663
|
-
let strategy = indentStrategy(tree);
|
|
664
|
-
if (strategy)
|
|
665
|
-
return strategy(new TreeIndentContext(cx, pos, tree));
|
|
666
|
-
}
|
|
667
|
-
return null;
|
|
815
|
+
return indentFrom(tree, pos, cx);
|
|
668
816
|
}
|
|
669
817
|
function ignoreClosed(cx) {
|
|
670
818
|
var _a, _b;
|
|
@@ -681,31 +829,52 @@ function indentStrategy(tree) {
|
|
|
681
829
|
}
|
|
682
830
|
return tree.parent == null ? topIndent : null;
|
|
683
831
|
}
|
|
832
|
+
function indentFrom(node, pos, base) {
|
|
833
|
+
for (; node; node = node.parent) {
|
|
834
|
+
let strategy = indentStrategy(node);
|
|
835
|
+
if (strategy)
|
|
836
|
+
return strategy(new TreeIndentContext(base, pos, node));
|
|
837
|
+
}
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
684
840
|
function topIndent() { return 0; }
|
|
685
|
-
|
|
686
|
-
|
|
841
|
+
/**
|
|
842
|
+
Objects of this type provide context information and helper
|
|
843
|
+
methods to indentation functions.
|
|
844
|
+
*/
|
|
687
845
|
class TreeIndentContext extends IndentContext {
|
|
688
|
-
|
|
846
|
+
/**
|
|
847
|
+
@internal
|
|
848
|
+
*/
|
|
689
849
|
constructor(base,
|
|
690
|
-
|
|
850
|
+
/**
|
|
851
|
+
The position at which indentation is being computed.
|
|
852
|
+
*/
|
|
691
853
|
pos,
|
|
692
|
-
|
|
693
|
-
|
|
854
|
+
/**
|
|
855
|
+
The syntax tree node to which the indentation strategy
|
|
856
|
+
applies.
|
|
857
|
+
*/
|
|
694
858
|
node) {
|
|
695
859
|
super(base.state, base.options);
|
|
860
|
+
this.base = base;
|
|
696
861
|
this.pos = pos;
|
|
697
862
|
this.node = node;
|
|
698
863
|
}
|
|
699
|
-
|
|
700
|
-
|
|
864
|
+
/**
|
|
865
|
+
Get the text directly after `this.pos`, either the entire line
|
|
866
|
+
or the next 100 characters, whichever is shorter.
|
|
867
|
+
*/
|
|
701
868
|
get textAfter() {
|
|
702
869
|
return this.textAfterPos(this.pos);
|
|
703
870
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
871
|
+
/**
|
|
872
|
+
Get the indentation at the reference line for `this.node`, which
|
|
873
|
+
is the line on which it starts, unless there is a node that is
|
|
874
|
+
_not_ a parent of this node covering the start of that line. If
|
|
875
|
+
so, the line at the start of that node is tried, again skipping
|
|
876
|
+
on if it is covered by another such node.
|
|
877
|
+
*/
|
|
709
878
|
get baseIndent() {
|
|
710
879
|
let line = this.state.doc.lineAt(this.node.from);
|
|
711
880
|
// Skip line starts that are covered by a sibling (or cousin, etc)
|
|
@@ -719,6 +888,14 @@ class TreeIndentContext extends IndentContext {
|
|
|
719
888
|
}
|
|
720
889
|
return this.lineIndent(line);
|
|
721
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
Continue looking for indentations in the node's parent nodes,
|
|
893
|
+
and return the result of that.
|
|
894
|
+
*/
|
|
895
|
+
continue() {
|
|
896
|
+
let parent = this.node.parent;
|
|
897
|
+
return parent ? indentFrom(parent, this.pos, this.base) : 0;
|
|
898
|
+
}
|
|
722
899
|
}
|
|
723
900
|
function isParent(parent, of) {
|
|
724
901
|
for (let cur = of; cur; cur = cur.parent)
|
|
@@ -747,15 +924,17 @@ function bracketedAligned(context) {
|
|
|
747
924
|
pos = next.to;
|
|
748
925
|
}
|
|
749
926
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
927
|
+
/**
|
|
928
|
+
An indentation strategy for delimited (usually bracketed) nodes.
|
|
929
|
+
Will, by default, indent one unit more than the parent's base
|
|
930
|
+
indent unless the line starts with a closing token. When `align`
|
|
931
|
+
is true and there are non-skipped nodes on the node's opening
|
|
932
|
+
line, the content of the node will be aligned with the end of the
|
|
933
|
+
opening node, like this:
|
|
934
|
+
|
|
935
|
+
foo(bar,
|
|
936
|
+
baz)
|
|
937
|
+
*/
|
|
759
938
|
function delimitedIndent({ closing, align = true, units = 1 }) {
|
|
760
939
|
return (context) => delimitedStrategy(context, align, units, closing);
|
|
761
940
|
}
|
|
@@ -767,15 +946,19 @@ function delimitedStrategy(context, align, units, closing, closedAt) {
|
|
|
767
946
|
return closed ? context.column(aligned.from) : context.column(aligned.to);
|
|
768
947
|
return context.baseIndent + (closed ? 0 : context.unit * units);
|
|
769
948
|
}
|
|
770
|
-
|
|
771
|
-
|
|
949
|
+
/**
|
|
950
|
+
An indentation strategy that aligns a node's content to its base
|
|
951
|
+
indentation.
|
|
952
|
+
*/
|
|
772
953
|
const flatIndent = (context) => context.baseIndent;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
954
|
+
/**
|
|
955
|
+
Creates an indentation strategy that, by default, indents
|
|
956
|
+
continued lines one unit more than the node's base indentation.
|
|
957
|
+
You can provide `except` to prevent indentation of lines that
|
|
958
|
+
match a pattern (for example `/^else\b/` in `if`/`else`
|
|
959
|
+
constructs), and you can change the amount of units used with the
|
|
960
|
+
`units` option.
|
|
961
|
+
*/
|
|
779
962
|
function continuedIndent({ except, units = 1 } = {}) {
|
|
780
963
|
return (context) => {
|
|
781
964
|
let matchExcept = except && except.test(context.textAfter);
|
|
@@ -783,17 +966,19 @@ function continuedIndent({ except, units = 1 } = {}) {
|
|
|
783
966
|
};
|
|
784
967
|
}
|
|
785
968
|
const DontIndentBeyond = 200;
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
969
|
+
/**
|
|
970
|
+
Enables reindentation on input. When a language defines an
|
|
971
|
+
`indentOnInput` field in its [language
|
|
972
|
+
data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt), which must hold a regular
|
|
973
|
+
expression, the line at the cursor will be reindented whenever new
|
|
974
|
+
text is typed and the input from the start of the line up to the
|
|
975
|
+
cursor matches that regexp.
|
|
976
|
+
|
|
977
|
+
To avoid unneccesary reindents, it is recommended to start the
|
|
978
|
+
regexp with `^` (usually followed by `\s*`), and end it with `$`.
|
|
979
|
+
For example, `/^\s*\}$/` will reindent when a closing brace is
|
|
980
|
+
added at the start of a line.
|
|
981
|
+
*/
|
|
797
982
|
function indentOnInput() {
|
|
798
983
|
return state.EditorState.transactionFilter.of(tr => {
|
|
799
984
|
if (!tr.docChanged || tr.annotation(state.Transaction.userEvent) != "input")
|
|
@@ -825,16 +1010,29 @@ function indentOnInput() {
|
|
|
825
1010
|
});
|
|
826
1011
|
}
|
|
827
1012
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1013
|
+
/**
|
|
1014
|
+
A facet that registers a code folding service. When called with
|
|
1015
|
+
the extent of a line, such a function should return a foldable
|
|
1016
|
+
range that starts on that line (but continues beyond it), if one
|
|
1017
|
+
can be found.
|
|
1018
|
+
*/
|
|
832
1019
|
const foldService = state.Facet.define();
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1020
|
+
/**
|
|
1021
|
+
This node prop is used to associate folding information with
|
|
1022
|
+
syntax node types. Given a syntax node, it should check whether
|
|
1023
|
+
that tree is foldable and return the range that can be collapsed
|
|
1024
|
+
when it is.
|
|
1025
|
+
*/
|
|
837
1026
|
const foldNodeProp = new lezerTree.NodeProp();
|
|
1027
|
+
/**
|
|
1028
|
+
[Fold](https://codemirror.net/6/docs/ref/#language.foldNodeProp) function that folds everything but
|
|
1029
|
+
the first and the last child of a syntax node. Useful for nodes
|
|
1030
|
+
that start and end with delimiters.
|
|
1031
|
+
*/
|
|
1032
|
+
function foldInside(node) {
|
|
1033
|
+
let first = node.firstChild, last = node.lastChild;
|
|
1034
|
+
return first && first.to < last.from ? { from: first.to, to: last.type.isError ? node.to : last.from } : null;
|
|
1035
|
+
}
|
|
838
1036
|
function syntaxFolding(state, start, end) {
|
|
839
1037
|
let tree = syntaxTree(state);
|
|
840
1038
|
if (tree.length == 0)
|
|
@@ -855,12 +1053,14 @@ function syntaxFolding(state, start, end) {
|
|
|
855
1053
|
}
|
|
856
1054
|
return found;
|
|
857
1055
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1056
|
+
/**
|
|
1057
|
+
Check whether the given line is foldable. First asks any fold
|
|
1058
|
+
services registered through
|
|
1059
|
+
[`foldService`](https://codemirror.net/6/docs/ref/#language.foldService), and if none of them return
|
|
1060
|
+
a result, tries to query the [fold node
|
|
1061
|
+
prop](https://codemirror.net/6/docs/ref/#language.foldNodeProp) of syntax nodes that cover the end
|
|
1062
|
+
of the line.
|
|
1063
|
+
*/
|
|
864
1064
|
function foldable(state, lineStart, lineEnd) {
|
|
865
1065
|
for (let service of state.facet(foldService)) {
|
|
866
1066
|
let result = service(state, lineStart, lineEnd);
|
|
@@ -882,6 +1082,7 @@ exports.defineLanguageFacet = defineLanguageFacet;
|
|
|
882
1082
|
exports.delimitedIndent = delimitedIndent;
|
|
883
1083
|
exports.ensureSyntaxTree = ensureSyntaxTree;
|
|
884
1084
|
exports.flatIndent = flatIndent;
|
|
1085
|
+
exports.foldInside = foldInside;
|
|
885
1086
|
exports.foldNodeProp = foldNodeProp;
|
|
886
1087
|
exports.foldService = foldService;
|
|
887
1088
|
exports.foldable = foldable;
|