@hydranium/core 1.0.0-next.387 → 1.0.0-next.390
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/lib/langium/document-builder/document-builder.d.ts +14 -1
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +15 -0
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +118 -2
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +286 -3
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts +15 -8
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js +25 -10
- package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +1 -1
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +3 -0
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/package.json +5 -5
- package/src/langium/document-builder/document-builder.ts +17 -1
- package/src/langium/integrity/integrity-service.ts +325 -2
- package/src/langium/workspace/hydranium-workspace-lock.ts +25 -10
- package/src/testing/stub-document-builder.ts +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stub-document-builder.js","sourceRoot":"","sources":["../../src/testing/stub-document-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAMJ,oBAAoB,EAItB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAA0B,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAIJ,wBAAwB,EAC1B,MAAM,iDAAiD,CAAC;
|
|
1
|
+
{"version":3,"file":"stub-document-builder.js","sourceRoot":"","sources":["../../src/testing/stub-document-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAMJ,oBAAoB,EAItB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAA0B,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAIJ,wBAAwB,EAC1B,MAAM,iDAAiD,CAAC;AAoHzD;;;;;;;;;;GAUG;AACH,SAAS,OAAO,CAAC,MAAe;IAC7B,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACnD,cAAc,CAAC,GAAG,EAAE;YACjB,MAAM,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACN,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,aAA6B;IAClE,MAAM,cAAc,GAAG,IAAI,GAAG,EAA0C,CAAC;IACzE,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAmD,CAAC;IACtF,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAA0C,CAAC;IAC9E,MAAM,iBAAiB,GAA6B,EAAE,CAAC;IACvD,MAAM,mBAAmB,GAAyB,EAAE,CAAC;IACrD,MAAM,KAAK,GAA+C,EAAE,CAAC;IAC7D,MAAM,WAAW,GAA0C,EAAE,CAAC;IAC9D,MAAM,cAAc,GAA4D,EAAE,CAAC;IAEnF,MAAM,YAAY,GAAG,CAAC,MAAc,EAAS,EAAE;QAC5C,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,yEAAyE,CAAC,CAAC;IAC3H,CAAC,CAAC;IACF,MAAM,IAAI,GAAwB;QAC/B,uEAAuE;QACvE,uCAAuC;QACvC,kBAAkB,EAAE,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE;QACxE,wEAAwE;QACxE,eAAe,EAAE,GAAG,EAAE,CAAC,wBAAwB,CAAC,SAAS,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QACpF,uEAAuE;QACvE,oEAAoE;QACpE,sBAAsB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC;QACzE,0CAA0C;QAC1C,mBAAmB,EAAE,GAAG,EAAE,CAAC,KAAK;QAChC,oBAAoB,EAAE,GAAG,EAAE,CAAC,KAAK;QACjC,mDAAmD;QACnD,YAAY,EAAE,GAAG,EAAE,CAAC,KAAK;QACzB,WAAW,EAAE,SAAS;QACtB,cAAc,EAAE,GAAG,EAAE,CAAC,SAAS;QAC/B,IAAI,WAAW;YACZ,OAAO,WAAW,CAAC;QACtB,CAAC;QACD,IAAI,cAAc;YACf,OAAO,cAAc,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,OAAc,EAAE,OAAc;YACxC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,cAAc,CAAC,OAAc,EAAE,OAAc;YAC1C,OAAO,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACnH,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,KAAoB,EAAE,GAAS;YAC5C,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;YAC3B,IAAI,IAAI,EAAE,CAAC;gBACR,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;YAC1D,CAAC;YACD,OAAO,GAAG,CAAC;QACd,CAAC;QACD,eAAe,CAAC,KAAoB,EAAE,QAA+B;YAClE,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAC7C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACpB,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAChC,OAAO,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACnC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;oBACZ,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACvB,CAAC;YACJ,CAAC,CAAC,CAAC;QACN,CAAC;QACD,QAAQ,CAAC,QAAgC;YACtC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACjC,OAAO,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBAC3B,MAAM,GAAG,GAAG,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAChD,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;oBACZ,iBAAiB,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACpC,CAAC;YACJ,CAAC,CAAC,CAAC;QACN,CAAC;QACD,SAAS,CAAC,KAAoB,EAAE,QAAyB,EAAE,WAA+B;YACvF,MAAM,KAAK,GACR,WAAW;gBACV;oBACE,uBAAuB,EAAE,KAAK;oBAC9B,uBAAuB,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;iBAC7C,CAAC;YAC3B,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAClD,MAAM,SAAS,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACtD,oEAAoE;YACpE,qEAAqE;YACrE,4DAA4D;YAC5D,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YACzE,mEAAmE;YACnE,qEAAqE;YACrE,uBAAuB;YACvB,IAAI,kBAAkB,GAAG,CAAC,CAAC;YAC3B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;gBAChC,IAAI,KAAK,CAAC,uBAAuB,EAAE,CAAC;oBACjC,kBAAkB,EAAE,CAAC;oBACrB,SAAS;gBACZ,CAAC;gBACD,IAAI,CAAC;oBACF,OAAO,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;gBACtC,CAAC;gBAAC,OAAO,GAAY,EAAE,CAAC;oBACrB,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAC;wBAC9B,MAAM,GAAG,CAAC;oBACb,CAAC;oBACD,kBAAkB,EAAE,CAAC;gBACxB,CAAC;YACJ,CAAC;YACD,IAAI,kBAAkB,KAAK,CAAC,EAAE,CAAC;gBAC5B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;oBAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAC/B,CAAC;YACJ,CAAC;QACJ,CAAC;QACD,wBAAwB,CAAC,KAAoB,EAAE,QAAwC;YACpF,MAAM,IAAI,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACjD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACpB,kBAAkB,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACpC,OAAO,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACnC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;oBACZ,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACvB,CAAC;YACJ,CAAC,CAAC,CAAC;QACN,CAAC;QACD,YAAY,CAAC,OAAc,EAAE,OAAc;YACxC,KAAK,MAAM,QAAQ,IAAI,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC;gBAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;YACvC,CAAC;QACJ,CAAC;QACD,YAAY,CAAC,QAA4B;YACtC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACnC,OAAO,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBAC3B,MAAM,GAAG,GAAG,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAClD,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;oBACZ,mBAAmB,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACtC,CAAC;YACJ,CAAC,CAAC,CAAC;QACN,CAAC;QACD,cAAc,CAAC,KAAsB;YAClC,MAAM,QAAQ,GAAc,EAAE,CAAC;YAC/B,KAAK,MAAM,QAAQ,IAAI,mBAAmB,CAAC,KAAK,EAAE,EAAE,CAAC;gBAClD,IAAI,CAAC;oBACF,QAAQ,CAAC,KAAK,CAAC,CAAC;gBACnB,CAAC;gBAAC,OAAO,KAAc,EAAE,CAAC;oBACvB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACxB,CAAC;YACJ,CAAC;YACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;YACrB,CAAC;QACJ,CAAC;QACD,YAAY,CAAC,KAAoB,EAAE,QAA+B;YAC/D,MAAM,IAAI,GAAG,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAClD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACpB,mBAAmB,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACrC,OAAO,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACnC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;oBACZ,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACvB,CAAC;YACJ,CAAC,CAAC,CAAC;QACN,CAAC;QACD,cAAc,CAAC,KAAoB,EAAE,KAAwB,EAAE,WAA+B;YAC3F,MAAM,KAAK,GACR,WAAW;gBACV;oBACE,uBAAuB,EAAE,KAAK;oBAC9B,uBAAuB,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;iBAC7C,CAAC;YAC3B,KAAK,MAAM,QAAQ,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC3D,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;YACnC,CAAC;QACJ,CAAC;QACD,iBAAiB;YACd,IAAI,OAAiC,CAAC;YACtC,MAAM,KAAK,GAAG;gBACX,IAAI,CAAC,OAAmB;oBACrB,OAAO,GAAG,OAAO,CAAC;gBACrB,CAAC;aACH,CAAC;YACF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClB,OAAO;gBACJ,IAAI,QAAQ;oBACT,OAAO,OAAO,KAAK,SAAS,CAAC;gBAChC,CAAC;gBACD,OAAO;oBACJ,IAAI,CAAC,OAAO,EAAE,CAAC;wBACZ,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;wBACpC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;4BACf,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;wBAC3B,CAAC;wBACD,MAAM,IAAI,KAAK,CACZ,6FAA6F;4BAC1F,wGAAwG,CAC7G,CAAC;oBACL,CAAC;oBACD,OAAO,EAAE,CAAC;gBACb,CAAC;aACH,CAAC;QACL,CAAC;QACD,KAAK;YACF,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,YAAY;YACT,OAAO,YAAY,CAAC,cAAc,CAAC,CAAC;QACvC,CAAC;QACD,KAAK;YACF,cAAc,CAAC,KAAK,EAAE,CAAC;YACvB,kBAAkB,CAAC,KAAK,EAAE,CAAC;YAC3B,iBAAiB,CAAC,MAAM,GAAG,CAAC,CAAC;YAC7B,mBAAmB,CAAC,MAAM,GAAG,CAAC,CAAC;YAC/B,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjB,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC;YACvB,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;QAC7B,CAAC;KACH,CAAC;IACF,OAAO,IAAI,CAAC;AACf,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydranium/core",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.390",
|
|
4
4
|
"description": "Foundational runtime of the hydranium framework: AST coordination layer (services, integrity, AST extension, serialization, multi-client documents) plus the LSP textual head at the /lsp subpath. Consumed by protocol-head packages (@hydranium/data-server, @hydranium/glsp-server).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
"diff": "^5.2.0"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
|
-
"@hydranium/langium": "1.0.0-next.
|
|
91
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
90
|
+
"@hydranium/langium": "1.0.0-next.390",
|
|
91
|
+
"@hydranium/protocol": "1.0.0-next.390",
|
|
92
92
|
"@playwright/test": "^1.55.0",
|
|
93
93
|
"@types/diff": "^5.2.0",
|
|
94
94
|
"fake-indexeddb": "^6.2.5",
|
|
@@ -101,8 +101,8 @@
|
|
|
101
101
|
"vscode-languageserver-types": "3.18.0"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
|
-
"@hydranium/langium": "1.0.0-next.
|
|
105
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
104
|
+
"@hydranium/langium": "1.0.0-next.390",
|
|
105
|
+
"@hydranium/protocol": "1.0.0-next.390",
|
|
106
106
|
"@playwright/test": "^1.40.0",
|
|
107
107
|
"vscode-jsonrpc": "^9.0.0",
|
|
108
108
|
"vscode-languageserver": "~10.0.1",
|
|
@@ -414,6 +414,15 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
414
414
|
return this.insideBuildUpdate;
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Whether the build calling the `onUpdate` listeners overlaps another build
|
|
419
|
+
* that still runs, such as an update outside the lock while a locked build
|
|
420
|
+
* runs. A listener reads this before its first await.
|
|
421
|
+
*/
|
|
422
|
+
overlapsRunningBuild(): boolean {
|
|
423
|
+
return this.runningBuilds > 1;
|
|
424
|
+
}
|
|
425
|
+
|
|
417
426
|
/**
|
|
418
427
|
* Resolves once no build is in progress, however each ended, and never
|
|
419
428
|
* rejects. It is `undefined` between builds. During a lock read no locked
|
|
@@ -1088,8 +1097,11 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
1088
1097
|
* Treating an un-indexed document as affected is the conservative reading and
|
|
1089
1098
|
* costs nothing in steady state — documents sit at `Validated` there, so this
|
|
1090
1099
|
* branch is only taken for documents an interrupted build left behind.
|
|
1100
|
+
*
|
|
1101
|
+
* Public so a service that requests a build can skip one that would relink
|
|
1102
|
+
* nothing.
|
|
1091
1103
|
*/
|
|
1092
|
-
|
|
1104
|
+
override shouldRelink(document: LangiumDocument, changedUris: Set<string>): boolean {
|
|
1093
1105
|
if (document.state < DocumentState.IndexedReferences) {
|
|
1094
1106
|
return true;
|
|
1095
1107
|
}
|
|
@@ -1210,6 +1222,10 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
1210
1222
|
* re-parsed AST would otherwise lack them. The caller
|
|
1211
1223
|
* runs inside the `Linked` build phase; the build carries the document
|
|
1212
1224
|
* through IndexedReferences + Validation (and their `onDocumentPhase`) next.
|
|
1225
|
+
*
|
|
1226
|
+
* Only this document is relinked. The documents that reference it still
|
|
1227
|
+
* hold nodes a re-parse replaced, or names a mutation changed, so the
|
|
1228
|
+
* caller has to relink them in a later build.
|
|
1213
1229
|
*/
|
|
1214
1230
|
async reparseAndRelink(document: LangiumDocument, cancelToken: CancellationToken = CancellationToken.None): Promise<void> {
|
|
1215
1231
|
const languageServices = this.serviceRegistry.getServices(document.uri);
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { isPromiseLike, Logger, type ProfileSession, type Tracer } from '@hydranium/protocol';
|
|
10
|
+
import { describeErrorWithStack, isPromiseLike, Logger, type ProfileSession, textHash, type Tracer } from '@hydranium/protocol';
|
|
11
11
|
import {
|
|
12
12
|
type AstNode,
|
|
13
13
|
AstUtils,
|
|
14
14
|
DocumentState,
|
|
15
15
|
interruptAndCheck,
|
|
16
|
+
isMultiReference,
|
|
16
17
|
isOperationCancelled,
|
|
17
18
|
type LangiumDocument,
|
|
19
|
+
type URI,
|
|
18
20
|
UriUtils
|
|
19
21
|
} from '@hydranium/langium';
|
|
20
22
|
import { CancellationToken, type Disposable } from 'vscode-languageserver';
|
|
@@ -94,6 +96,13 @@ export namespace IntegrityService {
|
|
|
94
96
|
* `textDocument` still mirrors disk — read the repair off the AST, or from
|
|
95
97
|
* the staged content the next open consumes.
|
|
96
98
|
*
|
|
99
|
+
* A `Linked`-phase repair settles the documents that reference the repaired
|
|
100
|
+
* one in the same build against what the repair replaced. A build after it
|
|
101
|
+
* relinks them, and they settle again. Past `maxRelinkChain` such builds in
|
|
102
|
+
* a row they keep stale links. A closed `'editor'` document whose CST was
|
|
103
|
+
* shed re-reads its unrepaired text in that build and repeats the repair,
|
|
104
|
+
* so they resolve a renamed element by its old name until the next change.
|
|
105
|
+
*
|
|
97
106
|
* Invariant: integrity rules only register at Parsed or Linked, so the build
|
|
98
107
|
* is guaranteed post-integrity once it advances past `onBuildPhase(Linked)`
|
|
99
108
|
* into `IndexedReferences`. {@link IntegrityService.register} enforces this at
|
|
@@ -111,6 +120,9 @@ export namespace IntegrityService {
|
|
|
111
120
|
*/
|
|
112
121
|
const silentWriteReported = new WeakSet<ServerSharedServices>();
|
|
113
122
|
|
|
123
|
+
/** The running builds that relink dependents, per server, because a repair in one language can extend a chain in another. */
|
|
124
|
+
const runningRelinksByServer = new WeakMap<ServerSharedServices, Set<DependentRelink>>();
|
|
125
|
+
|
|
114
126
|
/**
|
|
115
127
|
* Construction options for {@link DefaultIntegrityService}. All fields
|
|
116
128
|
* optional — the defaults reproduce the framework's behaviour exactly.
|
|
@@ -124,6 +136,26 @@ export interface IntegrityServiceOptions extends LogNameOptions {
|
|
|
124
136
|
* to `'silent'`. See {@link IntegritySyncMode}.
|
|
125
137
|
*/
|
|
126
138
|
readonly syncMode?: IntegritySyncMode;
|
|
139
|
+
/**
|
|
140
|
+
* How many builds in a row may relink the dependents of `Linked`-phase
|
|
141
|
+
* repairs before a repair only warns. A rule that settles within it gets
|
|
142
|
+
* every build it needs. An integer of at least 1, 3 by default.
|
|
143
|
+
*/
|
|
144
|
+
readonly maxRelinkChain?: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The text a `Linked`-phase repair was computed from, and the text it produced. */
|
|
148
|
+
export interface IntegrityRepairTexts {
|
|
149
|
+
readonly from: string;
|
|
150
|
+
readonly to: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The build a repaired document has requested to relink its dependents. */
|
|
154
|
+
export interface DependentRelink {
|
|
155
|
+
/** The repaired document, whose dependents the build relinks. */
|
|
156
|
+
readonly document: LangiumDocument;
|
|
157
|
+
/** Its place in a chain of such builds, each requested by a repair of a document the one before relinked. */
|
|
158
|
+
readonly chain: number;
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
/**
|
|
@@ -173,6 +205,25 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
173
205
|
* Linked-phase correction (see {@link resyncDocument}).
|
|
174
206
|
*/
|
|
175
207
|
protected readonly documentBuilder: HydraniumDocumentBuilder;
|
|
208
|
+
/** Each document's {@link DependentRelink}, from its request until it settles. See {@link relinkDependents}. */
|
|
209
|
+
protected readonly relinkBuilds = new WeakMap<LangiumDocument, DependentRelink>();
|
|
210
|
+
/** The {@link replayKey} of the latest repair of each document that {@link stagesRepairs}, with the hash of the text it came from. */
|
|
211
|
+
protected readonly replayKeys = new WeakMap<LangiumDocument, { readonly key: string; readonly fromHash: string }>();
|
|
212
|
+
/** The relinks the check inside the lock found needed, until their build starts. */
|
|
213
|
+
protected readonly scheduledRelinks = new WeakSet<DependentRelink>();
|
|
214
|
+
/** The relinks of every language whose build runs, from its start until it settles or the next build starts. */
|
|
215
|
+
protected readonly runningRelinks: Set<DependentRelink>;
|
|
216
|
+
/** The rules of a repair while a relink's build waited or ran. Only one while it ran gets the next build in the chain. */
|
|
217
|
+
protected readonly repairedAgain = new WeakMap<DependentRelink, readonly IntegrityRule[]>();
|
|
218
|
+
/**
|
|
219
|
+
* Marks {@link scheduledRelinks} running as their builds start. Set on the
|
|
220
|
+
* first request only so it subscribes once, and never disposed.
|
|
221
|
+
*/
|
|
222
|
+
protected relinkStarts?: Disposable;
|
|
223
|
+
/** See {@link IntegrityServiceOptions.maxRelinkChain}. */
|
|
224
|
+
protected readonly maxRelinkChain: number;
|
|
225
|
+
/** The rules that mutated each document in its latest `Linked`-phase {@link enforceIntegrity} pass, so a warning can name them. */
|
|
226
|
+
protected readonly mutatingRules = new WeakMap<LangiumDocument, Set<IntegrityRule>>();
|
|
176
227
|
|
|
177
228
|
constructor(
|
|
178
229
|
protected readonly services: ServerLanguageServices,
|
|
@@ -183,6 +234,14 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
183
234
|
this.tracer = services.shared.Tracer.for(options.logName ?? 'Integrity').trace('instantiated');
|
|
184
235
|
this.documentBuilder = services.shared.workspace.DocumentBuilder;
|
|
185
236
|
this.syncMode = options.syncMode ?? 'silent';
|
|
237
|
+
this.maxRelinkChain = options.maxRelinkChain ?? 3;
|
|
238
|
+
if (!Number.isInteger(this.maxRelinkChain) || this.maxRelinkChain < 1) {
|
|
239
|
+
throw new RangeError(
|
|
240
|
+
`The integrity service's maxRelinkChain option must be an integer of at least 1, not ${this.maxRelinkChain}.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
this.runningRelinks = runningRelinksByServer.get(services.shared) ?? new Set();
|
|
244
|
+
runningRelinksByServer.set(services.shared, this.runningRelinks);
|
|
186
245
|
|
|
187
246
|
// Read the language's IntegrityRuleContribution group and let each
|
|
188
247
|
// contribution register one or many rules through this service (the
|
|
@@ -216,6 +275,10 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
216
275
|
// debug threshold is active, so the production `info` path allocates
|
|
217
276
|
// nothing and the hot rule loop stays a single const-branch off `session`.
|
|
218
277
|
const session = Logger.isLevelEnabled('debug') ? this.tracer.profile(`integrity ${DocumentState[phase]}`) : undefined;
|
|
278
|
+
// Before any skip or cancel, so text with parse errors also ends a replay.
|
|
279
|
+
for (const document of documents) {
|
|
280
|
+
this.forgetReplayOfOtherText(document);
|
|
281
|
+
}
|
|
219
282
|
for (const document of documents) {
|
|
220
283
|
if (this.isStructurallyBroken(document)) {
|
|
221
284
|
this.tracer.with(document.uri.toString()).debug('Parse errors: rules skipped until the document parses');
|
|
@@ -322,6 +385,13 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
322
385
|
if (rulesByType.size === 0) {
|
|
323
386
|
return false;
|
|
324
387
|
}
|
|
388
|
+
// Read only after a Linked-phase repair. Cleared when such a pass starts, so
|
|
389
|
+
// one that records nothing leaves no older record to be named in its place.
|
|
390
|
+
const recorded = phase > DocumentState.Parsed;
|
|
391
|
+
if (recorded) {
|
|
392
|
+
this.mutatingRules.delete(document);
|
|
393
|
+
}
|
|
394
|
+
let mutating: Set<IntegrityRule> | undefined;
|
|
325
395
|
const relativeUri = this.services.shared.workspace.WorkspaceManager.wsRelativePath(document.uri);
|
|
326
396
|
for (const node of AstUtils.streamAst(document.parseResult.value)) {
|
|
327
397
|
for (const rule of rulesByType.get(node.$type) ?? NO_RULES) {
|
|
@@ -333,6 +403,12 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
333
403
|
: rule.enforce(node, document, ruleLogger);
|
|
334
404
|
const mutated = isPromiseLike(enforced) ? await enforced : enforced;
|
|
335
405
|
if (mutated) {
|
|
406
|
+
if (recorded && mutating === undefined) {
|
|
407
|
+
// Only a mutating pass leads to a read, so a pass that mutates nothing allocates nothing.
|
|
408
|
+
mutating = new Set();
|
|
409
|
+
this.mutatingRules.set(document, mutating);
|
|
410
|
+
}
|
|
411
|
+
mutating?.add(rule);
|
|
336
412
|
ruleLogger.trace(`[${relativeUri}] Mutated ${this.formatNode(node)}`);
|
|
337
413
|
}
|
|
338
414
|
changed = mutated || changed;
|
|
@@ -409,6 +485,8 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
409
485
|
// mutation in place would answer questions about this document from a
|
|
410
486
|
// parse of text nobody has. Reconciling discards it by re-reading the
|
|
411
487
|
// store, which is what the document now has to agree with.
|
|
488
|
+
//
|
|
489
|
+
// No dependents are relinked, because the edit's build relinks them.
|
|
412
490
|
await this.reconcileDocument(document, cancelToken);
|
|
413
491
|
return;
|
|
414
492
|
}
|
|
@@ -423,6 +501,16 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
423
501
|
|
|
424
502
|
await this.syncCorrections(textDocument, parsedFrom);
|
|
425
503
|
|
|
504
|
+
if (document.state > DocumentState.Parsed) {
|
|
505
|
+
// Requested before the in-place relink, and an error logged, so neither skips the other.
|
|
506
|
+
try {
|
|
507
|
+
this.relinkDependents(document, { from: parsedFrom, to: newText });
|
|
508
|
+
} catch (error: unknown) {
|
|
509
|
+
this.tracer
|
|
510
|
+
.with(document.uri.toString())
|
|
511
|
+
.error(`Relinking the dependents of a repaired document failed. ${describeErrorWithStack(error)}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
426
514
|
await this.reconcileDocument(document, cancelToken);
|
|
427
515
|
|
|
428
516
|
// Re-version against the REPAIRED text, after either branch. The repair is
|
|
@@ -483,6 +571,232 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
483
571
|
}
|
|
484
572
|
}
|
|
485
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Relink the documents that reference `document` in a build after this
|
|
576
|
+
* one, where it counts as changed. They were linked before the repair, so
|
|
577
|
+
* they hold nodes a re-parse replaced, or names the repair changed.
|
|
578
|
+
*
|
|
579
|
+
* Each document has at most one such build, its {@link DependentRelink},
|
|
580
|
+
* which goes through four steps.
|
|
581
|
+
*
|
|
582
|
+
* 1. Requested here, and kept in {@link relinkBuilds}. A repair while it
|
|
583
|
+
* waits is relinked by it.
|
|
584
|
+
* 2. Checked inside the lock, which skips it unless {@link isRelinkNeeded}.
|
|
585
|
+
* A needed one joins {@link scheduledRelinks}. While a build outside the
|
|
586
|
+
* lock runs, it is requested again once that build ends.
|
|
587
|
+
* 3. Started, when `onUpdate` reports its build. An explicit `build` starts
|
|
588
|
+
* none. It is in {@link runningRelinks} until it settles or the next
|
|
589
|
+
* build starts that overlaps no other. A repair while it runs is kept in
|
|
590
|
+
* {@link repairedAgain} and gets the next build.
|
|
591
|
+
* 4. Settled, when its request resolves, which drops it from every record.
|
|
592
|
+
*
|
|
593
|
+
* A repair of a document that a running build relinked continues that
|
|
594
|
+
* build's chain, which {@link maxRelinkChain} bounds. A repair with the
|
|
595
|
+
* {@link replayKey} of the one before requests none.
|
|
596
|
+
*/
|
|
597
|
+
protected relinkDependents(document: LangiumDocument, repair: IntegrityRepairTexts): void {
|
|
598
|
+
const key = this.replayKey(document, repair);
|
|
599
|
+
if (key !== undefined && this.replayKeys.get(document)?.key === key) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const rules = [...(this.mutatingRules.get(document) ?? [])];
|
|
603
|
+
const pending = this.relinkBuilds.get(document);
|
|
604
|
+
let requested = true;
|
|
605
|
+
if (pending !== undefined) {
|
|
606
|
+
this.repairedAgain.set(pending, rules);
|
|
607
|
+
} else {
|
|
608
|
+
const causes = [...this.runningRelinks].filter(relink => this.isRelinkedBy(document, relink.document));
|
|
609
|
+
requested = this.relinkInChain(document, Math.max(0, ...causes.map(relink => relink.chain)) + 1, rules);
|
|
610
|
+
}
|
|
611
|
+
// Recorded after the request, so one that throws or is past the bound leaves no key that skips the next repair.
|
|
612
|
+
if (key !== undefined && requested) {
|
|
613
|
+
this.replayKeys.set(document, { key, fromHash: textHash(repair.from) });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Whether a build where `changed` counts as changed relinks `document`, as
|
|
619
|
+
* Langium's `shouldRelink` decides. Read from the document's links, because
|
|
620
|
+
* its build has dropped its indexed references until it indexes again.
|
|
621
|
+
*/
|
|
622
|
+
protected isRelinkedBy(document: LangiumDocument, changed: LangiumDocument): boolean {
|
|
623
|
+
const target = changed.uri.toString();
|
|
624
|
+
return document.references.some(reference => {
|
|
625
|
+
const descriptions = isMultiReference(reference)
|
|
626
|
+
? reference.items.map(item => item.$nodeDescription)
|
|
627
|
+
: [reference.$nodeDescription];
|
|
628
|
+
return reference.error !== undefined || descriptions.some(description => description?.documentUri.toString() === target);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Request the build at `chain`, or warn naming `rules` past
|
|
634
|
+
* {@link maxRelinkChain} and drop the {@link replayKey} of `document`.
|
|
635
|
+
* Returns whether it requested the build.
|
|
636
|
+
*/
|
|
637
|
+
protected relinkInChain(document: LangiumDocument, chain: number, rules: readonly IntegrityRule[]): boolean {
|
|
638
|
+
if (chain > this.maxRelinkChain) {
|
|
639
|
+
this.replayKeys.delete(document);
|
|
640
|
+
const builds = this.maxRelinkChain === 1 ? '1 build in a row that relinks' : `${this.maxRelinkChain} builds in a row that relink`;
|
|
641
|
+
this.tracer
|
|
642
|
+
.with(document.uri.toString())
|
|
643
|
+
.warn(
|
|
644
|
+
`${this.formatRules(rules)} repaired documents in ${builds} their dependents, ` +
|
|
645
|
+
'which keep stale links until the next change. A rule that does not settle causes this, ' +
|
|
646
|
+
'or documents with linking errors that rules repair one after another.'
|
|
647
|
+
);
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
this.requestRelink(document, chain);
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Request the build {@link relinkInChain} decided on, skipped unless {@link isRelinkNeeded}. */
|
|
655
|
+
protected requestRelink(document: LangiumDocument, chain: number): void {
|
|
656
|
+
const uri = document.uri;
|
|
657
|
+
this.relinkStarts ??= this.documentBuilder.onUpdate(changed => this.startRelinks(changed));
|
|
658
|
+
const relink: DependentRelink = { document, chain };
|
|
659
|
+
let unlockedBuild: Promise<void> | undefined;
|
|
660
|
+
const stillNeeded = (): boolean => {
|
|
661
|
+
// No locked build runs during the lock read, so one seen here runs outside the lock.
|
|
662
|
+
unlockedBuild = this.documentBuilder.activeBuild;
|
|
663
|
+
if (unlockedBuild !== undefined) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
// Marked before the deferral check, so a deferred relink stays scheduled until it settles.
|
|
667
|
+
const needed = this.isRelinkNeeded(document);
|
|
668
|
+
if (needed) {
|
|
669
|
+
this.scheduledRelinks.add(relink);
|
|
670
|
+
}
|
|
671
|
+
return needed;
|
|
672
|
+
};
|
|
673
|
+
const settled = (): void => {
|
|
674
|
+
this.relinkBuilds.delete(document);
|
|
675
|
+
this.scheduledRelinks.delete(relink);
|
|
676
|
+
this.runningRelinks.delete(relink);
|
|
677
|
+
};
|
|
678
|
+
const requestFailed = (error: unknown): void => {
|
|
679
|
+
// No build relinks the dependents, so the same repair again needs one.
|
|
680
|
+
this.replayKeys.delete(document);
|
|
681
|
+
this.tracer
|
|
682
|
+
.with(uri.toString())
|
|
683
|
+
.error(`Requesting the next build that relinks the dependents of a repaired document failed. ${describeErrorWithStack(error)}`);
|
|
684
|
+
};
|
|
685
|
+
const request = this.services.shared.workspace.VersionSyncService.requestRecoveryBuild(uri, { stillNeeded });
|
|
686
|
+
// Recorded after the request, so one that throws leaves no relink that never settles.
|
|
687
|
+
this.relinkBuilds.set(document, relink);
|
|
688
|
+
request
|
|
689
|
+
.then(
|
|
690
|
+
() => {
|
|
691
|
+
if (unlockedBuild !== undefined) {
|
|
692
|
+
// Requested again once that build ends, so the two never overlap. A repair meanwhile waits for it.
|
|
693
|
+
unlockedBuild
|
|
694
|
+
.then(() => {
|
|
695
|
+
settled();
|
|
696
|
+
this.requestRelink(document, chain);
|
|
697
|
+
})
|
|
698
|
+
.catch(requestFailed);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
// A build that never started, or was cancelled and not run again,
|
|
702
|
+
// leaves the repair to the build that held it back. Read before the
|
|
703
|
+
// next build's `onUpdate` clears it, which the lock's queue orders later.
|
|
704
|
+
const ran = this.runningRelinks.has(relink);
|
|
705
|
+
settled();
|
|
706
|
+
const again = this.repairedAgain.get(relink);
|
|
707
|
+
if (ran && again !== undefined) {
|
|
708
|
+
this.relinkInChain(document, chain + 1, again);
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
(error: unknown) => {
|
|
712
|
+
settled();
|
|
713
|
+
// The dependents were not relinked, so the same repair again needs its build.
|
|
714
|
+
this.replayKeys.delete(document);
|
|
715
|
+
this.tracer
|
|
716
|
+
.with(uri.toString())
|
|
717
|
+
.error(`The build that relinks the dependents of a repaired document failed. ${describeErrorWithStack(error)}`);
|
|
718
|
+
}
|
|
719
|
+
)
|
|
720
|
+
.catch(requestFailed);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Mark the {@link scheduledRelinks} of the `changed` documents running, as the build that carries them starts. */
|
|
724
|
+
protected startRelinks(changed: readonly URI[]): void {
|
|
725
|
+
// An explicit build relinks no dependents and ends no build it overlaps.
|
|
726
|
+
if (this.documentBuilder.isInsideBuildUpdate()) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
// The build before this one ended, done or cancelled, so its relinks run no more,
|
|
730
|
+
// unless this one overlaps it, as a reentrant build does.
|
|
731
|
+
// The set is shared by every language. Each service clears only its own,
|
|
732
|
+
// because another's listener may already have marked this build's relinks running.
|
|
733
|
+
if (!this.documentBuilder.overlapsRunningBuild()) {
|
|
734
|
+
for (const relink of this.runningRelinks) {
|
|
735
|
+
if (this.relinkBuilds.get(relink.document) === relink) {
|
|
736
|
+
this.runningRelinks.delete(relink);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const documents = this.services.shared.workspace.LangiumDocuments;
|
|
741
|
+
for (const uri of changed) {
|
|
742
|
+
const document = documents.getDocument(uri);
|
|
743
|
+
const relink = document === undefined ? undefined : this.relinkBuilds.get(document);
|
|
744
|
+
if (relink !== undefined && this.scheduledRelinks.delete(relink)) {
|
|
745
|
+
// The starting build relinks any repair while it waited.
|
|
746
|
+
this.repairedAgain.delete(relink);
|
|
747
|
+
this.runningRelinks.add(relink);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Whether a build where `document` counts as changed relinks another
|
|
754
|
+
* document, by the builder's own test, which counts a document a cancelled
|
|
755
|
+
* build left unindexed as relinked.
|
|
756
|
+
*/
|
|
757
|
+
protected isRelinkNeeded(document: LangiumDocument): boolean {
|
|
758
|
+
const documents = this.services.shared.workspace.LangiumDocuments;
|
|
759
|
+
const changed = new Set([document.uri.toString()]);
|
|
760
|
+
return (
|
|
761
|
+
documents.getDocument(document.uri) === document &&
|
|
762
|
+
documents.all.some(other => other !== document && this.documentBuilder.shouldRelink(other, changed))
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Whether `document` stages its repairs rather than writing them, as a
|
|
768
|
+
* closed document does in `'editor'` mode, so its source keeps the
|
|
769
|
+
* unrepaired text.
|
|
770
|
+
*/
|
|
771
|
+
protected stagesRepairs(document: LangiumDocument): boolean {
|
|
772
|
+
return this.syncMode === 'editor' && !this.textDocuments.isOpen(document.textDocument.uri);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* What tells `repair` made again from the same text apart from a new one,
|
|
777
|
+
* for a document that {@link stagesRepairs}, which reads the unrepaired
|
|
778
|
+
* text again in every build that re-parses it after its CST was shed. Such
|
|
779
|
+
* a repair requests no build, because the build that re-parsed the
|
|
780
|
+
* document relinked its dependents, and another would replay it again.
|
|
781
|
+
* Hashes, so the record keeps no text the shed freed.
|
|
782
|
+
*/
|
|
783
|
+
protected replayKey(document: LangiumDocument, repair: IntegrityRepairTexts): string | undefined {
|
|
784
|
+
return this.stagesRepairs(document) ? textHash(repair.from) + textHash(repair.to) : undefined;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Drop the {@link replayKey} of `document` once it was parsed from other
|
|
789
|
+
* text than its repair came from, such as a save and a revert. A repair of
|
|
790
|
+
* that text again is a new one, whose dependents need their build.
|
|
791
|
+
*/
|
|
792
|
+
protected forgetReplayOfOtherText(document: LangiumDocument): void {
|
|
793
|
+
const recorded = this.replayKeys.get(document);
|
|
794
|
+
const parsedFrom = document.parseResult.value.$cstNode?.root.fullText;
|
|
795
|
+
if (recorded !== undefined && parsedFrom !== undefined && recorded.fromHash !== textHash(parsedFrom)) {
|
|
796
|
+
this.replayKeys.delete(document);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
486
800
|
/**
|
|
487
801
|
* Propagate a correction to disk and/or the editor. The corrected text is
|
|
488
802
|
* already in the synced store (in-place in {@link resyncDocument}); this
|
|
@@ -633,7 +947,7 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
633
947
|
|
|
634
948
|
/**
|
|
635
949
|
* Format a rule's name for inclusion in user-facing diagnostic strings
|
|
636
|
-
* (error messages thrown during registration). Defaults to
|
|
950
|
+
* (error messages thrown during registration, and log lines). Defaults to
|
|
637
951
|
* `label ?? id`. Adopters override when their error reporting needs
|
|
638
952
|
* richer rule context.
|
|
639
953
|
*/
|
|
@@ -641,6 +955,15 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
641
955
|
return rule.label ?? rule.id;
|
|
642
956
|
}
|
|
643
957
|
|
|
958
|
+
/** `rules` as the subject of a log line, or a rule left unnamed when an override recorded none. */
|
|
959
|
+
protected formatRules(rules: readonly IntegrityRule[]): string {
|
|
960
|
+
if (rules.length === 0) {
|
|
961
|
+
return 'An integrity rule';
|
|
962
|
+
}
|
|
963
|
+
const names = rules.map(rule => `'${this.formatRule(rule)}'`).join(', ');
|
|
964
|
+
return rules.length === 1 ? `Integrity rule ${names}` : `Integrity rules ${names}`;
|
|
965
|
+
}
|
|
966
|
+
|
|
644
967
|
/**
|
|
645
968
|
* Format an AST node for the per-mutation `trace` log line. Returns
|
|
646
969
|
* `'TypeName'` or `"TypeName('label')"` when the node carries a string
|
|
@@ -56,6 +56,8 @@ export class HydraniumWorkspaceLock extends DefaultWorkspaceLock {
|
|
|
56
56
|
protected readonly tracer: Tracer;
|
|
57
57
|
protected readonly stalledWriteWarnMs: number;
|
|
58
58
|
protected runningWrite?: RunningWrite;
|
|
59
|
+
/** Writes queued and not yet started. */
|
|
60
|
+
protected queuedWrites = 0;
|
|
59
61
|
|
|
60
62
|
constructor(services: Pick<ServerSharedServicesMinimal, 'Clock' | 'Tracer'>, options: HydraniumWorkspaceLockOptions = {}) {
|
|
61
63
|
super();
|
|
@@ -65,19 +67,28 @@ export class HydraniumWorkspaceLock extends DefaultWorkspaceLock {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
|
-
* How many calls
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
70
|
+
* How many calls to `write` or `cancelWrite` came while a write was still
|
|
71
|
+
* queued or running. A caller that reads this right after queuing a
|
|
72
|
+
* write, and reads the same number later, holds a write nothing has
|
|
73
|
+
* cancelled, whether it is still queued or already running. Its token
|
|
74
|
+
* answers that only once the write runs, since a queued write has not been
|
|
75
|
+
* handed it yet. A different number read after the caller's write ended
|
|
76
|
+
* does not prove that write was cancelled, since a later write that
|
|
77
|
+
* cancels another queued or running write counts too. A caller that
|
|
78
|
+
* retries on it builds once more than it needs to.
|
|
74
79
|
*/
|
|
75
80
|
get writeCancellations(): number {
|
|
76
81
|
return this.cancelledWriteCount;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
override write(action: (token: CancellationToken) => MaybePromise<void>): Promise<void> {
|
|
80
|
-
|
|
85
|
+
const queued = super.write(token => {
|
|
86
|
+
this.queuedWrites--;
|
|
87
|
+
return runInWriteLockScope(() => this.runWrite(action, token));
|
|
88
|
+
});
|
|
89
|
+
// Counted after Langium's `write` has cancelled the write before it, so the new write does not count itself.
|
|
90
|
+
this.queuedWrites++;
|
|
91
|
+
return queued;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
/** Run `action` as {@link runningWrite}, so a cancellation while it runs can time it. */
|
|
@@ -96,11 +107,15 @@ export class HydraniumWorkspaceLock extends DefaultWorkspaceLock {
|
|
|
96
107
|
|
|
97
108
|
/**
|
|
98
109
|
* Counted here rather than in {@link write}, because Langium's `write` cancels
|
|
99
|
-
* through this method too. A write
|
|
100
|
-
*
|
|
110
|
+
* through this method too. A write that has ended is not counted, so the
|
|
111
|
+
* next write does not count as cancelling it. A write running when this is
|
|
112
|
+
* called has been cancelled, by this call or an earlier one, so its timer
|
|
113
|
+
* starts here.
|
|
101
114
|
*/
|
|
102
115
|
override cancelWrite(): void {
|
|
103
|
-
this.
|
|
116
|
+
if (this.queuedWrites > 0 || this.runningWrite !== undefined) {
|
|
117
|
+
this.cancelledWriteCount++;
|
|
118
|
+
}
|
|
104
119
|
const running = this.runningWrite;
|
|
105
120
|
if (running && !running.stallTimer && this.stalledWriteWarnMs > 0) {
|
|
106
121
|
running.stallTimer = this.clock.setTimer(() => this.reportStalledWrite(), this.stalledWriteWarnMs);
|
|
@@ -89,6 +89,8 @@ export interface StubDocumentBuilder
|
|
|
89
89
|
| 'finalBuildState'
|
|
90
90
|
| 'onBuildEnded'
|
|
91
91
|
| 'isInsideBuildUpdate'
|
|
92
|
+
| 'overlapsRunningBuild'
|
|
93
|
+
| 'shouldRelink'
|
|
92
94
|
| 'activeBuild'
|
|
93
95
|
| 'markNextReason'
|
|
94
96
|
| 'shouldValidateDocument'
|
|
@@ -193,6 +195,9 @@ export function makeStubDocumentBuilder(workspaceLock?: WorkspaceLock): StubDocu
|
|
|
193
195
|
shouldValidateDocument: () => Boolean(stub.updateBuildOptions.validation),
|
|
194
196
|
// The stub never runs a build of its own.
|
|
195
197
|
isInsideBuildUpdate: () => false,
|
|
198
|
+
overlapsRunningBuild: () => false,
|
|
199
|
+
// It builds no document, so a change relinks none.
|
|
200
|
+
shouldRelink: () => false,
|
|
196
201
|
activeBuild: undefined,
|
|
197
202
|
markNextReason: () => undefined,
|
|
198
203
|
get updateCalls() {
|