@pgcorp/ui-kit 0.9.0 → 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.
- package/README.md +188 -9
- package/docs/getting-started.md +135 -0
- package/docs/public-api.md +53 -4
- package/docs/rich-document.md +216 -0
- package/docs/scroll-viewport.md +167 -0
- package/package.json +103 -50
- package/src/components/layout/SAppShell.vue +3 -1
- package/src/components/layout/SDockRegion.vue +5 -3
- package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
- package/src/components/shared/_internal/SDialogSurface.css +5 -0
- package/src/components/shared/_internal/SDialogSurface.vue +3 -1
- package/src/components/shared/_internal/SToastItem.css +4 -0
- package/src/components/shared/_internal/SToastItem.vue +44 -8
- package/src/components/shared/_internal/STreeNode.vue +24 -11
- package/src/components/shared/complex/SKanbanLane.vue +4 -2
- package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
- package/src/components/shared/containers/SDrawer.vue +5 -2
- package/src/components/shared/containers/SModal.vue +5 -2
- package/src/components/shared/containers/SPanel.vue +4 -2
- package/src/components/shared/containers/SPopover.vue +7 -3
- package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
- package/src/components/shared/controls/SButton.css +14 -10
- package/src/components/shared/controls/SCombobox.vue +8 -6
- package/src/components/shared/controls/SComboboxTrigger.css +7 -4
- package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
- package/src/components/shared/controls/SContextToggleButton.vue +6 -4
- package/src/components/shared/controls/SDragHandle.vue +8 -4
- package/src/components/shared/controls/SDropdownMenu.vue +7 -4
- package/src/components/shared/controls/SInputText.css +5 -4
- package/src/components/shared/controls/SListbox.vue +4 -2
- package/src/components/shared/controls/SSelect.vue +4 -2
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
- package/src/components/shared/data-display/SChart.vue +7 -5
- package/src/components/shared/data-display/SChip.vue +3 -1
- package/src/components/shared/data-display/SCodeBlock.css +4 -0
- package/src/components/shared/data-display/SCodeBlock.vue +42 -20
- package/src/components/shared/data-display/SCodeEditor.css +17 -0
- package/src/components/shared/data-display/SCodeEditor.vue +173 -8
- package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
- package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
- package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
- package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
- package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
- package/src/components/shared/data-display/SCopyField.vue +12 -10
- package/src/components/shared/data-display/SDiffViewer.css +122 -0
- package/src/components/shared/data-display/SDiffViewer.vue +499 -0
- package/src/components/shared/data-display/SDocBlock.vue +99 -71
- package/src/components/shared/data-display/SExpandableText.vue +10 -8
- package/src/components/shared/data-display/SJsonTree.vue +4 -2
- package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
- package/src/components/shared/data-display/SMessage.vue +6 -6
- package/src/components/shared/data-display/SRichDocument.css +8 -0
- package/src/components/shared/data-display/SRichDocument.vue +65 -0
- package/src/components/shared/data-display/SScrollViewport.css +53 -0
- package/src/components/shared/data-display/SScrollViewport.vue +470 -0
- package/src/components/shared/data-display/STable.vue +38 -19
- package/src/components/shared/data-display/SToastContainer.vue +22 -17
- package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
- package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
- package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
- package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
- package/src/components/shared/data-display/codeEditor.ts +200 -0
- package/src/components/shared/data-display/codeWorkspace.ts +233 -0
- package/src/components/shared/data-display/diff.ts +541 -0
- package/src/components/shared/data-display/richDocument.ts +876 -0
- package/src/components/shared/data-display/scrollViewport.ts +47 -0
- package/src/components/shared/data-display/toast.ts +194 -0
- package/src/components/shared/database/SDataGrid.vue +7 -4
- package/src/components/shared/database/SSqlEditor.vue +39 -39
- package/src/components/shared/feedback/SAsyncState.vue +3 -1
- package/src/components/shared/graph/SGraphViewport.vue +11 -9
- package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
- package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
- package/src/components/shared/navigation/STabList.vue +10 -6
- package/src/components/shared/navigation/STabs.vue +15 -11
- package/src/components/shared/navigation/SWizardSteps.vue +4 -2
- package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
- package/src/components/shared/persona/_internal/labels.ts +66 -57
- package/src/components/shared/persona/presentation.ts +140 -110
- package/src/composables/useFloatingPosition.ts +39 -2
- package/src/composables/useUiKitLocale.ts +17 -0
- package/src/internal/codeHighlight.ts +110 -0
- package/src/internal/diffEngine.ts +464 -0
- package/src/internal/diffRuntime.ts +189 -0
- package/src/internal/feedbackPresentation.ts +33 -14
- package/src/internal/localeContext.ts +22 -0
- package/src/internal/ownedAttrs.ts +8 -0
- package/src/internal/runtimeContract.ts +11 -9
- package/src/internal/semanticSizing.ts +13 -0
- package/src/locales/catalog.ts +634 -0
- package/src/locales/locale.ts +472 -0
- package/src/locales/messages.generated.ts +1826 -0
- package/src/locales/messages.ts +150 -0
- package/src/stores/useNotifierStore.ts +61 -44
- package/src/styles/tokens.css +20 -2
- package/src/theme.ts +59 -32
- package/src/workers/diff.worker.ts +73 -0
- package/src/workers/diffProtocol.ts +155 -0
- 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
|
+
}
|