@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,541 @@
|
|
|
1
|
+
import type { CodeEditorLanguageContract } from '../../../internal/codeEditorContract'
|
|
2
|
+
import { computeDiffModel } from '../../../internal/diffEngine'
|
|
3
|
+
import {
|
|
4
|
+
validateBoundedInteger,
|
|
5
|
+
validateExactString,
|
|
6
|
+
validateNonEmptyString,
|
|
7
|
+
} from '../../../internal/runtimeContract'
|
|
8
|
+
import { validateCodeEditorLanguage } from '../../../internal/codeEditorContract'
|
|
9
|
+
|
|
10
|
+
export type SDiffSourceRevision = string | number
|
|
11
|
+
export type SDiffLanguage = CodeEditorLanguageContract
|
|
12
|
+
|
|
13
|
+
/** Exact text input. UI labels and revisions remain metadata and never participate in line equality. */
|
|
14
|
+
export interface SDiffSource {
|
|
15
|
+
readonly text: string
|
|
16
|
+
readonly label?: string
|
|
17
|
+
readonly revision?: SDiffSourceRevision
|
|
18
|
+
readonly language?: SDiffLanguage
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SDiffComputationOptions {
|
|
22
|
+
readonly contextLines?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SDiffLineBreak = 'lf' | 'crlf' | 'none'
|
|
26
|
+
export type SDiffLineKind = 'unchanged' | 'removed' | 'added'
|
|
27
|
+
|
|
28
|
+
export interface SDiffLineSide {
|
|
29
|
+
readonly lineNumber: number
|
|
30
|
+
readonly text: string
|
|
31
|
+
readonly lineBreak: SDiffLineBreak
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One semantic line operation. Missing sides are omitted instead of encoded as null. */
|
|
35
|
+
export interface SDiffLine {
|
|
36
|
+
readonly id: string
|
|
37
|
+
readonly kind: SDiffLineKind
|
|
38
|
+
readonly before?: SDiffLineSide
|
|
39
|
+
readonly after?: SDiffLineSide
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SDiffHunk {
|
|
43
|
+
readonly id: string
|
|
44
|
+
readonly lineStart: number
|
|
45
|
+
readonly lineEnd: number
|
|
46
|
+
readonly beforeStart: number
|
|
47
|
+
readonly beforeLineCount: number
|
|
48
|
+
readonly afterStart: number
|
|
49
|
+
readonly afterLineCount: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Hidden unchanged interval between hunk windows. Indices use the half-open result.lines domain. */
|
|
53
|
+
export interface SDiffGap {
|
|
54
|
+
readonly id: string
|
|
55
|
+
readonly lineStart: number
|
|
56
|
+
readonly lineEnd: number
|
|
57
|
+
readonly beforeStart: number
|
|
58
|
+
readonly afterStart: number
|
|
59
|
+
readonly lineCount: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SDiffSourceSnapshot {
|
|
63
|
+
readonly lineCount: number
|
|
64
|
+
readonly label?: string
|
|
65
|
+
readonly revision?: SDiffSourceRevision
|
|
66
|
+
readonly language?: SDiffLanguage
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SDiffSummary {
|
|
70
|
+
readonly beforeLineCount: number
|
|
71
|
+
readonly afterLineCount: number
|
|
72
|
+
readonly unchangedLineCount: number
|
|
73
|
+
readonly removedLineCount: number
|
|
74
|
+
readonly addedLineCount: number
|
|
75
|
+
readonly changedLineCount: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Serializable, presentation-neutral result shared by synchronous and worker execution paths. */
|
|
79
|
+
export interface SDiffResult {
|
|
80
|
+
readonly before: SDiffSourceSnapshot
|
|
81
|
+
readonly after: SDiffSourceSnapshot
|
|
82
|
+
readonly lines: readonly SDiffLine[]
|
|
83
|
+
readonly hunks: readonly SDiffHunk[]
|
|
84
|
+
readonly gaps: readonly SDiffGap[]
|
|
85
|
+
readonly summary: SDiffSummary
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const S_DIFF_DEFAULT_CONTEXT_LINES = 3
|
|
89
|
+
export const S_DIFF_MAX_CONTEXT_LINES = 100
|
|
90
|
+
|
|
91
|
+
const SOURCE_KEYS = new Set(['text', 'label', 'revision', 'language'])
|
|
92
|
+
const OPTIONS_KEYS = new Set(['contextLines'])
|
|
93
|
+
const RESULT_KEYS = new Set(['before', 'after', 'lines', 'hunks', 'gaps', 'summary'])
|
|
94
|
+
const SNAPSHOT_KEYS = new Set(['lineCount', 'label', 'revision', 'language'])
|
|
95
|
+
const LINE_KEYS = new Set(['id', 'kind', 'before', 'after'])
|
|
96
|
+
const LINE_SIDE_KEYS = new Set(['lineNumber', 'text', 'lineBreak'])
|
|
97
|
+
const HUNK_KEYS = new Set([
|
|
98
|
+
'id',
|
|
99
|
+
'lineStart',
|
|
100
|
+
'lineEnd',
|
|
101
|
+
'beforeStart',
|
|
102
|
+
'beforeLineCount',
|
|
103
|
+
'afterStart',
|
|
104
|
+
'afterLineCount',
|
|
105
|
+
])
|
|
106
|
+
const GAP_KEYS = new Set(['id', 'lineStart', 'lineEnd', 'beforeStart', 'afterStart', 'lineCount'])
|
|
107
|
+
const SUMMARY_KEYS = new Set([
|
|
108
|
+
'beforeLineCount',
|
|
109
|
+
'afterLineCount',
|
|
110
|
+
'unchangedLineCount',
|
|
111
|
+
'removedLineCount',
|
|
112
|
+
'addedLineCount',
|
|
113
|
+
'changedLineCount',
|
|
114
|
+
])
|
|
115
|
+
const LINE_KINDS = ['unchanged', 'removed', 'added'] as const
|
|
116
|
+
const LINE_BREAKS = ['lf', 'crlf', 'none'] as const
|
|
117
|
+
|
|
118
|
+
function assertPlainExactRecord(
|
|
119
|
+
owner: string,
|
|
120
|
+
coordinate: string,
|
|
121
|
+
value: unknown,
|
|
122
|
+
allowed: ReadonlySet<string>,
|
|
123
|
+
): Record<string, unknown> {
|
|
124
|
+
if (
|
|
125
|
+
typeof value !== 'object'
|
|
126
|
+
|| value === null
|
|
127
|
+
|| Array.isArray(value)
|
|
128
|
+
|| Object.getPrototypeOf(value) !== Object.prototype
|
|
129
|
+
) {
|
|
130
|
+
throw new TypeError(
|
|
131
|
+
`${owner}: ${coordinate} должен быть plain object. / ${owner}: ${coordinate} must be a plain object.`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
const record = value as Record<string, unknown>
|
|
135
|
+
const unexpected = Object.keys(record).filter((key) => !allowed.has(key))
|
|
136
|
+
if (unexpected.length > 0) {
|
|
137
|
+
throw new TypeError(
|
|
138
|
+
`${owner}.${coordinate}: неизвестные поля ${unexpected.map((key) => JSON.stringify(key)).join(', ')}. `
|
|
139
|
+
+ `/ ${owner}.${coordinate}: unknown fields ${unexpected.map((key) => JSON.stringify(key)).join(', ')}.`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
return record
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateStableString(owner: string, coordinate: string, value: unknown): string {
|
|
146
|
+
const validated = validateNonEmptyString(owner, coordinate, value)
|
|
147
|
+
if (validated !== validated.trim()) {
|
|
148
|
+
throw new TypeError(
|
|
149
|
+
`${owner}: ${coordinate} не должен содержать окружающие пробелы. `
|
|
150
|
+
+ `/ ${owner}: ${coordinate} must not contain surrounding whitespace.`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
return validated
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateRevision(owner: string, coordinate: string, value: unknown): SDiffSourceRevision {
|
|
157
|
+
if (typeof value === 'string') return validateStableString(owner, coordinate, value)
|
|
158
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) return value
|
|
159
|
+
throw new TypeError(
|
|
160
|
+
`${owner}: ${coordinate} должен быть непустой строкой или неотрицательным safe integer. `
|
|
161
|
+
+ `/ ${owner}: ${coordinate} must be a non-empty string or non-negative safe integer.`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateLanguage(
|
|
166
|
+
owner: string,
|
|
167
|
+
coordinate: string,
|
|
168
|
+
value: unknown,
|
|
169
|
+
): SDiffLanguage {
|
|
170
|
+
const language = validateCodeEditorLanguage(`${owner}.${coordinate}`, value)
|
|
171
|
+
return Object.freeze(language)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Validates an exact source record without coercion or newline normalization. */
|
|
175
|
+
export function validateDiffSource(owner: string, coordinate: string, value: unknown): SDiffSource {
|
|
176
|
+
const record = assertPlainExactRecord(owner, coordinate, value, SOURCE_KEYS)
|
|
177
|
+
if (typeof record.text !== 'string') {
|
|
178
|
+
throw new TypeError(
|
|
179
|
+
`${owner}: ${coordinate}.text должен быть string. / ${owner}: ${coordinate}.text must be a string.`,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
text: record.text,
|
|
184
|
+
...(record.label === undefined
|
|
185
|
+
? {}
|
|
186
|
+
: { label: validateNonEmptyString(owner, `${coordinate}.label`, record.label) }),
|
|
187
|
+
...(record.revision === undefined
|
|
188
|
+
? {}
|
|
189
|
+
: { revision: validateRevision(owner, `${coordinate}.revision`, record.revision) }),
|
|
190
|
+
...(record.language === undefined
|
|
191
|
+
? {}
|
|
192
|
+
: { language: validateLanguage(owner, `${coordinate}.language`, record.language) }),
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Validates computation options and supplies the documented context default. */
|
|
197
|
+
export function validateDiffComputationOptions(
|
|
198
|
+
owner: string,
|
|
199
|
+
value: unknown,
|
|
200
|
+
): Required<SDiffComputationOptions> {
|
|
201
|
+
const record = value === undefined
|
|
202
|
+
? {}
|
|
203
|
+
: assertPlainExactRecord(owner, 'options', value, OPTIONS_KEYS)
|
|
204
|
+
return Object.freeze({
|
|
205
|
+
contextLines: validateBoundedInteger(
|
|
206
|
+
owner,
|
|
207
|
+
'options.contextLines',
|
|
208
|
+
record.contextLines ?? S_DIFF_DEFAULT_CONTEXT_LINES,
|
|
209
|
+
{ minimum: 0, maximum: S_DIFF_MAX_CONTEXT_LINES },
|
|
210
|
+
),
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validateSnapshot(
|
|
215
|
+
owner: string,
|
|
216
|
+
coordinate: string,
|
|
217
|
+
value: unknown,
|
|
218
|
+
): SDiffSourceSnapshot {
|
|
219
|
+
const record = assertPlainExactRecord(owner, coordinate, value, SNAPSHOT_KEYS)
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
lineCount: validateBoundedInteger(owner, `${coordinate}.lineCount`, record.lineCount, {
|
|
222
|
+
minimum: 0,
|
|
223
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
224
|
+
}),
|
|
225
|
+
...(record.label === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: { label: validateNonEmptyString(owner, `${coordinate}.label`, record.label) }),
|
|
228
|
+
...(record.revision === undefined
|
|
229
|
+
? {}
|
|
230
|
+
: { revision: validateRevision(owner, `${coordinate}.revision`, record.revision) }),
|
|
231
|
+
...(record.language === undefined
|
|
232
|
+
? {}
|
|
233
|
+
: { language: validateLanguage(owner, `${coordinate}.language`, record.language) }),
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function validateLineSide(
|
|
238
|
+
owner: string,
|
|
239
|
+
coordinate: string,
|
|
240
|
+
value: unknown,
|
|
241
|
+
maximumLineNumber: number,
|
|
242
|
+
): SDiffLineSide {
|
|
243
|
+
const record = assertPlainExactRecord(owner, coordinate, value, LINE_SIDE_KEYS)
|
|
244
|
+
if (typeof record.text !== 'string') {
|
|
245
|
+
throw new TypeError(`${owner}: ${coordinate}.text должен быть string. / ${owner}: ${coordinate}.text must be a string.`)
|
|
246
|
+
}
|
|
247
|
+
return Object.freeze({
|
|
248
|
+
lineNumber: validateBoundedInteger(owner, `${coordinate}.lineNumber`, record.lineNumber, {
|
|
249
|
+
minimum: 1,
|
|
250
|
+
maximum: maximumLineNumber,
|
|
251
|
+
}),
|
|
252
|
+
text: record.text,
|
|
253
|
+
lineBreak: validateExactString(owner, `${coordinate}.lineBreak`, record.lineBreak, LINE_BREAKS),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function validateLine(
|
|
258
|
+
owner: string,
|
|
259
|
+
coordinate: string,
|
|
260
|
+
value: unknown,
|
|
261
|
+
beforeLineCount: number,
|
|
262
|
+
afterLineCount: number,
|
|
263
|
+
): SDiffLine {
|
|
264
|
+
const record = assertPlainExactRecord(owner, coordinate, value, LINE_KEYS)
|
|
265
|
+
const kind = validateExactString(owner, `${coordinate}.kind`, record.kind, LINE_KINDS)
|
|
266
|
+
const before = record.before === undefined
|
|
267
|
+
? undefined
|
|
268
|
+
: validateLineSide(owner, `${coordinate}.before`, record.before, beforeLineCount)
|
|
269
|
+
const after = record.after === undefined
|
|
270
|
+
? undefined
|
|
271
|
+
: validateLineSide(owner, `${coordinate}.after`, record.after, afterLineCount)
|
|
272
|
+
if (kind === 'unchanged' && (before === undefined || after === undefined)) {
|
|
273
|
+
throw new TypeError(`${owner}: ${coordinate} unchanged line требует обе стороны. / unchanged line requires both sides.`)
|
|
274
|
+
}
|
|
275
|
+
if (kind === 'removed' && (before === undefined || after !== undefined)) {
|
|
276
|
+
throw new TypeError(`${owner}: ${coordinate} removed line требует только before. / removed line requires before only.`)
|
|
277
|
+
}
|
|
278
|
+
if (kind === 'added' && (before !== undefined || after === undefined)) {
|
|
279
|
+
throw new TypeError(`${owner}: ${coordinate} added line требует только after. / added line requires after only.`)
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
kind === 'unchanged'
|
|
283
|
+
&& before !== undefined
|
|
284
|
+
&& after !== undefined
|
|
285
|
+
&& (before.text !== after.text || before.lineBreak !== after.lineBreak)
|
|
286
|
+
) {
|
|
287
|
+
throw new TypeError(`${owner}: ${coordinate} unchanged line содержит разные стороны. / unchanged line contains different sides.`)
|
|
288
|
+
}
|
|
289
|
+
return Object.freeze({
|
|
290
|
+
id: validateStableString(owner, `${coordinate}.id`, record.id),
|
|
291
|
+
kind,
|
|
292
|
+
...(before === undefined ? {} : { before }),
|
|
293
|
+
...(after === undefined ? {} : { after }),
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function validateHunk(
|
|
298
|
+
owner: string,
|
|
299
|
+
coordinate: string,
|
|
300
|
+
value: unknown,
|
|
301
|
+
lineCount: number,
|
|
302
|
+
beforeLineCount: number,
|
|
303
|
+
afterLineCount: number,
|
|
304
|
+
): SDiffHunk {
|
|
305
|
+
const record = assertPlainExactRecord(owner, coordinate, value, HUNK_KEYS)
|
|
306
|
+
const lineStart = validateBoundedInteger(owner, `${coordinate}.lineStart`, record.lineStart, {
|
|
307
|
+
minimum: 0,
|
|
308
|
+
maximum: lineCount,
|
|
309
|
+
})
|
|
310
|
+
const lineEnd = validateBoundedInteger(owner, `${coordinate}.lineEnd`, record.lineEnd, {
|
|
311
|
+
minimum: lineStart + 1,
|
|
312
|
+
maximum: lineCount,
|
|
313
|
+
})
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
id: validateStableString(owner, `${coordinate}.id`, record.id),
|
|
316
|
+
lineStart,
|
|
317
|
+
lineEnd,
|
|
318
|
+
beforeStart: validateBoundedInteger(owner, `${coordinate}.beforeStart`, record.beforeStart, {
|
|
319
|
+
minimum: 1,
|
|
320
|
+
maximum: beforeLineCount + 1,
|
|
321
|
+
}),
|
|
322
|
+
beforeLineCount: validateBoundedInteger(owner, `${coordinate}.beforeLineCount`, record.beforeLineCount, {
|
|
323
|
+
minimum: 0,
|
|
324
|
+
maximum: beforeLineCount,
|
|
325
|
+
}),
|
|
326
|
+
afterStart: validateBoundedInteger(owner, `${coordinate}.afterStart`, record.afterStart, {
|
|
327
|
+
minimum: 1,
|
|
328
|
+
maximum: afterLineCount + 1,
|
|
329
|
+
}),
|
|
330
|
+
afterLineCount: validateBoundedInteger(owner, `${coordinate}.afterLineCount`, record.afterLineCount, {
|
|
331
|
+
minimum: 0,
|
|
332
|
+
maximum: afterLineCount,
|
|
333
|
+
}),
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function validateGap(
|
|
338
|
+
owner: string,
|
|
339
|
+
coordinate: string,
|
|
340
|
+
value: unknown,
|
|
341
|
+
lineCount: number,
|
|
342
|
+
beforeLineCount: number,
|
|
343
|
+
afterLineCount: number,
|
|
344
|
+
): SDiffGap {
|
|
345
|
+
const record = assertPlainExactRecord(owner, coordinate, value, GAP_KEYS)
|
|
346
|
+
const lineStart = validateBoundedInteger(owner, `${coordinate}.lineStart`, record.lineStart, {
|
|
347
|
+
minimum: 0,
|
|
348
|
+
maximum: lineCount,
|
|
349
|
+
})
|
|
350
|
+
const lineEnd = validateBoundedInteger(owner, `${coordinate}.lineEnd`, record.lineEnd, {
|
|
351
|
+
minimum: lineStart + 1,
|
|
352
|
+
maximum: lineCount,
|
|
353
|
+
})
|
|
354
|
+
const intervalLength = lineEnd - lineStart
|
|
355
|
+
return Object.freeze({
|
|
356
|
+
id: validateStableString(owner, `${coordinate}.id`, record.id),
|
|
357
|
+
lineStart,
|
|
358
|
+
lineEnd,
|
|
359
|
+
beforeStart: validateBoundedInteger(owner, `${coordinate}.beforeStart`, record.beforeStart, {
|
|
360
|
+
minimum: 1,
|
|
361
|
+
maximum: beforeLineCount,
|
|
362
|
+
}),
|
|
363
|
+
afterStart: validateBoundedInteger(owner, `${coordinate}.afterStart`, record.afterStart, {
|
|
364
|
+
minimum: 1,
|
|
365
|
+
maximum: afterLineCount,
|
|
366
|
+
}),
|
|
367
|
+
lineCount: validateBoundedInteger(owner, `${coordinate}.lineCount`, record.lineCount, {
|
|
368
|
+
minimum: intervalLength,
|
|
369
|
+
maximum: intervalLength,
|
|
370
|
+
}),
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function validateSummary(owner: string, value: unknown): SDiffSummary {
|
|
375
|
+
const record = assertPlainExactRecord(owner, 'summary', value, SUMMARY_KEYS)
|
|
376
|
+
const readCount = (coordinate: string): number => validateBoundedInteger(
|
|
377
|
+
owner,
|
|
378
|
+
`summary.${coordinate}`,
|
|
379
|
+
record[coordinate],
|
|
380
|
+
{ minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
|
381
|
+
)
|
|
382
|
+
return Object.freeze({
|
|
383
|
+
beforeLineCount: readCount('beforeLineCount'),
|
|
384
|
+
afterLineCount: readCount('afterLineCount'),
|
|
385
|
+
unchangedLineCount: readCount('unchangedLineCount'),
|
|
386
|
+
removedLineCount: readCount('removedLineCount'),
|
|
387
|
+
addedLineCount: readCount('addedLineCount'),
|
|
388
|
+
changedLineCount: readCount('changedLineCount'),
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Validates a worker or persisted result and all cross-record line/count invariants. */
|
|
393
|
+
export function validateDiffResult(owner: string, value: unknown): SDiffResult {
|
|
394
|
+
const record = assertPlainExactRecord(owner, 'result', value, RESULT_KEYS)
|
|
395
|
+
const before = validateSnapshot(owner, 'result.before', record.before)
|
|
396
|
+
const after = validateSnapshot(owner, 'result.after', record.after)
|
|
397
|
+
if (!Array.isArray(record.lines) || !Array.isArray(record.hunks) || !Array.isArray(record.gaps)) {
|
|
398
|
+
throw new TypeError(`${owner}: result lines/hunks/gaps должны быть arrays. / result lines/hunks/gaps must be arrays.`)
|
|
399
|
+
}
|
|
400
|
+
const lines = record.lines.map((line, index) => validateLine(
|
|
401
|
+
owner,
|
|
402
|
+
`result.lines[${index}]`,
|
|
403
|
+
line,
|
|
404
|
+
before.lineCount,
|
|
405
|
+
after.lineCount,
|
|
406
|
+
))
|
|
407
|
+
let nextBeforeLine = 1
|
|
408
|
+
let nextAfterLine = 1
|
|
409
|
+
const beforeSides: SDiffLineSide[] = []
|
|
410
|
+
const afterSides: SDiffLineSide[] = []
|
|
411
|
+
const counts = { unchanged: 0, removed: 0, added: 0 }
|
|
412
|
+
for (const [index, line] of lines.entries()) {
|
|
413
|
+
if (line.before !== undefined && line.before.lineNumber !== nextBeforeLine) {
|
|
414
|
+
throw new RangeError(`${owner}: result.lines[${index}].before.lineNumber нарушает последовательность.`)
|
|
415
|
+
}
|
|
416
|
+
if (line.after !== undefined && line.after.lineNumber !== nextAfterLine) {
|
|
417
|
+
throw new RangeError(`${owner}: result.lines[${index}].after.lineNumber нарушает последовательность.`)
|
|
418
|
+
}
|
|
419
|
+
if (line.before !== undefined) {
|
|
420
|
+
beforeSides.push(line.before)
|
|
421
|
+
nextBeforeLine += 1
|
|
422
|
+
}
|
|
423
|
+
if (line.after !== undefined) {
|
|
424
|
+
afterSides.push(line.after)
|
|
425
|
+
nextAfterLine += 1
|
|
426
|
+
}
|
|
427
|
+
counts[line.kind] += 1
|
|
428
|
+
}
|
|
429
|
+
if (nextBeforeLine - 1 !== before.lineCount || nextAfterLine - 1 !== after.lineCount) {
|
|
430
|
+
throw new RangeError(`${owner}: result line inventory не соответствует source snapshots.`)
|
|
431
|
+
}
|
|
432
|
+
for (const [sideName, sides] of [['before', beforeSides], ['after', afterSides]] as const) {
|
|
433
|
+
const invalidTerminatorIndex = sides.findIndex(
|
|
434
|
+
(side, index) => index < sides.length - 1 && side.lineBreak === 'none',
|
|
435
|
+
)
|
|
436
|
+
if (invalidTerminatorIndex !== -1) {
|
|
437
|
+
throw new RangeError(
|
|
438
|
+
`${owner}: result ${sideName} line ${invalidTerminatorIndex + 1} теряет межстрочный terminator.`,
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const hunks = record.hunks.map((hunk, index) => validateHunk(
|
|
443
|
+
owner,
|
|
444
|
+
`result.hunks[${index}]`,
|
|
445
|
+
hunk,
|
|
446
|
+
lines.length,
|
|
447
|
+
before.lineCount,
|
|
448
|
+
after.lineCount,
|
|
449
|
+
))
|
|
450
|
+
const gaps = record.gaps.map((gap, index) => validateGap(
|
|
451
|
+
owner,
|
|
452
|
+
`result.gaps[${index}]`,
|
|
453
|
+
gap,
|
|
454
|
+
lines.length,
|
|
455
|
+
before.lineCount,
|
|
456
|
+
after.lineCount,
|
|
457
|
+
))
|
|
458
|
+
for (const [index, hunk] of hunks.entries()) {
|
|
459
|
+
if (!lines.slice(hunk.lineStart, hunk.lineEnd).some((line) => line.kind !== 'unchanged')) {
|
|
460
|
+
throw new RangeError(`${owner}: result.hunks[${index}] не содержит changed line.`)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const [index, gap] of gaps.entries()) {
|
|
464
|
+
if (lines.slice(gap.lineStart, gap.lineEnd).some((line) => line.kind !== 'unchanged')) {
|
|
465
|
+
throw new RangeError(`${owner}: result.gaps[${index}] содержит changed line.`)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const ids = new Set<string>()
|
|
469
|
+
for (const segment of [...hunks, ...gaps]) {
|
|
470
|
+
if (ids.has(segment.id)) throw new RangeError(`${owner}: duplicate result segment id ${JSON.stringify(segment.id)}.`)
|
|
471
|
+
ids.add(segment.id)
|
|
472
|
+
}
|
|
473
|
+
if (counts.removed + counts.added === 0) {
|
|
474
|
+
if (hunks.length !== 0 || gaps.length !== 0) {
|
|
475
|
+
throw new RangeError(`${owner}: unchanged result не должен содержать hunks/gaps.`)
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
const segments = [
|
|
479
|
+
...hunks.map((hunk) => ({ kind: 'hunk' as const, value: hunk })),
|
|
480
|
+
...gaps.map((gap) => ({ kind: 'gap' as const, value: gap })),
|
|
481
|
+
].sort((left, right) => left.value.lineStart - right.value.lineStart)
|
|
482
|
+
let expectedStart = 0
|
|
483
|
+
for (const segment of segments) {
|
|
484
|
+
if (segment.value.lineStart !== expectedStart) {
|
|
485
|
+
throw new RangeError(`${owner}: result hunks/gaps не образуют exact partition lines.`)
|
|
486
|
+
}
|
|
487
|
+
expectedStart = segment.value.lineEnd
|
|
488
|
+
const slice = lines.slice(segment.value.lineStart, segment.value.lineEnd)
|
|
489
|
+
const beforeCount = slice.filter((line) => line.before !== undefined).length
|
|
490
|
+
const afterCount = slice.filter((line) => line.after !== undefined).length
|
|
491
|
+
const expectedBeforeStart = lines
|
|
492
|
+
.slice(0, segment.value.lineStart)
|
|
493
|
+
.filter((line) => line.before !== undefined).length + 1
|
|
494
|
+
const expectedAfterStart = lines
|
|
495
|
+
.slice(0, segment.value.lineStart)
|
|
496
|
+
.filter((line) => line.after !== undefined).length + 1
|
|
497
|
+
if (
|
|
498
|
+
segment.value.beforeStart !== expectedBeforeStart
|
|
499
|
+
|| segment.value.afterStart !== expectedAfterStart
|
|
500
|
+
|| (segment.kind === 'hunk'
|
|
501
|
+
&& (segment.value.beforeLineCount !== beforeCount || segment.value.afterLineCount !== afterCount))
|
|
502
|
+
) {
|
|
503
|
+
throw new RangeError(`${owner}: result segment coordinates не соответствуют lines.`)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (expectedStart !== lines.length) {
|
|
507
|
+
throw new RangeError(`${owner}: result hunks/gaps не покрывают lines полностью.`)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const summary = validateSummary(owner, record.summary)
|
|
511
|
+
if (
|
|
512
|
+
summary.beforeLineCount !== before.lineCount
|
|
513
|
+
|| summary.afterLineCount !== after.lineCount
|
|
514
|
+
|| summary.unchangedLineCount !== counts.unchanged
|
|
515
|
+
|| summary.removedLineCount !== counts.removed
|
|
516
|
+
|| summary.addedLineCount !== counts.added
|
|
517
|
+
|| summary.changedLineCount !== counts.removed + counts.added
|
|
518
|
+
) {
|
|
519
|
+
throw new RangeError(`${owner}: result.summary не соответствует lines/source snapshots.`)
|
|
520
|
+
}
|
|
521
|
+
return Object.freeze({
|
|
522
|
+
before,
|
|
523
|
+
after,
|
|
524
|
+
lines: Object.freeze(lines),
|
|
525
|
+
hunks: Object.freeze(hunks),
|
|
526
|
+
gaps: Object.freeze(gaps),
|
|
527
|
+
summary,
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Computes the canonical deterministic result on the current thread. Large UI inputs use the worker runtime. */
|
|
532
|
+
export function computeDiff(
|
|
533
|
+
beforeValue: unknown,
|
|
534
|
+
afterValue: unknown,
|
|
535
|
+
optionsValue?: unknown,
|
|
536
|
+
): SDiffResult {
|
|
537
|
+
const before = validateDiffSource('computeDiff', 'before', beforeValue)
|
|
538
|
+
const after = validateDiffSource('computeDiff', 'after', afterValue)
|
|
539
|
+
const options = validateDiffComputationOptions('computeDiff', optionsValue)
|
|
540
|
+
return computeDiffModel(before, after, options)
|
|
541
|
+
}
|