@hydranium/core 1.0.0-next.115 → 1.0.0-next.117
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/README.md +3 -1
- package/lib/documents/ast-document-manager.d.ts +52 -2
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +67 -5
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/client-session-errors.d.ts +44 -0
- package/lib/documents/client-session-errors.d.ts.map +1 -0
- package/lib/documents/client-session-errors.js +57 -0
- package/lib/documents/client-session-errors.js.map +1 -0
- package/lib/documents/client-session-registry.d.ts +118 -0
- package/lib/documents/client-session-registry.d.ts.map +1 -0
- package/lib/documents/client-session-registry.js +171 -0
- package/lib/documents/client-session-registry.js.map +1 -0
- package/lib/documents/hydranium-text-documents.d.ts +69 -17
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +135 -44
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/index.d.ts +2 -0
- package/lib/documents/index.d.ts.map +1 -1
- package/lib/documents/index.js +2 -0
- package/lib/documents/index.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +8 -7
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +35 -27
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +120 -0
- package/lib/langium/model-service/client-session.d.ts.map +1 -0
- package/lib/langium/model-service/client-session.js +175 -0
- package/lib/langium/model-service/client-session.js.map +1 -0
- package/lib/langium/model-service/index.d.ts +1 -0
- package/lib/langium/model-service/index.d.ts.map +1 -1
- package/lib/langium/model-service/index.js +1 -0
- package/lib/langium/model-service/index.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +57 -1
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +123 -9
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts +6 -0
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +20 -3
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +4 -3
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +3 -0
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +13 -5
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +45 -14
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +107 -6
- package/src/documents/client-session-errors.ts +54 -0
- package/src/documents/client-session-registry.ts +211 -0
- package/src/documents/hydranium-text-documents.ts +150 -47
- package/src/documents/index.ts +2 -0
- package/src/langium/integrity/integrity-service.ts +36 -26
- package/src/langium/model-service/client-session.ts +257 -0
- package/src/langium/model-service/index.ts +1 -0
- package/src/langium/model-service/model-service.ts +151 -9
- package/src/lsp/hydranium-document-update-handler.ts +22 -4
- package/src/testing/stub-ast-document-manager.ts +7 -2
- package/src/testing/stub-hydranium-text-documents.ts +62 -18
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// on lsp-server, contradicting the peer architecture.
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
22
22
|
import { NormalizedTextDocuments } from '@hydranium/langium/lsp';
|
|
23
|
-
import { UriUtils } from '@hydranium/langium';
|
|
23
|
+
import { type URI, UriUtils } from '@hydranium/langium';
|
|
24
24
|
import { type ServerSharedServices } from '../langium/module.js';
|
|
25
25
|
import {
|
|
26
26
|
type ApplyWorkspaceEditResult,
|
|
@@ -49,6 +49,8 @@ import { type DocumentUri, TextDocument, type TextDocumentContentChangeEvent } f
|
|
|
49
49
|
import { type CanonicalUri, type LanguageClientUri, asLanguageClientUri, DisposableCollection, type Tracer } from '@hydranium/protocol';
|
|
50
50
|
import { type LogNameOptions } from '../langium/diagnostics/logger.js';
|
|
51
51
|
import { LANGUAGE_CLIENT_ID } from './client-ids.js';
|
|
52
|
+
import { INTEGRITY_CLIENT_ID } from '../langium/integrity/integrity-rule.js';
|
|
53
|
+
import { type ClientSessionClosedEvent, ClientSessionRegistry, type OpenOptions } from './client-session-registry.js';
|
|
52
54
|
import { isFullReplace, LanguageClientTextShadow } from './language-client-text-shadow.js';
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -97,8 +99,6 @@ export interface HydraniumTextDocumentsOptions<T extends TextDocument = TextDocu
|
|
|
97
99
|
* change that caused it.
|
|
98
100
|
*/
|
|
99
101
|
export interface DocumentTrackingRecord {
|
|
100
|
-
/** Client ids currently holding this document open (multi-client membership). */
|
|
101
|
-
readonly clients: Set<string>;
|
|
102
102
|
/** Author of each version, sparse-indexed by the SHARED (server-assigned) version number. */
|
|
103
103
|
readonly versionAuthors: string[];
|
|
104
104
|
/**
|
|
@@ -252,7 +252,8 @@ function contentHash(text: string): string {
|
|
|
252
252
|
* Multi-client text-document tracking on top of Langium's `NormalizedTextDocuments`.
|
|
253
253
|
*
|
|
254
254
|
* Adds the framework features used by the integrity, model-server, and GLSP layers:
|
|
255
|
-
* - Per-document client membership (multiple clients can attach to the same
|
|
255
|
+
* - Per-document client membership (multiple clients can attach to the same
|
|
256
|
+
* URI) and the client-session table, both kept by {@link __sessions}.
|
|
256
257
|
* - A SERVER-OWNED shared version sequence: per-URI, monotonic across
|
|
257
258
|
* close/reopen cycles, advancing exactly when the synced content changes.
|
|
258
259
|
* Client-declared version ids (Monaco's buffer numbering) feed only a
|
|
@@ -270,15 +271,20 @@ function contentHash(text: string): string {
|
|
|
270
271
|
*/
|
|
271
272
|
export class HydraniumTextDocuments<T extends TextDocument = TextDocument> extends NormalizedTextDocuments<T> {
|
|
272
273
|
/**
|
|
273
|
-
* Per-URI client-facing tracking ({@link DocumentTrackingRecord})
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* {@link __shadow} (its own abstraction); the parsed-document store stays in
|
|
278
|
-
* the inherited `__syncedDocuments`.
|
|
274
|
+
* Per-URI client-facing tracking ({@link DocumentTrackingRecord}), keyed by
|
|
275
|
+
* canonical URI. One record per URI, so dropping it clears every axis at
|
|
276
|
+
* once. Which client has the document open is kept apart, in
|
|
277
|
+
* {@link __sessions}.
|
|
279
278
|
*/
|
|
280
279
|
protected __documents = new Map<CanonicalUri, DocumentTrackingRecord>();
|
|
281
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Which client has which document open, and which client ids are registered
|
|
283
|
+
* sessions. Every open-state predicate on this class reads it, so an open
|
|
284
|
+
* recorded anywhere else is invisible to the last-close transition.
|
|
285
|
+
*/
|
|
286
|
+
protected readonly __sessions = new ClientSessionRegistry();
|
|
287
|
+
|
|
282
288
|
/**
|
|
283
289
|
* Per-URI shared-version continuity across close/reopen cycles
|
|
284
290
|
* ({@link VersionSequence}), written at last-client close and consulted by
|
|
@@ -496,7 +502,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
496
502
|
// the update) from "incoming version older than ours" (stale race).
|
|
497
503
|
const reason =
|
|
498
504
|
lastSeen === td.version ? `already at version ${lastSeen}` : `incoming version ${td.version} older than current ${lastSeen}`;
|
|
499
|
-
this.logUri(uri, `Ignore update by ${clientId}: ${reason}`, 'debug');
|
|
505
|
+
this.logUri(uri, `Ignore update by ${this.formatClientId(clientId)}: ${reason}`, 'debug');
|
|
500
506
|
return;
|
|
501
507
|
}
|
|
502
508
|
record.clientVersions.set(clientId, td.version);
|
|
@@ -562,7 +568,10 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
562
568
|
return;
|
|
563
569
|
}
|
|
564
570
|
}
|
|
565
|
-
this.log(
|
|
571
|
+
this.log(
|
|
572
|
+
document.uri,
|
|
573
|
+
`Update to version ${document.version} by ${this.formatClientId(clientId)}${changed ? '' : ' (content unchanged)'}`
|
|
574
|
+
);
|
|
566
575
|
this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
|
|
567
576
|
}
|
|
568
577
|
}
|
|
@@ -596,22 +605,23 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
596
605
|
this.__syncedDocuments.set(key, document);
|
|
597
606
|
this.setAuthor(key, document.version, clientId);
|
|
598
607
|
}
|
|
599
|
-
this.log(
|
|
608
|
+
this.log(
|
|
609
|
+
document.uri,
|
|
610
|
+
`Update to version ${document.version} by ${this.formatClientId(clientId)}${changed ? '' : ' (content unchanged)'}`
|
|
611
|
+
);
|
|
600
612
|
this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
|
|
601
613
|
return document.version;
|
|
602
614
|
}
|
|
603
615
|
|
|
604
616
|
public notifyDidCloseTextDocument(event: DidCloseTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void {
|
|
605
617
|
const uri = this.documentKey(event.textDocument.uri);
|
|
606
|
-
if (!this.
|
|
618
|
+
if (!this.__sessions.removeOpen(uri, clientId)) {
|
|
607
619
|
return;
|
|
608
620
|
}
|
|
609
|
-
|
|
610
|
-
closingRecord?.clients.delete(clientId);
|
|
611
|
-
closingRecord?.clientVersions.delete(clientId);
|
|
621
|
+
this.__documents.get(uri)?.clientVersions.delete(clientId);
|
|
612
622
|
const syncedDocument = this.__syncedDocuments.get(uri);
|
|
613
623
|
if (syncedDocument !== undefined) {
|
|
614
|
-
this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${clientId}`);
|
|
624
|
+
this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${this.formatClientId(clientId)}`);
|
|
615
625
|
this.__onDidClose.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
616
626
|
|
|
617
627
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
@@ -623,7 +633,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
623
633
|
this.__shadow.invalidate(droppedUri);
|
|
624
634
|
this.__pendingPushes.delete(droppedUri);
|
|
625
635
|
}
|
|
626
|
-
if (!this.
|
|
636
|
+
if (!this.__sessions.isOpen(uri)) {
|
|
627
637
|
// Last client closed the document; delete sync state. The downstream
|
|
628
638
|
// "rebuild from disk for file URIs / drop from index for ephemeral
|
|
629
639
|
// URIs" decision lives in `HydraniumDocumentUpdateHandler.didCloseDocument`,
|
|
@@ -668,7 +678,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
668
678
|
public notifyDidSaveTextDocument(event: DidSaveTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void {
|
|
669
679
|
const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
|
|
670
680
|
if (syncedDocument !== undefined) {
|
|
671
|
-
this.log(syncedDocument.uri, `Saved synced document: ${syncedDocument.version} by ${clientId}`);
|
|
681
|
+
this.log(syncedDocument.uri, `Saved synced document: ${syncedDocument.version} by ${this.formatClientId(clientId)}`);
|
|
672
682
|
this.__onDidSave.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
673
683
|
}
|
|
674
684
|
}
|
|
@@ -698,9 +708,9 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
698
708
|
return;
|
|
699
709
|
}
|
|
700
710
|
let document = this.__syncedDocuments.get(uri);
|
|
711
|
+
const existingClients = this.__sessions.clientsOf(uri);
|
|
712
|
+
this.__sessions.addOpen(uri, clientId);
|
|
701
713
|
const record = this.trackingFor(uri);
|
|
702
|
-
const existingClients = [...record.clients];
|
|
703
|
-
record.clients.add(clientId);
|
|
704
714
|
// Baseline the per-client staleness guard at the version id the client
|
|
705
715
|
// declared for its own buffer (client-owned per LSP).
|
|
706
716
|
record.clientVersions.set(clientId, td.version);
|
|
@@ -723,7 +733,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
723
733
|
const sequence = this.__versionSequences.get(uri);
|
|
724
734
|
const version =
|
|
725
735
|
sequence === undefined ? td.version : sequence.contentHash === contentHash(text) ? sequence.version : sequence.version + 1;
|
|
726
|
-
this.log(uri, `Open document: Version ${version} by ${clientId} [first client${source}]`);
|
|
736
|
+
this.log(uri, `Open document: Version ${version} by ${this.formatClientId(clientId)} [first client${source}]`);
|
|
727
737
|
document = this.configuration.create(uri, td.languageId, version, text);
|
|
728
738
|
this.__syncedDocuments.set(uri, document);
|
|
729
739
|
this.setAuthor(uri, version, clientId);
|
|
@@ -786,9 +796,9 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
786
796
|
if (!document || this.isOpenInClient(key, clientId)) {
|
|
787
797
|
return false;
|
|
788
798
|
}
|
|
799
|
+
const existingClients = this.__sessions.clientsOf(key);
|
|
800
|
+
this.__sessions.addOpen(key, clientId);
|
|
789
801
|
const record = this.trackingFor(key);
|
|
790
|
-
const existingClients = [...record.clients];
|
|
791
|
-
record.clients.add(clientId);
|
|
792
802
|
record.clientVersions.set(clientId, document.version);
|
|
793
803
|
this.logClientJoined(key, clientId, document.version, existingClients);
|
|
794
804
|
return true;
|
|
@@ -797,8 +807,8 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
797
807
|
protected logClientJoined(uri: DocumentUri, clientId: string, version: number, existingClients: readonly string[]): void {
|
|
798
808
|
this.log(
|
|
799
809
|
uri,
|
|
800
|
-
`Attach client: ${clientId} joined existing document (version ${version}, ` +
|
|
801
|
-
`now open in: ${[...existingClients, clientId].join(', ')})`
|
|
810
|
+
`Attach client: ${this.formatClientId(clientId)} joined existing document (version ${version}, ` +
|
|
811
|
+
`now open in: ${[...existingClients, clientId].map(id => this.formatClientId(id)).join(', ')})`
|
|
802
812
|
);
|
|
803
813
|
}
|
|
804
814
|
|
|
@@ -808,7 +818,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
808
818
|
// Trigger a (re-)build by firing a change event.
|
|
809
819
|
const timer = this.startTimerForUri(
|
|
810
820
|
syncedDocument.uri,
|
|
811
|
-
`Refresh synced document: Version ${syncedDocument.version} by ${clientId}`
|
|
821
|
+
`Refresh synced document: Version ${syncedDocument.version} by ${this.formatClientId(clientId)}`
|
|
812
822
|
);
|
|
813
823
|
this.__onDidChangeContent.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
814
824
|
timer.dispose();
|
|
@@ -846,7 +856,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
846
856
|
protected trackingFor(uri: CanonicalUri): DocumentTrackingRecord {
|
|
847
857
|
let record = this.__documents.get(uri);
|
|
848
858
|
if (!record) {
|
|
849
|
-
record = {
|
|
859
|
+
record = { versionAuthors: [], clientVersions: new Map() };
|
|
850
860
|
this.__documents.set(uri, record);
|
|
851
861
|
}
|
|
852
862
|
return record;
|
|
@@ -946,10 +956,12 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
946
956
|
* its own numbering, so requiring the two to agree would refuse every commit
|
|
947
957
|
* on the path this exists for. Content is the thing both sides can be held to.
|
|
948
958
|
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
959
|
+
* A changed text is a new version authored by {@link INTEGRITY_CLIENT_ID}.
|
|
960
|
+
* Kept at the old version, the repair is invisible to every reader keyed on
|
|
961
|
+
* versions: a write based on the unrepaired version passes the based-on gate
|
|
962
|
+
* and replaces the repair, and an echo filter credits the repair to the
|
|
963
|
+
* client whose edit it corrected. No change event is fired: the repair
|
|
964
|
+
* rides the build already under way, and an event would re-enter it.
|
|
953
965
|
*/
|
|
954
966
|
commitRepair(uri: DocumentUri, parsedFrom: string, repaired: string): RepairCommit<T> {
|
|
955
967
|
const key = this.documentKey(uri);
|
|
@@ -961,11 +973,16 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
961
973
|
this.logUri(key, 'Refuse repair commit: the open document moved on from the text the AST was parsed from', 'debug');
|
|
962
974
|
return { status: 'stale' };
|
|
963
975
|
}
|
|
976
|
+
if (repaired === parsedFrom) {
|
|
977
|
+
return { status: 'committed', document };
|
|
978
|
+
}
|
|
964
979
|
// Reassigned rather than mutated in place: the default configuration
|
|
965
980
|
// updates and returns the SAME instance, but an adopter-supplied one may
|
|
966
981
|
// return a new object, and the store must end up holding whichever it is.
|
|
967
|
-
const updated = this.configuration.update(document, [{ text: repaired }], document.version);
|
|
982
|
+
const updated = this.configuration.update(document, [{ text: repaired }], document.version + 1);
|
|
968
983
|
this.__syncedDocuments.set(key, updated);
|
|
984
|
+
this.setAuthor(key, updated.version, INTEGRITY_CLIENT_ID);
|
|
985
|
+
this.log(updated.uri, `Update to version ${updated.version} by ${this.formatClientId(INTEGRITY_CLIENT_ID)} (repair)`);
|
|
969
986
|
return { status: 'committed', document: updated };
|
|
970
987
|
}
|
|
971
988
|
|
|
@@ -989,17 +1006,17 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
989
1006
|
* True iff any client still holds `uri` open. Distinct from {@link isOpen},
|
|
990
1007
|
* which reads `__syncedDocuments` — that map is cleared only AFTER the
|
|
991
1008
|
* `onDidClose` event fires for the last client, so `isOpen` returns `true`
|
|
992
|
-
* during the close event itself. `isOpenInAnyClient` reads the
|
|
993
|
-
*
|
|
1009
|
+
* during the close event itself. `isOpenInAnyClient` reads the open table
|
|
1010
|
+
* ({@link __sessions}), which is updated BEFORE the fire, so an `onDidClose`
|
|
994
1011
|
* subscriber that finds this `false` knows the last client just closed and a
|
|
995
1012
|
* disk re-read / rebuild can proceed.
|
|
996
1013
|
*/
|
|
997
1014
|
isOpenInAnyClient(uri: DocumentUri): boolean {
|
|
998
|
-
return
|
|
1015
|
+
return this.__sessions.isOpen(this.documentKey(uri));
|
|
999
1016
|
}
|
|
1000
1017
|
|
|
1001
1018
|
isOpenInClient(uri: DocumentUri, client: string): boolean {
|
|
1002
|
-
return
|
|
1019
|
+
return this.__sessions.isOpenIn(this.documentKey(uri), client);
|
|
1003
1020
|
}
|
|
1004
1021
|
|
|
1005
1022
|
isOpenInLanguageClient(uri: DocumentUri): boolean {
|
|
@@ -1010,14 +1027,14 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1010
1027
|
}
|
|
1011
1028
|
|
|
1012
1029
|
isOnlyOpenInClient(uri: DocumentUri, client: string): boolean {
|
|
1013
|
-
const
|
|
1014
|
-
return
|
|
1030
|
+
const clients = this.__sessions.clientsOf(this.documentKey(uri));
|
|
1031
|
+
return clients.length === 1 && clients[0] === client;
|
|
1015
1032
|
}
|
|
1016
1033
|
|
|
1017
1034
|
/**
|
|
1018
1035
|
* Every document currently held open by at least one client, with the client
|
|
1019
|
-
* ids holding it. Reads the same
|
|
1020
|
-
*
|
|
1036
|
+
* ids holding it. Reads the same open table as {@link isOpenInAnyClient}, so
|
|
1037
|
+
* it reflects a last-client close immediately.
|
|
1021
1038
|
*
|
|
1022
1039
|
* Diagnostics-oriented — the server-state snapshot lists these so an operator
|
|
1023
1040
|
* can see WHY a document is pinned: a document that lingers here after its
|
|
@@ -1025,13 +1042,99 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1025
1042
|
* under a shedding policy, explains why its CST is never shed).
|
|
1026
1043
|
*/
|
|
1027
1044
|
openDocuments(): OpenDocument[] {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1045
|
+
return this.__sessions.openDocuments();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Fires once a client session has ended, after every document it had open was closed. */
|
|
1049
|
+
get onDidCloseSession(): Event<ClientSessionClosedEvent> {
|
|
1050
|
+
return this.__sessions.onDidCloseSession;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Start a client session under `clientId`. Throws `DuplicateClientIdError`
|
|
1055
|
+
* where {@link ClientSessionRegistry.register} refuses the id.
|
|
1056
|
+
*/
|
|
1057
|
+
registerSession(clientId: string): void {
|
|
1058
|
+
this.__sessions.register(clientId);
|
|
1059
|
+
this.tracer.info(`Session started: ${this.formatClientId(clientId)}`);
|
|
1060
|
+
this.tracer.trace(`Session started: ${clientId}`);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* End the client session `clientId`: close every document it has open, then
|
|
1065
|
+
* free the id. Immediate — each close runs the ordinary close path before
|
|
1066
|
+
* this returns. A no-op for an id that is not a registered session.
|
|
1067
|
+
*/
|
|
1068
|
+
closeSession(clientId: string): void {
|
|
1069
|
+
if (!this.__sessions.isRegistered(clientId)) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
for (const uri of this.__sessions.beginClose(clientId)) {
|
|
1074
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri } }, clientId);
|
|
1075
|
+
}
|
|
1076
|
+
} finally {
|
|
1077
|
+
this.__sessions.unregister(clientId);
|
|
1078
|
+
this.tracer.info(`Session closed: ${this.formatClientId(clientId)}`);
|
|
1079
|
+
this.tracer.trace(`Session closed: ${clientId}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** The options `clientId` opened `uri` with, or `undefined` when it gave none or does not have it open. */
|
|
1084
|
+
openOptions(uri: DocumentUri, clientId: string): OpenOptions | undefined {
|
|
1085
|
+
return this.__sessions.openOptions(this.documentKey(uri), clientId);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** Replace the options of an existing open. Does nothing when `clientId` does not have `uri` open. */
|
|
1089
|
+
setOpenOptions(uri: DocumentUri, clientId: string, options: OpenOptions | undefined): void {
|
|
1090
|
+
this.__sessions.setOpenOptions(this.documentKey(uri), clientId, options);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* The file behind `uri` was deleted: close every open of it except the
|
|
1095
|
+
* language client's.
|
|
1096
|
+
*
|
|
1097
|
+
* The editor's open is left alone because the editor owns it: it keeps the
|
|
1098
|
+
* buffer of a deleted file and goes on sending changes for it, and a store
|
|
1099
|
+
* that had closed the document would drop every one of them until the editor
|
|
1100
|
+
* reopened.
|
|
1101
|
+
*/
|
|
1102
|
+
notifyDocumentDeleted(uri: DocumentUri): void {
|
|
1103
|
+
const key = this.documentKey(uri);
|
|
1104
|
+
for (const clientId of this.__sessions.clientsOf(key)) {
|
|
1105
|
+
if (clientId !== LANGUAGE_CLIENT_ID) {
|
|
1106
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
|
|
1032
1107
|
}
|
|
1033
1108
|
}
|
|
1034
|
-
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Remove the document for `uri`, closing every open of it first — the
|
|
1113
|
+
* editor's included, since the document it would keep editing is gone.
|
|
1114
|
+
*
|
|
1115
|
+
* The base removes the synced document without consulting the open table,
|
|
1116
|
+
* which leaves every open recorded against a document that no longer exists:
|
|
1117
|
+
* the store then answers that the URI is open, and the next open attaches to
|
|
1118
|
+
* nothing.
|
|
1119
|
+
*/
|
|
1120
|
+
override delete(uri: string | URI | T): void {
|
|
1121
|
+
const key = this.documentKey((typeof uri === 'object' && 'uri' in uri ? uri.uri : uri).toString());
|
|
1122
|
+
for (const clientId of this.__sessions.clientsOf(key)) {
|
|
1123
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
|
|
1124
|
+
}
|
|
1125
|
+
super.delete(key);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* A client id as log lines print it. An id containing `#` is cut eight
|
|
1130
|
+
* characters after its last `#`, which leaves a minted `label#uuid` as the
|
|
1131
|
+
* label and enough of the UUID to tell sessions apart; an id without `#`
|
|
1132
|
+
* prints whole. The full id goes out at trace level wherever a session
|
|
1133
|
+
* starts or ends.
|
|
1134
|
+
*/
|
|
1135
|
+
protected formatClientId(clientId: string): string {
|
|
1136
|
+
const separator = clientId.lastIndexOf('#');
|
|
1137
|
+
return separator < 0 ? clientId : clientId.slice(0, separator + 9);
|
|
1035
1138
|
}
|
|
1036
1139
|
|
|
1037
1140
|
/**
|
package/src/documents/index.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
export * from './client-ids.js';
|
|
11
|
+
export * from './client-session-errors.js';
|
|
12
|
+
export * from './client-session-registry.js';
|
|
11
13
|
export * from './language-client-text-shadow.js';
|
|
12
14
|
export * from './hydranium-text-documents.js';
|
|
13
15
|
export * from './ast-document-manager.js';
|
|
@@ -368,9 +368,11 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
368
368
|
return;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
371
|
+
// A committed repair is a new version; a document no client has open keeps
|
|
372
|
+
// its number here, and the re-versioning below moves it once the build
|
|
373
|
+
// has reconciled. Through the store's `update`, so the `instanceof
|
|
374
|
+
// FullTextDocument` gate in the bare `TextDocument.update` does not trip
|
|
375
|
+
// on adopter-custom text-document types.
|
|
374
376
|
const textDocument =
|
|
375
377
|
commit.status === 'committed' ? commit.document : this.textDocuments.update(document.textDocument, [{ text: newText }], version);
|
|
376
378
|
|
|
@@ -504,8 +506,13 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
504
506
|
|
|
505
507
|
// Closed files, silent mode: write to disk directly.
|
|
506
508
|
if (this.syncMode === 'silent') {
|
|
509
|
+
const repaired = document.getText();
|
|
507
510
|
// Async write so the integrity pass does not block the event loop on a slow filesystem.
|
|
508
|
-
|
|
511
|
+
// In the URI's disk queue, behind any save already queued: written
|
|
512
|
+
// beside one, the older repair can land last and stay on disk.
|
|
513
|
+
await this.services.shared.workspace.AstDocumentManager.queueDiskTask(document.uri, () =>
|
|
514
|
+
this.fileSystemProvider.writeFile(UriUtils.toUri(document.uri), repaired)
|
|
515
|
+
);
|
|
509
516
|
return;
|
|
510
517
|
}
|
|
511
518
|
|
|
@@ -518,32 +525,35 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
518
525
|
* the text the repair was computed from — so the write carries the repair
|
|
519
526
|
* and nothing a holder could still discard.
|
|
520
527
|
*
|
|
521
|
-
* The
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
+
* The read, the compare and the write run as one task in the URI's disk
|
|
529
|
+
* queue, so no save of the file lands between them. The store is compared
|
|
530
|
+
* again once the read returns, and the write is skipped if it no longer
|
|
531
|
+
* holds the repair: an edit does not wait for the queue, and writing the
|
|
532
|
+
* older repair then puts text on disk that no participant holds. The
|
|
533
|
+
* skipped repair loses nothing: the edit drives a build of its own, and a
|
|
534
|
+
* save writes the store's text. A file that cannot be read is left alone
|
|
535
|
+
* too: there is no evidence it matches.
|
|
528
536
|
*/
|
|
529
537
|
protected async persistIfDiskMatches(document: TextDocument, parsedFrom: string): Promise<void> {
|
|
530
538
|
const uri = UriUtils.toUri(document.uri);
|
|
531
539
|
const repaired = document.getText();
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
this.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
540
|
+
await this.services.shared.workspace.AstDocumentManager.queueDiskTask(document.uri, async () => {
|
|
541
|
+
let onDisk: string;
|
|
542
|
+
try {
|
|
543
|
+
onDisk = await this.fileSystemProvider.readFile(uri);
|
|
544
|
+
} catch (error: unknown) {
|
|
545
|
+
this.tracer.with(document.uri).debug(`Held document unreadable on disk, repair left to the next save: ${String(error)}`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (onDisk !== parsedFrom) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (this.textDocuments.get(document.uri)?.getText() !== repaired) {
|
|
552
|
+
this.tracer.with(document.uri).debug('Held document changed while disk was read, repair left to that change');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
await this.fileSystemProvider.writeFile(uri, repaired);
|
|
556
|
+
});
|
|
547
557
|
}
|
|
548
558
|
|
|
549
559
|
/**
|