@hydranium/core 1.0.0-next.205 → 1.0.0-next.208

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.
Files changed (99) hide show
  1. package/lib/documents/ast-document-manager.js +4 -4
  2. package/lib/documents/ast-document-manager.js.map +1 -1
  3. package/lib/documents/client-ids.d.ts +1 -1
  4. package/lib/documents/client-ids.d.ts.map +1 -1
  5. package/lib/documents/client-ids.js +1 -1
  6. package/lib/documents/client-ids.js.map +1 -1
  7. package/lib/documents/client-session-registry.d.ts +24 -17
  8. package/lib/documents/client-session-registry.d.ts.map +1 -1
  9. package/lib/documents/client-session-registry.js +49 -29
  10. package/lib/documents/client-session-registry.js.map +1 -1
  11. package/lib/documents/dirty-state-tracker.d.ts +79 -0
  12. package/lib/documents/dirty-state-tracker.d.ts.map +1 -0
  13. package/lib/documents/dirty-state-tracker.js +69 -0
  14. package/lib/documents/dirty-state-tracker.js.map +1 -0
  15. package/lib/documents/document-release-handler.d.ts +179 -0
  16. package/lib/documents/document-release-handler.d.ts.map +1 -0
  17. package/lib/documents/document-release-handler.js +283 -0
  18. package/lib/documents/document-release-handler.js.map +1 -0
  19. package/lib/documents/document-release-scheduler.d.ts +67 -0
  20. package/lib/documents/document-release-scheduler.d.ts.map +1 -0
  21. package/lib/documents/document-release-scheduler.js +80 -0
  22. package/lib/documents/document-release-scheduler.js.map +1 -0
  23. package/lib/documents/hydranium-text-documents.d.ts +137 -455
  24. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  25. package/lib/documents/hydranium-text-documents.js +373 -835
  26. package/lib/documents/hydranium-text-documents.js.map +1 -1
  27. package/lib/documents/index.d.ts +5 -1
  28. package/lib/documents/index.d.ts.map +1 -1
  29. package/lib/documents/index.js +5 -1
  30. package/lib/documents/index.js.map +1 -1
  31. package/lib/documents/language-client-shadow.d.ts +221 -0
  32. package/lib/documents/language-client-shadow.d.ts.map +1 -0
  33. package/lib/documents/language-client-shadow.js +288 -0
  34. package/lib/documents/language-client-shadow.js.map +1 -0
  35. package/lib/documents/text-ledger.d.ts +64 -0
  36. package/lib/documents/text-ledger.d.ts.map +1 -0
  37. package/lib/documents/text-ledger.js +63 -0
  38. package/lib/documents/text-ledger.js.map +1 -0
  39. package/lib/langium/document-builder/document-builder.d.ts +8 -7
  40. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  41. package/lib/langium/document-builder/document-builder.js +8 -7
  42. package/lib/langium/document-builder/document-builder.js.map +1 -1
  43. package/lib/langium/integrity/integrity-service.d.ts +1 -1
  44. package/lib/langium/integrity/integrity-service.js +4 -4
  45. package/lib/langium/integrity/integrity-service.js.map +1 -1
  46. package/lib/langium/model-service/client-session.d.ts +4 -4
  47. package/lib/langium/model-service/client-session.js +2 -2
  48. package/lib/langium/model-service/client-session.js.map +1 -1
  49. package/lib/langium/model-service/model-events.d.ts +8 -11
  50. package/lib/langium/model-service/model-events.d.ts.map +1 -1
  51. package/lib/langium/model-service/model-service.d.ts +18 -4
  52. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  53. package/lib/langium/model-service/model-service.js +4 -1
  54. package/lib/langium/model-service/model-service.js.map +1 -1
  55. package/lib/langium/module.d.ts +7 -0
  56. package/lib/langium/module.d.ts.map +1 -1
  57. package/lib/langium/module.js +3 -1
  58. package/lib/langium/module.js.map +1 -1
  59. package/lib/langium/workspace/file-not-found.d.ts +7 -1
  60. package/lib/langium/workspace/file-not-found.d.ts.map +1 -1
  61. package/lib/langium/workspace/file-not-found.js +14 -3
  62. package/lib/langium/workspace/file-not-found.js.map +1 -1
  63. package/lib/lsp/hydranium-document-update-handler.d.ts +6 -6
  64. package/lib/lsp/hydranium-document-update-handler.js +10 -10
  65. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  66. package/lib/testing/make-test-services.d.ts +3 -0
  67. package/lib/testing/make-test-services.d.ts.map +1 -1
  68. package/lib/testing/make-test-services.js +5 -2
  69. package/lib/testing/make-test-services.js.map +1 -1
  70. package/lib/testing/stub-hydranium-text-documents.d.ts +9 -6
  71. package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
  72. package/lib/testing/stub-hydranium-text-documents.js +25 -11
  73. package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
  74. package/package.json +5 -5
  75. package/src/documents/ast-document-manager.ts +4 -4
  76. package/src/documents/client-ids.ts +1 -1
  77. package/src/documents/client-session-registry.ts +55 -34
  78. package/src/documents/dirty-state-tracker.ts +130 -0
  79. package/src/documents/document-release-handler.ts +365 -0
  80. package/src/documents/document-release-scheduler.ts +121 -0
  81. package/src/documents/hydranium-text-documents.ts +407 -1033
  82. package/src/documents/index.ts +5 -1
  83. package/src/documents/language-client-shadow.ts +463 -0
  84. package/src/documents/text-ledger.ts +112 -0
  85. package/src/langium/document-builder/document-builder.ts +8 -7
  86. package/src/langium/integrity/integrity-service.ts +4 -4
  87. package/src/langium/model-service/client-session.ts +6 -6
  88. package/src/langium/model-service/model-events.ts +12 -11
  89. package/src/langium/model-service/model-service.ts +22 -4
  90. package/src/langium/module.ts +9 -1
  91. package/src/langium/workspace/file-not-found.ts +15 -3
  92. package/src/lsp/hydranium-document-update-handler.ts +10 -10
  93. package/src/testing/make-test-services.ts +7 -2
  94. package/src/testing/stub-hydranium-text-documents.ts +41 -21
  95. package/lib/documents/language-client-text-shadow.d.ts +0 -133
  96. package/lib/documents/language-client-text-shadow.d.ts.map +0 -1
  97. package/lib/documents/language-client-text-shadow.js +0 -201
  98. package/lib/documents/language-client-text-shadow.js.map +0 -1
  99. package/src/documents/language-client-text-shadow.ts +0 -223
@@ -1,201 +0,0 @@
1
- /********************************************************************************
2
- * Copyright (c) 2026 CrossBreeze, 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 { diffLines } from 'diff';
10
- import { Range, uinteger } from 'vscode-languageserver';
11
- import { TextDocument } from 'vscode-languageserver-textdocument';
12
- /** The range every full-document replace emitted here carries. */
13
- const FULL_RANGE = Range.create(0, 0, uinteger.MAX_VALUE, uinteger.MAX_VALUE);
14
- /**
15
- * Whether `edits` is the single full-document replace {@link LanguageClientTextShadow.computeEdits}
16
- * emits when it has no usable baseline.
17
- *
18
- * The distinction matters at the `workspace/applyEdit` egress: a full replace is
19
- * position-INDEPENDENT and therefore lands correctly on any client buffer, while
20
- * a line-keyed diff only lands correctly on the exact text it was diffed
21
- * against. Only the latter needs a client-version gate.
22
- */
23
- export function isFullReplace(edits) {
24
- if (edits.length !== 1) {
25
- return false;
26
- }
27
- const { start, end } = edits[0].range;
28
- return start.line === FULL_RANGE.start.line && start.character === FULL_RANGE.start.character && end.line === FULL_RANGE.end.line;
29
- }
30
- /**
31
- * Tracks the text content the language client (typically Monaco) is believed to
32
- * currently hold so outgoing `workspace/applyEdit` requests can ship minimal
33
- * diffs instead of full-document replaces.
34
- *
35
- * Why it matters: a full-range replace on a moderately-sized YAML or DSL source
36
- * forces Monaco to re-tokenise the entire document (observed multi-second hangs
37
- * on large diagrams), whereas line-level diffs usually reduce that to a handful
38
- * of small edits.
39
- *
40
- * Update points the caller is responsible for:
41
- * - {@link computeEdits} after each outgoing server→client sync (updates the shadow optimistically).
42
- * - {@link set} when the server receives a `didChange` from the language client (the client now
43
- * holds that text; we shouldn't resend it).
44
- * - {@link setOpenedText} when the client opens a document the server did not baseline for it —
45
- * an equality-only baseline, since that text is a snapshot rather than a tracked one.
46
- * - {@link invalidate} when the client rejects an edit (`applied=false`) or a doc is closed —
47
- * the shadow is now stale and the next sync must fall back to a full replace.
48
- */
49
- export class LanguageClientTextShadow {
50
- onFallback;
51
- documents;
52
- shadow = new Map();
53
- /**
54
- * Per URI, the text the client declared it opened the document with, held
55
- * only until a real baseline exists. Consulted by {@link computeEdits} when
56
- * there is no shadow, and dropped on {@link invalidate}: a snapshot that
57
- * outlived what the client holds would let the equality check skip an edit
58
- * the client still needs.
59
- */
60
- opened = new Map();
61
- /**
62
- * @param onFallback invoked when {@link computeEdits} falls back to a full-range replace due to
63
- * apply-verify mismatch. Useful for logging regressions.
64
- * @param documents the {@link ShadowDocumentSource} used to materialise the throwaway probe
65
- * document in {@link computeEdits}. In production this is the owning
66
- * `HydraniumTextDocuments` (whose `create` threads through its configured
67
- * `TextDocumentsConfiguration`); tests pass a minimal stub. Routing
68
- * through the manager rather than `TextDocument.create` directly avoids the
69
- * `instanceof FullTextDocument` gate in the bare LSP `TextDocument.update`,
70
- * keeping the verifier honest for adopters with a custom text-document type.
71
- */
72
- constructor(onFallback, documents) {
73
- this.onFallback = onFallback;
74
- this.documents = documents;
75
- }
76
- /** Record the text the language client is known to hold (e.g. after a `didChange` echo). */
77
- set(uri, text) {
78
- this.shadow.set(uri, text);
79
- }
80
- /**
81
- * Record the buffer the language client declared at `didOpen`, for a URI
82
- * with no baseline yet.
83
- *
84
- * Deliberately NOT a {@link set}: this text is only ever compared for
85
- * EQUALITY, never diffed against. A line-keyed diff is position-dependent,
86
- * so keying one to a `didOpen` snapshot the client may have moved past
87
- * splices its buffer — the failure {@link computeEdits}'s full-replace
88
- * fallback exists to avoid.
89
- */
90
- setOpenedText(uri, text) {
91
- this.opened.set(uri, text);
92
- }
93
- /**
94
- * The text the language client is believed to hold: the tracked text, else
95
- * the buffer it declared at `didOpen`.
96
- *
97
- * Read at the `workspace/applyEdit` egress BEFORE {@link computeEdits},
98
- * which overwrites the tracked text and drops the opened snapshot. A change
99
- * the client sends carries ranges addressing this text and nothing else, so
100
- * losing it leaves that change unreconstructable — and applying its ranges
101
- * to the already-advanced synced text splices lines they never addressed.
102
- *
103
- * Deliberately wider than what {@link computeEdits} may diff from: an
104
- * opened snapshot is a position-dependent key to a buffer the client may
105
- * have moved past, so keying a diff to it splices that buffer.
106
- */
107
- clientText(uri) {
108
- return this.shadow.get(uri) ?? this.opened.get(uri);
109
- }
110
- /**
111
- * Whether a tracked text exists for `uri`, which is what decides whether a
112
- * push can be diffed at all. Distinct from {@link clientText}, which also
113
- * answers from the opened snapshot and so cannot settle this.
114
- */
115
- isTracked(uri) {
116
- return this.shadow.has(uri);
117
- }
118
- /** Forget the shadow for a URI; next {@link computeEdits} falls back to a full-document replace. */
119
- invalidate(uri) {
120
- this.shadow.delete(uri);
121
- // Drops the opened snapshot too, and this is the only place that has to:
122
- // `computeEdits` clears it whenever it consults it, so the sole way it can
123
- // outlive what the client holds is surviving under a live shadow until that
124
- // shadow is dropped here.
125
- this.opened.delete(uri);
126
- }
127
- /**
128
- * Return the minimal LSP {@link TextEdit}s required to bring the language client's content from
129
- * the tracked shadow to `newText`. Updates the shadow to `newText` so subsequent calls diff
130
- * against it.
131
- *
132
- * - Empty array when the shadow already equals `newText` (caller should skip the RPC),
133
- * or when there is no shadow and the client opened the document with exactly `newText`
134
- * (see {@link setOpenedText}) — pushing it anyway dirties the client's buffer on open.
135
- * - Full-range replace when there is no shadow (first sync or post-{@link invalidate}).
136
- * - Line-level diff otherwise. Before returning, we locally reconstruct `newText` by applying
137
- * the diff to `old`; if that doesn't match, we fall back to a full-range replace and notify
138
- * the fallback callback. This catches any algorithmic regression in {@link diffToEdits}
139
- * without risking client corruption.
140
- */
141
- computeEdits(uri, newText) {
142
- const old = this.shadow.get(uri);
143
- if (old === newText) {
144
- return [];
145
- }
146
- this.shadow.set(uri, newText);
147
- const fullReplace = { range: FULL_RANGE, newText };
148
- if (old === undefined) {
149
- // The opened snapshot has served its purpose either way: the shadow above
150
- // is now the baseline, so leaving it would only risk a later stale match.
151
- const openedText = this.opened.get(uri);
152
- this.opened.delete(uri);
153
- return openedText === newText ? [] : [fullReplace];
154
- }
155
- const edits = diffToEdits(old, newText);
156
- const verifyDoc = this.documents.create(uri, 'plaintext', 0, old);
157
- if (TextDocument.applyEdits(verifyDoc, edits) !== newText) {
158
- this.onFallback(uri, 'apply-verify-mismatch');
159
- return [fullReplace];
160
- }
161
- return edits;
162
- }
163
- }
164
- /**
165
- * Convert a {@link diffLines} result to a list of LSP {@link TextEdit}s keyed on line positions
166
- * in the *old* text. Contiguous added/removed hunks are coalesced into a single replace so the
167
- * client sees one edit per changed region.
168
- */
169
- export function diffToEdits(oldText, newText) {
170
- const hunks = diffLines(oldText, newText);
171
- const edits = [];
172
- let oldLine = 0;
173
- let i = 0;
174
- while (i < hunks.length) {
175
- const hunk = hunks[i];
176
- if (!hunk.added && !hunk.removed) {
177
- oldLine += hunk.count ?? 0;
178
- i++;
179
- continue;
180
- }
181
- // Coalesce a run of add/remove hunks into a single replace for this region.
182
- let removedLines = 0;
183
- let addedText = '';
184
- while (i < hunks.length && (hunks[i].added || hunks[i].removed)) {
185
- if (hunks[i].removed) {
186
- removedLines += hunks[i].count ?? 0;
187
- }
188
- if (hunks[i].added) {
189
- addedText += hunks[i].value;
190
- }
191
- i++;
192
- }
193
- edits.push({
194
- range: Range.create(oldLine, 0, oldLine + removedLines, 0),
195
- newText: addedText
196
- });
197
- oldLine += removedLines;
198
- }
199
- return edits;
200
- }
201
- //# sourceMappingURL=language-client-text-shadow.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"language-client-text-shadow.js","sourceRoot":"","sources":["../../src/documents/language-client-text-shadow.ts"],"names":[],"mappings":"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"}
@@ -1,223 +0,0 @@
1
- /********************************************************************************
2
- * Copyright (c) 2026 CrossBreeze, 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 { diffLines } from 'diff';
11
- import { Range, type TextEdit, uinteger } from 'vscode-languageserver';
12
- import { TextDocument } from 'vscode-languageserver-textdocument';
13
-
14
- /**
15
- * The minimum surface the shadow needs from a `HydraniumTextDocuments`:
16
- * a `create` method that materialises a fresh text document via the
17
- * configured `TextDocumentsConfiguration`. Declared as a narrow
18
- * structural interface so the manager satisfies it without an explicit
19
- * import (sidestepping the circular `HydraniumTextDocuments → shadow → manager`
20
- * dependency) and so tests can stub it with a minimal object literal.
21
- */
22
- export interface ShadowDocumentSource {
23
- create(uri: string, languageId: string, version: number, content: string): TextDocument;
24
- }
25
-
26
- /** The range every full-document replace emitted here carries. */
27
- const FULL_RANGE = Range.create(0, 0, uinteger.MAX_VALUE, uinteger.MAX_VALUE);
28
-
29
- /**
30
- * Whether `edits` is the single full-document replace {@link LanguageClientTextShadow.computeEdits}
31
- * emits when it has no usable baseline.
32
- *
33
- * The distinction matters at the `workspace/applyEdit` egress: a full replace is
34
- * position-INDEPENDENT and therefore lands correctly on any client buffer, while
35
- * a line-keyed diff only lands correctly on the exact text it was diffed
36
- * against. Only the latter needs a client-version gate.
37
- */
38
- export function isFullReplace(edits: readonly TextEdit[]): boolean {
39
- if (edits.length !== 1) {
40
- return false;
41
- }
42
- const { start, end } = edits[0].range;
43
- return start.line === FULL_RANGE.start.line && start.character === FULL_RANGE.start.character && end.line === FULL_RANGE.end.line;
44
- }
45
-
46
- /**
47
- * Tracks the text content the language client (typically Monaco) is believed to
48
- * currently hold so outgoing `workspace/applyEdit` requests can ship minimal
49
- * diffs instead of full-document replaces.
50
- *
51
- * Why it matters: a full-range replace on a moderately-sized YAML or DSL source
52
- * forces Monaco to re-tokenise the entire document (observed multi-second hangs
53
- * on large diagrams), whereas line-level diffs usually reduce that to a handful
54
- * of small edits.
55
- *
56
- * Update points the caller is responsible for:
57
- * - {@link computeEdits} after each outgoing server→client sync (updates the shadow optimistically).
58
- * - {@link set} when the server receives a `didChange` from the language client (the client now
59
- * holds that text; we shouldn't resend it).
60
- * - {@link setOpenedText} when the client opens a document the server did not baseline for it —
61
- * an equality-only baseline, since that text is a snapshot rather than a tracked one.
62
- * - {@link invalidate} when the client rejects an edit (`applied=false`) or a doc is closed —
63
- * the shadow is now stale and the next sync must fall back to a full replace.
64
- */
65
- export class LanguageClientTextShadow {
66
- protected shadow = new Map<string, string>();
67
-
68
- /**
69
- * Per URI, the text the client declared it opened the document with, held
70
- * only until a real baseline exists. Consulted by {@link computeEdits} when
71
- * there is no shadow, and dropped on {@link invalidate}: a snapshot that
72
- * outlived what the client holds would let the equality check skip an edit
73
- * the client still needs.
74
- */
75
- protected opened = new Map<string, string>();
76
-
77
- /**
78
- * @param onFallback invoked when {@link computeEdits} falls back to a full-range replace due to
79
- * apply-verify mismatch. Useful for logging regressions.
80
- * @param documents the {@link ShadowDocumentSource} used to materialise the throwaway probe
81
- * document in {@link computeEdits}. In production this is the owning
82
- * `HydraniumTextDocuments` (whose `create` threads through its configured
83
- * `TextDocumentsConfiguration`); tests pass a minimal stub. Routing
84
- * through the manager rather than `TextDocument.create` directly avoids the
85
- * `instanceof FullTextDocument` gate in the bare LSP `TextDocument.update`,
86
- * keeping the verifier honest for adopters with a custom text-document type.
87
- */
88
- constructor(
89
- protected onFallback: (uri: string, reason: string) => void,
90
- protected documents: ShadowDocumentSource
91
- ) {}
92
-
93
- /** Record the text the language client is known to hold (e.g. after a `didChange` echo). */
94
- set(uri: string, text: string): void {
95
- this.shadow.set(uri, text);
96
- }
97
-
98
- /**
99
- * Record the buffer the language client declared at `didOpen`, for a URI
100
- * with no baseline yet.
101
- *
102
- * Deliberately NOT a {@link set}: this text is only ever compared for
103
- * EQUALITY, never diffed against. A line-keyed diff is position-dependent,
104
- * so keying one to a `didOpen` snapshot the client may have moved past
105
- * splices its buffer — the failure {@link computeEdits}'s full-replace
106
- * fallback exists to avoid.
107
- */
108
- setOpenedText(uri: string, text: string): void {
109
- this.opened.set(uri, text);
110
- }
111
-
112
- /**
113
- * The text the language client is believed to hold: the tracked text, else
114
- * the buffer it declared at `didOpen`.
115
- *
116
- * Read at the `workspace/applyEdit` egress BEFORE {@link computeEdits},
117
- * which overwrites the tracked text and drops the opened snapshot. A change
118
- * the client sends carries ranges addressing this text and nothing else, so
119
- * losing it leaves that change unreconstructable — and applying its ranges
120
- * to the already-advanced synced text splices lines they never addressed.
121
- *
122
- * Deliberately wider than what {@link computeEdits} may diff from: an
123
- * opened snapshot is a position-dependent key to a buffer the client may
124
- * have moved past, so keying a diff to it splices that buffer.
125
- */
126
- clientText(uri: string): string | undefined {
127
- return this.shadow.get(uri) ?? this.opened.get(uri);
128
- }
129
-
130
- /**
131
- * Whether a tracked text exists for `uri`, which is what decides whether a
132
- * push can be diffed at all. Distinct from {@link clientText}, which also
133
- * answers from the opened snapshot and so cannot settle this.
134
- */
135
- isTracked(uri: string): boolean {
136
- return this.shadow.has(uri);
137
- }
138
-
139
- /** Forget the shadow for a URI; next {@link computeEdits} falls back to a full-document replace. */
140
- invalidate(uri: string): void {
141
- this.shadow.delete(uri);
142
- // Drops the opened snapshot too, and this is the only place that has to:
143
- // `computeEdits` clears it whenever it consults it, so the sole way it can
144
- // outlive what the client holds is surviving under a live shadow until that
145
- // shadow is dropped here.
146
- this.opened.delete(uri);
147
- }
148
-
149
- /**
150
- * Return the minimal LSP {@link TextEdit}s required to bring the language client's content from
151
- * the tracked shadow to `newText`. Updates the shadow to `newText` so subsequent calls diff
152
- * against it.
153
- *
154
- * - Empty array when the shadow already equals `newText` (caller should skip the RPC),
155
- * or when there is no shadow and the client opened the document with exactly `newText`
156
- * (see {@link setOpenedText}) — pushing it anyway dirties the client's buffer on open.
157
- * - Full-range replace when there is no shadow (first sync or post-{@link invalidate}).
158
- * - Line-level diff otherwise. Before returning, we locally reconstruct `newText` by applying
159
- * the diff to `old`; if that doesn't match, we fall back to a full-range replace and notify
160
- * the fallback callback. This catches any algorithmic regression in {@link diffToEdits}
161
- * without risking client corruption.
162
- */
163
- computeEdits(uri: string, newText: string): TextEdit[] {
164
- const old = this.shadow.get(uri);
165
- if (old === newText) {
166
- return [];
167
- }
168
- this.shadow.set(uri, newText);
169
- const fullReplace: TextEdit = { range: FULL_RANGE, newText };
170
- if (old === undefined) {
171
- // The opened snapshot has served its purpose either way: the shadow above
172
- // is now the baseline, so leaving it would only risk a later stale match.
173
- const openedText = this.opened.get(uri);
174
- this.opened.delete(uri);
175
- return openedText === newText ? [] : [fullReplace];
176
- }
177
- const edits = diffToEdits(old, newText);
178
- const verifyDoc = this.documents.create(uri, 'plaintext', 0, old);
179
- if (TextDocument.applyEdits(verifyDoc, edits) !== newText) {
180
- this.onFallback(uri, 'apply-verify-mismatch');
181
- return [fullReplace];
182
- }
183
- return edits;
184
- }
185
- }
186
-
187
- /**
188
- * Convert a {@link diffLines} result to a list of LSP {@link TextEdit}s keyed on line positions
189
- * in the *old* text. Contiguous added/removed hunks are coalesced into a single replace so the
190
- * client sees one edit per changed region.
191
- */
192
- export function diffToEdits(oldText: string, newText: string): TextEdit[] {
193
- const hunks = diffLines(oldText, newText);
194
- const edits: TextEdit[] = [];
195
- let oldLine = 0;
196
- let i = 0;
197
- while (i < hunks.length) {
198
- const hunk = hunks[i];
199
- if (!hunk.added && !hunk.removed) {
200
- oldLine += hunk.count ?? 0;
201
- i++;
202
- continue;
203
- }
204
- // Coalesce a run of add/remove hunks into a single replace for this region.
205
- let removedLines = 0;
206
- let addedText = '';
207
- while (i < hunks.length && (hunks[i].added || hunks[i].removed)) {
208
- if (hunks[i].removed) {
209
- removedLines += hunks[i].count ?? 0;
210
- }
211
- if (hunks[i].added) {
212
- addedText += hunks[i].value;
213
- }
214
- i++;
215
- }
216
- edits.push({
217
- range: Range.create(oldLine, 0, oldLine + removedLines, 0),
218
- newText: addedText
219
- });
220
- oldLine += removedLines;
221
- }
222
- return edits;
223
- }