@pgcorp/ui-kit 0.8.1 → 0.10.0

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 (113) hide show
  1. package/README.md +332 -9
  2. package/docs/getting-started.md +135 -0
  3. package/docs/public-api.md +53 -4
  4. package/docs/rich-document.md +216 -0
  5. package/docs/scroll-viewport.md +167 -0
  6. package/docs/theming.md +17 -0
  7. package/package.json +108 -50
  8. package/src/components/layout/SAppShell.vue +3 -1
  9. package/src/components/layout/SDockRegion.vue +5 -3
  10. package/src/components/layout/SResizeHandle.vue +45 -5
  11. package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
  12. package/src/components/shared/_internal/SDialogSurface.css +23 -0
  13. package/src/components/shared/_internal/SDialogSurface.vue +9 -2
  14. package/src/components/shared/_internal/SToastItem.css +4 -0
  15. package/src/components/shared/_internal/SToastItem.vue +44 -8
  16. package/src/components/shared/_internal/STreeNode.vue +24 -11
  17. package/src/components/shared/_internal/dialogSurface.ts +7 -1
  18. package/src/components/shared/complex/SKanbanLane.vue +4 -2
  19. package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
  20. package/src/components/shared/containers/SDrawer.css +11 -2
  21. package/src/components/shared/containers/SDrawer.vue +48 -9
  22. package/src/components/shared/containers/SModal.vue +5 -2
  23. package/src/components/shared/containers/SPanel.vue +4 -2
  24. package/src/components/shared/containers/SPopover.vue +7 -3
  25. package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
  26. package/src/components/shared/controls/SButton.css +14 -10
  27. package/src/components/shared/controls/SCombobox.vue +8 -6
  28. package/src/components/shared/controls/SComboboxTrigger.css +7 -4
  29. package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
  30. package/src/components/shared/controls/SContextToggleButton.vue +6 -4
  31. package/src/components/shared/controls/SDragHandle.vue +8 -4
  32. package/src/components/shared/controls/SDropdownMenu.vue +7 -4
  33. package/src/components/shared/controls/SInputText.css +5 -4
  34. package/src/components/shared/controls/SListbox.vue +4 -2
  35. package/src/components/shared/controls/SSelect.vue +4 -2
  36. package/src/components/shared/controls/STextarea.ts +13 -0
  37. package/src/components/shared/controls/STextarea.vue +391 -37
  38. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
  39. package/src/components/shared/data-display/SChart.vue +7 -5
  40. package/src/components/shared/data-display/SChip.vue +3 -1
  41. package/src/components/shared/data-display/SCodeBlock.css +4 -0
  42. package/src/components/shared/data-display/SCodeBlock.vue +46 -22
  43. package/src/components/shared/data-display/SCodeEditor.css +17 -0
  44. package/src/components/shared/data-display/SCodeEditor.vue +173 -8
  45. package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
  46. package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
  47. package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
  48. package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
  49. package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
  50. package/src/components/shared/data-display/SCopyField.vue +12 -10
  51. package/src/components/shared/data-display/SDiffViewer.css +122 -0
  52. package/src/components/shared/data-display/SDiffViewer.vue +499 -0
  53. package/src/components/shared/data-display/SDocBlock.vue +99 -71
  54. package/src/components/shared/data-display/SExpandableText.vue +10 -8
  55. package/src/components/shared/data-display/SJsonTree.vue +4 -2
  56. package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
  57. package/src/components/shared/data-display/SMessage.vue +6 -6
  58. package/src/components/shared/data-display/SRichDocument.css +8 -0
  59. package/src/components/shared/data-display/SRichDocument.vue +65 -0
  60. package/src/components/shared/data-display/SScrollViewport.css +53 -0
  61. package/src/components/shared/data-display/SScrollViewport.vue +470 -0
  62. package/src/components/shared/data-display/STable.vue +38 -19
  63. package/src/components/shared/data-display/SToastContainer.vue +22 -17
  64. package/src/components/shared/data-display/SVirtualList.ts +33 -0
  65. package/src/components/shared/data-display/SVirtualList.vue +321 -36
  66. package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
  67. package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
  68. package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
  69. package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
  70. package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
  71. package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
  72. package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
  73. package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
  74. package/src/components/shared/data-display/codeEditor.ts +200 -0
  75. package/src/components/shared/data-display/codeWorkspace.ts +233 -0
  76. package/src/components/shared/data-display/diff.ts +541 -0
  77. package/src/components/shared/data-display/richDocument.ts +876 -0
  78. package/src/components/shared/data-display/scrollViewport.ts +47 -0
  79. package/src/components/shared/data-display/toast.ts +194 -0
  80. package/src/components/shared/database/SDataGrid.vue +7 -4
  81. package/src/components/shared/database/SSqlEditor.vue +39 -39
  82. package/src/components/shared/feedback/SAsyncState.vue +3 -1
  83. package/src/components/shared/graph/SGraphViewport.vue +11 -9
  84. package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
  85. package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
  86. package/src/components/shared/navigation/STabList.vue +10 -6
  87. package/src/components/shared/navigation/STabs.vue +15 -11
  88. package/src/components/shared/navigation/SWizardSteps.vue +4 -2
  89. package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
  90. package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
  91. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
  92. package/src/components/shared/persona/_internal/labels.ts +66 -57
  93. package/src/components/shared/persona/presentation.ts +140 -110
  94. package/src/composables/useFloatingPosition.ts +39 -2
  95. package/src/composables/useUiKitLocale.ts +17 -0
  96. package/src/internal/codeHighlight.ts +110 -0
  97. package/src/internal/diffEngine.ts +464 -0
  98. package/src/internal/diffRuntime.ts +189 -0
  99. package/src/internal/feedbackPresentation.ts +33 -14
  100. package/src/internal/localeContext.ts +22 -0
  101. package/src/internal/ownedAttrs.ts +8 -0
  102. package/src/internal/runtimeContract.ts +11 -9
  103. package/src/internal/semanticSizing.ts +13 -0
  104. package/src/locales/catalog.ts +634 -0
  105. package/src/locales/locale.ts +472 -0
  106. package/src/locales/messages.generated.ts +1826 -0
  107. package/src/locales/messages.ts +150 -0
  108. package/src/stores/useNotifierStore.ts +61 -44
  109. package/src/styles/tokens.css +23 -2
  110. package/src/theme.ts +59 -32
  111. package/src/workers/diff.worker.ts +73 -0
  112. package/src/workers/diffProtocol.ts +155 -0
  113. package/src/components/shared/data-display/SToastContainer.css +0 -53
@@ -0,0 +1,464 @@
1
+ import type {
2
+ SDiffComputationOptions,
3
+ SDiffGap,
4
+ SDiffHunk,
5
+ SDiffLine,
6
+ SDiffLineBreak,
7
+ SDiffLineKind,
8
+ SDiffLineSide,
9
+ SDiffResult,
10
+ SDiffSource,
11
+ SDiffSourceSnapshot,
12
+ } from '../components/shared/data-display/diff'
13
+
14
+ interface DiffToken {
15
+ readonly text: string
16
+ readonly lineBreak: SDiffLineBreak
17
+ }
18
+
19
+ interface DiffOperation {
20
+ readonly kind: SDiffLineKind
21
+ readonly beforeIndex?: number
22
+ readonly afterIndex?: number
23
+ }
24
+
25
+ export type DiffCancellationProbe = () => boolean
26
+
27
+ export class DiffEngineCancelledError extends Error {
28
+ constructor() {
29
+ super('Diff computation was cancelled.')
30
+ this.name = 'DiffEngineCancelledError'
31
+ }
32
+ }
33
+
34
+ const CANCELLATION_CHECK_INTERVAL = 1024
35
+
36
+ class CancellationCheckpoint {
37
+ private operations = 0
38
+
39
+ constructor(private readonly isCancelled: DiffCancellationProbe | undefined) {}
40
+
41
+ check(force = false): void {
42
+ this.operations += 1
43
+ if (!force && this.operations % CANCELLATION_CHECK_INTERVAL !== 0) return
44
+ if (this.isCancelled?.() === true) throw new DiffEngineCancelledError()
45
+ }
46
+ }
47
+
48
+ function tokenize(text: string): readonly DiffToken[] {
49
+ if (text.length === 0) return Object.freeze([])
50
+ const tokens: DiffToken[] = []
51
+ let start = 0
52
+ for (let index = 0; index < text.length; index += 1) {
53
+ if (text[index] !== '\n') continue
54
+ const hasCarriageReturn = index > start && text[index - 1] === '\r'
55
+ tokens.push(Object.freeze({
56
+ text: text.slice(start, hasCarriageReturn ? index - 1 : index),
57
+ lineBreak: hasCarriageReturn ? 'crlf' : 'lf',
58
+ }))
59
+ start = index + 1
60
+ }
61
+ if (start < text.length) {
62
+ tokens.push(Object.freeze({ text: text.slice(start), lineBreak: 'none' }))
63
+ }
64
+ return Object.freeze(tokens)
65
+ }
66
+
67
+ function tokensEqual(left: DiffToken, right: DiffToken): boolean {
68
+ return left.text === right.text && left.lineBreak === right.lineBreak
69
+ }
70
+
71
+ function findToken(
72
+ tokens: readonly DiffToken[],
73
+ start: number,
74
+ end: number,
75
+ target: DiffToken,
76
+ checkpoint: CancellationCheckpoint,
77
+ ): number {
78
+ for (let index = start; index < end; index += 1) {
79
+ checkpoint.check()
80
+ if (tokensEqual(tokens[index], target)) return index
81
+ }
82
+ return -1
83
+ }
84
+
85
+ function emitRemoved(operations: DiffOperation[], start: number, end: number): void {
86
+ for (let index = start; index < end; index += 1) {
87
+ operations.push({ kind: 'removed', beforeIndex: index })
88
+ }
89
+ }
90
+
91
+ function emitAdded(operations: DiffOperation[], start: number, end: number): void {
92
+ for (let index = start; index < end; index += 1) {
93
+ operations.push({ kind: 'added', afterIndex: index })
94
+ }
95
+ }
96
+
97
+ function emitUnchanged(
98
+ operations: DiffOperation[],
99
+ beforeStart: number,
100
+ afterStart: number,
101
+ length: number,
102
+ ): void {
103
+ for (let offset = 0; offset < length; offset += 1) {
104
+ operations.push({
105
+ kind: 'unchanged',
106
+ beforeIndex: beforeStart + offset,
107
+ afterIndex: afterStart + offset,
108
+ })
109
+ }
110
+ }
111
+
112
+ interface BisectSplit {
113
+ readonly before: number
114
+ readonly after: number
115
+ }
116
+
117
+ function findBisectSplit(
118
+ before: readonly DiffToken[],
119
+ beforeStart: number,
120
+ beforeEnd: number,
121
+ after: readonly DiffToken[],
122
+ afterStart: number,
123
+ afterEnd: number,
124
+ checkpoint: CancellationCheckpoint,
125
+ ): BisectSplit | null {
126
+ const beforeLength = beforeEnd - beforeStart
127
+ const afterLength = afterEnd - afterStart
128
+ const maxDistance = Math.ceil((beforeLength + afterLength) / 2)
129
+ const vectorOffset = maxDistance
130
+ const vectorLength = maxDistance * 2 + 1
131
+ const forward = new Int32Array(vectorLength)
132
+ const reverse = new Int32Array(vectorLength)
133
+ forward.fill(-1)
134
+ reverse.fill(-1)
135
+ forward[vectorOffset + 1] = 0
136
+ reverse[vectorOffset + 1] = 0
137
+ const delta = beforeLength - afterLength
138
+ const frontOverlap = delta % 2 !== 0
139
+ let forwardStart = 0
140
+ let forwardEnd = 0
141
+ let reverseStart = 0
142
+ let reverseEnd = 0
143
+
144
+ for (let distance = 0; distance < maxDistance; distance += 1) {
145
+ checkpoint.check(true)
146
+ for (
147
+ let diagonal = -distance + forwardStart;
148
+ diagonal <= distance - forwardEnd;
149
+ diagonal += 2
150
+ ) {
151
+ const vectorIndex = vectorOffset + diagonal
152
+ let beforeOffset = diagonal === -distance
153
+ || (diagonal !== distance && forward[vectorIndex - 1] < forward[vectorIndex + 1])
154
+ ? forward[vectorIndex + 1]
155
+ : forward[vectorIndex - 1] + 1
156
+ let afterOffset = beforeOffset - diagonal
157
+ while (
158
+ beforeOffset < beforeLength
159
+ && afterOffset < afterLength
160
+ && tokensEqual(before[beforeStart + beforeOffset], after[afterStart + afterOffset])
161
+ ) {
162
+ checkpoint.check()
163
+ beforeOffset += 1
164
+ afterOffset += 1
165
+ }
166
+ forward[vectorIndex] = beforeOffset
167
+ if (beforeOffset > beforeLength) forwardEnd += 2
168
+ else if (afterOffset > afterLength) forwardStart += 2
169
+ else if (frontOverlap) {
170
+ const reverseDiagonal = delta - diagonal
171
+ const reverseIndex = vectorOffset + reverseDiagonal
172
+ if (
173
+ reverseIndex >= 0
174
+ && reverseIndex < vectorLength
175
+ && reverse[reverseIndex] !== -1
176
+ && beforeOffset >= beforeLength - reverse[reverseIndex]
177
+ ) {
178
+ return {
179
+ before: beforeStart + beforeOffset,
180
+ after: afterStart + afterOffset,
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ for (
187
+ let diagonal = -distance + reverseStart;
188
+ diagonal <= distance - reverseEnd;
189
+ diagonal += 2
190
+ ) {
191
+ const vectorIndex = vectorOffset + diagonal
192
+ let beforeOffset = diagonal === -distance
193
+ || (diagonal !== distance && reverse[vectorIndex - 1] < reverse[vectorIndex + 1])
194
+ ? reverse[vectorIndex + 1]
195
+ : reverse[vectorIndex - 1] + 1
196
+ let afterOffset = beforeOffset - diagonal
197
+ while (
198
+ beforeOffset < beforeLength
199
+ && afterOffset < afterLength
200
+ && tokensEqual(before[beforeEnd - beforeOffset - 1], after[afterEnd - afterOffset - 1])
201
+ ) {
202
+ checkpoint.check()
203
+ beforeOffset += 1
204
+ afterOffset += 1
205
+ }
206
+ reverse[vectorIndex] = beforeOffset
207
+ if (beforeOffset > beforeLength) reverseEnd += 2
208
+ else if (afterOffset > afterLength) reverseStart += 2
209
+ else if (!frontOverlap) {
210
+ const forwardDiagonal = delta - diagonal
211
+ const forwardIndex = vectorOffset + forwardDiagonal
212
+ if (
213
+ forwardIndex >= 0
214
+ && forwardIndex < vectorLength
215
+ && forward[forwardIndex] !== -1
216
+ && forward[forwardIndex] >= beforeLength - beforeOffset
217
+ ) {
218
+ const splitBefore = beforeStart + forward[forwardIndex]
219
+ return {
220
+ before: splitBefore,
221
+ after: afterStart + splitBefore - beforeStart - forwardDiagonal,
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ return null
228
+ }
229
+
230
+ function diffRange(
231
+ before: readonly DiffToken[],
232
+ beforeStartValue: number,
233
+ beforeEndValue: number,
234
+ after: readonly DiffToken[],
235
+ afterStartValue: number,
236
+ afterEndValue: number,
237
+ operations: DiffOperation[],
238
+ checkpoint: CancellationCheckpoint,
239
+ ): void {
240
+ checkpoint.check(true)
241
+ let beforeStart = beforeStartValue
242
+ let afterStart = afterStartValue
243
+ while (
244
+ beforeStart < beforeEndValue
245
+ && afterStart < afterEndValue
246
+ && tokensEqual(before[beforeStart], after[afterStart])
247
+ ) {
248
+ checkpoint.check()
249
+ operations.push({ kind: 'unchanged', beforeIndex: beforeStart, afterIndex: afterStart })
250
+ beforeStart += 1
251
+ afterStart += 1
252
+ }
253
+
254
+ let beforeEnd = beforeEndValue
255
+ let afterEnd = afterEndValue
256
+ let suffixLength = 0
257
+ while (
258
+ beforeStart < beforeEnd
259
+ && afterStart < afterEnd
260
+ && tokensEqual(before[beforeEnd - 1], after[afterEnd - 1])
261
+ ) {
262
+ checkpoint.check()
263
+ beforeEnd -= 1
264
+ afterEnd -= 1
265
+ suffixLength += 1
266
+ }
267
+
268
+ const beforeLength = beforeEnd - beforeStart
269
+ const afterLength = afterEnd - afterStart
270
+ if (beforeLength === 0) emitAdded(operations, afterStart, afterEnd)
271
+ else if (afterLength === 0) emitRemoved(operations, beforeStart, beforeEnd)
272
+ else if (beforeLength === 1) {
273
+ const match = findToken(after, afterStart, afterEnd, before[beforeStart], checkpoint)
274
+ if (match === -1) {
275
+ emitRemoved(operations, beforeStart, beforeEnd)
276
+ emitAdded(operations, afterStart, afterEnd)
277
+ } else {
278
+ emitAdded(operations, afterStart, match)
279
+ emitUnchanged(operations, beforeStart, match, 1)
280
+ emitAdded(operations, match + 1, afterEnd)
281
+ }
282
+ } else if (afterLength === 1) {
283
+ const match = findToken(before, beforeStart, beforeEnd, after[afterStart], checkpoint)
284
+ if (match === -1) {
285
+ emitRemoved(operations, beforeStart, beforeEnd)
286
+ emitAdded(operations, afterStart, afterEnd)
287
+ } else {
288
+ emitRemoved(operations, beforeStart, match)
289
+ emitUnchanged(operations, match, afterStart, 1)
290
+ emitRemoved(operations, match + 1, beforeEnd)
291
+ }
292
+ } else {
293
+ const split = findBisectSplit(
294
+ before,
295
+ beforeStart,
296
+ beforeEnd,
297
+ after,
298
+ afterStart,
299
+ afterEnd,
300
+ checkpoint,
301
+ )
302
+ if (
303
+ split === null
304
+ || (split.before === beforeStart && split.after === afterStart)
305
+ || (split.before === beforeEnd && split.after === afterEnd)
306
+ ) {
307
+ emitRemoved(operations, beforeStart, beforeEnd)
308
+ emitAdded(operations, afterStart, afterEnd)
309
+ } else {
310
+ diffRange(before, beforeStart, split.before, after, afterStart, split.after, operations, checkpoint)
311
+ diffRange(before, split.before, beforeEnd, after, split.after, afterEnd, operations, checkpoint)
312
+ }
313
+ }
314
+
315
+ emitUnchanged(operations, beforeEnd, afterEnd, suffixLength)
316
+ }
317
+
318
+ function createLineSide(token: DiffToken, lineNumber: number): SDiffLineSide {
319
+ return Object.freeze({ lineNumber, text: token.text, lineBreak: token.lineBreak })
320
+ }
321
+
322
+ function buildLines(
323
+ before: readonly DiffToken[],
324
+ after: readonly DiffToken[],
325
+ operations: readonly DiffOperation[],
326
+ ): readonly SDiffLine[] {
327
+ return Object.freeze(operations.map((operation) => {
328
+ const beforeIndex = operation.beforeIndex
329
+ const afterIndex = operation.afterIndex
330
+ const beforeSide = beforeIndex === undefined
331
+ ? undefined
332
+ : createLineSide(before[beforeIndex], beforeIndex + 1)
333
+ const afterSide = afterIndex === undefined
334
+ ? undefined
335
+ : createLineSide(after[afterIndex], afterIndex + 1)
336
+ const beforeCoordinate = beforeIndex === undefined ? '-' : String(beforeIndex + 1)
337
+ const afterCoordinate = afterIndex === undefined ? '-' : String(afterIndex + 1)
338
+ return Object.freeze({
339
+ id: `line:${operation.kind}:${beforeCoordinate}:${afterCoordinate}`,
340
+ kind: operation.kind,
341
+ ...(beforeSide === undefined ? {} : { before: beforeSide }),
342
+ ...(afterSide === undefined ? {} : { after: afterSide }),
343
+ })
344
+ }))
345
+ }
346
+
347
+ function buildPrefixCounts(lines: readonly SDiffLine[]): {
348
+ readonly before: Int32Array
349
+ readonly after: Int32Array
350
+ } {
351
+ const before = new Int32Array(lines.length + 1)
352
+ const after = new Int32Array(lines.length + 1)
353
+ for (let index = 0; index < lines.length; index += 1) {
354
+ before[index + 1] = before[index] + (lines[index].before === undefined ? 0 : 1)
355
+ after[index + 1] = after[index] + (lines[index].after === undefined ? 0 : 1)
356
+ }
357
+ return { before, after }
358
+ }
359
+
360
+ function buildHunksAndGaps(
361
+ lines: readonly SDiffLine[],
362
+ contextLines: number,
363
+ ): { readonly hunks: readonly SDiffHunk[]; readonly gaps: readonly SDiffGap[] } {
364
+ const changedIndices: number[] = []
365
+ lines.forEach((line, index) => {
366
+ if (line.kind !== 'unchanged') changedIndices.push(index)
367
+ })
368
+ if (changedIndices.length === 0) {
369
+ return { hunks: Object.freeze([]), gaps: Object.freeze([]) }
370
+ }
371
+
372
+ const windows: Array<{ start: number; end: number }> = []
373
+ for (const changedIndex of changedIndices) {
374
+ const start = Math.max(0, changedIndex - contextLines)
375
+ const end = Math.min(lines.length, changedIndex + contextLines + 1)
376
+ const previous = windows[windows.length - 1]
377
+ if (previous !== undefined && start <= previous.end) previous.end = Math.max(previous.end, end)
378
+ else windows.push({ start, end })
379
+ }
380
+
381
+ const prefix = buildPrefixCounts(lines)
382
+ const hunks = windows.map((window) => Object.freeze({
383
+ id: `hunk:${prefix.before[window.start] + 1}:${prefix.after[window.start] + 1}`,
384
+ lineStart: window.start,
385
+ lineEnd: window.end,
386
+ beforeStart: prefix.before[window.start] + 1,
387
+ beforeLineCount: prefix.before[window.end] - prefix.before[window.start],
388
+ afterStart: prefix.after[window.start] + 1,
389
+ afterLineCount: prefix.after[window.end] - prefix.after[window.start],
390
+ }))
391
+ const gaps: SDiffGap[] = []
392
+ let previousEnd = 0
393
+ for (const window of windows) {
394
+ if (window.start > previousEnd) {
395
+ const lineCount = window.start - previousEnd
396
+ gaps.push(Object.freeze({
397
+ id: `gap:${prefix.before[previousEnd] + 1}:${prefix.after[previousEnd] + 1}:${lineCount}`,
398
+ lineStart: previousEnd,
399
+ lineEnd: window.start,
400
+ beforeStart: prefix.before[previousEnd] + 1,
401
+ afterStart: prefix.after[previousEnd] + 1,
402
+ lineCount,
403
+ }))
404
+ }
405
+ previousEnd = window.end
406
+ }
407
+ if (previousEnd < lines.length) {
408
+ const lineCount = lines.length - previousEnd
409
+ gaps.push(Object.freeze({
410
+ id: `gap:${prefix.before[previousEnd] + 1}:${prefix.after[previousEnd] + 1}:${lineCount}`,
411
+ lineStart: previousEnd,
412
+ lineEnd: lines.length,
413
+ beforeStart: prefix.before[previousEnd] + 1,
414
+ afterStart: prefix.after[previousEnd] + 1,
415
+ lineCount,
416
+ }))
417
+ }
418
+ return { hunks: Object.freeze(hunks), gaps: Object.freeze(gaps) }
419
+ }
420
+
421
+ function snapshot(source: SDiffSource, lineCount: number): SDiffSourceSnapshot {
422
+ return Object.freeze({
423
+ lineCount,
424
+ ...(source.label === undefined ? {} : { label: source.label }),
425
+ ...(source.revision === undefined ? {} : { revision: source.revision }),
426
+ ...(source.language === undefined ? {} : { language: source.language }),
427
+ })
428
+ }
429
+
430
+ /** Pure deterministic engine used by both direct computation and the module worker. */
431
+ export function computeDiffModel(
432
+ beforeSource: SDiffSource,
433
+ afterSource: SDiffSource,
434
+ options: Required<SDiffComputationOptions>,
435
+ isCancelled?: DiffCancellationProbe,
436
+ ): SDiffResult {
437
+ const checkpoint = new CancellationCheckpoint(isCancelled)
438
+ checkpoint.check(true)
439
+ const before = tokenize(beforeSource.text)
440
+ const after = tokenize(afterSource.text)
441
+ const operations: DiffOperation[] = []
442
+ diffRange(before, 0, before.length, after, 0, after.length, operations, checkpoint)
443
+ checkpoint.check(true)
444
+ const lines = buildLines(before, after, operations)
445
+ const { hunks, gaps } = buildHunksAndGaps(lines, options.contextLines)
446
+ const unchangedLineCount = lines.filter((line) => line.kind === 'unchanged').length
447
+ const removedLineCount = lines.filter((line) => line.kind === 'removed').length
448
+ const addedLineCount = lines.filter((line) => line.kind === 'added').length
449
+ return Object.freeze({
450
+ before: snapshot(beforeSource, before.length),
451
+ after: snapshot(afterSource, after.length),
452
+ lines,
453
+ hunks,
454
+ gaps,
455
+ summary: Object.freeze({
456
+ beforeLineCount: before.length,
457
+ afterLineCount: after.length,
458
+ unchangedLineCount,
459
+ removedLineCount,
460
+ addedLineCount,
461
+ changedLineCount: removedLineCount + addedLineCount,
462
+ }),
463
+ })
464
+ }
@@ -0,0 +1,189 @@
1
+ import {
2
+ computeDiff,
3
+ validateDiffComputationOptions,
4
+ validateDiffSource,
5
+ type SDiffComputationOptions,
6
+ type SDiffResult,
7
+ type SDiffSource,
8
+ } from '../components/shared/data-display/diff'
9
+ import {
10
+ S_DIFF_WORKER_PROTOCOL_VERSION,
11
+ validateDiffWorkerResponse,
12
+ type SDiffWorkerRequest,
13
+ } from '../workers/diffProtocol'
14
+
15
+ export const S_DIFF_WORKER_TEXT_THRESHOLD = 32_768
16
+ export const S_DIFF_WORKER_LINE_THRESHOLD = 1_200
17
+
18
+ export interface SDiffWorkerLike {
19
+ postMessage(message: SDiffWorkerRequest): void
20
+ addEventListener(type: 'message', listener: (event: MessageEvent<unknown>) => void): void
21
+ addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
22
+ removeEventListener(type: 'message', listener: (event: MessageEvent<unknown>) => void): void
23
+ removeEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
24
+ terminate(): void
25
+ }
26
+
27
+ export type SDiffWorkerFactory = () => SDiffWorkerLike
28
+
29
+ export interface SDiffRuntime {
30
+ compute(
31
+ before: SDiffSource,
32
+ after: SDiffSource,
33
+ options?: SDiffComputationOptions,
34
+ signal?: AbortSignal,
35
+ ): Promise<SDiffResult>
36
+ dispose(): void
37
+ }
38
+
39
+ export class SDiffComputationCancelledError extends Error {
40
+ constructor(message = 'Diff computation was cancelled.') {
41
+ super(message)
42
+ this.name = 'SDiffComputationCancelledError'
43
+ }
44
+ }
45
+
46
+ interface ActiveRequest {
47
+ readonly cancel: (message?: string) => void
48
+ }
49
+
50
+ function countLines(text: string): number {
51
+ if (text.length === 0) return 0
52
+ let lineCount = text.endsWith('\n') ? 0 : 1
53
+ for (const character of text) {
54
+ if (character === '\n') lineCount += 1
55
+ }
56
+ return lineCount
57
+ }
58
+
59
+ export function shouldUseDiffWorker(before: SDiffSource, after: SDiffSource): boolean {
60
+ return before.text.length + after.text.length >= S_DIFF_WORKER_TEXT_THRESHOLD
61
+ || countLines(before.text) + countLines(after.text) >= S_DIFF_WORKER_LINE_THRESHOLD
62
+ }
63
+
64
+ function createDefaultWorker(): SDiffWorkerLike {
65
+ return new Worker(new URL('../workers/diff.worker.ts', import.meta.url), {
66
+ type: 'module',
67
+ name: 's-ui-kit-diff',
68
+ })
69
+ }
70
+
71
+ /** Creates an instance-owned worker runtime. Disposal cancels every retained computation resource. */
72
+ export function createDiffRuntime(workerFactory: SDiffWorkerFactory = createDefaultWorker): SDiffRuntime {
73
+ let requestSequence = 0
74
+ let disposed = false
75
+ const activeRequests = new Map<string, ActiveRequest>()
76
+
77
+ const computeInWorker = (
78
+ before: SDiffSource,
79
+ after: SDiffSource,
80
+ options: Required<SDiffComputationOptions>,
81
+ signal: AbortSignal | undefined,
82
+ ): Promise<SDiffResult> => {
83
+ const requestId = `diff-request-${requestSequence += 1}`
84
+ const worker = workerFactory()
85
+ return new Promise<SDiffResult>((resolve, reject) => {
86
+ let settled = false
87
+ const cleanup = (): void => {
88
+ signal?.removeEventListener('abort', onAbort)
89
+ worker.removeEventListener('message', onMessage)
90
+ worker.removeEventListener('error', onError)
91
+ worker.terminate()
92
+ activeRequests.delete(requestId)
93
+ }
94
+ const failCancelled = (message?: string): void => {
95
+ if (settled) return
96
+ settled = true
97
+ try {
98
+ worker.postMessage({
99
+ version: S_DIFF_WORKER_PROTOCOL_VERSION,
100
+ type: 'cancel',
101
+ requestId,
102
+ })
103
+ } catch (cause) {
104
+ cleanup()
105
+ reject(cause)
106
+ return
107
+ }
108
+ cleanup()
109
+ reject(new SDiffComputationCancelledError(message))
110
+ }
111
+ const onAbort = (): void => failCancelled('Diff computation was aborted.')
112
+ const onError = (event: ErrorEvent): void => {
113
+ if (settled) return
114
+ settled = true
115
+ cleanup()
116
+ reject(new Error(`Diff worker failed: ${event.message}`))
117
+ }
118
+ const onMessage = (event: MessageEvent<unknown>): void => {
119
+ if (settled) return
120
+ let response
121
+ try {
122
+ response = validateDiffWorkerResponse(event.data)
123
+ } catch (cause) {
124
+ settled = true
125
+ cleanup()
126
+ reject(cause)
127
+ return
128
+ }
129
+ if (response.requestId !== requestId) {
130
+ settled = true
131
+ cleanup()
132
+ reject(new Error(
133
+ `Diff worker returned requestId ${JSON.stringify(response.requestId)} for active ${JSON.stringify(requestId)}.`,
134
+ ))
135
+ return
136
+ }
137
+ settled = true
138
+ cleanup()
139
+ if (response.type === 'result') resolve(response.result)
140
+ else if (response.type === 'cancelled') reject(new SDiffComputationCancelledError())
141
+ else {
142
+ const error = new Error(response.error.message)
143
+ error.name = response.error.name
144
+ reject(error)
145
+ }
146
+ }
147
+ activeRequests.set(requestId, { cancel: failCancelled })
148
+ signal?.addEventListener('abort', onAbort, { once: true })
149
+ worker.addEventListener('message', onMessage)
150
+ worker.addEventListener('error', onError)
151
+ try {
152
+ worker.postMessage({
153
+ version: S_DIFF_WORKER_PROTOCOL_VERSION,
154
+ type: 'compute',
155
+ requestId,
156
+ before,
157
+ after,
158
+ options,
159
+ })
160
+ } catch (cause) {
161
+ settled = true
162
+ cleanup()
163
+ reject(cause)
164
+ }
165
+ })
166
+ }
167
+
168
+ return Object.freeze({
169
+ async compute(
170
+ beforeValue: SDiffSource,
171
+ afterValue: SDiffSource,
172
+ optionsValue?: SDiffComputationOptions,
173
+ signal?: AbortSignal,
174
+ ): Promise<SDiffResult> {
175
+ if (disposed) throw new Error('Diff runtime is disposed.')
176
+ if (signal?.aborted === true) throw new SDiffComputationCancelledError('Diff computation was aborted.')
177
+ const before = validateDiffSource('SDiffRuntime', 'before', beforeValue)
178
+ const after = validateDiffSource('SDiffRuntime', 'after', afterValue)
179
+ const options = validateDiffComputationOptions('SDiffRuntime', optionsValue)
180
+ if (!shouldUseDiffWorker(before, after)) return computeDiff(before, after, options)
181
+ return computeInWorker(before, after, options, signal)
182
+ },
183
+ dispose(): void {
184
+ if (disposed) return
185
+ disposed = true
186
+ for (const request of [...activeRequests.values()]) request.cancel('Diff runtime was disposed.')
187
+ },
188
+ })
189
+ }