@pgcorp/ui-kit 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +332 -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/docs/theming.md +17 -0
- package/package.json +108 -50
- package/src/components/layout/SAppShell.vue +3 -1
- package/src/components/layout/SDockRegion.vue +5 -3
- package/src/components/layout/SResizeHandle.vue +45 -5
- package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
- package/src/components/shared/_internal/SDialogSurface.css +23 -0
- package/src/components/shared/_internal/SDialogSurface.vue +9 -2
- 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/_internal/dialogSurface.ts +7 -1
- package/src/components/shared/complex/SKanbanLane.vue +4 -2
- package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
- package/src/components/shared/containers/SDrawer.css +11 -2
- package/src/components/shared/containers/SDrawer.vue +48 -9
- 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/STextarea.ts +13 -0
- package/src/components/shared/controls/STextarea.vue +391 -37
- 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 +46 -22
- 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/SVirtualList.ts +33 -0
- package/src/components/shared/data-display/SVirtualList.vue +321 -36
- 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 +23 -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,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateBoolean,
|
|
3
|
+
validateBoundedInteger,
|
|
4
|
+
validateExactString,
|
|
5
|
+
validateNonEmptyString,
|
|
6
|
+
} from '../../../internal/runtimeContract'
|
|
7
|
+
|
|
8
|
+
/** Serializable half-open document range. */
|
|
9
|
+
export interface SCodeEditorRange {
|
|
10
|
+
readonly from: number
|
|
11
|
+
readonly to: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Serializable primary selection. Direction is preserved through anchor/head. */
|
|
15
|
+
export interface SCodeEditorSelectionRange extends SCodeEditorRange {
|
|
16
|
+
readonly anchor: number
|
|
17
|
+
readonly head: number
|
|
18
|
+
readonly empty: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Selection snapshot returned only through an explicit consumer action. */
|
|
22
|
+
export interface SCodeEditorSelectionSnapshot extends SCodeEditorSelectionRange {
|
|
23
|
+
readonly text: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Serializable editor view state without a CodeMirror object or DOM reference. */
|
|
27
|
+
export interface SCodeEditorViewState {
|
|
28
|
+
readonly selection: SCodeEditorSelectionRange
|
|
29
|
+
readonly scrollTop: number
|
|
30
|
+
readonly scrollLeft: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Serializable imperative surface; CodeMirror objects and DOM nodes never cross the public boundary. */
|
|
34
|
+
export interface SCodeEditorApi {
|
|
35
|
+
applySelection(position: number): void
|
|
36
|
+
revealRange(range: SCodeEditorRange): void
|
|
37
|
+
getSelection(): SCodeEditorSelectionSnapshot
|
|
38
|
+
captureViewState(): SCodeEditorViewState
|
|
39
|
+
applyViewState(viewState: SCodeEditorViewState): void
|
|
40
|
+
focus(): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const S_CODE_EDITOR_DIAGNOSTIC_SEVERITIES = ['error', 'warning', 'info', 'hint'] as const
|
|
44
|
+
export type SCodeEditorDiagnosticSeverity = (typeof S_CODE_EDITOR_DIAGNOSTIC_SEVERITIES)[number]
|
|
45
|
+
export type SCodeEditorDiagnosticId = string | number
|
|
46
|
+
|
|
47
|
+
/** Controlled diagnostic record rendered by SCodeEditor. */
|
|
48
|
+
export interface SCodeEditorDiagnostic<Id extends SCodeEditorDiagnosticId = string> extends SCodeEditorRange {
|
|
49
|
+
readonly id: Id
|
|
50
|
+
readonly severity: SCodeEditorDiagnosticSeverity
|
|
51
|
+
readonly message: string
|
|
52
|
+
readonly source?: string
|
|
53
|
+
readonly code?: string | number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const RANGE_KEYS = new Set(['from', 'to'])
|
|
57
|
+
const SELECTION_KEYS = new Set(['anchor', 'head', 'from', 'to', 'empty'])
|
|
58
|
+
const VIEW_STATE_KEYS = new Set(['selection', 'scrollTop', 'scrollLeft'])
|
|
59
|
+
const DIAGNOSTIC_KEYS = new Set(['id', 'severity', 'message', 'from', 'to', 'source', 'code'])
|
|
60
|
+
|
|
61
|
+
function assertPlainExactRecord(
|
|
62
|
+
owner: string,
|
|
63
|
+
coordinate: string,
|
|
64
|
+
value: unknown,
|
|
65
|
+
allowed: ReadonlySet<string>,
|
|
66
|
+
): Record<string, unknown> {
|
|
67
|
+
if (
|
|
68
|
+
typeof value !== 'object'
|
|
69
|
+
|| value === null
|
|
70
|
+
|| Array.isArray(value)
|
|
71
|
+
|| Object.getPrototypeOf(value) !== Object.prototype
|
|
72
|
+
) {
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
`${owner}: ${coordinate} должен быть plain object. / ${owner}: ${coordinate} must be a plain object.`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
const record = value as Record<string, unknown>
|
|
78
|
+
const unknownFields = Object.keys(record).filter((key) => !allowed.has(key))
|
|
79
|
+
if (unknownFields.length > 0) {
|
|
80
|
+
throw new TypeError(
|
|
81
|
+
`${owner}.${coordinate}: неизвестные поля ${unknownFields.map((key) => JSON.stringify(key)).join(', ')}. `
|
|
82
|
+
+ `/ ${owner}.${coordinate}: unknown fields ${unknownFields.map((key) => JSON.stringify(key)).join(', ')}.`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return record
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateNonNegativeFiniteNumber(owner: string, coordinate: string, value: unknown): number {
|
|
89
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return value
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
`${owner}: ${coordinate} должен быть конечным неотрицательным числом. `
|
|
92
|
+
+ `/ ${owner}: ${coordinate} must be a finite non-negative number.`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateDiagnosticId(owner: string, coordinate: string, value: unknown): SCodeEditorDiagnosticId {
|
|
97
|
+
if (typeof value === 'string') return validateNonEmptyString(owner, coordinate, value)
|
|
98
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`${owner}: ${coordinate} должен быть непустой строкой или конечным числом. `
|
|
101
|
+
+ `/ ${owner}: ${coordinate} must be a non-empty string or finite number.`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Validates a half-open range against the current document without clamping. */
|
|
106
|
+
export function validateCodeEditorRange(
|
|
107
|
+
owner: string,
|
|
108
|
+
coordinate: string,
|
|
109
|
+
value: unknown,
|
|
110
|
+
documentLength: number,
|
|
111
|
+
): SCodeEditorRange {
|
|
112
|
+
const length = validateBoundedInteger(owner, 'documentLength', documentLength, {
|
|
113
|
+
minimum: 0,
|
|
114
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
115
|
+
})
|
|
116
|
+
const record = assertPlainExactRecord(owner, coordinate, value, RANGE_KEYS)
|
|
117
|
+
const from = validateBoundedInteger(owner, `${coordinate}.from`, record.from, { minimum: 0, maximum: length })
|
|
118
|
+
const to = validateBoundedInteger(owner, `${coordinate}.to`, record.to, { minimum: from, maximum: length })
|
|
119
|
+
return Object.freeze({ from, to })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Validates the primary selection and its derived invariants. */
|
|
123
|
+
export function validateCodeEditorSelectionRange(
|
|
124
|
+
owner: string,
|
|
125
|
+
coordinate: string,
|
|
126
|
+
value: unknown,
|
|
127
|
+
documentLength: number,
|
|
128
|
+
): SCodeEditorSelectionRange {
|
|
129
|
+
const record = assertPlainExactRecord(owner, coordinate, value, SELECTION_KEYS)
|
|
130
|
+
const anchor = validateBoundedInteger(owner, `${coordinate}.anchor`, record.anchor, {
|
|
131
|
+
minimum: 0,
|
|
132
|
+
maximum: documentLength,
|
|
133
|
+
})
|
|
134
|
+
const head = validateBoundedInteger(owner, `${coordinate}.head`, record.head, {
|
|
135
|
+
minimum: 0,
|
|
136
|
+
maximum: documentLength,
|
|
137
|
+
})
|
|
138
|
+
const expectedFrom = Math.min(anchor, head)
|
|
139
|
+
const expectedTo = Math.max(anchor, head)
|
|
140
|
+
const from = validateBoundedInteger(owner, `${coordinate}.from`, record.from, {
|
|
141
|
+
minimum: expectedFrom,
|
|
142
|
+
maximum: expectedFrom,
|
|
143
|
+
})
|
|
144
|
+
const to = validateBoundedInteger(owner, `${coordinate}.to`, record.to, {
|
|
145
|
+
minimum: expectedTo,
|
|
146
|
+
maximum: expectedTo,
|
|
147
|
+
})
|
|
148
|
+
const empty = validateBoolean(owner, `${coordinate}.empty`, record.empty)
|
|
149
|
+
if (empty !== (anchor === head)) {
|
|
150
|
+
throw new TypeError(
|
|
151
|
+
`${owner}: ${coordinate}.empty не соответствует anchor/head. `
|
|
152
|
+
+ `/ ${owner}: ${coordinate}.empty does not match anchor/head.`,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
return Object.freeze({ anchor, head, from, to, empty })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Validates a serializable view-state snapshot against the current document. */
|
|
159
|
+
export function validateCodeEditorViewState(
|
|
160
|
+
owner: string,
|
|
161
|
+
value: unknown,
|
|
162
|
+
documentLength: number,
|
|
163
|
+
): SCodeEditorViewState {
|
|
164
|
+
const record = assertPlainExactRecord(owner, 'viewState', value, VIEW_STATE_KEYS)
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
selection: validateCodeEditorSelectionRange(owner, 'viewState.selection', record.selection, documentLength),
|
|
167
|
+
scrollTop: validateNonNegativeFiniteNumber(owner, 'viewState.scrollTop', record.scrollTop),
|
|
168
|
+
scrollLeft: validateNonNegativeFiniteNumber(owner, 'viewState.scrollLeft', record.scrollLeft),
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Runtime-validates controlled diagnostics and preserves their consumer-owned IDs. */
|
|
173
|
+
export function validateCodeEditorDiagnostics(
|
|
174
|
+
owner: string,
|
|
175
|
+
value: unknown,
|
|
176
|
+
documentLength: number,
|
|
177
|
+
): readonly SCodeEditorDiagnostic<SCodeEditorDiagnosticId>[] {
|
|
178
|
+
if (!Array.isArray(value)) {
|
|
179
|
+
throw new TypeError(`${owner}: diagnostics должен быть массивом. / ${owner}: diagnostics must be an array.`)
|
|
180
|
+
}
|
|
181
|
+
const ids = new Set<SCodeEditorDiagnosticId>()
|
|
182
|
+
value.forEach((candidate: unknown, index: number) => {
|
|
183
|
+
const coordinate = `diagnostics[${index}]`
|
|
184
|
+
const record = assertPlainExactRecord(owner, coordinate, candidate, DIAGNOSTIC_KEYS)
|
|
185
|
+
const id = validateDiagnosticId(owner, `${coordinate}.id`, record.id)
|
|
186
|
+
if (ids.has(id)) {
|
|
187
|
+
throw new TypeError(
|
|
188
|
+
`${owner}: ${coordinate}.id дублирует ${JSON.stringify(id)}. `
|
|
189
|
+
+ `/ ${owner}: ${coordinate}.id duplicates ${JSON.stringify(id)}.`,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
ids.add(id)
|
|
193
|
+
validateExactString(owner, `${coordinate}.severity`, record.severity, S_CODE_EDITOR_DIAGNOSTIC_SEVERITIES)
|
|
194
|
+
validateNonEmptyString(owner, `${coordinate}.message`, record.message)
|
|
195
|
+
validateCodeEditorRange(owner, coordinate, { from: record.from, to: record.to }, documentLength)
|
|
196
|
+
if (record.source !== undefined) validateNonEmptyString(owner, `${coordinate}.source`, record.source)
|
|
197
|
+
if (record.code !== undefined) validateDiagnosticId(owner, `${coordinate}.code`, record.code)
|
|
198
|
+
})
|
|
199
|
+
return value as readonly SCodeEditorDiagnostic<SCodeEditorDiagnosticId>[]
|
|
200
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateBoolean,
|
|
3
|
+
validateNonEmptyString,
|
|
4
|
+
} from '../../../internal/runtimeContract'
|
|
5
|
+
import {
|
|
6
|
+
validateCodeEditorCapabilities,
|
|
7
|
+
validateCodeEditorLanguage,
|
|
8
|
+
validateCodeEditorVariant,
|
|
9
|
+
type CodeEditorCapabilitiesContract,
|
|
10
|
+
type CodeEditorLanguageContract,
|
|
11
|
+
} from '../../../internal/codeEditorContract'
|
|
12
|
+
import {
|
|
13
|
+
validateCodeEditorDiagnostics,
|
|
14
|
+
type SCodeEditorDiagnostic,
|
|
15
|
+
type SCodeEditorDiagnosticId,
|
|
16
|
+
type SCodeEditorRange,
|
|
17
|
+
type SCodeEditorSelectionRange,
|
|
18
|
+
type SCodeEditorSelectionSnapshot,
|
|
19
|
+
type SCodeEditorViewState,
|
|
20
|
+
} from './codeEditor'
|
|
21
|
+
|
|
22
|
+
export type SCodeWorkspaceDocumentKey = string
|
|
23
|
+
export type SCodeWorkspaceSessionKey = string
|
|
24
|
+
export type SCodeWorkspaceRevision = string | number
|
|
25
|
+
|
|
26
|
+
/** Controlled document descriptor. Revision changes replace only this editor session and reset its local history. */
|
|
27
|
+
export interface SCodeWorkspaceDocument {
|
|
28
|
+
readonly key: SCodeWorkspaceDocumentKey
|
|
29
|
+
readonly sessionKey: SCodeWorkspaceSessionKey
|
|
30
|
+
readonly label: string
|
|
31
|
+
readonly value: string
|
|
32
|
+
readonly revision: SCodeWorkspaceRevision
|
|
33
|
+
readonly language: CodeEditorLanguageContract
|
|
34
|
+
readonly capabilities: CodeEditorCapabilitiesContract
|
|
35
|
+
readonly diagnostics?: readonly SCodeEditorDiagnostic<SCodeEditorDiagnosticId>[]
|
|
36
|
+
readonly readOnly?: boolean
|
|
37
|
+
readonly disabled?: boolean
|
|
38
|
+
readonly invalid?: boolean
|
|
39
|
+
readonly closable?: boolean
|
|
40
|
+
readonly placeholder?: string
|
|
41
|
+
readonly variant?: 'code' | 'sql'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Controlled text change. Revision stays unchanged until the consumer supplies an authoritative replacement. */
|
|
45
|
+
export interface SCodeWorkspaceDocumentChange {
|
|
46
|
+
readonly key: SCodeWorkspaceDocumentKey
|
|
47
|
+
readonly sessionKey: SCodeWorkspaceSessionKey
|
|
48
|
+
readonly revision: SCodeWorkspaceRevision
|
|
49
|
+
readonly value: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SCodeWorkspaceSelectionChange {
|
|
53
|
+
readonly key: SCodeWorkspaceDocumentKey
|
|
54
|
+
readonly sessionKey: SCodeWorkspaceSessionKey
|
|
55
|
+
readonly selection: SCodeEditorSelectionRange
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SCodeWorkspaceViewStateChange {
|
|
59
|
+
readonly key: SCodeWorkspaceDocumentKey
|
|
60
|
+
readonly sessionKey: SCodeWorkspaceSessionKey
|
|
61
|
+
readonly viewState: SCodeEditorViewState
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SCodeWorkspaceReleaseRequest {
|
|
65
|
+
readonly key: SCodeWorkspaceDocumentKey
|
|
66
|
+
readonly sessionKey: SCodeWorkspaceSessionKey
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SCodeWorkspaceDocumentState {
|
|
70
|
+
readonly key: SCodeWorkspaceDocumentKey
|
|
71
|
+
readonly sessionKey: SCodeWorkspaceSessionKey
|
|
72
|
+
readonly revision: SCodeWorkspaceRevision
|
|
73
|
+
readonly selection: SCodeEditorSelectionSnapshot
|
|
74
|
+
readonly viewState: SCodeEditorViewState
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface SCodeWorkspaceEditorApi {
|
|
78
|
+
focusActive(): void
|
|
79
|
+
revealRange(key: SCodeWorkspaceDocumentKey, range: SCodeEditorRange): void
|
|
80
|
+
captureDocumentState(key: SCodeWorkspaceDocumentKey): SCodeWorkspaceDocumentState
|
|
81
|
+
releaseSession(sessionKey: SCodeWorkspaceSessionKey): void
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DOCUMENT_KEYS = new Set([
|
|
85
|
+
'key',
|
|
86
|
+
'sessionKey',
|
|
87
|
+
'label',
|
|
88
|
+
'value',
|
|
89
|
+
'revision',
|
|
90
|
+
'language',
|
|
91
|
+
'capabilities',
|
|
92
|
+
'diagnostics',
|
|
93
|
+
'readOnly',
|
|
94
|
+
'disabled',
|
|
95
|
+
'invalid',
|
|
96
|
+
'closable',
|
|
97
|
+
'placeholder',
|
|
98
|
+
'variant',
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
function assertPlainExactRecord(
|
|
102
|
+
owner: string,
|
|
103
|
+
coordinate: string,
|
|
104
|
+
value: unknown,
|
|
105
|
+
allowed: ReadonlySet<string>,
|
|
106
|
+
): Record<string, unknown> {
|
|
107
|
+
if (
|
|
108
|
+
typeof value !== 'object'
|
|
109
|
+
|| value === null
|
|
110
|
+
|| Array.isArray(value)
|
|
111
|
+
|| Object.getPrototypeOf(value) !== Object.prototype
|
|
112
|
+
) {
|
|
113
|
+
throw new TypeError(
|
|
114
|
+
`${owner}: ${coordinate} должен быть plain object. / ${owner}: ${coordinate} must be a plain object.`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
const record = value as Record<string, unknown>
|
|
118
|
+
const unexpected = Object.keys(record).filter((key) => !allowed.has(key))
|
|
119
|
+
if (unexpected.length > 0) {
|
|
120
|
+
throw new TypeError(
|
|
121
|
+
`${owner}.${coordinate}: неизвестные поля ${unexpected.map((key) => JSON.stringify(key)).join(', ')}. `
|
|
122
|
+
+ `/ ${owner}.${coordinate}: unknown fields ${unexpected.map((key) => JSON.stringify(key)).join(', ')}.`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return record
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateStableKey(owner: string, coordinate: string, value: unknown): string {
|
|
129
|
+
const result = validateNonEmptyString(owner, coordinate, value)
|
|
130
|
+
if (result !== result.trim()) {
|
|
131
|
+
throw new TypeError(
|
|
132
|
+
`${owner}: ${coordinate} не должен содержать окружающие пробелы. `
|
|
133
|
+
+ `/ ${owner}: ${coordinate} must not contain surrounding whitespace.`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return result
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateRevision(owner: string, coordinate: string, value: unknown): SCodeWorkspaceRevision {
|
|
140
|
+
if (typeof value === 'string') return validateStableKey(owner, coordinate, value)
|
|
141
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) return value
|
|
142
|
+
throw new TypeError(
|
|
143
|
+
`${owner}: ${coordinate} должен быть непустой строкой или неотрицательным safe integer. `
|
|
144
|
+
+ `/ ${owner}: ${coordinate} must be a non-empty string or non-negative safe integer.`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validateOptionalBoolean(owner: string, coordinate: string, value: unknown, fallback: boolean): boolean {
|
|
149
|
+
return value === undefined ? fallback : validateBoolean(owner, coordinate, value)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateOptionalString(owner: string, coordinate: string, value: unknown): string | undefined {
|
|
153
|
+
return value === undefined ? undefined : validateNonEmptyString(owner, coordinate, value)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Validates and freezes the exact controlled document contract without normalizing invalid input. */
|
|
157
|
+
export function validateCodeWorkspaceDocument(
|
|
158
|
+
owner: string,
|
|
159
|
+
coordinate: string,
|
|
160
|
+
value: unknown,
|
|
161
|
+
): SCodeWorkspaceDocument {
|
|
162
|
+
const record = assertPlainExactRecord(owner, coordinate, value, DOCUMENT_KEYS)
|
|
163
|
+
if (typeof record.value !== 'string') {
|
|
164
|
+
throw new TypeError(`${owner}: ${coordinate}.value должен быть строкой. / ${owner}: ${coordinate}.value must be a string.`)
|
|
165
|
+
}
|
|
166
|
+
const diagnostics = validateCodeEditorDiagnostics(
|
|
167
|
+
owner,
|
|
168
|
+
record.diagnostics ?? [],
|
|
169
|
+
record.value.length,
|
|
170
|
+
)
|
|
171
|
+
const placeholder = validateOptionalString(owner, `${coordinate}.placeholder`, record.placeholder)
|
|
172
|
+
const variant = record.variant === undefined
|
|
173
|
+
? 'code'
|
|
174
|
+
: validateCodeEditorVariant(owner, record.variant)
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
key: validateStableKey(owner, `${coordinate}.key`, record.key),
|
|
177
|
+
sessionKey: validateStableKey(owner, `${coordinate}.sessionKey`, record.sessionKey),
|
|
178
|
+
label: validateNonEmptyString(owner, `${coordinate}.label`, record.label),
|
|
179
|
+
value: record.value,
|
|
180
|
+
revision: validateRevision(owner, `${coordinate}.revision`, record.revision),
|
|
181
|
+
language: validateCodeEditorLanguage(owner, record.language),
|
|
182
|
+
capabilities: validateCodeEditorCapabilities(owner, record.capabilities),
|
|
183
|
+
diagnostics,
|
|
184
|
+
readOnly: validateOptionalBoolean(owner, `${coordinate}.readOnly`, record.readOnly, false),
|
|
185
|
+
disabled: validateOptionalBoolean(owner, `${coordinate}.disabled`, record.disabled, false),
|
|
186
|
+
invalid: validateOptionalBoolean(owner, `${coordinate}.invalid`, record.invalid, false),
|
|
187
|
+
closable: validateOptionalBoolean(owner, `${coordinate}.closable`, record.closable, true),
|
|
188
|
+
...(placeholder === undefined ? {} : { placeholder }),
|
|
189
|
+
variant,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Validates document/session uniqueness for one workspace inventory. */
|
|
194
|
+
export function validateCodeWorkspaceDocuments(
|
|
195
|
+
owner: string,
|
|
196
|
+
value: unknown,
|
|
197
|
+
): readonly SCodeWorkspaceDocument[] {
|
|
198
|
+
if (!Array.isArray(value)) {
|
|
199
|
+
throw new TypeError(`${owner}: documents должен быть массивом. / ${owner}: documents must be an array.`)
|
|
200
|
+
}
|
|
201
|
+
const keys = new Set<string>()
|
|
202
|
+
const sessionKeys = new Set<string>()
|
|
203
|
+
const documents = value.map((document, index) => {
|
|
204
|
+
const validated = validateCodeWorkspaceDocument(owner, `documents[${index}]`, document)
|
|
205
|
+
if (keys.has(validated.key)) {
|
|
206
|
+
throw new RangeError(`${owner}: duplicate document key ${JSON.stringify(validated.key)}.`)
|
|
207
|
+
}
|
|
208
|
+
if (sessionKeys.has(validated.sessionKey)) {
|
|
209
|
+
throw new RangeError(`${owner}: duplicate sessionKey ${JSON.stringify(validated.sessionKey)}.`)
|
|
210
|
+
}
|
|
211
|
+
keys.add(validated.key)
|
|
212
|
+
sessionKeys.add(validated.sessionKey)
|
|
213
|
+
return validated
|
|
214
|
+
})
|
|
215
|
+
return Object.freeze(documents)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Validates controlled selection and rejects missing active documents. */
|
|
219
|
+
export function validateCodeWorkspaceActiveKey(
|
|
220
|
+
owner: string,
|
|
221
|
+
value: unknown,
|
|
222
|
+
documents: readonly SCodeWorkspaceDocument[],
|
|
223
|
+
): SCodeWorkspaceDocumentKey | null {
|
|
224
|
+
if (documents.length === 0) {
|
|
225
|
+
if (value === null) return null
|
|
226
|
+
throw new RangeError(`${owner}: activeKey должен быть null для пустого workspace. / ${owner}: activeKey must be null for an empty workspace.`)
|
|
227
|
+
}
|
|
228
|
+
const key = validateStableKey(owner, 'activeKey', value)
|
|
229
|
+
if (!documents.some((document) => document.key === key)) {
|
|
230
|
+
throw new RangeError(`${owner}: activeKey ${JSON.stringify(key)} отсутствует в documents. / ${owner}: activeKey ${JSON.stringify(key)} is absent from documents.`)
|
|
231
|
+
}
|
|
232
|
+
return key
|
|
233
|
+
}
|