@hydranium/core 1.0.0-next.333 → 1.0.0-next.335
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/lsp/edit-requests-guard.d.ts +31 -0
- package/lib/lsp/edit-requests-guard.d.ts.map +1 -0
- package/lib/lsp/edit-requests-guard.js +125 -0
- package/lib/lsp/edit-requests-guard.js.map +1 -0
- package/lib/lsp/index.d.ts +2 -1
- package/lib/lsp/index.d.ts.map +1 -1
- package/lib/lsp/index.js +2 -1
- package/lib/lsp/index.js.map +1 -1
- package/lib/lsp/start-language-server.d.ts +9 -8
- package/lib/lsp/start-language-server.d.ts.map +1 -1
- package/lib/lsp/start-language-server.js +11 -8
- package/lib/lsp/start-language-server.js.map +1 -1
- package/package.json +5 -5
- package/src/lsp/edit-requests-guard.ts +152 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/start-language-server.ts +11 -8
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
import { type LangiumSharedServices } from '@hydranium/langium/lsp';
|
|
10
|
+
import { type Connection } from 'vscode-languageserver';
|
|
11
|
+
/**
|
|
12
|
+
* Refuse with `ContentModified` a request whose answer edits documents, when
|
|
13
|
+
* the editor of the request's document is behind the store as it arrives, or
|
|
14
|
+
* when a document the answer edits changed while it was computed, by another
|
|
15
|
+
* client, by a rebuild of a closed document or by its release, or its editor
|
|
16
|
+
* is then behind.
|
|
17
|
+
*
|
|
18
|
+
* The editor applies the edits by position to the text it holds, and sent the
|
|
19
|
+
* request's position in it, so an answer computed on other text rewrites the
|
|
20
|
+
* wrong characters. The check runs on the answer, since the documents a rename
|
|
21
|
+
* or code action edits are only known there, and on the connection, so an
|
|
22
|
+
* adopter's provider for these requests is covered. Edits that arrive by a
|
|
23
|
+
* resolve request, a file operation, `willSaveWaitUntil`, an inlay hint or a
|
|
24
|
+
* command's `workspace/applyEdit` are not.
|
|
25
|
+
*
|
|
26
|
+
* Read-only answers are not refused, since a refusal would blank them. An
|
|
27
|
+
* editor ahead of the store does not exist, as its changes reach the store
|
|
28
|
+
* before any request it sends after them.
|
|
29
|
+
*/
|
|
30
|
+
export declare function guardEditRequests(connection: Connection, services: LangiumSharedServices): void;
|
|
31
|
+
//# sourceMappingURL=edit-requests-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-requests-guard.d.ts","sourceRoot":"","sources":["../../src/lsp/edit-requests-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAGlF,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACJ,KAAK,UAAU,EAMjB,MAAM,uBAAuB,CAAC;AAS/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAgG/F"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
import { LSPErrorCodes, ResponseError } from 'vscode-languageserver';
|
|
10
|
+
import { LANGUAGE_CLIENT_ID } from '../documents/client-ids.js';
|
|
11
|
+
import { HydraniumTextDocuments } from '../documents/hydranium-text-documents.js';
|
|
12
|
+
const wrappedConnections = new WeakSet();
|
|
13
|
+
/**
|
|
14
|
+
* Refuse with `ContentModified` a request whose answer edits documents, when
|
|
15
|
+
* the editor of the request's document is behind the store as it arrives, or
|
|
16
|
+
* when a document the answer edits changed while it was computed, by another
|
|
17
|
+
* client, by a rebuild of a closed document or by its release, or its editor
|
|
18
|
+
* is then behind.
|
|
19
|
+
*
|
|
20
|
+
* The editor applies the edits by position to the text it holds, and sent the
|
|
21
|
+
* request's position in it, so an answer computed on other text rewrites the
|
|
22
|
+
* wrong characters. The check runs on the answer, since the documents a rename
|
|
23
|
+
* or code action edits are only known there, and on the connection, so an
|
|
24
|
+
* adopter's provider for these requests is covered. Edits that arrive by a
|
|
25
|
+
* resolve request, a file operation, `willSaveWaitUntil`, an inlay hint or a
|
|
26
|
+
* command's `workspace/applyEdit` are not.
|
|
27
|
+
*
|
|
28
|
+
* Read-only answers are not refused, since a refusal would blank them. An
|
|
29
|
+
* editor ahead of the store does not exist, as its changes reach the store
|
|
30
|
+
* before any request it sends after them.
|
|
31
|
+
*/
|
|
32
|
+
export function guardEditRequests(connection, services) {
|
|
33
|
+
const store = services.workspace.TextDocuments;
|
|
34
|
+
if (wrappedConnections.has(connection) || !(store instanceof HydraniumTextDocuments)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
wrappedConnections.add(connection);
|
|
38
|
+
// A Hydranium store implies the Hydranium shared services.
|
|
39
|
+
const keyOf = (uri) => services.workspace.DocumentUriPolicy.canonicalUri(uri);
|
|
40
|
+
const versions = () => new Map(store.all().map(document => [document.uri, document.version]));
|
|
41
|
+
// A closed document records no author per version, so a rebuild of it
|
|
42
|
+
// while an answer is computed is what refuses the answer. A release clears
|
|
43
|
+
// the authors, so a reopen would attribute another client's change to the
|
|
44
|
+
// editor, and refuses it as well.
|
|
45
|
+
const watching = new Set();
|
|
46
|
+
services.workspace.DocumentBuilder.onUpdate((changed, deleted) => {
|
|
47
|
+
if (watching.size === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const keys = [...changed, ...deleted].map(keyOf);
|
|
51
|
+
for (const watched of watching) {
|
|
52
|
+
keys.forEach(key => watched.rebuilt.add(key));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
store.onDidReleaseDocument(event => {
|
|
56
|
+
for (const watched of watching) {
|
|
57
|
+
watched.released.add(event.uri);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// The editor's own changes are not counted: it discards or re-filters an
|
|
61
|
+
// answer that predates them, and counting them would blank completion
|
|
62
|
+
// while the user types.
|
|
63
|
+
const changedByAnother = (uri, from, to) => {
|
|
64
|
+
if (from === undefined) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
for (let version = from + 1; version <= to; version++) {
|
|
68
|
+
if (store.getAuthor(uri, version) !== LANGUAGE_CLIENT_ID) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
const refused = () => new ResponseError(LSPErrorCodes.ContentModified, 'The editor does not hold the text the answer was computed on');
|
|
75
|
+
// The request's position is in the editor's text, which an answer
|
|
76
|
+
// computed on the store's would be applied past.
|
|
77
|
+
const behind = (uri) => store.isLanguageClientBehind(uri, store.version(uri));
|
|
78
|
+
/** Watches the documents an answer may edit while it is computed; dispose it after. */
|
|
79
|
+
const watch = () => {
|
|
80
|
+
const computedOn = versions();
|
|
81
|
+
const watched = { rebuilt: new Set(), released: new Set() };
|
|
82
|
+
watching.add(watched);
|
|
83
|
+
return {
|
|
84
|
+
changed: uris => [...new Set(uris)].some(uri => {
|
|
85
|
+
const document = store.get(uri);
|
|
86
|
+
const key = keyOf(uri);
|
|
87
|
+
const changed = watched.released.has(key) ||
|
|
88
|
+
(document === undefined
|
|
89
|
+
? watched.rebuilt.has(key)
|
|
90
|
+
: changedByAnother(uri, computedOn.get(document.uri), document.version));
|
|
91
|
+
return changed || behind(uri);
|
|
92
|
+
}),
|
|
93
|
+
dispose: () => watching.delete(watched)
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const guard = (register, edited = () => []) => handler => register(async (params, ...rest) => {
|
|
97
|
+
const uri = params.textDocument.uri;
|
|
98
|
+
if (behind(uri)) {
|
|
99
|
+
return refused();
|
|
100
|
+
}
|
|
101
|
+
const answer = watch();
|
|
102
|
+
try {
|
|
103
|
+
const result = await handler(params, ...rest);
|
|
104
|
+
return result instanceof ResponseError || !answer.changed([uri, ...edited(result)]) ? result : refused();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
answer.dispose();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
connection.onCompletion = guard(connection.onCompletion.bind(connection));
|
|
111
|
+
connection.onDocumentFormatting = guard(connection.onDocumentFormatting.bind(connection));
|
|
112
|
+
connection.onDocumentRangeFormatting = guard(connection.onDocumentRangeFormatting.bind(connection));
|
|
113
|
+
connection.onDocumentOnTypeFormatting = guard(connection.onDocumentOnTypeFormatting.bind(connection));
|
|
114
|
+
connection.onRenameRequest = guard(connection.onRenameRequest.bind(connection), editedUris);
|
|
115
|
+
connection.onCodeAction = guard(connection.onCodeAction.bind(connection), actions => (actions ?? []).flatMap(action => ('edit' in action && action.edit ? [...editedUris(action.edit)] : [])));
|
|
116
|
+
}
|
|
117
|
+
function* editedUris(edit) {
|
|
118
|
+
yield* Object.keys(edit?.changes ?? {});
|
|
119
|
+
for (const change of edit?.documentChanges ?? []) {
|
|
120
|
+
if ('textDocument' in change) {
|
|
121
|
+
yield change.textDocument.uri;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=edit-requests-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-requests-guard.js","sourceRoot":"","sources":["../../src/lsp/edit-requests-guard.ts"],"names":[],"mappings":"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"}
|
package/lib/lsp/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './completion/hydranium-completion-provider.js';
|
|
|
10
10
|
export * from './lsp-latency.js';
|
|
11
11
|
export * from './connection-features.js';
|
|
12
12
|
export * from './diagnostics-connection.js';
|
|
13
|
+
export * from './edit-requests-guard.js';
|
|
13
14
|
export * from './semantic-token-provider.js';
|
|
14
15
|
export * from './language-module.js';
|
|
15
16
|
export * from './shared-module.js';
|
|
@@ -20,7 +21,7 @@ export * from './save-text-document.js';
|
|
|
20
21
|
* head: framework overrides + the start function live behind one
|
|
21
22
|
* `@hydranium/core/lsp` subpath, mirroring Langium's own `langium/lsp`
|
|
22
23
|
* convention. Signature-compatible with Langium's `startLanguageServer`, plus
|
|
23
|
-
*
|
|
24
|
+
* the composition check and the connection guards listed in its doc.
|
|
24
25
|
*/
|
|
25
26
|
export * from './start-language-server.js';
|
|
26
27
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/lsp/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lsp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,+CAA+C,CAAC;AAC9D,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wCAAwC,CAAC;AACvD,cAAc,yBAAyB,CAAC;AAExC;;;;;;GAMG;AACH,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lsp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,+CAA+C,CAAC;AAC9D,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wCAAwC,CAAC;AACvD,cAAc,yBAAyB,CAAC;AAExC;;;;;;GAMG;AACH,cAAc,4BAA4B,CAAC"}
|
package/lib/lsp/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from './completion/hydranium-completion-provider.js';
|
|
|
10
10
|
export * from './lsp-latency.js';
|
|
11
11
|
export * from './connection-features.js';
|
|
12
12
|
export * from './diagnostics-connection.js';
|
|
13
|
+
export * from './edit-requests-guard.js';
|
|
13
14
|
export * from './semantic-token-provider.js';
|
|
14
15
|
export * from './language-module.js';
|
|
15
16
|
export * from './shared-module.js';
|
|
@@ -20,7 +21,7 @@ export * from './save-text-document.js';
|
|
|
20
21
|
* head: framework overrides + the start function live behind one
|
|
21
22
|
* `@hydranium/core/lsp` subpath, mirroring Langium's own `langium/lsp`
|
|
22
23
|
* convention. Signature-compatible with Langium's `startLanguageServer`, plus
|
|
23
|
-
*
|
|
24
|
+
* the composition check and the connection guards listed in its doc.
|
|
24
25
|
*/
|
|
25
26
|
export * from './start-language-server.js';
|
|
26
27
|
//# sourceMappingURL=index.js.map
|
package/lib/lsp/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lsp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,+CAA+C,CAAC;AAC9D,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wCAAwC,CAAC;AACvD,cAAc,yBAAyB,CAAC;AAExC;;;;;;GAMG;AACH,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lsp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,+CAA+C,CAAC;AAC9D,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wCAAwC,CAAC;AACvD,cAAc,yBAAyB,CAAC;AAExC;;;;;;GAMG;AACH,cAAc,4BAA4B,CAAC"}
|
|
@@ -34,14 +34,15 @@ import { type LangiumSharedServices, type ServiceRequirements } from '@hydranium
|
|
|
34
34
|
export declare function assertLspHeadComposed(services: LangiumSharedServices): void;
|
|
35
35
|
/**
|
|
36
36
|
* The framework's LSP server entry point: {@link assertLspHeadComposed}, then
|
|
37
|
-
* {@link guardDiagnosticsConnection}
|
|
38
|
-
* console's wrapping for the log file, then
|
|
39
|
-
* Signature-compatible with Langium's, so it
|
|
40
|
-
* importing from `@hydranium/core/lsp`. Calling
|
|
41
|
-
* composition check, the guard without which a
|
|
42
|
-
* the build that validated the document,
|
|
43
|
-
*
|
|
44
|
-
*
|
|
37
|
+
* {@link guardDiagnosticsConnection} and {@link guardEditRequests} on the
|
|
38
|
+
* tree's connection, then the console's wrapping for the log file, then
|
|
39
|
+
* Langium's `startLanguageServer`. Signature-compatible with Langium's, so it
|
|
40
|
+
* is a drop-in for adopters importing from `@hydranium/core/lsp`. Calling
|
|
41
|
+
* Langium's directly skips the composition check, the guard without which a
|
|
42
|
+
* publish racing teardown fails the build that validated the document, the
|
|
43
|
+
* guard without which an edit lands on text the editor does not hold, and the
|
|
44
|
+
* wrapping, which a stdio connection discards where a logger was built before
|
|
45
|
+
* it (see `installConsoleTee`).
|
|
45
46
|
*/
|
|
46
47
|
export declare function startLanguageServer(services: LangiumSharedServices, serviceRequirements?: Partial<ServiceRequirements>): void;
|
|
47
48
|
//# sourceMappingURL=start-language-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-language-server.d.ts","sourceRoot":"","sources":["../../src/lsp/start-language-server.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAEJ,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAC1B,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"start-language-server.d.ts","sourceRoot":"","sources":["../../src/lsp/start-language-server.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAEJ,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAC1B,MAAM,wBAAwB,CAAC;AAMhC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAe3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,qBAAqB,EAAE,mBAAmB,GAAE,OAAO,CAAC,mBAAmB,CAAM,GAAG,IAAI,CASjI"}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { startLanguageServer as startLangiumLanguageServer } from '@hydranium/langium/lsp';
|
|
10
10
|
import { HydraniumDocumentUpdateHandler } from './hydranium-document-update-handler.js';
|
|
11
11
|
import { guardDiagnosticsConnection } from './diagnostics-connection.js';
|
|
12
|
+
import { guardEditRequests } from './edit-requests-guard.js';
|
|
12
13
|
import { installConsoleTee } from '../langium/diagnostics/console-tee.js';
|
|
13
14
|
/**
|
|
14
15
|
* Fail the LSP head's start when its shared module was never composed.
|
|
@@ -50,20 +51,22 @@ export function assertLspHeadComposed(services) {
|
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* The framework's LSP server entry point: {@link assertLspHeadComposed}, then
|
|
53
|
-
* {@link guardDiagnosticsConnection}
|
|
54
|
-
* console's wrapping for the log file, then
|
|
55
|
-
* Signature-compatible with Langium's, so it
|
|
56
|
-
* importing from `@hydranium/core/lsp`. Calling
|
|
57
|
-
* composition check, the guard without which a
|
|
58
|
-
* the build that validated the document,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
54
|
+
* {@link guardDiagnosticsConnection} and {@link guardEditRequests} on the
|
|
55
|
+
* tree's connection, then the console's wrapping for the log file, then
|
|
56
|
+
* Langium's `startLanguageServer`. Signature-compatible with Langium's, so it
|
|
57
|
+
* is a drop-in for adopters importing from `@hydranium/core/lsp`. Calling
|
|
58
|
+
* Langium's directly skips the composition check, the guard without which a
|
|
59
|
+
* publish racing teardown fails the build that validated the document, the
|
|
60
|
+
* guard without which an edit lands on text the editor does not hold, and the
|
|
61
|
+
* wrapping, which a stdio connection discards where a logger was built before
|
|
62
|
+
* it (see `installConsoleTee`).
|
|
61
63
|
*/
|
|
62
64
|
export function startLanguageServer(services, serviceRequirements = {}) {
|
|
63
65
|
assertLspHeadComposed(services);
|
|
64
66
|
const connection = services.lsp.Connection;
|
|
65
67
|
if (connection) {
|
|
66
68
|
guardDiagnosticsConnection(connection);
|
|
69
|
+
guardEditRequests(connection, services);
|
|
67
70
|
}
|
|
68
71
|
installConsoleTee();
|
|
69
72
|
startLangiumLanguageServer(services, serviceRequirements);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-language-server.js","sourceRoot":"","sources":["../../src/lsp/start-language-server.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EACJ,mBAAmB,IAAI,0BAA0B,EAGnD,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,8BAA8B,EAAE,MAAM,wCAAwC,CAAC;AACxF,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAA+B;IAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC;QAC7B,OAAO;IACV,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,qBAAqB,CAAC;IACnD,IAAI,CAAC,CAAC,OAAO,YAAY,8BAA8B,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CACZ,oGAAoG;YACjG,IAAI,OAAO,EAAE,WAAW,EAAE,IAAI,IAAI,SAAS,uEAAuE;YAClH,kHAAkH;YAClH,kGAAkG;YAClG,4GAA4G;YAC5G,4CAA4C,CACjD,CAAC;IACL,CAAC;AACJ,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"start-language-server.js","sourceRoot":"","sources":["../../src/lsp/start-language-server.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EACJ,mBAAmB,IAAI,0BAA0B,EAGnD,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,8BAA8B,EAAE,MAAM,wCAAwC,CAAC;AACxF,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAA+B;IAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC;QAC7B,OAAO;IACV,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,qBAAqB,CAAC;IACnD,IAAI,CAAC,CAAC,OAAO,YAAY,8BAA8B,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CACZ,oGAAoG;YACjG,IAAI,OAAO,EAAE,WAAW,EAAE,IAAI,IAAI,SAAS,uEAAuE;YAClH,kHAAkH;YAClH,kGAAkG;YAClG,4GAA4G;YAC5G,4CAA4C,CACjD,CAAC;IACL,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAA+B,EAAE,mBAAmB,GAAiC,EAAE;IACxH,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC;IAC3C,IAAI,UAAU,EAAE,CAAC;QACd,0BAA0B,CAAC,UAAU,CAAC,CAAC;QACvC,iBAAiB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC;IACD,iBAAiB,EAAE,CAAC;IACpB,0BAA0B,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC;AAC7D,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.335",
|
|
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.335",
|
|
91
|
+
"@hydranium/protocol": "1.0.0-next.335",
|
|
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.17.5"
|
|
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.335",
|
|
105
|
+
"@hydranium/protocol": "1.0.0-next.335",
|
|
106
106
|
"@playwright/test": "^1.40.0",
|
|
107
107
|
"vscode-jsonrpc": "^9.0.0",
|
|
108
108
|
"vscode-languageserver": "~10.0.1",
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type URI } from '@hydranium/langium';
|
|
11
|
+
import { type LangiumSharedServices } from '@hydranium/langium/lsp';
|
|
12
|
+
import {
|
|
13
|
+
type Connection,
|
|
14
|
+
type Disposable,
|
|
15
|
+
type HandlerResult,
|
|
16
|
+
LSPErrorCodes,
|
|
17
|
+
ResponseError,
|
|
18
|
+
type WorkspaceEdit
|
|
19
|
+
} from 'vscode-languageserver';
|
|
20
|
+
import { LANGUAGE_CLIENT_ID } from '../documents/client-ids.js';
|
|
21
|
+
import { HydraniumTextDocuments } from '../documents/hydranium-text-documents.js';
|
|
22
|
+
import { type ServerSharedServicesMinimal } from '../langium/shared-services.js';
|
|
23
|
+
|
|
24
|
+
const wrappedConnections = new WeakSet<object>();
|
|
25
|
+
|
|
26
|
+
type Register<P, A extends unknown[], R, E> = (handler: (params: P, ...rest: A) => HandlerResult<R, E>) => Disposable;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Refuse with `ContentModified` a request whose answer edits documents, when
|
|
30
|
+
* the editor of the request's document is behind the store as it arrives, or
|
|
31
|
+
* when a document the answer edits changed while it was computed, by another
|
|
32
|
+
* client, by a rebuild of a closed document or by its release, or its editor
|
|
33
|
+
* is then behind.
|
|
34
|
+
*
|
|
35
|
+
* The editor applies the edits by position to the text it holds, and sent the
|
|
36
|
+
* request's position in it, so an answer computed on other text rewrites the
|
|
37
|
+
* wrong characters. The check runs on the answer, since the documents a rename
|
|
38
|
+
* or code action edits are only known there, and on the connection, so an
|
|
39
|
+
* adopter's provider for these requests is covered. Edits that arrive by a
|
|
40
|
+
* resolve request, a file operation, `willSaveWaitUntil`, an inlay hint or a
|
|
41
|
+
* command's `workspace/applyEdit` are not.
|
|
42
|
+
*
|
|
43
|
+
* Read-only answers are not refused, since a refusal would blank them. An
|
|
44
|
+
* editor ahead of the store does not exist, as its changes reach the store
|
|
45
|
+
* before any request it sends after them.
|
|
46
|
+
*/
|
|
47
|
+
export function guardEditRequests(connection: Connection, services: LangiumSharedServices): void {
|
|
48
|
+
const store = services.workspace.TextDocuments;
|
|
49
|
+
if (wrappedConnections.has(connection) || !(store instanceof HydraniumTextDocuments)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
wrappedConnections.add(connection);
|
|
53
|
+
// A Hydranium store implies the Hydranium shared services.
|
|
54
|
+
const keyOf = (uri: URI | string): string =>
|
|
55
|
+
(services as LangiumSharedServices & ServerSharedServicesMinimal).workspace.DocumentUriPolicy.canonicalUri(uri);
|
|
56
|
+
const versions = (): ReadonlyMap<string, number> => new Map(store.all().map(document => [document.uri, document.version]));
|
|
57
|
+
// A closed document records no author per version, so a rebuild of it
|
|
58
|
+
// while an answer is computed is what refuses the answer. A release clears
|
|
59
|
+
// the authors, so a reopen would attribute another client's change to the
|
|
60
|
+
// editor, and refuses it as well.
|
|
61
|
+
const watching = new Set<{ readonly rebuilt: Set<string>; readonly released: Set<string> }>();
|
|
62
|
+
services.workspace.DocumentBuilder.onUpdate((changed, deleted) => {
|
|
63
|
+
if (watching.size === 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const keys = [...changed, ...deleted].map(keyOf);
|
|
67
|
+
for (const watched of watching) {
|
|
68
|
+
keys.forEach(key => watched.rebuilt.add(key));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
store.onDidReleaseDocument(event => {
|
|
72
|
+
for (const watched of watching) {
|
|
73
|
+
watched.released.add(event.uri);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// The editor's own changes are not counted: it discards or re-filters an
|
|
77
|
+
// answer that predates them, and counting them would blank completion
|
|
78
|
+
// while the user types.
|
|
79
|
+
const changedByAnother = (uri: string, from: number | undefined, to: number): boolean => {
|
|
80
|
+
if (from === undefined) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
for (let version = from + 1; version <= to; version++) {
|
|
84
|
+
if (store.getAuthor(uri, version) !== LANGUAGE_CLIENT_ID) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
const refused = <E>(): ResponseError<E> =>
|
|
91
|
+
new ResponseError<E>(LSPErrorCodes.ContentModified, 'The editor does not hold the text the answer was computed on');
|
|
92
|
+
// The request's position is in the editor's text, which an answer
|
|
93
|
+
// computed on the store's would be applied past.
|
|
94
|
+
const behind = (uri: string): boolean => store.isLanguageClientBehind(uri, store.version(uri));
|
|
95
|
+
/** Watches the documents an answer may edit while it is computed; dispose it after. */
|
|
96
|
+
const watch = (): { changed(uris: Iterable<string>): boolean; dispose(): void } => {
|
|
97
|
+
const computedOn = versions();
|
|
98
|
+
const watched = { rebuilt: new Set<string>(), released: new Set<string>() };
|
|
99
|
+
watching.add(watched);
|
|
100
|
+
return {
|
|
101
|
+
changed: uris =>
|
|
102
|
+
[...new Set(uris)].some(uri => {
|
|
103
|
+
const document = store.get(uri);
|
|
104
|
+
const key = keyOf(uri);
|
|
105
|
+
const changed =
|
|
106
|
+
watched.released.has(key) ||
|
|
107
|
+
(document === undefined
|
|
108
|
+
? watched.rebuilt.has(key)
|
|
109
|
+
: changedByAnother(uri, computedOn.get(document.uri), document.version));
|
|
110
|
+
return changed || behind(uri);
|
|
111
|
+
}),
|
|
112
|
+
dispose: () => watching.delete(watched)
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const guard =
|
|
116
|
+
<P extends { readonly textDocument: { readonly uri: string } }, A extends unknown[], R, E>(
|
|
117
|
+
register: Register<P, A, R, E>,
|
|
118
|
+
edited: (result: R | null | undefined | void) => Iterable<string> = () => []
|
|
119
|
+
): Register<P, A, R, E> =>
|
|
120
|
+
handler =>
|
|
121
|
+
register(async (params, ...rest) => {
|
|
122
|
+
const uri = params.textDocument.uri;
|
|
123
|
+
if (behind(uri)) {
|
|
124
|
+
return refused<E>();
|
|
125
|
+
}
|
|
126
|
+
const answer = watch();
|
|
127
|
+
try {
|
|
128
|
+
const result = await handler(params, ...rest);
|
|
129
|
+
return result instanceof ResponseError || !answer.changed([uri, ...edited(result)]) ? result : refused<E>();
|
|
130
|
+
} finally {
|
|
131
|
+
answer.dispose();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
connection.onCompletion = guard(connection.onCompletion.bind(connection));
|
|
136
|
+
connection.onDocumentFormatting = guard(connection.onDocumentFormatting.bind(connection));
|
|
137
|
+
connection.onDocumentRangeFormatting = guard(connection.onDocumentRangeFormatting.bind(connection));
|
|
138
|
+
connection.onDocumentOnTypeFormatting = guard(connection.onDocumentOnTypeFormatting.bind(connection));
|
|
139
|
+
connection.onRenameRequest = guard(connection.onRenameRequest.bind(connection), editedUris);
|
|
140
|
+
connection.onCodeAction = guard(connection.onCodeAction.bind(connection), actions =>
|
|
141
|
+
(actions ?? []).flatMap(action => ('edit' in action && action.edit ? [...editedUris(action.edit)] : []))
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function* editedUris(edit: WorkspaceEdit | null | undefined | void): Iterable<string> {
|
|
146
|
+
yield* Object.keys(edit?.changes ?? {});
|
|
147
|
+
for (const change of edit?.documentChanges ?? []) {
|
|
148
|
+
if ('textDocument' in change) {
|
|
149
|
+
yield change.textDocument.uri;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/lsp/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './completion/hydranium-completion-provider.js';
|
|
|
11
11
|
export * from './lsp-latency.js';
|
|
12
12
|
export * from './connection-features.js';
|
|
13
13
|
export * from './diagnostics-connection.js';
|
|
14
|
+
export * from './edit-requests-guard.js';
|
|
14
15
|
export * from './semantic-token-provider.js';
|
|
15
16
|
export * from './language-module.js';
|
|
16
17
|
export * from './shared-module.js';
|
|
@@ -22,6 +23,6 @@ export * from './save-text-document.js';
|
|
|
22
23
|
* head: framework overrides + the start function live behind one
|
|
23
24
|
* `@hydranium/core/lsp` subpath, mirroring Langium's own `langium/lsp`
|
|
24
25
|
* convention. Signature-compatible with Langium's `startLanguageServer`, plus
|
|
25
|
-
*
|
|
26
|
+
* the composition check and the connection guards listed in its doc.
|
|
26
27
|
*/
|
|
27
28
|
export * from './start-language-server.js';
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '@hydranium/langium/lsp';
|
|
15
15
|
import { HydraniumDocumentUpdateHandler } from './hydranium-document-update-handler.js';
|
|
16
16
|
import { guardDiagnosticsConnection } from './diagnostics-connection.js';
|
|
17
|
+
import { guardEditRequests } from './edit-requests-guard.js';
|
|
17
18
|
import { installConsoleTee } from '../langium/diagnostics/console-tee.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -59,20 +60,22 @@ export function assertLspHeadComposed(services: LangiumSharedServices): void {
|
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* The framework's LSP server entry point: {@link assertLspHeadComposed}, then
|
|
62
|
-
* {@link guardDiagnosticsConnection}
|
|
63
|
-
* console's wrapping for the log file, then
|
|
64
|
-
* Signature-compatible with Langium's, so it
|
|
65
|
-
* importing from `@hydranium/core/lsp`. Calling
|
|
66
|
-
* composition check, the guard without which a
|
|
67
|
-
* the build that validated the document,
|
|
68
|
-
*
|
|
69
|
-
*
|
|
63
|
+
* {@link guardDiagnosticsConnection} and {@link guardEditRequests} on the
|
|
64
|
+
* tree's connection, then the console's wrapping for the log file, then
|
|
65
|
+
* Langium's `startLanguageServer`. Signature-compatible with Langium's, so it
|
|
66
|
+
* is a drop-in for adopters importing from `@hydranium/core/lsp`. Calling
|
|
67
|
+
* Langium's directly skips the composition check, the guard without which a
|
|
68
|
+
* publish racing teardown fails the build that validated the document, the
|
|
69
|
+
* guard without which an edit lands on text the editor does not hold, and the
|
|
70
|
+
* wrapping, which a stdio connection discards where a logger was built before
|
|
71
|
+
* it (see `installConsoleTee`).
|
|
70
72
|
*/
|
|
71
73
|
export function startLanguageServer(services: LangiumSharedServices, serviceRequirements: Partial<ServiceRequirements> = {}): void {
|
|
72
74
|
assertLspHeadComposed(services);
|
|
73
75
|
const connection = services.lsp.Connection;
|
|
74
76
|
if (connection) {
|
|
75
77
|
guardDiagnosticsConnection(connection);
|
|
78
|
+
guardEditRequests(connection, services);
|
|
76
79
|
}
|
|
77
80
|
installConsoleTee();
|
|
78
81
|
startLangiumLanguageServer(services, serviceRequirements);
|