@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,499 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-diff-viewer"
|
|
5
|
+
:data-layout="validatedLayout"
|
|
6
|
+
:data-viewport="validatedViewport"
|
|
7
|
+
:data-state="status"
|
|
8
|
+
:aria-label="validatedAriaLabel"
|
|
9
|
+
:style="rootStyle"
|
|
10
|
+
>
|
|
11
|
+
<header class="s-diff-viewer__header">
|
|
12
|
+
<div class="s-diff-viewer__sources">
|
|
13
|
+
<span class="s-diff-viewer__source" data-side="before">{{ beforeLabel }}</span>
|
|
14
|
+
<span class="s-diff-viewer__direction" aria-hidden="true">→</span>
|
|
15
|
+
<span class="s-diff-viewer__source" data-side="after">{{ afterLabel }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
<span v-if="result" class="s-diff-viewer__summary">
|
|
18
|
+
{{ t('editor.diffSummary', { added: result.summary.addedLineCount, removed: result.summary.removedLineCount }) }}
|
|
19
|
+
</span>
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
<div v-if="validatedLayout === 'split' && status === 'ready'" class="s-diff-viewer__split-header" aria-hidden="true">
|
|
23
|
+
<span>{{ beforeLabel }}</span>
|
|
24
|
+
<span>{{ afterLabel }}</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<SAsyncState :state="asyncState" presentation="block">
|
|
28
|
+
<template #error="{ message }">
|
|
29
|
+
<div class="s-diff-viewer__error">
|
|
30
|
+
<SMessage severity="danger" size="sm">{{ message }}</SMessage>
|
|
31
|
+
<SButton
|
|
32
|
+
:label="t('actions.retry')"
|
|
33
|
+
appearance="soft"
|
|
34
|
+
severity="secondary"
|
|
35
|
+
size="sm"
|
|
36
|
+
@click="retry"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<div class="s-diff-viewer__viewport">
|
|
42
|
+
<SVirtualList
|
|
43
|
+
:items="renderRows"
|
|
44
|
+
:item-size="DIFF_ROW_HEIGHT"
|
|
45
|
+
:overscan="8"
|
|
46
|
+
:height="null"
|
|
47
|
+
:item-key="rowKey"
|
|
48
|
+
:item-label="rowLabel"
|
|
49
|
+
:accessible-label="validatedAriaLabel"
|
|
50
|
+
>
|
|
51
|
+
<template #default="{ item }">
|
|
52
|
+
<div v-if="item.kind === 'gap'" class="s-diff-viewer__gap-row">
|
|
53
|
+
<SButton
|
|
54
|
+
:label="gapButtonLabel(item)"
|
|
55
|
+
:disclosure="{ expanded: item.expanded, controls: gapControlsId(item.gap.id), iconPlacement: 'leading' }"
|
|
56
|
+
appearance="text"
|
|
57
|
+
severity="secondary"
|
|
58
|
+
size="xs"
|
|
59
|
+
density="compact"
|
|
60
|
+
width="full"
|
|
61
|
+
@click="toggleGap(item.gap.id)"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
v-else-if="item.kind === 'unified-line'"
|
|
67
|
+
class="s-diff-viewer__unified-row"
|
|
68
|
+
:data-kind="item.line.kind"
|
|
69
|
+
:id="item.gapId ? gapControlsId(item.gapId) : undefined"
|
|
70
|
+
>
|
|
71
|
+
<span class="s-diff-viewer__marker" aria-hidden="true">{{ unifiedMarker(item.line.kind) }}</span>
|
|
72
|
+
<span class="s-diff-viewer__line-number" aria-hidden="true">{{ item.line.before?.lineNumber ?? '' }}</span>
|
|
73
|
+
<span class="s-diff-viewer__line-number" aria-hidden="true">{{ item.line.after?.lineNumber ?? '' }}</span>
|
|
74
|
+
<span class="s-diff-viewer__code-cell">
|
|
75
|
+
<SCodeTokenLine
|
|
76
|
+
:text="unifiedSide(item.line).text"
|
|
77
|
+
:language="unifiedLanguage(item.line)"
|
|
78
|
+
/>
|
|
79
|
+
<span
|
|
80
|
+
v-if="unifiedSide(item.line).lineBreak === 'none'"
|
|
81
|
+
class="s-diff-viewer__missing-break"
|
|
82
|
+
:aria-label="t('editor.diffMissingLineBreak')"
|
|
83
|
+
>∅</span>
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div
|
|
88
|
+
v-else
|
|
89
|
+
class="s-diff-viewer__split-row"
|
|
90
|
+
:id="item.gapId ? gapControlsId(item.gapId) : undefined"
|
|
91
|
+
>
|
|
92
|
+
<div class="s-diff-viewer__split-cell" :data-kind="item.beforeKind">
|
|
93
|
+
<span class="s-diff-viewer__line-number" aria-hidden="true">{{ item.before?.lineNumber ?? '' }}</span>
|
|
94
|
+
<span v-if="item.before" class="s-diff-viewer__code-cell">
|
|
95
|
+
<SCodeTokenLine :text="item.before.text" :language="validatedBefore.language" wrap />
|
|
96
|
+
<span
|
|
97
|
+
v-if="item.before.lineBreak === 'none'"
|
|
98
|
+
class="s-diff-viewer__missing-break"
|
|
99
|
+
:aria-label="t('editor.diffMissingLineBreak')"
|
|
100
|
+
>∅</span>
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="s-diff-viewer__split-cell" :data-kind="item.afterKind">
|
|
104
|
+
<span class="s-diff-viewer__line-number" aria-hidden="true">{{ item.after?.lineNumber ?? '' }}</span>
|
|
105
|
+
<span v-if="item.after" class="s-diff-viewer__code-cell">
|
|
106
|
+
<SCodeTokenLine :text="item.after.text" :language="validatedAfter.language" wrap />
|
|
107
|
+
<span
|
|
108
|
+
v-if="item.after.lineBreak === 'none'"
|
|
109
|
+
class="s-diff-viewer__missing-break"
|
|
110
|
+
:aria-label="t('editor.diffMissingLineBreak')"
|
|
111
|
+
>∅</span>
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
</SVirtualList>
|
|
117
|
+
</div>
|
|
118
|
+
</SAsyncState>
|
|
119
|
+
</section>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<script setup lang="ts">
|
|
123
|
+
import {
|
|
124
|
+
computed,
|
|
125
|
+
onBeforeUnmount,
|
|
126
|
+
ref,
|
|
127
|
+
watch,
|
|
128
|
+
type CSSProperties,
|
|
129
|
+
} from 'vue'
|
|
130
|
+
|
|
131
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale'
|
|
132
|
+
import {
|
|
133
|
+
createDiffRuntime,
|
|
134
|
+
SDiffComputationCancelledError,
|
|
135
|
+
} from '../../../internal/diffRuntime'
|
|
136
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
137
|
+
import {
|
|
138
|
+
validateBoundedInteger,
|
|
139
|
+
validateExactString,
|
|
140
|
+
validateNonEmptyString,
|
|
141
|
+
} from '../../../internal/runtimeContract'
|
|
142
|
+
import SButton from '../controls/SButton.vue'
|
|
143
|
+
import SAsyncState, { type SAsyncStateContract } from '../feedback/SAsyncState.vue'
|
|
144
|
+
import SMessage from './SMessage.vue'
|
|
145
|
+
import SCodeTokenLine from './_internal/SCodeTokenLine.vue'
|
|
146
|
+
import SVirtualList from './SVirtualList.vue'
|
|
147
|
+
import {
|
|
148
|
+
S_DIFF_MAX_CONTEXT_LINES,
|
|
149
|
+
validateDiffSource,
|
|
150
|
+
type SDiffGap,
|
|
151
|
+
type SDiffLanguage,
|
|
152
|
+
type SDiffLine,
|
|
153
|
+
type SDiffLineKind,
|
|
154
|
+
type SDiffLineSide,
|
|
155
|
+
type SDiffResult,
|
|
156
|
+
type SDiffSource,
|
|
157
|
+
} from './diff'
|
|
158
|
+
|
|
159
|
+
defineOptions({ inheritAttrs: false })
|
|
160
|
+
|
|
161
|
+
/** Presentation of canonical line operations. Split mode pairs adjacent remove/add runs by position. */
|
|
162
|
+
export type SDiffViewerLayout = 'unified' | 'split'
|
|
163
|
+
/** Semantic viewport sizes shared with code surfaces; fill consumes the containing block. */
|
|
164
|
+
export type SDiffViewerViewport = 'content' | 'compact' | 'standard' | 'expanded' | 'fill'
|
|
165
|
+
|
|
166
|
+
/** Public exact-text diff contract. Inputs are validated without newline or payload normalization. */
|
|
167
|
+
export interface Props {
|
|
168
|
+
/** Previous exact source snapshot. */
|
|
169
|
+
before: SDiffSource
|
|
170
|
+
/** Current exact source snapshot. */
|
|
171
|
+
after: SDiffSource
|
|
172
|
+
layout?: SDiffViewerLayout
|
|
173
|
+
/** Unchanged lines retained around each changed hunk. */
|
|
174
|
+
contextLines?: number
|
|
175
|
+
/** Optional controlled set of expanded unchanged gap identities. */
|
|
176
|
+
expandedGapIds?: readonly string[]
|
|
177
|
+
viewport?: SDiffViewerViewport
|
|
178
|
+
/** Accessible name for the diff region and its virtualized line collection. */
|
|
179
|
+
ariaLabel: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type UnifiedLineRow = {
|
|
183
|
+
readonly kind: 'unified-line'
|
|
184
|
+
readonly key: string
|
|
185
|
+
readonly line: SDiffLine
|
|
186
|
+
readonly gapId?: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type SplitLineRow = {
|
|
190
|
+
readonly kind: 'split-line'
|
|
191
|
+
readonly key: string
|
|
192
|
+
readonly before?: SDiffLineSide
|
|
193
|
+
readonly after?: SDiffLineSide
|
|
194
|
+
readonly beforeKind: SDiffLineKind | 'empty'
|
|
195
|
+
readonly afterKind: SDiffLineKind | 'empty'
|
|
196
|
+
readonly gapId?: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type GapRow = {
|
|
200
|
+
readonly kind: 'gap'
|
|
201
|
+
readonly key: string
|
|
202
|
+
readonly gap: SDiffGap
|
|
203
|
+
readonly expanded: boolean
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type DiffRenderRow = UnifiedLineRow | SplitLineRow | GapRow
|
|
207
|
+
|
|
208
|
+
const DIFF_ROW_HEIGHT = 32
|
|
209
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
210
|
+
layout: 'unified',
|
|
211
|
+
contextLines: 3,
|
|
212
|
+
expandedGapIds: undefined,
|
|
213
|
+
viewport: 'standard',
|
|
214
|
+
})
|
|
215
|
+
const emit = defineEmits<{
|
|
216
|
+
(event: 'update:expandedGapIds', value: readonly string[]): void
|
|
217
|
+
(event: 'retry'): void
|
|
218
|
+
(event: 'computed', result: SDiffResult): void
|
|
219
|
+
}>()
|
|
220
|
+
const { t } = useUiKitLocale()
|
|
221
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SDiffViewer', owner: 'diff viewer root' })
|
|
222
|
+
const runtime = createDiffRuntime()
|
|
223
|
+
const result = ref<SDiffResult | null>(null)
|
|
224
|
+
const status = ref<'loading' | 'ready' | 'empty' | 'error'>('loading')
|
|
225
|
+
const errorMessage = ref('')
|
|
226
|
+
const uncontrolledExpandedGapIds = ref<readonly string[]>(Object.freeze([]))
|
|
227
|
+
let generation = 0
|
|
228
|
+
let abortController: AbortController | null = null
|
|
229
|
+
|
|
230
|
+
const validatedBefore = computed(() => validateDiffSource('SDiffViewer', 'before', props.before))
|
|
231
|
+
const validatedAfter = computed(() => validateDiffSource('SDiffViewer', 'after', props.after))
|
|
232
|
+
const validatedLayout = computed(() => validateExactString(
|
|
233
|
+
'SDiffViewer',
|
|
234
|
+
'layout',
|
|
235
|
+
props.layout,
|
|
236
|
+
['unified', 'split'] as const,
|
|
237
|
+
))
|
|
238
|
+
const validatedViewport = computed(() => validateExactString(
|
|
239
|
+
'SDiffViewer',
|
|
240
|
+
'viewport',
|
|
241
|
+
props.viewport,
|
|
242
|
+
['content', 'compact', 'standard', 'expanded', 'fill'] as const,
|
|
243
|
+
))
|
|
244
|
+
const validatedContextLines = computed(() => validateBoundedInteger(
|
|
245
|
+
'SDiffViewer',
|
|
246
|
+
'contextLines',
|
|
247
|
+
props.contextLines,
|
|
248
|
+
{ minimum: 0, maximum: S_DIFF_MAX_CONTEXT_LINES },
|
|
249
|
+
))
|
|
250
|
+
const validatedAriaLabel = computed(() => validateNonEmptyString('SDiffViewer', 'ariaLabel', props.ariaLabel))
|
|
251
|
+
const beforeLabel = computed(() => validatedBefore.value.label ?? t('editor.diffBefore'))
|
|
252
|
+
const afterLabel = computed(() => validatedAfter.value.label ?? t('editor.diffAfter'))
|
|
253
|
+
|
|
254
|
+
function validateExpandedGapIds(value: unknown, currentResult: SDiffResult | null): readonly string[] {
|
|
255
|
+
if (!Array.isArray(value)) throw new TypeError('SDiffViewer: expandedGapIds must be an array.')
|
|
256
|
+
const gapIds = new Set(currentResult?.gaps.map((gap) => gap.id) ?? [])
|
|
257
|
+
const seen = new Set<string>()
|
|
258
|
+
value.forEach((candidate, index) => {
|
|
259
|
+
const id = validateNonEmptyString('SDiffViewer', `expandedGapIds[${index}]`, candidate)
|
|
260
|
+
if (seen.has(id)) throw new TypeError(`SDiffViewer: expandedGapIds contains duplicate ${JSON.stringify(id)}.`)
|
|
261
|
+
if (currentResult !== null && !gapIds.has(id)) {
|
|
262
|
+
throw new RangeError(`SDiffViewer: expandedGapIds[${index}] ${JSON.stringify(id)} is stale.`)
|
|
263
|
+
}
|
|
264
|
+
seen.add(id)
|
|
265
|
+
})
|
|
266
|
+
return Object.freeze([...seen])
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const expandedGapIds = computed(() => props.expandedGapIds === undefined
|
|
270
|
+
? uncontrolledExpandedGapIds.value
|
|
271
|
+
: validateExpandedGapIds(props.expandedGapIds, result.value))
|
|
272
|
+
const expandedGapSet = computed(() => new Set(expandedGapIds.value))
|
|
273
|
+
|
|
274
|
+
const asyncState = computed<SAsyncStateContract>(() => {
|
|
275
|
+
if (status.value === 'loading') return { status: 'loading', message: t('editor.diffComputing') }
|
|
276
|
+
if (status.value === 'empty') return { status: 'empty', message: t('editor.diffNoChanges') }
|
|
277
|
+
if (status.value === 'error') return { status: 'error', message: errorMessage.value }
|
|
278
|
+
return { status: 'ready' }
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
function alignSplitLines(lines: readonly SDiffLine[]): SplitLineRow[] {
|
|
282
|
+
const rows: SplitLineRow[] = []
|
|
283
|
+
let index = 0
|
|
284
|
+
while (index < lines.length) {
|
|
285
|
+
const line = lines[index]
|
|
286
|
+
if (line.kind === 'unchanged') {
|
|
287
|
+
rows.push({
|
|
288
|
+
kind: 'split-line',
|
|
289
|
+
key: `split:${line.id}`,
|
|
290
|
+
before: line.before,
|
|
291
|
+
after: line.after,
|
|
292
|
+
beforeKind: 'unchanged',
|
|
293
|
+
afterKind: 'unchanged',
|
|
294
|
+
})
|
|
295
|
+
index += 1
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
const changedBlock: SDiffLine[] = []
|
|
299
|
+
while (index < lines.length && lines[index].kind !== 'unchanged') {
|
|
300
|
+
changedBlock.push(lines[index])
|
|
301
|
+
index += 1
|
|
302
|
+
}
|
|
303
|
+
const removed = changedBlock.filter((changedLine) => changedLine.kind === 'removed')
|
|
304
|
+
const added = changedBlock.filter((changedLine) => changedLine.kind === 'added')
|
|
305
|
+
const pairCount = Math.max(removed.length, added.length)
|
|
306
|
+
for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
|
|
307
|
+
const beforeLine = removed[pairIndex]
|
|
308
|
+
const afterLine = added[pairIndex]
|
|
309
|
+
rows.push({
|
|
310
|
+
kind: 'split-line',
|
|
311
|
+
key: `split:${beforeLine?.id ?? '-'}:${afterLine?.id ?? '-'}`,
|
|
312
|
+
...(beforeLine?.before === undefined ? {} : { before: beforeLine.before }),
|
|
313
|
+
...(afterLine?.after === undefined ? {} : { after: afterLine.after }),
|
|
314
|
+
beforeKind: beforeLine === undefined ? 'empty' : 'removed',
|
|
315
|
+
afterKind: afterLine === undefined ? 'empty' : 'added',
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return rows
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function lineRows(lines: readonly SDiffLine[], gapId?: string): Array<UnifiedLineRow | SplitLineRow> {
|
|
323
|
+
const rows: Array<UnifiedLineRow | SplitLineRow> = validatedLayout.value === 'split'
|
|
324
|
+
? alignSplitLines(lines)
|
|
325
|
+
: lines.map((line) => ({
|
|
326
|
+
kind: 'unified-line',
|
|
327
|
+
key: `unified:${line.id}`,
|
|
328
|
+
line,
|
|
329
|
+
}))
|
|
330
|
+
if (gapId === undefined || rows.length === 0) return rows
|
|
331
|
+
const [firstRow, ...remainingRows] = rows
|
|
332
|
+
return [{ ...firstRow, gapId }, ...remainingRows]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const renderRows = computed<readonly DiffRenderRow[]>(() => {
|
|
336
|
+
const current = result.value
|
|
337
|
+
if (current === null || current.summary.changedLineCount === 0) return Object.freeze([])
|
|
338
|
+
const segments = [
|
|
339
|
+
...current.hunks.map((hunk) => ({ kind: 'hunk' as const, value: hunk })),
|
|
340
|
+
...current.gaps.map((gap) => ({ kind: 'gap' as const, value: gap })),
|
|
341
|
+
].sort((left, right) => left.value.lineStart - right.value.lineStart)
|
|
342
|
+
const rows: DiffRenderRow[] = []
|
|
343
|
+
for (const segment of segments) {
|
|
344
|
+
if (segment.kind === 'hunk') {
|
|
345
|
+
rows.push(...lineRows(current.lines.slice(segment.value.lineStart, segment.value.lineEnd)))
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
const expanded = expandedGapSet.value.has(segment.value.id)
|
|
349
|
+
rows.push({
|
|
350
|
+
kind: 'gap',
|
|
351
|
+
key: `gap-control:${segment.value.id}`,
|
|
352
|
+
gap: segment.value,
|
|
353
|
+
expanded,
|
|
354
|
+
})
|
|
355
|
+
if (expanded) {
|
|
356
|
+
rows.push(...lineRows(
|
|
357
|
+
current.lines.slice(segment.value.lineStart, segment.value.lineEnd),
|
|
358
|
+
segment.value.id,
|
|
359
|
+
))
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return Object.freeze(rows)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
function estimateCodeColumns(text: string): number {
|
|
366
|
+
let columns = 0
|
|
367
|
+
for (const character of text) {
|
|
368
|
+
if (character === '\t') {
|
|
369
|
+
columns += 2 - (columns % 2)
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
if (/\p{Mark}/u.test(character)) continue
|
|
373
|
+
columns += /^[\u0000-\u007f]$/u.test(character) ? 1 : 2
|
|
374
|
+
}
|
|
375
|
+
return columns
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const unifiedCodeColumns = computed(() => {
|
|
379
|
+
let maximum = 1
|
|
380
|
+
for (const line of result.value?.lines ?? []) {
|
|
381
|
+
if (line.before !== undefined) maximum = Math.max(maximum, estimateCodeColumns(line.before.text))
|
|
382
|
+
if (line.after !== undefined) maximum = Math.max(maximum, estimateCodeColumns(line.after.text))
|
|
383
|
+
}
|
|
384
|
+
return maximum
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const rootStyle = computed<CSSProperties>(() => ({
|
|
388
|
+
'--s-diff-content-row-count': String(Math.max(3, Math.min(14, renderRows.value.length + 1))),
|
|
389
|
+
'--s-diff-unified-code-inline-size': `${unifiedCodeColumns.value}ch`,
|
|
390
|
+
}))
|
|
391
|
+
|
|
392
|
+
function rowKey(row: DiffRenderRow): string {
|
|
393
|
+
return row.key
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function describeLine(kind: SDiffLineKind, before: SDiffLineSide | undefined, after: SDiffLineSide | undefined): string {
|
|
397
|
+
if (kind === 'removed') return t('editor.diffRemovedLine', { line: before?.lineNumber ?? '' })
|
|
398
|
+
if (kind === 'added') return t('editor.diffAddedLine', { line: after?.lineNumber ?? '' })
|
|
399
|
+
return t('editor.diffUnchangedLine', {
|
|
400
|
+
before: before?.lineNumber ?? '',
|
|
401
|
+
after: after?.lineNumber ?? '',
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function rowLabel(row: DiffRenderRow): string {
|
|
406
|
+
if (row.kind === 'gap') return gapButtonLabel(row)
|
|
407
|
+
if (row.kind === 'unified-line') return describeLine(row.line.kind, row.line.before, row.line.after)
|
|
408
|
+
return [
|
|
409
|
+
row.before === undefined ? '' : describeLine(row.beforeKind as SDiffLineKind, row.before, undefined),
|
|
410
|
+
row.after === undefined ? '' : describeLine(row.afterKind as SDiffLineKind, undefined, row.after),
|
|
411
|
+
].filter(Boolean).join('; ')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function gapButtonLabel(row: GapRow): string {
|
|
415
|
+
return row.expanded
|
|
416
|
+
? t('editor.diffCollapseGap', { count: row.gap.lineCount })
|
|
417
|
+
: t('editor.diffExpandGap', { count: row.gap.lineCount })
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function gapControlsId(gapId: string): string {
|
|
421
|
+
return `s-diff-${gapId.replace(/[^a-zA-Z0-9_-]/gu, '-')}`
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function toggleGap(gapId: string): void {
|
|
425
|
+
const next = new Set(expandedGapIds.value)
|
|
426
|
+
if (next.has(gapId)) next.delete(gapId)
|
|
427
|
+
else next.add(gapId)
|
|
428
|
+
const value = Object.freeze([...next])
|
|
429
|
+
if (props.expandedGapIds === undefined) uncontrolledExpandedGapIds.value = value
|
|
430
|
+
emit('update:expandedGapIds', value)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function unifiedMarker(kind: SDiffLineKind): string {
|
|
434
|
+
return kind === 'removed' ? '−' : kind === 'added' ? '+' : ' '
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function unifiedSide(line: SDiffLine): SDiffLineSide {
|
|
438
|
+
const side = line.kind === 'removed' ? line.before : line.after
|
|
439
|
+
if (side === undefined) throw new Error(`SDiffViewer: ${line.id} has no renderable side.`)
|
|
440
|
+
return side
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function unifiedLanguage(line: SDiffLine): SDiffLanguage | undefined {
|
|
444
|
+
return line.kind === 'removed' ? validatedBefore.value.language : validatedAfter.value.language
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function computeCurrentDiff(): Promise<void> {
|
|
448
|
+
const currentGeneration = generation += 1
|
|
449
|
+
abortController?.abort()
|
|
450
|
+
abortController = new AbortController()
|
|
451
|
+
result.value = null
|
|
452
|
+
status.value = 'loading'
|
|
453
|
+
errorMessage.value = ''
|
|
454
|
+
try {
|
|
455
|
+
const computedResult = await runtime.compute(
|
|
456
|
+
validatedBefore.value,
|
|
457
|
+
validatedAfter.value,
|
|
458
|
+
{ contextLines: validatedContextLines.value },
|
|
459
|
+
abortController.signal,
|
|
460
|
+
)
|
|
461
|
+
if (currentGeneration !== generation) return
|
|
462
|
+
if (props.expandedGapIds !== undefined) {
|
|
463
|
+
validateExpandedGapIds(props.expandedGapIds, computedResult)
|
|
464
|
+
}
|
|
465
|
+
result.value = computedResult
|
|
466
|
+
if (props.expandedGapIds === undefined) {
|
|
467
|
+
const activeGapIds = new Set(computedResult.gaps.map((gap) => gap.id))
|
|
468
|
+
uncontrolledExpandedGapIds.value = Object.freeze(
|
|
469
|
+
uncontrolledExpandedGapIds.value.filter((gapId) => activeGapIds.has(gapId)),
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
status.value = computedResult.summary.changedLineCount === 0 ? 'empty' : 'ready'
|
|
473
|
+
emit('computed', computedResult)
|
|
474
|
+
} catch (cause) {
|
|
475
|
+
if (currentGeneration !== generation || cause instanceof SDiffComputationCancelledError) return
|
|
476
|
+
status.value = 'error'
|
|
477
|
+
errorMessage.value = cause instanceof Error ? cause.message : String(cause)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function retry(): void {
|
|
482
|
+
emit('retry')
|
|
483
|
+
void computeCurrentDiff()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
watch(
|
|
487
|
+
[validatedBefore, validatedAfter, validatedContextLines],
|
|
488
|
+
() => { void computeCurrentDiff() },
|
|
489
|
+
{ immediate: true },
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
onBeforeUnmount(() => {
|
|
493
|
+
generation += 1
|
|
494
|
+
abortController?.abort()
|
|
495
|
+
runtime.dispose()
|
|
496
|
+
})
|
|
497
|
+
</script>
|
|
498
|
+
|
|
499
|
+
<style lang="postcss" src="./SDiffViewer.css" scoped></style>
|