@singapore-editor/lsp-plugin 0.1.1

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 (185) hide show
  1. package/README.md +103 -0
  2. package/dist/bufferSync.d.ts +12 -0
  3. package/dist/bufferSync.d.ts.map +1 -0
  4. package/dist/bufferSync.js +35 -0
  5. package/dist/bufferSync.js.map +1 -0
  6. package/dist/codeActions.d.ts +83 -0
  7. package/dist/codeActions.d.ts.map +1 -0
  8. package/dist/codeActions.js +294 -0
  9. package/dist/codeActions.js.map +1 -0
  10. package/dist/colorAlpha.d.ts +4 -0
  11. package/dist/colorAlpha.d.ts.map +1 -0
  12. package/dist/colorAlpha.js +28 -0
  13. package/dist/colorAlpha.js.map +1 -0
  14. package/dist/completion.d.ts +124 -0
  15. package/dist/completion.d.ts.map +1 -0
  16. package/dist/completion.js +609 -0
  17. package/dist/completion.js.map +1 -0
  18. package/dist/completionCommit.d.ts +19 -0
  19. package/dist/completionCommit.d.ts.map +1 -0
  20. package/dist/completionCommit.js +57 -0
  21. package/dist/completionCommit.js.map +1 -0
  22. package/dist/completionController.d.ts +89 -0
  23. package/dist/completionController.d.ts.map +1 -0
  24. package/dist/completionController.js +376 -0
  25. package/dist/completionController.js.map +1 -0
  26. package/dist/completionProviders.d.ts +50 -0
  27. package/dist/completionProviders.d.ts.map +1 -0
  28. package/dist/completionProviders.js +64 -0
  29. package/dist/completionProviders.js.map +1 -0
  30. package/dist/connectionContext.d.ts +7 -0
  31. package/dist/connectionContext.d.ts.map +1 -0
  32. package/dist/definitionLinkController.d.ts +54 -0
  33. package/dist/definitionLinkController.d.ts.map +1 -0
  34. package/dist/definitionLinkController.js +221 -0
  35. package/dist/definitionLinkController.js.map +1 -0
  36. package/dist/definitionNavigation.d.ts +76 -0
  37. package/dist/definitionNavigation.d.ts.map +1 -0
  38. package/dist/definitionNavigation.js +143 -0
  39. package/dist/definitionNavigation.js.map +1 -0
  40. package/dist/diagnosticNotes.d.ts +7 -0
  41. package/dist/diagnosticNotes.d.ts.map +1 -0
  42. package/dist/diagnosticNotes.js +37 -0
  43. package/dist/diagnosticNotes.js.map +1 -0
  44. package/dist/diagnosticProjection.d.ts +31 -0
  45. package/dist/diagnosticProjection.d.ts.map +1 -0
  46. package/dist/diagnosticProjection.js +79 -0
  47. package/dist/diagnosticProjection.js.map +1 -0
  48. package/dist/diagnostics.d.ts +16 -0
  49. package/dist/diagnostics.d.ts.map +1 -0
  50. package/dist/diagnostics.js +94 -0
  51. package/dist/diagnostics.js.map +1 -0
  52. package/dist/diagnosticsPresenter.d.ts +64 -0
  53. package/dist/diagnosticsPresenter.d.ts.map +1 -0
  54. package/dist/diagnosticsPresenter.js +305 -0
  55. package/dist/diagnosticsPresenter.js.map +1 -0
  56. package/dist/document.d.ts +69 -0
  57. package/dist/document.d.ts.map +1 -0
  58. package/dist/document.js +190 -0
  59. package/dist/document.js.map +1 -0
  60. package/dist/documentHighlightController.d.ts +34 -0
  61. package/dist/documentHighlightController.d.ts.map +1 -0
  62. package/dist/documentHighlightController.js +101 -0
  63. package/dist/documentHighlightController.js.map +1 -0
  64. package/dist/documentSnapshot.d.ts +9 -0
  65. package/dist/documentSnapshot.d.ts.map +1 -0
  66. package/dist/documentSnapshot.js +31 -0
  67. package/dist/documentSnapshot.js.map +1 -0
  68. package/dist/documentSync.d.ts +53 -0
  69. package/dist/documentSync.d.ts.map +1 -0
  70. package/dist/documentSync.js +334 -0
  71. package/dist/documentSync.js.map +1 -0
  72. package/dist/documentSyncController.d.ts +26 -0
  73. package/dist/documentSyncController.d.ts.map +1 -0
  74. package/dist/documentSyncController.js +24 -0
  75. package/dist/documentSyncController.js.map +1 -0
  76. package/dist/formatOnType.d.ts +43 -0
  77. package/dist/formatOnType.d.ts.map +1 -0
  78. package/dist/formatOnType.js +198 -0
  79. package/dist/formatOnType.js.map +1 -0
  80. package/dist/formatting.d.ts +18 -0
  81. package/dist/formatting.d.ts.map +1 -0
  82. package/dist/formatting.js +52 -0
  83. package/dist/formatting.js.map +1 -0
  84. package/dist/fuzzyMatch.d.ts +22 -0
  85. package/dist/fuzzyMatch.d.ts.map +1 -0
  86. package/dist/fuzzyMatch.js +216 -0
  87. package/dist/fuzzyMatch.js.map +1 -0
  88. package/dist/hoverParticipant.d.ts +24 -0
  89. package/dist/hoverParticipant.d.ts.map +1 -0
  90. package/dist/hoverParticipant.js +156 -0
  91. package/dist/hoverParticipant.js.map +1 -0
  92. package/dist/index.d.ts +18 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +17 -0
  95. package/dist/keyCommands.d.ts +19 -0
  96. package/dist/keyCommands.d.ts.map +1 -0
  97. package/dist/keyCommands.js +21 -0
  98. package/dist/keyCommands.js.map +1 -0
  99. package/dist/lane.d.ts +28 -0
  100. package/dist/lane.d.ts.map +1 -0
  101. package/dist/lane.js +138 -0
  102. package/dist/lane.js.map +1 -0
  103. package/dist/lspConnection.d.ts +94 -0
  104. package/dist/lspConnection.d.ts.map +1 -0
  105. package/dist/lspConnection.js +195 -0
  106. package/dist/lspConnection.js.map +1 -0
  107. package/dist/lspConnectionPool.d.ts +34 -0
  108. package/dist/lspConnectionPool.d.ts.map +1 -0
  109. package/dist/lspConnectionPool.js +255 -0
  110. package/dist/lspConnectionPool.js.map +1 -0
  111. package/dist/minimalEdits.d.ts +21 -0
  112. package/dist/minimalEdits.d.ts.map +1 -0
  113. package/dist/minimalEdits.js +181 -0
  114. package/dist/minimalEdits.js.map +1 -0
  115. package/dist/paths.d.ts +6 -0
  116. package/dist/paths.d.ts.map +1 -0
  117. package/dist/paths.js +38 -0
  118. package/dist/paths.js.map +1 -0
  119. package/dist/plugin.d.ts +100 -0
  120. package/dist/plugin.d.ts.map +1 -0
  121. package/dist/plugin.js +837 -0
  122. package/dist/plugin.js.map +1 -0
  123. package/dist/plugin.styles.d.ts +31 -0
  124. package/dist/plugin.styles.d.ts.map +1 -0
  125. package/dist/plugin.styles.js +102 -0
  126. package/dist/plugin.styles.js.map +1 -0
  127. package/dist/pluginTypes.d.ts +41 -0
  128. package/dist/pluginTypes.d.ts.map +1 -0
  129. package/dist/pluginTypes.js +0 -0
  130. package/dist/pullDiagnostics.d.ts +30 -0
  131. package/dist/pullDiagnostics.d.ts.map +1 -0
  132. package/dist/pullDiagnostics.js +94 -0
  133. package/dist/pullDiagnostics.js.map +1 -0
  134. package/dist/renameWidget.d.ts +30 -0
  135. package/dist/renameWidget.d.ts.map +1 -0
  136. package/dist/renameWidget.js +117 -0
  137. package/dist/renameWidget.js.map +1 -0
  138. package/dist/semanticTokenDecoder.d.ts +81 -0
  139. package/dist/semanticTokenDecoder.d.ts.map +1 -0
  140. package/dist/semanticTokenDecoder.js +132 -0
  141. package/dist/semanticTokenDecoder.js.map +1 -0
  142. package/dist/semanticTokens.d.ts +47 -0
  143. package/dist/semanticTokens.d.ts.map +1 -0
  144. package/dist/semanticTokens.js +68 -0
  145. package/dist/semanticTokens.js.map +1 -0
  146. package/dist/serverSet.d.ts +69 -0
  147. package/dist/serverSet.d.ts.map +1 -0
  148. package/dist/serverSet.js +331 -0
  149. package/dist/serverSet.js.map +1 -0
  150. package/dist/signatureHelp.d.ts +40 -0
  151. package/dist/signatureHelp.d.ts.map +1 -0
  152. package/dist/signatureHelp.js +108 -0
  153. package/dist/signatureHelp.js.map +1 -0
  154. package/dist/signatureHelpController.d.ts +52 -0
  155. package/dist/signatureHelpController.d.ts.map +1 -0
  156. package/dist/signatureHelpController.js +160 -0
  157. package/dist/signatureHelpController.js.map +1 -0
  158. package/dist/sourceText.d.ts +22 -0
  159. package/dist/sourceText.d.ts.map +1 -0
  160. package/dist/sourceText.js +70 -0
  161. package/dist/sourceText.js.map +1 -0
  162. package/dist/types.d.ts +238 -0
  163. package/dist/types.d.ts.map +1 -0
  164. package/dist/types.js +17 -0
  165. package/dist/types.js.map +1 -0
  166. package/dist/viewDocumentSnapshot.d.ts +5 -0
  167. package/dist/viewDocumentSnapshot.d.ts.map +1 -0
  168. package/dist/viewDocumentSnapshot.js +12 -0
  169. package/dist/viewDocumentSnapshot.js.map +1 -0
  170. package/dist/websocket.d.ts +6 -0
  171. package/dist/websocket.d.ts.map +1 -0
  172. package/dist/websocket.js +4 -0
  173. package/dist/workspaceEdit.d.ts +66 -0
  174. package/dist/workspaceEdit.d.ts.map +1 -0
  175. package/dist/workspaceEdit.js +276 -0
  176. package/dist/workspaceEdit.js.map +1 -0
  177. package/dist/workspaceEditProvenance.d.ts +5 -0
  178. package/dist/workspaceEditProvenance.d.ts.map +1 -0
  179. package/dist/workspaceEditProvenance.js +16 -0
  180. package/dist/workspaceEditProvenance.js.map +1 -0
  181. package/dist/workspaceTextEdits.d.ts +60 -0
  182. package/dist/workspaceTextEdits.d.ts.map +1 -0
  183. package/dist/workspaceTextEdits.js +395 -0
  184. package/dist/workspaceTextEdits.js.map +1 -0
  185. package/package.json +118 -0
@@ -0,0 +1,255 @@
1
+ import { LspConnection } from "./lspConnection.js";
2
+ //#region src/lspConnectionPool.ts
3
+ /**
4
+ * How long a connection with no borrowers is kept. A file switch drops the lease count to zero for a
5
+ * tick or two on its way from one view to the next; the cost of holding on is one idle socket, the
6
+ * cost of letting go early is another handshake against a server that may still be indexing.
7
+ */
8
+ var DEFAULT_IDLE_GRACE_MS = 3e4;
9
+ /**
10
+ * One connection per key, outliving the views that borrow it. A view contribution is rebuilt on
11
+ * every file switch, and a connection it owns dies with it — thirteen handshakes in twenty-two
12
+ * seconds, measured. The key is opaque: how a server session is named is the host's vocabulary.
13
+ */
14
+ var LspConnectionPool = class {
15
+ #entries = /* @__PURE__ */ new Map();
16
+ #idleGraceMs;
17
+ #onEvent;
18
+ constructor(options = {}) {
19
+ this.#idleGraceMs = options.idleGraceMs ?? DEFAULT_IDLE_GRACE_MS;
20
+ this.#onEvent = options.onEvent;
21
+ }
22
+ /** A provider bound to one key, in the shape the plugin takes. */
23
+ provider(key) {
24
+ return { acquire: (options, callbacks) => this.acquire(key, options, callbacks) };
25
+ }
26
+ acquire(key, options, callbacks) {
27
+ const existing = this.#entries.get(key);
28
+ if (existing) this.#assertCompatible(existing, options);
29
+ const entry = existing ?? this.#create(key, options);
30
+ if (entry.idleTimer !== null) {
31
+ clearTimeout(entry.idleTimer);
32
+ entry.idleTimer = null;
33
+ }
34
+ const lease = {
35
+ callbacks,
36
+ notificationHandlers: options.notificationHandlers ?? {},
37
+ serverRequestHandlers: options.serverRequestHandlers ?? {}
38
+ };
39
+ this.#reportUndispatchable(entry, lease);
40
+ entry.leases.add(lease);
41
+ this.#emit(entry, existing ? "reused" : "created");
42
+ this.#replayTo(entry, lease);
43
+ return {
44
+ connection: entry.connection,
45
+ release: () => this.#release(entry, lease)
46
+ };
47
+ }
48
+ /** Closes every connection. For teardown and for tests, not for routine use. */
49
+ dispose() {
50
+ for (const entry of this.#entries.values()) {
51
+ if (entry.idleTimer !== null) clearTimeout(entry.idleTimer);
52
+ entry.connection.dispose();
53
+ }
54
+ this.#entries.clear();
55
+ }
56
+ #create(key, options) {
57
+ const notificationMethods = new Set(Object.keys(options.notificationHandlers ?? {}));
58
+ const entry = {
59
+ connected: false,
60
+ connection: null,
61
+ createdAt: now(),
62
+ idleTimer: null,
63
+ initializationContract: initializationContract(options),
64
+ key,
65
+ leases: /* @__PURE__ */ new Set(),
66
+ notificationMethods,
67
+ readyAt: null,
68
+ status: "idle"
69
+ };
70
+ entry.connection = new LspConnection({
71
+ ...options,
72
+ notificationHandlers: this.#dispatchers(entry, notificationMethods),
73
+ serverRequestHandlers: requestDispatchers(entry, options.serverRequestHandlers ?? {})
74
+ }, this.#fanOut(entry));
75
+ this.#entries.set(key, entry);
76
+ entry.connection.connect();
77
+ return entry;
78
+ }
79
+ #assertCompatible(entry, options) {
80
+ const incoming = initializationContract(options);
81
+ if (structurallyEqual(entry.initializationContract, incoming)) return;
82
+ throw new Error(`LSP connection pool key "${entry.key}" already has a different initialization contract.`);
83
+ }
84
+ /** Handled is the disjunction: one view claiming a notification must not hide it from another. */
85
+ #dispatchers(entry, methods) {
86
+ const handlers = {};
87
+ for (const method of methods) handlers[method] = (client, params, message) => {
88
+ let handled = false;
89
+ for (const lease of leasesOf(entry)) handled = (lease.notificationHandlers[method]?.(client, params, message) ?? false) || handled;
90
+ return handled;
91
+ };
92
+ return handlers;
93
+ }
94
+ /** Diagnostics broadcast rather than routed: `DocumentSync` already drops uris it does not hold. */
95
+ #fanOut(entry) {
96
+ return {
97
+ onConnected: () => {
98
+ entry.connected = true;
99
+ entry.readyAt = now();
100
+ this.#emit(entry, "ready", { durationMs: Math.round(entry.readyAt - entry.createdAt) });
101
+ for (const lease of leasesOf(entry)) lease.callbacks.onConnected();
102
+ },
103
+ onDiagnosticRefresh: () => {
104
+ for (const lease of leasesOf(entry)) lease.callbacks.onDiagnosticRefresh?.();
105
+ },
106
+ onReconnecting: (error) => {
107
+ entry.connected = false;
108
+ this.#emit(entry, "reconnecting", {
109
+ durationMs: Math.round(now() - entry.createdAt),
110
+ error,
111
+ reachedReady: entry.readyAt !== null
112
+ });
113
+ for (const lease of leasesOf(entry)) lease.callbacks.onReconnecting?.(error);
114
+ },
115
+ onUnavailable: () => {
116
+ entry.connected = false;
117
+ this.#retire(entry);
118
+ for (const lease of leasesOf(entry)) lease.callbacks.onUnavailable();
119
+ },
120
+ onPublishDiagnostics: (params) => {
121
+ for (const lease of leasesOf(entry)) lease.callbacks.onPublishDiagnostics(params);
122
+ },
123
+ onStatusChange: (status) => {
124
+ entry.status = status;
125
+ for (const lease of leasesOf(entry)) lease.callbacks.onStatusChange?.(status);
126
+ },
127
+ onError: (error) => {
128
+ this.#emit(entry, "error", {
129
+ durationMs: Math.round(now() - entry.createdAt),
130
+ error,
131
+ reachedReady: entry.readyAt !== null
132
+ });
133
+ for (const lease of leasesOf(entry)) lease.callbacks.onError?.(error);
134
+ }
135
+ };
136
+ }
137
+ /** A microtask, not a straight call: `acquire` runs inside the contribution's constructor. */
138
+ #replayTo(entry, lease) {
139
+ if (entry.status === "idle") return;
140
+ const status = entry.status;
141
+ const connected = entry.connected;
142
+ queueMicrotask(() => {
143
+ if (!entry.leases.has(lease)) return;
144
+ lease.callbacks.onStatusChange?.(status);
145
+ if (connected) lease.callbacks.onConnected();
146
+ });
147
+ }
148
+ #release(entry, lease) {
149
+ if (!entry.leases.delete(lease)) return;
150
+ this.#emit(entry, "released");
151
+ if (entry.leases.size > 0) return;
152
+ if (this.#entries.get(entry.key) !== entry) return;
153
+ entry.idleTimer = setTimeout(() => this.#closeIdle(entry), this.#idleGraceMs);
154
+ }
155
+ #closeIdle(entry) {
156
+ entry.idleTimer = null;
157
+ if (entry.leases.size > 0) return;
158
+ if (this.#entries.get(entry.key) !== entry) return;
159
+ this.#entries.delete(entry.key);
160
+ entry.connection.dispose();
161
+ this.#emit(entry, "closed", {
162
+ durationMs: Math.round(now() - entry.createdAt),
163
+ reachedReady: entry.readyAt !== null
164
+ });
165
+ }
166
+ #retire(entry) {
167
+ if (entry.idleTimer !== null) clearTimeout(entry.idleTimer);
168
+ entry.idleTimer = null;
169
+ if (this.#entries.get(entry.key) === entry) this.#entries.delete(entry.key);
170
+ entry.connection.dispose();
171
+ this.#emit(entry, "retired", {
172
+ durationMs: Math.round(now() - entry.createdAt),
173
+ reachedReady: entry.readyAt !== null
174
+ });
175
+ }
176
+ #reportUndispatchable(entry, lease) {
177
+ const methods = Object.keys(lease.notificationHandlers).filter((method) => !entry.notificationMethods.has(method));
178
+ if (methods.length === 0) return;
179
+ this.#emit(entry, "handler_ignored", { methods });
180
+ }
181
+ #emit(entry, kind, extra = {}) {
182
+ this.#onEvent?.({
183
+ key: entry.key,
184
+ kind,
185
+ leaseCount: entry.leases.size,
186
+ status: entry.status,
187
+ ...extra
188
+ });
189
+ }
190
+ };
191
+ /** Snapshotted: a callback can release its own lease mid-iteration. */
192
+ function leasesOf(entry) {
193
+ return Array.from(entry.leases);
194
+ }
195
+ function now() {
196
+ return typeof performance === "undefined" ? Date.now() : performance.now();
197
+ }
198
+ function initializationContract(options) {
199
+ return {
200
+ capabilities: normalizeComparable(options.capabilities),
201
+ clientInfo: normalizeComparable(options.clientInfo),
202
+ initializationOptions: normalizeComparable(options.initializationOptions),
203
+ rootUri: options.rootUri
204
+ };
205
+ }
206
+ function normalizeComparable(value) {
207
+ if (Array.isArray(value)) return value.map(normalizeComparable);
208
+ if (!isRecord(value)) return value;
209
+ const normalized = {};
210
+ const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
211
+ for (const key of keys) normalized[key] = normalizeComparable(value[key]);
212
+ return normalized;
213
+ }
214
+ function structurallyEqual(left, right) {
215
+ if (Object.is(left, right)) return true;
216
+ if (Array.isArray(left) || Array.isArray(right)) return equalArrays(left, right);
217
+ if (!isRecord(left) || !isRecord(right)) return false;
218
+ return equalRecords(left, right);
219
+ }
220
+ function equalArrays(left, right) {
221
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
222
+ if (left.length !== right.length) return false;
223
+ for (let index = 0; index < left.length; index += 1) if (!structurallyEqual(left[index], right[index])) return false;
224
+ return true;
225
+ }
226
+ function equalRecords(left, right) {
227
+ const leftKeys = Object.keys(left);
228
+ const rightKeys = Object.keys(right);
229
+ if (leftKeys.length !== rightKeys.length) return false;
230
+ for (const key of leftKeys) {
231
+ if (!(key in right)) return false;
232
+ if (!structurallyEqual(left[key], right[key])) return false;
233
+ }
234
+ return true;
235
+ }
236
+ function isRecord(value) {
237
+ return typeof value === "object" && value !== null && !Array.isArray(value);
238
+ }
239
+ /**
240
+ * A request has one answer, so the first borrower that handles the method gives it; the methods
241
+ * are fixed by the first borrower, like the notification methods.
242
+ */
243
+ function requestDispatchers(entry, initial) {
244
+ const handlers = {};
245
+ for (const method of Object.keys(initial)) handlers[method] = (client, params, message) => {
246
+ const handler = leasesOf(entry).find((lease) => lease.serverRequestHandlers[method])?.serverRequestHandlers[method];
247
+ if (!handler) throw new Error(`No editor on this connection answers ${method}`);
248
+ return handler(client, params, message);
249
+ };
250
+ return handlers;
251
+ }
252
+ //#endregion
253
+ export { LspConnectionPool };
254
+
255
+ //# sourceMappingURL=lspConnectionPool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lspConnectionPool.js","names":[],"sources":["../src/lspConnectionPool.ts"],"sourcesContent":["import type {\n LspClient,\n LspNotificationHandler,\n LspServerRequestHandler,\n} from '@singapore-editor/lsp'\n\nimport {\n LspConnection,\n type LspConnectionCallbacks,\n type LspConnectionLease,\n type LspConnectionOptions,\n type LspConnectionProvider,\n} from './lspConnection'\nimport type { LanguageServerStatus } from './types'\n\n/**\n * How long a connection with no borrowers is kept. A file switch drops the lease count to zero for a\n * tick or two on its way from one view to the next; the cost of holding on is one idle socket, the\n * cost of letting go early is another handshake against a server that may still be indexing.\n */\nconst DEFAULT_IDLE_GRACE_MS = 30_000\n\n/** What a pooled connection just did, for a host that wants to log it. */\nexport type LspConnectionPoolEvent = {\n readonly kind:\n | 'created'\n | 'reused'\n | 'ready'\n | 'released'\n | 'closed'\n | 'retired'\n | 'reconnecting'\n | 'error'\n | 'handler_ignored'\n readonly key: string\n readonly leaseCount: number\n readonly status: LanguageServerStatus\n /** Time to `initialize` for `ready`; the connection's life so far for `closed`, `retired` and `error`. */\n readonly durationMs?: number\n readonly reachedReady?: boolean\n readonly error?: unknown\n /** Methods a late borrower declared that this connection cannot dispatch. */\n readonly methods?: readonly string[]\n}\n\nexport type LspConnectionPoolOptions = {\n readonly idleGraceMs?: number\n readonly onEvent?: (event: LspConnectionPoolEvent) => void\n}\n\ntype NotificationHandlers = Readonly<Record<string, LspNotificationHandler<LspClient>>>\ntype ServerRequestHandlers = Readonly<Record<string, LspServerRequestHandler<LspClient>>>\n\n/** Handlers are per borrower: the first one's are bound to a view the reader may have left. */\ntype ConnectionLease = {\n readonly callbacks: LspConnectionCallbacks\n readonly notificationHandlers: NotificationHandlers\n readonly serverRequestHandlers: ServerRequestHandlers\n}\n\ntype PooledConnection = {\n readonly initializationContract: ImmutableInitializationContract\n readonly key: string\n readonly leases: Set<ConnectionLease>\n readonly createdAt: number\n /** Fixed at construction, when the client reads the record. A later borrower outside it is a defect. */\n readonly notificationMethods: ReadonlySet<string>\n connection: LspConnection\n status: LanguageServerStatus\n connected: boolean\n readyAt: number | null\n idleTimer: ReturnType<typeof setTimeout> | null\n}\n\ntype ImmutableInitializationContract = {\n readonly capabilities: unknown\n readonly clientInfo: unknown\n readonly initializationOptions: unknown\n readonly rootUri: string | null\n}\n\n/**\n * One connection per key, outliving the views that borrow it. A view contribution is rebuilt on\n * every file switch, and a connection it owns dies with it — thirteen handshakes in twenty-two\n * seconds, measured. The key is opaque: how a server session is named is the host's vocabulary.\n */\nexport class LspConnectionPool {\n readonly #entries = new Map<string, PooledConnection>()\n readonly #idleGraceMs: number\n readonly #onEvent?: (event: LspConnectionPoolEvent) => void\n\n public constructor(options: LspConnectionPoolOptions = {}) {\n this.#idleGraceMs = options.idleGraceMs ?? DEFAULT_IDLE_GRACE_MS\n this.#onEvent = options.onEvent\n }\n\n /** A provider bound to one key, in the shape the plugin takes. */\n public provider(key: string): LspConnectionProvider {\n return { acquire: (options, callbacks) => this.acquire(key, options, callbacks) }\n }\n\n public acquire(\n key: string,\n options: LspConnectionOptions,\n callbacks: LspConnectionCallbacks,\n ): LspConnectionLease {\n const existing = this.#entries.get(key)\n if (existing) this.#assertCompatible(existing, options)\n\n const entry = existing ?? this.#create(key, options)\n if (entry.idleTimer !== null) {\n clearTimeout(entry.idleTimer)\n entry.idleTimer = null\n }\n\n const lease: ConnectionLease = {\n callbacks,\n notificationHandlers: options.notificationHandlers ?? {},\n serverRequestHandlers: options.serverRequestHandlers ?? {},\n }\n this.#reportUndispatchable(entry, lease)\n entry.leases.add(lease)\n this.#emit(entry, existing ? 'reused' : 'created')\n this.#replayTo(entry, lease)\n\n return {\n connection: entry.connection,\n release: () => this.#release(entry, lease),\n }\n }\n\n /** Closes every connection. For teardown and for tests, not for routine use. */\n public dispose(): void {\n for (const entry of this.#entries.values()) {\n if (entry.idleTimer !== null) clearTimeout(entry.idleTimer)\n entry.connection.dispose()\n }\n this.#entries.clear()\n }\n\n #create(key: string, options: LspConnectionOptions): PooledConnection {\n const notificationMethods = new Set(Object.keys(options.notificationHandlers ?? {}))\n const entry: PooledConnection = {\n connected: false,\n connection: null as unknown as LspConnection,\n createdAt: now(),\n idleTimer: null,\n initializationContract: initializationContract(options),\n key,\n leases: new Set(),\n notificationMethods,\n readyAt: null,\n status: 'idle',\n }\n // Assigned after the record exists because the callbacks close over it: the\n // connection reports `loading` from inside `connect()`, which is before any\n // `new LspConnection(...)` expression could have returned.\n entry.connection = new LspConnection(\n {\n ...options,\n notificationHandlers: this.#dispatchers(entry, notificationMethods),\n serverRequestHandlers: requestDispatchers(entry, options.serverRequestHandlers ?? {}),\n },\n this.#fanOut(entry),\n )\n this.#entries.set(key, entry)\n entry.connection.connect()\n\n return entry\n }\n\n #assertCompatible(entry: PooledConnection, options: LspConnectionOptions): void {\n const incoming = initializationContract(options)\n if (structurallyEqual(entry.initializationContract, incoming)) return\n\n throw new Error(\n `LSP connection pool key \"${entry.key}\" already has a different initialization contract.`,\n )\n }\n\n /** Handled is the disjunction: one view claiming a notification must not hide it from another. */\n #dispatchers(entry: PooledConnection, methods: ReadonlySet<string>): NotificationHandlers {\n const handlers: Record<string, LspNotificationHandler<LspClient>> = {}\n for (const method of methods) {\n handlers[method] = (client, params, message) => {\n let handled = false\n for (const lease of leasesOf(entry))\n handled =\n (lease.notificationHandlers[method]?.(client, params, message) ?? false) || handled\n\n return handled\n }\n }\n\n return handlers\n }\n\n /** Diagnostics broadcast rather than routed: `DocumentSync` already drops uris it does not hold. */\n #fanOut(entry: PooledConnection): LspConnectionCallbacks {\n return {\n onConnected: () => {\n entry.connected = true\n entry.readyAt = now()\n this.#emit(entry, 'ready', { durationMs: Math.round(entry.readyAt - entry.createdAt) })\n for (const lease of leasesOf(entry)) lease.callbacks.onConnected()\n },\n onDiagnosticRefresh: () => {\n for (const lease of leasesOf(entry)) lease.callbacks.onDiagnosticRefresh?.()\n },\n onReconnecting: (error) => {\n entry.connected = false\n this.#emit(entry, 'reconnecting', {\n durationMs: Math.round(now() - entry.createdAt),\n error,\n reachedReady: entry.readyAt !== null,\n })\n for (const lease of leasesOf(entry)) lease.callbacks.onReconnecting?.(error)\n },\n onUnavailable: () => {\n entry.connected = false\n this.#retire(entry)\n for (const lease of leasesOf(entry)) lease.callbacks.onUnavailable()\n },\n onPublishDiagnostics: (params) => {\n for (const lease of leasesOf(entry)) lease.callbacks.onPublishDiagnostics(params)\n },\n onStatusChange: (status) => {\n entry.status = status\n for (const lease of leasesOf(entry)) lease.callbacks.onStatusChange?.(status)\n },\n onError: (error) => {\n this.#emit(entry, 'error', {\n durationMs: Math.round(now() - entry.createdAt),\n error,\n reachedReady: entry.readyAt !== null,\n })\n for (const lease of leasesOf(entry)) lease.callbacks.onError?.(error)\n },\n }\n }\n\n /** A microtask, not a straight call: `acquire` runs inside the contribution's constructor. */\n #replayTo(entry: PooledConnection, lease: ConnectionLease): void {\n if (entry.status === 'idle') return\n\n const status = entry.status\n const connected = entry.connected\n queueMicrotask(() => {\n if (!entry.leases.has(lease)) return\n\n lease.callbacks.onStatusChange?.(status)\n if (connected) lease.callbacks.onConnected()\n })\n }\n\n #release(entry: PooledConnection, lease: ConnectionLease): void {\n if (!entry.leases.delete(lease)) return\n\n this.#emit(entry, 'released')\n if (entry.leases.size > 0) return\n if (this.#entries.get(entry.key) !== entry) return\n\n entry.idleTimer = setTimeout(() => this.#closeIdle(entry), this.#idleGraceMs)\n }\n\n #closeIdle(entry: PooledConnection): void {\n entry.idleTimer = null\n if (entry.leases.size > 0) return\n if (this.#entries.get(entry.key) !== entry) return\n\n this.#entries.delete(entry.key)\n entry.connection.dispose()\n this.#emit(entry, 'closed', {\n durationMs: Math.round(now() - entry.createdAt),\n reachedReady: entry.readyAt !== null,\n })\n }\n\n #retire(entry: PooledConnection): void {\n if (entry.idleTimer !== null) clearTimeout(entry.idleTimer)\n entry.idleTimer = null\n if (this.#entries.get(entry.key) === entry) this.#entries.delete(entry.key)\n entry.connection.dispose()\n this.#emit(entry, 'retired', {\n durationMs: Math.round(now() - entry.createdAt),\n reachedReady: entry.readyAt !== null,\n })\n }\n\n #reportUndispatchable(entry: PooledConnection, lease: ConnectionLease): void {\n const methods = Object.keys(lease.notificationHandlers).filter(\n (method) => !entry.notificationMethods.has(method),\n )\n if (methods.length === 0) return\n\n this.#emit(entry, 'handler_ignored', { methods })\n }\n\n #emit(\n entry: PooledConnection,\n kind: LspConnectionPoolEvent['kind'],\n extra: Omit<LspConnectionPoolEvent, 'kind' | 'key' | 'leaseCount' | 'status'> = {},\n ): void {\n this.#onEvent?.({\n key: entry.key,\n kind,\n leaseCount: entry.leases.size,\n status: entry.status,\n ...extra,\n })\n }\n}\n\n/** Snapshotted: a callback can release its own lease mid-iteration. */\nfunction leasesOf(entry: PooledConnection): readonly ConnectionLease[] {\n return Array.from(entry.leases)\n}\n\nfunction now(): number {\n return typeof performance === 'undefined' ? Date.now() : performance.now()\n}\n\nfunction initializationContract(options: LspConnectionOptions): ImmutableInitializationContract {\n return {\n capabilities: normalizeComparable(options.capabilities),\n clientInfo: normalizeComparable(options.clientInfo),\n initializationOptions: normalizeComparable(options.initializationOptions),\n rootUri: options.rootUri,\n }\n}\n\nfunction normalizeComparable(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(normalizeComparable)\n if (!isRecord(value)) return value\n\n const normalized: Record<string, unknown> = {}\n const keys = Object.keys(value)\n .filter((key) => value[key] !== undefined)\n .sort()\n for (const key of keys) normalized[key] = normalizeComparable(value[key])\n return normalized\n}\n\nfunction structurallyEqual(left: unknown, right: unknown): boolean {\n if (Object.is(left, right)) return true\n if (Array.isArray(left) || Array.isArray(right)) return equalArrays(left, right)\n if (!isRecord(left) || !isRecord(right)) return false\n return equalRecords(left, right)\n}\n\nfunction equalArrays(left: unknown, right: unknown): boolean {\n if (!Array.isArray(left) || !Array.isArray(right)) return false\n if (left.length !== right.length) return false\n\n for (let index = 0; index < left.length; index += 1) {\n if (!structurallyEqual(left[index], right[index])) return false\n }\n return true\n}\n\nfunction equalRecords(left: Record<string, unknown>, right: Record<string, unknown>): boolean {\n const leftKeys = Object.keys(left)\n const rightKeys = Object.keys(right)\n if (leftKeys.length !== rightKeys.length) return false\n\n for (const key of leftKeys) {\n if (!(key in right)) return false\n if (!structurallyEqual(left[key], right[key])) return false\n }\n return true\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * A request has one answer, so the first borrower that handles the method gives it; the methods\n * are fixed by the first borrower, like the notification methods.\n */\nfunction requestDispatchers(\n entry: PooledConnection,\n initial: ServerRequestHandlers,\n): ServerRequestHandlers {\n const handlers: Record<string, LspServerRequestHandler<LspClient>> = {}\n for (const method of Object.keys(initial)) {\n handlers[method] = (client, params, message) => {\n const handler = leasesOf(entry).find((lease) => lease.serverRequestHandlers[method])\n ?.serverRequestHandlers[method]\n // An answer the server can mistake for an empty one is worse than an error it can report.\n if (!handler) throw new Error(`No editor on this connection answers ${method}`)\n return handler(client, params, message)\n }\n }\n return handlers\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { TextEdit } from '@singapore-editor/core';
2
+ /**
3
+ * The longest replacement still worth re-diffing.
4
+ *
5
+ * Past this the comparison costs more than it saves: a reply that rewrites this much has left
6
+ * almost no anchor, decoration or fold inside the span to preserve, and the diff itself becomes the
7
+ * expensive part of formatting.
8
+ */
9
+ export declare const REDIFF_LENGTH_LIMIT = 100000;
10
+ /**
11
+ * Splits a replacement into edits covering only what differs from the text it overwrites.
12
+ *
13
+ * A formatter answers with the whole document, or a whole line, even when three characters moved.
14
+ * Applying that verbatim rewrites text that did not change, and every anchor, decoration, fold and
15
+ * selection inside the rewritten span dies with it — so the reply is compared against the buffer
16
+ * first and only the differences are handed on.
17
+ *
18
+ * `offset` is where `original` starts in the document, so the returned edits address the buffer.
19
+ */
20
+ export declare function minimalReplacementEdits(original: string, replacement: string, offset: number): TextEdit[];
21
+ //# sourceMappingURL=minimalEdits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minimalEdits.d.ts","sourceRoot":"","sources":["../src/minimalEdits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAEtD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,SAAU,CAAA;AAkB1C;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,QAAQ,EAAE,CA6BZ"}
@@ -0,0 +1,181 @@
1
+ //#region src/minimalEdits.ts
2
+ /**
3
+ * The longest replacement still worth re-diffing.
4
+ *
5
+ * Past this the comparison costs more than it saves: a reply that rewrites this much has left
6
+ * almost no anchor, decoration or fold inside the span to preserve, and the diff itself becomes the
7
+ * expensive part of formatting.
8
+ */
9
+ var REDIFF_LENGTH_LIMIT = 1e5;
10
+ /**
11
+ * How far apart the two texts may be before the alignment is abandoned.
12
+ *
13
+ * One endpoint band per step is kept so the alignment can be walked back, so the memory held grows
14
+ * with the square of this number. A reply differing by more than this is a rewrite rather than a
15
+ * reformat, and is cheaper to apply whole.
16
+ */
17
+ var REDIFF_STEP_LIMIT = 400;
18
+ /**
19
+ * Splits a replacement into edits covering only what differs from the text it overwrites.
20
+ *
21
+ * A formatter answers with the whole document, or a whole line, even when three characters moved.
22
+ * Applying that verbatim rewrites text that did not change, and every anchor, decoration, fold and
23
+ * selection inside the rewritten span dies with it — so the reply is compared against the buffer
24
+ * first and only the differences are handed on.
25
+ *
26
+ * `offset` is where `original` starts in the document, so the returned edits address the buffer.
27
+ */
28
+ function minimalReplacementEdits(original, replacement, offset) {
29
+ if (original === replacement) return [];
30
+ if (Math.max(original.length, replacement.length) > 1e5) return [wholeReplacement(original, replacement, offset)];
31
+ const originalLines = splitAfterLineBreaks(original);
32
+ const replacementLines = splitAfterLineBreaks(replacement);
33
+ const regions = changedLineRegions(originalLines, replacementLines);
34
+ if (!regions) return [wholeReplacement(original, replacement, offset)];
35
+ const originalStarts = lineStartOffsets(originalLines);
36
+ const replacementStarts = lineStartOffsets(replacementLines);
37
+ const edits = [];
38
+ for (const region of regions) {
39
+ const edit = narrowToDifference(original, originalStarts[region.originalStart] ?? 0, originalStarts[region.originalEnd] ?? original.length, replacement.slice(replacementStarts[region.replacementStart] ?? 0, replacementStarts[region.replacementEnd] ?? replacement.length), offset);
40
+ if (edit.from !== edit.to || edit.text.length > 0) edits.push(edit);
41
+ }
42
+ return edits;
43
+ }
44
+ function wholeReplacement(original, replacement, offset) {
45
+ return {
46
+ from: offset,
47
+ text: replacement,
48
+ to: offset + original.length
49
+ };
50
+ }
51
+ /**
52
+ * Shrinks one region to the characters inside it that differ.
53
+ *
54
+ * Line alignment hands back the whole line an indentation change sits in; dropping what the two
55
+ * sides still share turns that into the handful of characters the formatter actually touched. A
56
+ * boundary steps back off a surrogate pair, because half a code point is not something the rest of
57
+ * the editor can anchor to.
58
+ */
59
+ function narrowToDifference(original, from, to, text, offset) {
60
+ const shared = Math.min(to - from, text.length);
61
+ let prefix = 0;
62
+ while (prefix < shared && original.charCodeAt(from + prefix) === text.charCodeAt(prefix)) prefix += 1;
63
+ if (prefix > 0 && isHighSurrogate(text.charCodeAt(prefix - 1))) prefix -= 1;
64
+ let suffix = 0;
65
+ while (suffix < shared - prefix && original.charCodeAt(to - 1 - suffix) === text.charCodeAt(text.length - 1 - suffix)) suffix += 1;
66
+ if (suffix > 0 && isLowSurrogate(text.charCodeAt(text.length - suffix))) suffix -= 1;
67
+ return {
68
+ from: offset + from + prefix,
69
+ text: text.slice(prefix, text.length - suffix),
70
+ to: offset + to - suffix
71
+ };
72
+ }
73
+ function isHighSurrogate(code) {
74
+ return code >= 55296 && code <= 56319;
75
+ }
76
+ function isLowSurrogate(code) {
77
+ return code >= 56320 && code <= 57343;
78
+ }
79
+ /** Lines keep their terminator, so concatenating them back gives the text they came from. */
80
+ function splitAfterLineBreaks(text) {
81
+ const lines = [];
82
+ let start = 0;
83
+ for (let index = 0; index < text.length; index += 1) {
84
+ if (text.charCodeAt(index) !== 10) continue;
85
+ lines.push(text.slice(start, index + 1));
86
+ start = index + 1;
87
+ }
88
+ lines.push(text.slice(start));
89
+ return lines;
90
+ }
91
+ function lineStartOffsets(lines) {
92
+ const starts = [0];
93
+ let offset = 0;
94
+ for (const line of lines) {
95
+ offset += line.length;
96
+ starts.push(offset);
97
+ }
98
+ return starts;
99
+ }
100
+ /**
101
+ * Aligns two line sequences and returns the runs that do not correspond, or null when they lie
102
+ * further apart than the step limit allows.
103
+ *
104
+ * The alignment is the shortest path that deletes original lines and inserts replacement ones,
105
+ * taking equal lines for free; each step's furthest reach per diagonal is recorded so the path can
106
+ * be walked back once the end is reached.
107
+ *
108
+ * Lines rather than characters, because a formatter's changes are line-shaped and aligning by line
109
+ * costs a fraction of what aligning by character would; `narrowToDifference` recovers the
110
+ * character-level precision afterwards, as a linear scan of one region.
111
+ */
112
+ function changedLineRegions(original, replacement) {
113
+ const originalCount = original.length;
114
+ const replacementCount = replacement.length;
115
+ const stepLimit = Math.min(originalCount + replacementCount, REDIFF_STEP_LIMIT);
116
+ const band = stepLimit + 1;
117
+ const endpoints = new Int32Array(2 * band + 1);
118
+ const trace = [];
119
+ for (let step = 0; step <= stepLimit; step += 1) {
120
+ trace.push(endpoints.slice(band - step, band + step + 1));
121
+ for (let diagonal = -step; diagonal <= step; diagonal += 2) {
122
+ const afterInsert = endpoints[band + diagonal + 1] ?? 0;
123
+ const afterDelete = endpoints[band + diagonal - 1] ?? 0;
124
+ let x = diagonal === -step || diagonal !== step && afterDelete < afterInsert ? afterInsert : afterDelete + 1;
125
+ let y = x - diagonal;
126
+ while (x < originalCount && y < replacementCount && original[x] === replacement[y]) {
127
+ x += 1;
128
+ y += 1;
129
+ }
130
+ endpoints[band + diagonal] = x;
131
+ if (x >= originalCount && y >= replacementCount) return regionsFromTrace(trace, originalCount, replacementCount);
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * Walks the recorded alignment back from the end, collecting every step that was not a match.
138
+ *
139
+ * Consecutive non-matching steps belong to one region: they are separated only once a run of equal
140
+ * lines has been walked back over, which moves both cursors away from the region being built.
141
+ */
142
+ function regionsFromTrace(trace, originalCount, replacementCount) {
143
+ const regions = [];
144
+ let open = null;
145
+ let x = originalCount;
146
+ let y = replacementCount;
147
+ for (let step = trace.length - 1; step > 0; step -= 1) {
148
+ const endpoints = trace[step];
149
+ if (!endpoints) break;
150
+ const diagonal = x - y;
151
+ const afterInsert = endpoints[diagonal + 1 + step] ?? 0;
152
+ const afterDelete = endpoints[diagonal - 1 + step] ?? 0;
153
+ const inserts = diagonal === -step || diagonal !== step && afterDelete < afterInsert;
154
+ const previousDiagonal = inserts ? diagonal + 1 : diagonal - 1;
155
+ const previousX = inserts ? afterInsert : afterDelete;
156
+ const previousY = previousX - previousDiagonal;
157
+ while (x > previousX && y > previousY) {
158
+ x -= 1;
159
+ y -= 1;
160
+ }
161
+ if (open && open.originalStart === x && open.replacementStart === y) {
162
+ open.originalStart = previousX;
163
+ open.replacementStart = previousY;
164
+ } else {
165
+ open = {
166
+ originalEnd: x,
167
+ originalStart: previousX,
168
+ replacementEnd: y,
169
+ replacementStart: previousY
170
+ };
171
+ regions.push(open);
172
+ }
173
+ x = previousX;
174
+ y = previousY;
175
+ }
176
+ return regions.reverse();
177
+ }
178
+ //#endregion
179
+ export { REDIFF_LENGTH_LIMIT, minimalReplacementEdits };
180
+
181
+ //# sourceMappingURL=minimalEdits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minimalEdits.js","names":[],"sources":["../src/minimalEdits.ts"],"sourcesContent":["import type { TextEdit } from '@singapore-editor/core'\n\n/**\n * The longest replacement still worth re-diffing.\n *\n * Past this the comparison costs more than it saves: a reply that rewrites this much has left\n * almost no anchor, decoration or fold inside the span to preserve, and the diff itself becomes the\n * expensive part of formatting.\n */\nexport const REDIFF_LENGTH_LIMIT = 100_000\n\n/**\n * How far apart the two texts may be before the alignment is abandoned.\n *\n * One endpoint band per step is kept so the alignment can be walked back, so the memory held grows\n * with the square of this number. A reply differing by more than this is a rewrite rather than a\n * reformat, and is cheaper to apply whole.\n */\nconst REDIFF_STEP_LIMIT = 400\n\ntype LineRegion = {\n originalStart: number\n originalEnd: number\n replacementStart: number\n replacementEnd: number\n}\n\n/**\n * Splits a replacement into edits covering only what differs from the text it overwrites.\n *\n * A formatter answers with the whole document, or a whole line, even when three characters moved.\n * Applying that verbatim rewrites text that did not change, and every anchor, decoration, fold and\n * selection inside the rewritten span dies with it — so the reply is compared against the buffer\n * first and only the differences are handed on.\n *\n * `offset` is where `original` starts in the document, so the returned edits address the buffer.\n */\nexport function minimalReplacementEdits(\n original: string,\n replacement: string,\n offset: number,\n): TextEdit[] {\n if (original === replacement) return []\n if (Math.max(original.length, replacement.length) > REDIFF_LENGTH_LIMIT) {\n return [wholeReplacement(original, replacement, offset)]\n }\n\n const originalLines = splitAfterLineBreaks(original)\n const replacementLines = splitAfterLineBreaks(replacement)\n const regions = changedLineRegions(originalLines, replacementLines)\n if (!regions) return [wholeReplacement(original, replacement, offset)]\n\n const originalStarts = lineStartOffsets(originalLines)\n const replacementStarts = lineStartOffsets(replacementLines)\n\n const edits: TextEdit[] = []\n for (const region of regions) {\n const edit = narrowToDifference(\n original,\n originalStarts[region.originalStart] ?? 0,\n originalStarts[region.originalEnd] ?? original.length,\n replacement.slice(\n replacementStarts[region.replacementStart] ?? 0,\n replacementStarts[region.replacementEnd] ?? replacement.length,\n ),\n offset,\n )\n if (edit.from !== edit.to || edit.text.length > 0) edits.push(edit)\n }\n return edits\n}\n\nfunction wholeReplacement(original: string, replacement: string, offset: number): TextEdit {\n return { from: offset, text: replacement, to: offset + original.length }\n}\n\n/**\n * Shrinks one region to the characters inside it that differ.\n *\n * Line alignment hands back the whole line an indentation change sits in; dropping what the two\n * sides still share turns that into the handful of characters the formatter actually touched. A\n * boundary steps back off a surrogate pair, because half a code point is not something the rest of\n * the editor can anchor to.\n */\nfunction narrowToDifference(\n original: string,\n from: number,\n to: number,\n text: string,\n offset: number,\n): TextEdit {\n const shared = Math.min(to - from, text.length)\n\n let prefix = 0\n while (prefix < shared && original.charCodeAt(from + prefix) === text.charCodeAt(prefix)) {\n prefix += 1\n }\n if (prefix > 0 && isHighSurrogate(text.charCodeAt(prefix - 1))) prefix -= 1\n\n let suffix = 0\n while (\n suffix < shared - prefix &&\n original.charCodeAt(to - 1 - suffix) === text.charCodeAt(text.length - 1 - suffix)\n ) {\n suffix += 1\n }\n if (suffix > 0 && isLowSurrogate(text.charCodeAt(text.length - suffix))) suffix -= 1\n\n return {\n from: offset + from + prefix,\n text: text.slice(prefix, text.length - suffix),\n to: offset + to - suffix,\n }\n}\n\nfunction isHighSurrogate(code: number): boolean {\n return code >= 0xd800 && code <= 0xdbff\n}\n\nfunction isLowSurrogate(code: number): boolean {\n return code >= 0xdc00 && code <= 0xdfff\n}\n\n/** Lines keep their terminator, so concatenating them back gives the text they came from. */\nfunction splitAfterLineBreaks(text: string): string[] {\n const lines: string[] = []\n let start = 0\n for (let index = 0; index < text.length; index += 1) {\n if (text.charCodeAt(index) !== 10) continue\n\n lines.push(text.slice(start, index + 1))\n start = index + 1\n }\n lines.push(text.slice(start))\n return lines\n}\n\nfunction lineStartOffsets(lines: readonly string[]): number[] {\n const starts = [0]\n let offset = 0\n for (const line of lines) {\n offset += line.length\n starts.push(offset)\n }\n return starts\n}\n\n/**\n * Aligns two line sequences and returns the runs that do not correspond, or null when they lie\n * further apart than the step limit allows.\n *\n * The alignment is the shortest path that deletes original lines and inserts replacement ones,\n * taking equal lines for free; each step's furthest reach per diagonal is recorded so the path can\n * be walked back once the end is reached.\n *\n * Lines rather than characters, because a formatter's changes are line-shaped and aligning by line\n * costs a fraction of what aligning by character would; `narrowToDifference` recovers the\n * character-level precision afterwards, as a linear scan of one region.\n */\nfunction changedLineRegions(\n original: readonly string[],\n replacement: readonly string[],\n): LineRegion[] | null {\n const originalCount = original.length\n const replacementCount = replacement.length\n const stepLimit = Math.min(originalCount + replacementCount, REDIFF_STEP_LIMIT)\n // A diagonal is reachable only after as many steps as it is far from the origin, so the band of\n // live endpoints never widens past the step limit.\n const band = stepLimit + 1\n const endpoints = new Int32Array(2 * band + 1)\n const trace: Int32Array[] = []\n\n for (let step = 0; step <= stepLimit; step += 1) {\n trace.push(endpoints.slice(band - step, band + step + 1))\n\n for (let diagonal = -step; diagonal <= step; diagonal += 2) {\n const afterInsert = endpoints[band + diagonal + 1] ?? 0\n const afterDelete = endpoints[band + diagonal - 1] ?? 0\n const inserts = diagonal === -step || (diagonal !== step && afterDelete < afterInsert)\n let x = inserts ? afterInsert : afterDelete + 1\n let y = x - diagonal\n while (x < originalCount && y < replacementCount && original[x] === replacement[y]) {\n x += 1\n y += 1\n }\n endpoints[band + diagonal] = x\n\n if (x >= originalCount && y >= replacementCount) {\n return regionsFromTrace(trace, originalCount, replacementCount)\n }\n }\n }\n\n return null\n}\n\n/**\n * Walks the recorded alignment back from the end, collecting every step that was not a match.\n *\n * Consecutive non-matching steps belong to one region: they are separated only once a run of equal\n * lines has been walked back over, which moves both cursors away from the region being built.\n */\nfunction regionsFromTrace(\n trace: readonly Int32Array[],\n originalCount: number,\n replacementCount: number,\n): LineRegion[] {\n const regions: LineRegion[] = []\n let open: LineRegion | null = null\n let x = originalCount\n let y = replacementCount\n\n for (let step = trace.length - 1; step > 0; step -= 1) {\n const endpoints = trace[step]\n if (!endpoints) break\n\n const diagonal = x - y\n const afterInsert = endpoints[diagonal + 1 + step] ?? 0\n const afterDelete = endpoints[diagonal - 1 + step] ?? 0\n const inserts = diagonal === -step || (diagonal !== step && afterDelete < afterInsert)\n const previousDiagonal = inserts ? diagonal + 1 : diagonal - 1\n const previousX = inserts ? afterInsert : afterDelete\n const previousY = previousX - previousDiagonal\n\n while (x > previousX && y > previousY) {\n x -= 1\n y -= 1\n }\n\n if (open && open.originalStart === x && open.replacementStart === y) {\n open.originalStart = previousX\n open.replacementStart = previousY\n } else {\n open = {\n originalEnd: x,\n originalStart: previousX,\n replacementEnd: y,\n replacementStart: previousY,\n }\n regions.push(open)\n }\n\n x = previousX\n y = previousY\n }\n\n return regions.reverse()\n}\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type * as lsp from 'vscode-languageserver-protocol';
2
+ export declare function sourcePathToFileName(path: string): string;
3
+ export declare function pathOrUriToDocumentUri(pathOrUri: string): lsp.DocumentUri;
4
+ export declare function fileNameToDocumentUri(fileName: string): lsp.DocumentUri;
5
+ export declare function documentUriToFileName(uri: string): string | null;
6
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,gCAAgC,CAAA;AAE1D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,WAAW,CAMzE;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,WAAW,CAGvE;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQhE"}
package/dist/paths.js ADDED
@@ -0,0 +1,38 @@
1
+ //#region src/paths.ts
2
+ function sourcePathToFileName(path) {
3
+ if (hasUriScheme(path)) return documentUriToFileName(path) ?? normalizeFileNamePath(path);
4
+ return normalizeFileNamePath(path);
5
+ }
6
+ function pathOrUriToDocumentUri(pathOrUri) {
7
+ if (!hasUriScheme(pathOrUri)) return fileNameToDocumentUri(sourcePathToFileName(pathOrUri));
8
+ const fileName = documentUriToFileName(pathOrUri);
9
+ if (!fileName) return pathOrUri;
10
+ return fileNameToDocumentUri(fileName);
11
+ }
12
+ function fileNameToDocumentUri(fileName) {
13
+ return `file://${encodePathname(normalizeFileNamePath(fileName))}`;
14
+ }
15
+ function documentUriToFileName(uri) {
16
+ try {
17
+ const url = new URL(uri);
18
+ if (url.protocol !== "file:") return null;
19
+ return normalizeFileNamePath(decodeURIComponent(url.pathname));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function normalizeFileNamePath(path) {
25
+ const normalized = path.replaceAll("\\", "/").replace(/^\/+/, "");
26
+ if (!normalized) return "/";
27
+ return `/${normalized}`;
28
+ }
29
+ function encodePathname(pathname) {
30
+ return pathname.split("/").map((part, index) => index === 0 && part === "" ? "" : encodeURIComponent(part)).join("/");
31
+ }
32
+ function hasUriScheme(value) {
33
+ return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value);
34
+ }
35
+ //#endregion
36
+ export { documentUriToFileName, fileNameToDocumentUri, pathOrUriToDocumentUri, sourcePathToFileName };
37
+
38
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","names":[],"sources":["../src/paths.ts"],"sourcesContent":["import type * as lsp from 'vscode-languageserver-protocol'\n\nexport function sourcePathToFileName(path: string): string {\n if (hasUriScheme(path)) return documentUriToFileName(path) ?? normalizeFileNamePath(path)\n return normalizeFileNamePath(path)\n}\n\nexport function pathOrUriToDocumentUri(pathOrUri: string): lsp.DocumentUri {\n if (!hasUriScheme(pathOrUri)) return fileNameToDocumentUri(sourcePathToFileName(pathOrUri))\n\n const fileName = documentUriToFileName(pathOrUri)\n if (!fileName) return pathOrUri\n return fileNameToDocumentUri(fileName)\n}\n\nexport function fileNameToDocumentUri(fileName: string): lsp.DocumentUri {\n const normalized = normalizeFileNamePath(fileName)\n return `file://${encodePathname(normalized)}`\n}\n\nexport function documentUriToFileName(uri: string): string | null {\n try {\n const url = new URL(uri)\n if (url.protocol !== 'file:') return null\n return normalizeFileNamePath(decodeURIComponent(url.pathname))\n } catch {\n return null\n }\n}\n\nfunction normalizeFileNamePath(path: string): string {\n const normalized = path.replaceAll('\\\\', '/').replace(/^\\/+/, '')\n if (!normalized) return '/'\n return `/${normalized}`\n}\n\nfunction encodePathname(pathname: string): string {\n return pathname\n .split('/')\n .map((part, index) => (index === 0 && part === '' ? '' : encodeURIComponent(part)))\n .join('/')\n}\n\nfunction hasUriScheme(value: string): boolean {\n return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)\n}\n"],"mappings":";AAEA,SAAgB,qBAAqB,MAAsB;CACzD,IAAI,aAAa,IAAI,GAAG,OAAO,sBAAsB,IAAI,KAAK,sBAAsB,IAAI;CACxF,OAAO,sBAAsB,IAAI;AACnC;AAEA,SAAgB,uBAAuB,WAAoC;CACzE,IAAI,CAAC,aAAa,SAAS,GAAG,OAAO,sBAAsB,qBAAqB,SAAS,CAAC;CAE1F,MAAM,WAAW,sBAAsB,SAAS;CAChD,IAAI,CAAC,UAAU,OAAO;CACtB,OAAO,sBAAsB,QAAQ;AACvC;AAEA,SAAgB,sBAAsB,UAAmC;CAEvE,OAAO,UAAU,eADE,sBAAsB,QACT,CAAU;AAC5C;AAEA,SAAgB,sBAAsB,KAA4B;CAChE,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,GAAG;EACvB,IAAI,IAAI,aAAa,SAAS,OAAO;EACrC,OAAO,sBAAsB,mBAAmB,IAAI,QAAQ,CAAC;CAC/D,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,sBAAsB,MAAsB;CACnD,MAAM,aAAa,KAAK,WAAW,MAAM,GAAG,CAAC,CAAC,QAAQ,QAAQ,EAAE;CAChE,IAAI,CAAC,YAAY,OAAO;CACxB,OAAO,IAAI;AACb;AAEA,SAAS,eAAe,UAA0B;CAChD,OAAO,SACJ,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,UAAW,UAAU,KAAK,SAAS,KAAK,KAAK,mBAAmB,IAAI,CAAE,CAAC,CAClF,KAAK,GAAG;AACb;AAEA,SAAS,aAAa,OAAwB;CAC5C,OAAO,4BAA4B,KAAK,KAAK;AAC/C"}