@codemirror/language 0.19.0 → 0.19.4
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 +28 -0
- package/dist/index.cjs +36 -40
- package/dist/index.d.ts +9 -3
- package/dist/index.js +37 -41
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
## 0.19.4 (2021-11-13)
|
|
2
|
+
|
|
3
|
+
### New features
|
|
4
|
+
|
|
5
|
+
`LanguageDescription.of` now takes an optional already-loaded extension.
|
|
6
|
+
|
|
7
|
+
## 0.19.3 (2021-09-13)
|
|
8
|
+
|
|
9
|
+
### Bug fixes
|
|
10
|
+
|
|
11
|
+
Fix an issue where a parse that skipped content with `skipUntilInView` would in some cases not be restarted when the range came into view.
|
|
12
|
+
|
|
13
|
+
## 0.19.2 (2021-08-11)
|
|
14
|
+
|
|
15
|
+
### Bug fixes
|
|
16
|
+
|
|
17
|
+
Fix a bug that caused `indentOnInput` to fire for the wrong kinds of transactions.
|
|
18
|
+
|
|
19
|
+
Fix a bug that could cause `indentOnInput` to apply its changes incorrectly.
|
|
20
|
+
|
|
21
|
+
## 0.19.1 (2021-08-11)
|
|
22
|
+
|
|
23
|
+
### Bug fixes
|
|
24
|
+
|
|
25
|
+
Fix incorrect versions for @lezer dependencies.
|
|
26
|
+
|
|
1
27
|
## 0.19.0 (2021-08-11)
|
|
2
28
|
|
|
3
29
|
### Breaking changes
|
|
@@ -10,6 +36,8 @@ CodeMirror now uses lezer 0.15, which means different package names (scoped with
|
|
|
10
36
|
|
|
11
37
|
`LezerLanguage` was renamed to `LRLanguage` (because all languages must emit Lezer-style trees, the name was misleading).
|
|
12
38
|
|
|
39
|
+
`Language.parseString` no longer exists. You can just call `.parser.parse(...)` instead.
|
|
40
|
+
|
|
13
41
|
### New features
|
|
14
42
|
|
|
15
43
|
New `IndentContext.lineAt` method to access lines in a way that is aware of simulated line breaks.
|
package/dist/index.cjs
CHANGED
|
@@ -233,7 +233,7 @@ class ParseContext {
|
|
|
233
233
|
The current editor viewport (or some overapproximation
|
|
234
234
|
thereof). Intended to be used for opportunistically avoiding
|
|
235
235
|
work (in which case
|
|
236
|
-
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.
|
|
236
|
+
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.ParseContext.skipUntilInView)
|
|
237
237
|
should be called to make sure the parser is restarted when the
|
|
238
238
|
skipped region becomes visible).
|
|
239
239
|
*/
|
|
@@ -271,7 +271,7 @@ class ParseContext {
|
|
|
271
271
|
work(time, upto) {
|
|
272
272
|
if (upto != null && upto >= this.state.doc.length)
|
|
273
273
|
upto = undefined;
|
|
274
|
-
if (this.tree != common.Tree.empty && (upto
|
|
274
|
+
if (this.tree != common.Tree.empty && this.isDone(upto !== null && upto !== void 0 ? upto : this.state.doc.length)) {
|
|
275
275
|
this.takeTree();
|
|
276
276
|
return true;
|
|
277
277
|
}
|
|
@@ -357,6 +357,8 @@ class ParseContext {
|
|
|
357
357
|
@internal
|
|
358
358
|
*/
|
|
359
359
|
updateViewport(viewport) {
|
|
360
|
+
if (this.viewport.from == viewport.from && this.viewport.to == viewport.to)
|
|
361
|
+
return false;
|
|
360
362
|
this.viewport = viewport;
|
|
361
363
|
let startLen = this.skipped.length;
|
|
362
364
|
for (let i = 0; i < this.skipped.length; i++) {
|
|
@@ -366,7 +368,10 @@ class ParseContext {
|
|
|
366
368
|
this.skipped.splice(i--, 1);
|
|
367
369
|
}
|
|
368
370
|
}
|
|
369
|
-
|
|
371
|
+
if (this.skipped.length >= startLen)
|
|
372
|
+
return false;
|
|
373
|
+
this.reset();
|
|
374
|
+
return true;
|
|
370
375
|
}
|
|
371
376
|
/**
|
|
372
377
|
@internal
|
|
@@ -425,6 +430,13 @@ class ParseContext {
|
|
|
425
430
|
return this.treeLen < pos && this.parse && this.parse.parsedPos >= pos;
|
|
426
431
|
}
|
|
427
432
|
/**
|
|
433
|
+
@internal
|
|
434
|
+
*/
|
|
435
|
+
isDone(upto) {
|
|
436
|
+
let frags = this.fragments;
|
|
437
|
+
return this.treeLen >= upto && frags.length && frags[0].from == 0 && frags[0].to >= upto;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
428
440
|
Get the context for the current parse, or `null` if no editor
|
|
429
441
|
parse is in progress.
|
|
430
442
|
*/
|
|
@@ -488,12 +500,8 @@ const parseWorker = view.ViewPlugin.fromClass(class ParseWorker {
|
|
|
488
500
|
}
|
|
489
501
|
update(update) {
|
|
490
502
|
let cx = this.view.state.field(Language.state).context;
|
|
491
|
-
if (update.
|
|
492
|
-
|
|
493
|
-
cx.reset();
|
|
494
|
-
if (this.view.viewport.to > cx.treeLen)
|
|
495
|
-
this.scheduleWork();
|
|
496
|
-
}
|
|
503
|
+
if (cx.updateViewport(update.view.viewport) || this.view.viewport.to > cx.treeLen)
|
|
504
|
+
this.scheduleWork();
|
|
497
505
|
if (update.docChanged) {
|
|
498
506
|
if (this.view.hasFocus)
|
|
499
507
|
this.chunkBudget += 50 /* ChangeBonus */;
|
|
@@ -504,11 +512,9 @@ const parseWorker = view.ViewPlugin.fromClass(class ParseWorker {
|
|
|
504
512
|
scheduleWork() {
|
|
505
513
|
if (this.working > -1)
|
|
506
514
|
return;
|
|
507
|
-
let { state } = this.view, field = state.field(Language.state)
|
|
508
|
-
if (field.tree
|
|
509
|
-
|
|
510
|
-
return;
|
|
511
|
-
this.working = requestIdle(this.work, { timeout: 500 /* Pause */ });
|
|
515
|
+
let { state } = this.view, field = state.field(Language.state);
|
|
516
|
+
if (field.tree != field.context.tree || !field.context.isDone(state.doc.length))
|
|
517
|
+
this.working = requestIdle(this.work, { timeout: 500 /* Pause */ });
|
|
512
518
|
}
|
|
513
519
|
work(deadline) {
|
|
514
520
|
this.working = -1;
|
|
@@ -605,16 +611,17 @@ class LanguageDescription {
|
|
|
605
611
|
Optional filename pattern that should be associated with this
|
|
606
612
|
language.
|
|
607
613
|
*/
|
|
608
|
-
filename, loadFunc
|
|
614
|
+
filename, loadFunc,
|
|
615
|
+
/**
|
|
616
|
+
If the language has been loaded, this will hold its value.
|
|
617
|
+
*/
|
|
618
|
+
support = undefined) {
|
|
609
619
|
this.name = name;
|
|
610
620
|
this.alias = alias;
|
|
611
621
|
this.extensions = extensions;
|
|
612
622
|
this.filename = filename;
|
|
613
623
|
this.loadFunc = loadFunc;
|
|
614
|
-
|
|
615
|
-
If the language has been loaded, this will hold its value.
|
|
616
|
-
*/
|
|
617
|
-
this.support = undefined;
|
|
624
|
+
this.support = support;
|
|
618
625
|
this.loading = null;
|
|
619
626
|
}
|
|
620
627
|
/**
|
|
@@ -629,7 +636,13 @@ class LanguageDescription {
|
|
|
629
636
|
Create a language description.
|
|
630
637
|
*/
|
|
631
638
|
static of(spec) {
|
|
632
|
-
|
|
639
|
+
let { load, support } = spec;
|
|
640
|
+
if (!load) {
|
|
641
|
+
if (!support)
|
|
642
|
+
throw new RangeError("Must pass either 'load' or 'support' to LanguageDescription.of");
|
|
643
|
+
load = () => Promise.resolve(support);
|
|
644
|
+
}
|
|
645
|
+
return new LanguageDescription(spec.name, (spec.alias || []).concat(spec.name).map(s => s.toLowerCase()), spec.extensions || [], spec.filename, load, support);
|
|
633
646
|
}
|
|
634
647
|
/**
|
|
635
648
|
Look for a language in the given array of descriptions that
|
|
@@ -839,24 +852,7 @@ definitive indentation can be determined.
|
|
|
839
852
|
const indentNodeProp = new common.NodeProp();
|
|
840
853
|
// Compute the indentation for a given position from the syntax tree.
|
|
841
854
|
function syntaxIndentation(cx, ast, pos) {
|
|
842
|
-
|
|
843
|
-
// Enter previous nodes that end in empty error terms, which means
|
|
844
|
-
// they were broken off by error recovery, so that indentation
|
|
845
|
-
// works even if the constructs haven't been finished.
|
|
846
|
-
for (let scan = tree, scanPos = pos;;) {
|
|
847
|
-
let last = scan.childBefore(scanPos);
|
|
848
|
-
if (!last)
|
|
849
|
-
break;
|
|
850
|
-
if (last.type.isError && last.from == last.to) {
|
|
851
|
-
tree = scan;
|
|
852
|
-
scanPos = last.from;
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
scan = last;
|
|
856
|
-
scanPos = scan.to + 1;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
return indentFrom(tree, pos, cx);
|
|
855
|
+
return indentFrom(ast.resolveInner(pos).enterUnfinishedNodesBefore(pos), pos, cx);
|
|
860
856
|
}
|
|
861
857
|
function ignoreClosed(cx) {
|
|
862
858
|
return cx.pos == cx.options.simulateBreak && cx.options.simulateDoubleBreak;
|
|
@@ -1023,7 +1019,7 @@ added at the start of a line.
|
|
|
1023
1019
|
*/
|
|
1024
1020
|
function indentOnInput() {
|
|
1025
1021
|
return state.EditorState.transactionFilter.of(tr => {
|
|
1026
|
-
if (!tr.docChanged || tr.isUserEvent("input.type"))
|
|
1022
|
+
if (!tr.docChanged || !tr.isUserEvent("input.type"))
|
|
1027
1023
|
return tr;
|
|
1028
1024
|
let rules = tr.startState.languageDataAt("indentOnInput", tr.startState.selection.main.head);
|
|
1029
1025
|
if (!rules.length)
|
|
@@ -1048,7 +1044,7 @@ function indentOnInput() {
|
|
|
1048
1044
|
if (cur != norm)
|
|
1049
1045
|
changes.push({ from: line.from, to: line.from + cur.length, insert: norm });
|
|
1050
1046
|
}
|
|
1051
|
-
return changes.length ? [tr, { changes }] : tr;
|
|
1047
|
+
return changes.length ? [tr, { changes, sequential: true }] : tr;
|
|
1052
1048
|
});
|
|
1053
1049
|
}
|
|
1054
1050
|
|
package/dist/index.d.ts
CHANGED
|
@@ -156,7 +156,7 @@ declare class ParseContext {
|
|
|
156
156
|
The current editor viewport (or some overapproximation
|
|
157
157
|
thereof). Intended to be used for opportunistically avoiding
|
|
158
158
|
work (in which case
|
|
159
|
-
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.
|
|
159
|
+
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.ParseContext.skipUntilInView)
|
|
160
160
|
should be called to make sure the parser is restarted when the
|
|
161
161
|
skipped region becomes visible).
|
|
162
162
|
*/
|
|
@@ -298,7 +298,8 @@ declare class LanguageDescription {
|
|
|
298
298
|
*/
|
|
299
299
|
alias?: readonly string[];
|
|
300
300
|
/**
|
|
301
|
-
An optional array of extensions associated with this
|
|
301
|
+
An optional array of filename extensions associated with this
|
|
302
|
+
language.
|
|
302
303
|
*/
|
|
303
304
|
extensions?: readonly string[];
|
|
304
305
|
/**
|
|
@@ -308,7 +309,12 @@ declare class LanguageDescription {
|
|
|
308
309
|
/**
|
|
309
310
|
A function that will asynchronously load the language.
|
|
310
311
|
*/
|
|
311
|
-
load
|
|
312
|
+
load?: () => Promise<LanguageSupport>;
|
|
313
|
+
/**
|
|
314
|
+
Alternatively to `load`, you can provide an already loaded
|
|
315
|
+
support object. Either this or `load` should be provided.
|
|
316
|
+
*/
|
|
317
|
+
support?: LanguageSupport;
|
|
312
318
|
}): LanguageDescription;
|
|
313
319
|
/**
|
|
314
320
|
Look for a language in the given array of descriptions that
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NodeProp, Tree, TreeFragment, Parser, NodeType } from '@lezer/common';
|
|
2
|
-
import {
|
|
2
|
+
import { StateEffect, StateField, Facet, EditorState } from '@codemirror/state';
|
|
3
3
|
import { ViewPlugin } from '@codemirror/view';
|
|
4
4
|
import { countColumn } from '@codemirror/text';
|
|
5
5
|
|
|
@@ -229,7 +229,7 @@ class ParseContext {
|
|
|
229
229
|
The current editor viewport (or some overapproximation
|
|
230
230
|
thereof). Intended to be used for opportunistically avoiding
|
|
231
231
|
work (in which case
|
|
232
|
-
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.
|
|
232
|
+
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.ParseContext.skipUntilInView)
|
|
233
233
|
should be called to make sure the parser is restarted when the
|
|
234
234
|
skipped region becomes visible).
|
|
235
235
|
*/
|
|
@@ -267,7 +267,7 @@ class ParseContext {
|
|
|
267
267
|
work(time, upto) {
|
|
268
268
|
if (upto != null && upto >= this.state.doc.length)
|
|
269
269
|
upto = undefined;
|
|
270
|
-
if (this.tree != Tree.empty && (upto
|
|
270
|
+
if (this.tree != Tree.empty && this.isDone(upto !== null && upto !== void 0 ? upto : this.state.doc.length)) {
|
|
271
271
|
this.takeTree();
|
|
272
272
|
return true;
|
|
273
273
|
}
|
|
@@ -353,6 +353,8 @@ class ParseContext {
|
|
|
353
353
|
@internal
|
|
354
354
|
*/
|
|
355
355
|
updateViewport(viewport) {
|
|
356
|
+
if (this.viewport.from == viewport.from && this.viewport.to == viewport.to)
|
|
357
|
+
return false;
|
|
356
358
|
this.viewport = viewport;
|
|
357
359
|
let startLen = this.skipped.length;
|
|
358
360
|
for (let i = 0; i < this.skipped.length; i++) {
|
|
@@ -362,7 +364,10 @@ class ParseContext {
|
|
|
362
364
|
this.skipped.splice(i--, 1);
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
|
-
|
|
367
|
+
if (this.skipped.length >= startLen)
|
|
368
|
+
return false;
|
|
369
|
+
this.reset();
|
|
370
|
+
return true;
|
|
366
371
|
}
|
|
367
372
|
/**
|
|
368
373
|
@internal
|
|
@@ -421,6 +426,13 @@ class ParseContext {
|
|
|
421
426
|
return this.treeLen < pos && this.parse && this.parse.parsedPos >= pos;
|
|
422
427
|
}
|
|
423
428
|
/**
|
|
429
|
+
@internal
|
|
430
|
+
*/
|
|
431
|
+
isDone(upto) {
|
|
432
|
+
let frags = this.fragments;
|
|
433
|
+
return this.treeLen >= upto && frags.length && frags[0].from == 0 && frags[0].to >= upto;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
424
436
|
Get the context for the current parse, or `null` if no editor
|
|
425
437
|
parse is in progress.
|
|
426
438
|
*/
|
|
@@ -484,12 +496,8 @@ const parseWorker = /*@__PURE__*/ViewPlugin.fromClass(class ParseWorker {
|
|
|
484
496
|
}
|
|
485
497
|
update(update) {
|
|
486
498
|
let cx = this.view.state.field(Language.state).context;
|
|
487
|
-
if (update.
|
|
488
|
-
|
|
489
|
-
cx.reset();
|
|
490
|
-
if (this.view.viewport.to > cx.treeLen)
|
|
491
|
-
this.scheduleWork();
|
|
492
|
-
}
|
|
499
|
+
if (cx.updateViewport(update.view.viewport) || this.view.viewport.to > cx.treeLen)
|
|
500
|
+
this.scheduleWork();
|
|
493
501
|
if (update.docChanged) {
|
|
494
502
|
if (this.view.hasFocus)
|
|
495
503
|
this.chunkBudget += 50 /* ChangeBonus */;
|
|
@@ -500,11 +508,9 @@ const parseWorker = /*@__PURE__*/ViewPlugin.fromClass(class ParseWorker {
|
|
|
500
508
|
scheduleWork() {
|
|
501
509
|
if (this.working > -1)
|
|
502
510
|
return;
|
|
503
|
-
let { state } = this.view, field = state.field(Language.state)
|
|
504
|
-
if (field.tree
|
|
505
|
-
|
|
506
|
-
return;
|
|
507
|
-
this.working = requestIdle(this.work, { timeout: 500 /* Pause */ });
|
|
511
|
+
let { state } = this.view, field = state.field(Language.state);
|
|
512
|
+
if (field.tree != field.context.tree || !field.context.isDone(state.doc.length))
|
|
513
|
+
this.working = requestIdle(this.work, { timeout: 500 /* Pause */ });
|
|
508
514
|
}
|
|
509
515
|
work(deadline) {
|
|
510
516
|
this.working = -1;
|
|
@@ -601,16 +607,17 @@ class LanguageDescription {
|
|
|
601
607
|
Optional filename pattern that should be associated with this
|
|
602
608
|
language.
|
|
603
609
|
*/
|
|
604
|
-
filename, loadFunc
|
|
610
|
+
filename, loadFunc,
|
|
611
|
+
/**
|
|
612
|
+
If the language has been loaded, this will hold its value.
|
|
613
|
+
*/
|
|
614
|
+
support = undefined) {
|
|
605
615
|
this.name = name;
|
|
606
616
|
this.alias = alias;
|
|
607
617
|
this.extensions = extensions;
|
|
608
618
|
this.filename = filename;
|
|
609
619
|
this.loadFunc = loadFunc;
|
|
610
|
-
|
|
611
|
-
If the language has been loaded, this will hold its value.
|
|
612
|
-
*/
|
|
613
|
-
this.support = undefined;
|
|
620
|
+
this.support = support;
|
|
614
621
|
this.loading = null;
|
|
615
622
|
}
|
|
616
623
|
/**
|
|
@@ -625,7 +632,13 @@ class LanguageDescription {
|
|
|
625
632
|
Create a language description.
|
|
626
633
|
*/
|
|
627
634
|
static of(spec) {
|
|
628
|
-
|
|
635
|
+
let { load, support } = spec;
|
|
636
|
+
if (!load) {
|
|
637
|
+
if (!support)
|
|
638
|
+
throw new RangeError("Must pass either 'load' or 'support' to LanguageDescription.of");
|
|
639
|
+
load = () => Promise.resolve(support);
|
|
640
|
+
}
|
|
641
|
+
return new LanguageDescription(spec.name, (spec.alias || []).concat(spec.name).map(s => s.toLowerCase()), spec.extensions || [], spec.filename, load, support);
|
|
629
642
|
}
|
|
630
643
|
/**
|
|
631
644
|
Look for a language in the given array of descriptions that
|
|
@@ -835,24 +848,7 @@ definitive indentation can be determined.
|
|
|
835
848
|
const indentNodeProp = /*@__PURE__*/new NodeProp();
|
|
836
849
|
// Compute the indentation for a given position from the syntax tree.
|
|
837
850
|
function syntaxIndentation(cx, ast, pos) {
|
|
838
|
-
|
|
839
|
-
// Enter previous nodes that end in empty error terms, which means
|
|
840
|
-
// they were broken off by error recovery, so that indentation
|
|
841
|
-
// works even if the constructs haven't been finished.
|
|
842
|
-
for (let scan = tree, scanPos = pos;;) {
|
|
843
|
-
let last = scan.childBefore(scanPos);
|
|
844
|
-
if (!last)
|
|
845
|
-
break;
|
|
846
|
-
if (last.type.isError && last.from == last.to) {
|
|
847
|
-
tree = scan;
|
|
848
|
-
scanPos = last.from;
|
|
849
|
-
}
|
|
850
|
-
else {
|
|
851
|
-
scan = last;
|
|
852
|
-
scanPos = scan.to + 1;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
return indentFrom(tree, pos, cx);
|
|
851
|
+
return indentFrom(ast.resolveInner(pos).enterUnfinishedNodesBefore(pos), pos, cx);
|
|
856
852
|
}
|
|
857
853
|
function ignoreClosed(cx) {
|
|
858
854
|
return cx.pos == cx.options.simulateBreak && cx.options.simulateDoubleBreak;
|
|
@@ -1019,7 +1015,7 @@ added at the start of a line.
|
|
|
1019
1015
|
*/
|
|
1020
1016
|
function indentOnInput() {
|
|
1021
1017
|
return EditorState.transactionFilter.of(tr => {
|
|
1022
|
-
if (!tr.docChanged || tr.isUserEvent("input.type"))
|
|
1018
|
+
if (!tr.docChanged || !tr.isUserEvent("input.type"))
|
|
1023
1019
|
return tr;
|
|
1024
1020
|
let rules = tr.startState.languageDataAt("indentOnInput", tr.startState.selection.main.head);
|
|
1025
1021
|
if (!rules.length)
|
|
@@ -1044,7 +1040,7 @@ function indentOnInput() {
|
|
|
1044
1040
|
if (cur != norm)
|
|
1045
1041
|
changes.push({ from: line.from, to: line.from + cur.length, insert: norm });
|
|
1046
1042
|
}
|
|
1047
|
-
return changes.length ? [tr, { changes }] : tr;
|
|
1043
|
+
return changes.length ? [tr, { changes, sequential: true }] : tr;
|
|
1048
1044
|
});
|
|
1049
1045
|
}
|
|
1050
1046
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemirror/language",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.4",
|
|
4
4
|
"description": "Language support infrastructure for the CodeMirror code editor",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "cm-runtests",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"@codemirror/state": "^0.19.0",
|
|
30
30
|
"@codemirror/text": "^0.19.0",
|
|
31
31
|
"@codemirror/view": "^0.19.0",
|
|
32
|
-
"@lezer/common": "^0.
|
|
33
|
-
"@lezer/lr": "^0.
|
|
32
|
+
"@lezer/common": "^0.15.5",
|
|
33
|
+
"@lezer/lr": "^0.15.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@codemirror/buildhelper": "^0.1.5",
|
|
37
|
-
"@lezer/javascript": "^0.
|
|
37
|
+
"@lezer/javascript": "^0.15.0"
|
|
38
38
|
},
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|