@workbench-kit/shell-react 0.0.2-prototype.0.2.6 → 0.0.2-prototype.0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +41 -0
  2. package/package.json +9 -9
  3. package/src/{chat-ai-mock-proposals.ts → chat/ai-mock-proposals.ts} +39 -39
  4. package/src/{chat-command-input.ts → chat/command-input.ts} +52 -52
  5. package/src/{chat-command-policy.ts → chat/command-policy.ts} +54 -54
  6. package/src/{chat-command-surface.tsx → chat/command-surface.tsx} +155 -155
  7. package/src/{use-workbench-chat-command-proposals.ts → chat/use-chat-command-proposals.ts} +194 -194
  8. package/src/{chat-view-data.ts → chat/view-data.ts} +20 -20
  9. package/src/{chat-view.tsx → chat/view.tsx} +220 -220
  10. package/src/{command-inspector-surface.tsx → commands/inspector-surface.tsx} +46 -46
  11. package/src/{use-workbench-command-descriptors.ts → commands/use-command-descriptors.ts} +41 -41
  12. package/src/{use-context-key-revision.ts → commands/use-context-key-revision.ts} +18 -18
  13. package/src/{commands-view-data.ts → commands/view-data.ts} +17 -17
  14. package/src/{commands-view.tsx → commands/view.tsx} +43 -43
  15. package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -124
  16. package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -17
  17. package/src/devtools/index.ts +11 -11
  18. package/src/devtools/use-workbench-devtools-snapshot.ts +84 -84
  19. package/src/devtools/workbench-devtools-snapshot.ts +140 -140
  20. package/src/devtools/workbench-devtools.css +82 -82
  21. package/src/{editor-area-dnd.ts → editor/area-dnd.ts} +69 -69
  22. package/src/{editor-area-model.ts → editor/area-model.ts} +71 -71
  23. package/src/{editor-area.css → editor/area.css} +454 -454
  24. package/src/{editor-area.tsx → editor/area.tsx} +676 -676
  25. package/src/{editor-host-surface.tsx → editor/host-surface.tsx} +631 -634
  26. package/src/{editor-pane-visibility.ts → editor/pane-visibility.ts} +130 -130
  27. package/src/{editor-resource.ts → editor/resource.ts} +36 -36
  28. package/src/{editor-state-storage.ts → editor/state-storage.ts} +182 -193
  29. package/src/{editor-tab-context-menu.ts → editor/tab-context-menu.ts} +118 -118
  30. package/src/{use-editor.ts → editor/use-editor.ts} +114 -107
  31. package/src/{editor-view-providers.tsx → editor/view-providers.tsx} +552 -552
  32. package/src/{editor-workspace-file-availability.ts → editor/workspace-file-availability.ts} +15 -15
  33. package/src/{editor-workspace-reconcile.tsx → editor/workspace-reconcile.tsx} +31 -28
  34. package/src/{explorer-context-menu.ts → explorer/context-menu.ts} +114 -114
  35. package/src/{createCommandWorkspaceExplorerPort.ts → explorer/create-command-workspace-explorer-port.ts} +86 -86
  36. package/src/{explorer-reveal.ts → explorer/reveal.ts} +112 -112
  37. package/src/{search-view-data.ts → explorer/search-view-data.ts} +15 -15
  38. package/src/{search-view.tsx → explorer/search-view.tsx} +65 -62
  39. package/src/{use-active-workspace-path.ts → explorer/use-active-workspace-path.ts} +15 -15
  40. package/src/{explorer-view-data.ts → explorer/view-data.ts} +58 -58
  41. package/src/{explorer-view.tsx → explorer/view.tsx} +227 -224
  42. package/src/{extension-context-menu.ts → extensions/context-menu.ts} +52 -52
  43. package/src/{extension-management-model.ts → extensions/management-model.ts} +313 -313
  44. package/src/{use-extension-management.ts → extensions/use-extension-management.ts} +241 -193
  45. package/src/{extensions-view-data.ts → extensions/view-data.ts} +19 -19
  46. package/src/{extensions-view.tsx → extensions/view.tsx} +63 -38
  47. package/src/{field-remap-demo.tsx → field-remap/demo.tsx} +18 -18
  48. package/src/{field-remap-editor-surface.tsx → field-remap/editor-surface.tsx} +36 -36
  49. package/src/{field-remap-flow-adapter.ts → field-remap/flow-adapter.ts} +240 -240
  50. package/src/{field-remap-flow.tsx → field-remap/flow.tsx} +369 -369
  51. package/src/{field-remap-graph.tsx → field-remap/graph.tsx} +559 -559
  52. package/src/field-remap/jsonata-transform.ts +166 -0
  53. package/src/{field-remap-panel.tsx → field-remap/panel.tsx} +193 -175
  54. package/src/{field-remap-samples.ts → field-remap/samples.ts} +352 -352
  55. package/src/{table-mapping-adapter.ts → field-remap/table-mapping-adapter.ts} +59 -59
  56. package/src/{field-remap-tree.tsx → field-remap/tree.tsx} +404 -404
  57. package/src/{field-remap-view-data.ts → field-remap/view-data.ts} +16 -16
  58. package/src/{field-remap-view.css → field-remap/view.css} +676 -676
  59. package/src/{field-remap-view.tsx → field-remap/view.tsx} +86 -86
  60. package/src/index.ts +212 -199
  61. package/src/{json-form-source-patch.ts → jdw/json-form-source-patch.ts} +323 -323
  62. package/src/{jdw-lab-view-data.ts → jdw/lab-view-data.ts} +25 -25
  63. package/src/{jdw-lab-view.css → jdw/lab-view.css} +21 -21
  64. package/src/{jdw-lab-view.tsx → jdw/lab-view.tsx} +79 -79
  65. package/src/{jdw-widget-form-view.tsx → jdw/widget-form-view.tsx} +68 -65
  66. package/src/{jdw-widget-preview-view.tsx → jdw/widget-preview-view.tsx} +54 -51
  67. package/src/{keybinding-overrides-storage.ts → management/keybinding-overrides-storage.ts} +49 -57
  68. package/src/{keybinding-management-settings.tsx → management/keybinding-settings.tsx} +16 -16
  69. package/src/{management-palette-commands.ts → management/palette-commands.ts} +38 -38
  70. package/src/{preference-settings-storage.ts → management/preference-settings-storage.ts} +50 -58
  71. package/src/{management-settings-ids.ts → management/settings-ids.ts} +12 -12
  72. package/src/{management-settings.tsx → management/settings.tsx} +96 -75
  73. package/src/{use-command-management.ts → management/use-command-management.ts} +191 -191
  74. package/src/{use-keybinding-management.ts → management/use-keybinding-management.ts} +130 -130
  75. package/src/{shell-model.tsx → shell/model.tsx} +149 -149
  76. package/src/{provider.tsx → shell/provider.tsx} +603 -573
  77. package/src/{shell-secondary-actions.tsx → shell/secondary-actions.tsx} +59 -59
  78. package/src/{shell-settings-constants.ts → shell/settings-constants.ts} +9 -9
  79. package/src/{shell-settings.tsx → shell/settings.tsx} +599 -599
  80. package/src/{shell.tsx → shell/shell.tsx} +680 -649
  81. package/src/{shell-titlebar-layout-controls.tsx → shell/titlebar-layout-controls.tsx} +4 -4
  82. package/src/{shell-view-host.tsx → shell/view-host.tsx} +212 -199
  83. package/src/{workbench-appearance-storage.ts → workbench/appearance-storage.ts} +107 -115
  84. package/src/{workbench-command-host.tsx → workbench/command-host.tsx} +243 -243
  85. package/src/{workbench-command-palette.ts → workbench/command-palette.ts} +208 -208
  86. package/src/{workbench-keybinding-bridge.ts → workbench/keybinding-bridge.ts} +54 -54
  87. package/src/{workbench-layout-storage.ts → workbench/layout-storage.ts} +134 -145
  88. package/src/{workbench-profile-modal.tsx → workbench/profile-modal.tsx} +118 -118
  89. package/src/{workbench-shell-command-registration.ts → workbench/shell-command-registration.ts} +58 -58
  90. package/src/{workbench-startup-gate.tsx → workbench/startup-gate.tsx} +136 -136
  91. package/src/{workbench-status-sections.ts → workbench/status-sections.ts} +43 -43
  92. package/src/{use-persisted-workbench-appearance.ts → workbench/use-persisted-appearance.ts} +33 -33
  93. package/src/{workbench-user-commands.ts → workbench/user-commands.ts} +46 -46
  94. package/src/{workspace-view-state.ts → workbench/workspace-view-state.ts} +35 -35
  95. package/src/jsonata-transform.ts +0 -36
@@ -1,634 +1,631 @@
1
- import {
2
- isValidElement,
3
- lazy,
4
- Suspense,
5
- useCallback,
6
- useEffect,
7
- useMemo,
8
- useRef,
9
- useState,
10
- type ReactNode,
11
- } from 'react';
12
- import { createPortal } from 'react-dom';
13
- import { IconButton, ScrollArea } from '@workbench-kit/react/primitives';
14
- import { SplitView } from '@workbench-kit/react/workbench/split-view';
15
- import type { WorkspaceEditorTheme } from '@workbench-kit/react/workbench/workspace/editor';
16
- import {
17
- EDITOR_SAVE_COMMAND_ID,
18
- type EditorHost,
19
- type EditorTabState,
20
- } from '@workbench-kit/workbench-core';
21
-
22
- import { CommandInspectorSurface } from './command-inspector-surface.js';
23
- import {
24
- resolveEditorDocumentViews,
25
- type EditorDocumentViewProvider,
26
- } from './editor-view-providers.js';
27
- import { mimeTypeForResource, pathForResource } from './editor-resource.js';
28
- import { FieldRemapEditorSurface } from './field-remap-editor-surface.js';
29
- import {
30
- editorViewModeToPaneVisibility,
31
- getVisibleEditorPaneKinds,
32
- paneVisibilityToPrimaryViewMode,
33
- resolveDefaultEditorPaneVisibility,
34
- resolveEffectiveEditorPaneVisibility,
35
- type EditorPaneKind,
36
- type EditorPaneVisibility,
37
- type EditorViewMode,
38
- } from './editor-pane-visibility.js';
39
- import { useWorkbench } from './provider.js';
40
- import { useEditorHost, useEditorService } from './use-editor.js';
41
-
42
- export type { EditorViewMode } from './editor-pane-visibility.js';
43
-
44
- const WorkspaceEditor = lazy(async () => {
45
- const module = await import('@workbench-kit/react/workbench/workspace/editor');
46
- return { default: module.WorkspaceEditor };
47
- });
48
-
49
- export interface EditorHostSurfaceProps {
50
- activeTab: EditorTabState | undefined;
51
- defaultViewModeForResource: ((resourceUri: string) => EditorViewMode | undefined) | undefined;
52
- modeToolbarHost: HTMLDivElement | null;
53
- onModeToolbarVisibleChange: (visible: boolean) => void;
54
- theme: WorkspaceEditorTheme | undefined;
55
- viewProviders: readonly EditorDocumentViewProvider[];
56
- }
57
-
58
- export function EditorHostSurface({
59
- activeTab,
60
- defaultViewModeForResource,
61
- modeToolbarHost,
62
- onModeToolbarVisibleChange,
63
- theme,
64
- viewProviders,
65
- }: EditorHostSurfaceProps) {
66
- const editorService = useEditorService();
67
- const host = useEditorHost(activeTab?.id);
68
- const hostFrameRef = useRef<HTMLDivElement>(null);
69
-
70
- useEffect(() => {
71
- if (!host || !activeTab) {
72
- return undefined;
73
- }
74
-
75
- host.onDidChangeDirty = (dirty) => {
76
- editorService.setDirty(activeTab.id, dirty);
77
- };
78
-
79
- return () => {
80
- host.onDidChangeDirty = undefined;
81
- };
82
- }, [activeTab, editorService, host]);
83
-
84
- if (!activeTab || !host) {
85
- return null;
86
- }
87
-
88
- const rendered = host.render();
89
-
90
- if (isMissingResourceEditorRenderPayload(rendered)) {
91
- return (
92
- <MissingResourceEditorSurface
93
- message={rendered.message}
94
- resourceUri={rendered.resourceUri}
95
- tabId={activeTab.id}
96
- title={host.title ?? activeTab.title}
97
- />
98
- );
99
- }
100
-
101
- if (isCommandInspectorEditorRenderPayload(rendered)) {
102
- return (
103
- <CommandInspectorSurface
104
- commandId={rendered.commandId}
105
- resourceUri={rendered.resourceUri}
106
- tabId={activeTab.id}
107
- />
108
- );
109
- }
110
-
111
- if (isFieldRemapEditorRenderPayload(rendered)) {
112
- return (
113
- <FieldRemapEditorSurface
114
- resourceUri={rendered.resourceUri}
115
- surfaceId={rendered.surfaceId}
116
- tabId={activeTab.id}
117
- />
118
- );
119
- }
120
-
121
- if (isTextEditorRenderPayload(rendered)) {
122
- return (
123
- <TextEditorSurface
124
- defaultViewModeForResource={defaultViewModeForResource}
125
- host={host as TextEditorHostLike}
126
- initialContent={rendered.initialContent}
127
- modeToolbarHost={modeToolbarHost}
128
- mimeType={rendered.mimeType}
129
- onModeToolbarVisibleChange={onModeToolbarVisibleChange}
130
- resourceUri={rendered.resourceUri}
131
- tabId={activeTab.id}
132
- theme={theme}
133
- viewProviders={viewProviders}
134
- />
135
- );
136
- }
137
-
138
- return (
139
- <div
140
- ref={hostFrameRef}
141
- aria-label={host.title ?? activeTab.title ?? 'Editor'}
142
- className="workbench-editor-area__host"
143
- data-editor-host-id={activeTab.id}
144
- >
145
- {toReactNode(rendered)}
146
- </div>
147
- );
148
- }
149
-
150
- interface TextEditorHostLike extends EditorHost {
151
- getContent?(): string;
152
- markDirty?(): void;
153
- setContent?(content: string): void;
154
- setDirty?(dirty: boolean): void;
155
- }
156
-
157
- interface TextEditorRenderPayload {
158
- initialContent: string;
159
- kind: 'workbench-kit.builtin.editor/text';
160
- mimeType?: string | undefined;
161
- resourceUri: string;
162
- }
163
-
164
- function TextEditorSurface({
165
- defaultViewModeForResource,
166
- host,
167
- initialContent,
168
- modeToolbarHost,
169
- mimeType,
170
- onModeToolbarVisibleChange,
171
- resourceUri,
172
- tabId,
173
- theme,
174
- viewProviders,
175
- }: {
176
- defaultViewModeForResource: ((resourceUri: string) => EditorViewMode | undefined) | undefined;
177
- host: TextEditorHostLike;
178
- initialContent: string;
179
- modeToolbarHost: HTMLDivElement | null;
180
- mimeType?: string | undefined;
181
- onModeToolbarVisibleChange: (visible: boolean) => void;
182
- resourceUri: string;
183
- tabId: string;
184
- theme: WorkspaceEditorTheme | undefined;
185
- viewProviders: readonly EditorDocumentViewProvider[];
186
- }) {
187
- const editorService = useEditorService();
188
- const { executeCommand, workspaceHostPort } = useWorkbench();
189
- const [content, setContent] = useState(initialContent);
190
- const previousResourceUriRef = useRef<string | undefined>(undefined);
191
- const splitSizesRef = useRef<Record<string, number>>({});
192
- const editorDocument = useMemo(
193
- () => ({
194
- content,
195
- mimeType: mimeType ?? mimeTypeForResource(resourceUri),
196
- path: pathForResource(resourceUri),
197
- resourceUri,
198
- }),
199
- [content, mimeType, resourceUri],
200
- );
201
- const { formProvider, previewProvider } = useMemo(
202
- () => resolveEditorDocumentViews(editorDocument, viewProviders),
203
- [editorDocument, viewProviders],
204
- );
205
- const formEligible = Boolean(formProvider);
206
- const previewEligible = Boolean(previewProvider);
207
- const [panePreference, setPanePreference] = useState<EditorPaneVisibility>(() =>
208
- resolveDefaultEditorPaneVisibility(defaultViewModeForResource?.(resourceUri), {
209
- formEligible,
210
- }),
211
- );
212
- const paneVisibility = useMemo(
213
- () => resolveEffectiveEditorPaneVisibility(panePreference, { formEligible, previewEligible }),
214
- [formEligible, panePreference, previewEligible],
215
- );
216
- const codeIcon = isJsonSourceDocument(editorDocument) ? 'codicon-json' : 'codicon-code';
217
- const modeToolbarVisible = formEligible || previewEligible;
218
-
219
- useEffect(() => {
220
- const resourceChanged = previousResourceUriRef.current !== resourceUri;
221
- previousResourceUriRef.current = resourceUri;
222
-
223
- if (resourceChanged) {
224
- setContent(initialContent);
225
- setPanePreference(
226
- resolveDefaultEditorPaneVisibility(defaultViewModeForResource?.(resourceUri), {
227
- formEligible,
228
- }),
229
- );
230
- return;
231
- }
232
-
233
- setContent((current) => (current === initialContent ? current : initialContent));
234
- }, [defaultViewModeForResource, formEligible, initialContent, resourceUri]);
235
-
236
- useEffect(() => {
237
- onModeToolbarVisibleChange(modeToolbarVisible);
238
- }, [modeToolbarVisible, onModeToolbarVisibleChange]);
239
-
240
- useEffect(() => {
241
- return () => {
242
- onModeToolbarVisibleChange(false);
243
- };
244
- }, [onModeToolbarVisibleChange]);
245
-
246
- const getSplitSize = useCallback((key: string, defaultPercent: number) => {
247
- return splitSizesRef.current[key] ?? defaultPercent;
248
- }, []);
249
-
250
- const setSplitSize = useCallback((key: string, percent: number) => {
251
- splitSizesRef.current[key] = percent;
252
- }, []);
253
-
254
- const handleChange = useCallback(
255
- (nextContent: string) => {
256
- setContent(nextContent);
257
- host.setContent?.(nextContent);
258
- host.markDirty?.();
259
- editorService.promotePreviewOnEdit(tabId);
260
- },
261
- [editorService, host, tabId],
262
- );
263
-
264
- const handleSave = useCallback(
265
- (nextContent: string) => {
266
- if (nextContent !== content) {
267
- handleChange(nextContent);
268
- }
269
-
270
- void executeCommand(EDITOR_SAVE_COMMAND_ID);
271
- },
272
- [content, executeCommand, handleChange],
273
- );
274
-
275
- const sourcePane = (
276
- <div className="workbench-editor-area__source-pane">
277
- <Suspense
278
- fallback={
279
- <div className="workbench-editor-area__editor-loading" role="status">
280
- Loading editor...
281
- </div>
282
- }
283
- >
284
- <WorkspaceEditor
285
- file={editorDocument}
286
- showFileBar
287
- showHeader={false}
288
- theme={theme}
289
- value={content}
290
- onChange={handleChange}
291
- onSave={workspaceHostPort ? handleSave : undefined}
292
- />
293
- </Suspense>
294
- </div>
295
- );
296
-
297
- const previewPane = previewProvider ? (
298
- <ScrollArea
299
- aria-label="Preview"
300
- as="section"
301
- className="workbench-editor-area__preview-pane"
302
- orientation="vertical"
303
- >
304
- {toReactNode(
305
- previewProvider.render({ document: editorDocument, onContentChange: handleChange }),
306
- )}
307
- </ScrollArea>
308
- ) : null;
309
- const formPane = formProvider
310
- ? toReactNode(formProvider.render({ document: editorDocument, onContentChange: handleChange }))
311
- : null;
312
- const paneByKind: Record<EditorPaneKind, ReactNode | null> = {
313
- code: sourcePane,
314
- form: formPane,
315
- preview: previewPane,
316
- };
317
- const visiblePaneEntries = getVisibleEditorPaneKinds(paneVisibility)
318
- .map((kind) => ({ kind, node: paneByKind[kind] }))
319
- .filter((entry): entry is { kind: EditorPaneKind; node: ReactNode } => entry.node !== null);
320
- const editorBody = composeEditorPaneLayout(visiblePaneEntries, getSplitSize, setSplitSize);
321
- const modeToolbar =
322
- modeToolbarVisible && modeToolbarHost
323
- ? createPortal(
324
- <EditorViewModeIconSelect
325
- codeIcon={codeIcon}
326
- formEligible={formEligible}
327
- previewEligible={previewEligible}
328
- value={paneVisibilityToPrimaryViewMode(paneVisibility)}
329
- onValueChange={(mode) => {
330
- setPanePreference(editorViewModeToPaneVisibility(mode));
331
- }}
332
- />,
333
- modeToolbarHost,
334
- )
335
- : null;
336
-
337
- return (
338
- <>
339
- {modeToolbar}
340
- <section
341
- aria-label={host.title ?? resourceUri}
342
- className="workbench-editor-area__text-editor"
343
- data-resource-uri={resourceUri}
344
- >
345
- <div className="workbench-editor-area__text-editor-body">{editorBody}</div>
346
- </section>
347
- </>
348
- );
349
- }
350
-
351
- function composeEditorPaneLayout(
352
- panes: readonly { kind: EditorPaneKind; node: ReactNode }[],
353
- getSplitSize: (key: string, defaultPercent: number) => number,
354
- onSplitSizeChange: (key: string, percent: number) => void,
355
- ): ReactNode {
356
- if (panes.length === 0) {
357
- return null;
358
- }
359
-
360
- if (panes.length === 1) {
361
- return panes[0]?.node ?? null;
362
- }
363
-
364
- const layoutSignature = panes.map((pane) => pane.kind).join('|');
365
-
366
- return panes.slice(1).reduce<ReactNode>((primary, entry, splitIndex) => {
367
- const primaryKind = panes[0]?.kind;
368
- const primaryLabel = primaryKind ? editorPaneKindLabel(primaryKind) : 'Editor pane';
369
- const splitKey = `${layoutSignature}#${splitIndex}`;
370
-
371
- return (
372
- <SplitView
373
- key={splitKey}
374
- className="workbench-editor-area__split"
375
- defaultPrimarySizePercent={50}
376
- minPrimarySizePercent={25}
377
- primarySizePercent={getSplitSize(splitKey, 50)}
378
- onPrimarySizePercentChange={(percent) => {
379
- onSplitSizeChange(splitKey, percent);
380
- }}
381
- primary={
382
- <section aria-label={primaryLabel} className="workbench-editor-area__split-pane">
383
- {primary}
384
- </section>
385
- }
386
- secondary={
387
- <section
388
- aria-label={editorPaneKindLabel(entry.kind)}
389
- className="workbench-editor-area__split-pane"
390
- >
391
- {entry.node}
392
- </section>
393
- }
394
- />
395
- );
396
- }, panes[0]?.node ?? null);
397
- }
398
-
399
- function editorPaneKindLabel(kind: EditorPaneKind): string {
400
- switch (kind) {
401
- case 'code':
402
- return 'Code';
403
- case 'form':
404
- return 'Form';
405
- case 'preview':
406
- return 'Preview';
407
- }
408
- }
409
-
410
- function EditorViewModeIconSelect({
411
- codeIcon,
412
- formEligible,
413
- onValueChange,
414
- previewEligible,
415
- value,
416
- }: {
417
- codeIcon: string;
418
- formEligible: boolean;
419
- onValueChange: (mode: EditorViewMode) => void;
420
- previewEligible: boolean;
421
- value: EditorViewMode;
422
- }) {
423
- const resolvedValue =
424
- (value === 'form' && !formEligible) || (value === 'preview' && !previewEligible)
425
- ? 'code'
426
- : value;
427
-
428
- return (
429
- <div
430
- aria-label="Editor view mode"
431
- className="workbench-editor-area__view-toolbar"
432
- data-testid="editor-view-mode-select"
433
- role="radiogroup"
434
- >
435
- <EditorViewModeIconButton
436
- checked={resolvedValue === 'code'}
437
- icon={codeIcon}
438
- label="Code"
439
- onSelect={() => {
440
- onValueChange('code');
441
- }}
442
- />
443
- {formEligible ? (
444
- <EditorViewModeIconButton
445
- checked={resolvedValue === 'form'}
446
- icon="codicon-symbol-field"
447
- label="Form"
448
- onSelect={() => {
449
- onValueChange('form');
450
- }}
451
- />
452
- ) : null}
453
- {previewEligible ? (
454
- <EditorViewModeIconButton
455
- checked={resolvedValue === 'preview'}
456
- icon="codicon-preview"
457
- label="Preview"
458
- onSelect={() => {
459
- onValueChange('preview');
460
- }}
461
- />
462
- ) : null}
463
- </div>
464
- );
465
- }
466
-
467
- function EditorViewModeIconButton({
468
- checked,
469
- icon,
470
- label,
471
- onSelect,
472
- }: {
473
- checked: boolean;
474
- icon: string;
475
- label: string;
476
- onSelect: () => void;
477
- }) {
478
- return (
479
- <IconButton
480
- aria-checked={checked}
481
- className={[
482
- 'workbench-editor-area__view-button',
483
- checked && 'workbench-editor-area__view-button--active',
484
- ]
485
- .filter(Boolean)
486
- .join(' ')}
487
- compact
488
- icon={icon}
489
- label={label}
490
- role="radio"
491
- onClick={onSelect}
492
- />
493
- );
494
- }
495
-
496
- function isJsonSourceDocument(document: { mimeType?: string | undefined; path: string }): boolean {
497
- const mimeType = document.mimeType?.toLowerCase();
498
- return document.path.toLowerCase().endsWith('.json') || Boolean(mimeType?.includes('json'));
499
- }
500
-
501
- interface CommandInspectorEditorRenderPayload {
502
- readonly commandId: string;
503
- readonly kind: 'workbench-kit.builtin.commands/inspector';
504
- readonly resourceUri: string;
505
- }
506
-
507
- function isCommandInspectorEditorRenderPayload(
508
- value: unknown,
509
- ): value is CommandInspectorEditorRenderPayload {
510
- if (typeof value !== 'object' || value === null) {
511
- return false;
512
- }
513
-
514
- const candidate = value as Partial<CommandInspectorEditorRenderPayload>;
515
- return (
516
- candidate.kind === 'workbench-kit.builtin.commands/inspector' &&
517
- typeof candidate.commandId === 'string' &&
518
- typeof candidate.resourceUri === 'string'
519
- );
520
- }
521
-
522
- interface FieldRemapEditorRenderPayload {
523
- readonly kind: 'workbench-kit.samples.field-remap/editor';
524
- readonly resourceUri: string;
525
- readonly surfaceId: string;
526
- }
527
-
528
- function isFieldRemapEditorRenderPayload(value: unknown): value is FieldRemapEditorRenderPayload {
529
- if (typeof value !== 'object' || value === null) {
530
- return false;
531
- }
532
-
533
- const candidate = value as Partial<FieldRemapEditorRenderPayload>;
534
- return (
535
- candidate.kind === 'workbench-kit.samples.field-remap/editor' &&
536
- typeof candidate.resourceUri === 'string' &&
537
- typeof candidate.surfaceId === 'string' &&
538
- candidate.surfaceId.length > 0
539
- );
540
- }
541
-
542
- function isTextEditorRenderPayload(value: unknown): value is TextEditorRenderPayload {
543
- if (typeof value !== 'object' || value === null) {
544
- return false;
545
- }
546
-
547
- const candidate = value as Partial<TextEditorRenderPayload>;
548
- return (
549
- candidate.kind === 'workbench-kit.builtin.editor/text' &&
550
- typeof candidate.resourceUri === 'string' &&
551
- typeof candidate.initialContent === 'string'
552
- );
553
- }
554
-
555
- interface MissingResourceEditorRenderPayload {
556
- readonly kind: 'workbench-kit.builtin.editor/missing-resource';
557
- readonly message: string;
558
- readonly resourceUri: string;
559
- }
560
-
561
- function isMissingResourceEditorRenderPayload(
562
- value: unknown,
563
- ): value is MissingResourceEditorRenderPayload {
564
- if (typeof value !== 'object' || value === null) {
565
- return false;
566
- }
567
-
568
- const candidate = value as Partial<MissingResourceEditorRenderPayload>;
569
- return (
570
- candidate.kind === 'workbench-kit.builtin.editor/missing-resource' &&
571
- typeof candidate.resourceUri === 'string' &&
572
- typeof candidate.message === 'string'
573
- );
574
- }
575
-
576
- function MissingResourceEditorSurface({
577
- message,
578
- resourceUri,
579
- tabId,
580
- title,
581
- }: {
582
- message: string;
583
- resourceUri: string;
584
- tabId: string;
585
- title?: string | undefined;
586
- }) {
587
- const editorService = useEditorService();
588
- const path = pathForResource(resourceUri);
589
-
590
- return (
591
- <section
592
- aria-label={title ?? path}
593
- className="workbench-editor-area__missing-resource"
594
- data-resource-uri={resourceUri}
595
- >
596
- <div className="workbench-editor-area__missing-resource-body" role="alert">
597
- <p className="workbench-editor-area__missing-resource-title">Unable to open editor</p>
598
- <p className="workbench-editor-area__missing-resource-message">{message}</p>
599
- <p className="workbench-editor-area__missing-resource-path">{path}</p>
600
- <button
601
- className="workbench-editor-area__missing-resource-close"
602
- type="button"
603
- onClick={() => {
604
- editorService.closeEditor(tabId);
605
- }}
606
- >
607
- Close tab
608
- </button>
609
- </div>
610
- </section>
611
- );
612
- }
613
-
614
- function toReactNode(value: unknown): ReactNode {
615
- if (
616
- value === undefined ||
617
- value === null ||
618
- typeof value === 'boolean' ||
619
- typeof value === 'symbol' ||
620
- typeof value === 'function'
621
- ) {
622
- return null;
623
- }
624
-
625
- if (typeof value === 'string' || typeof value === 'number' || isValidElement(value)) {
626
- return value;
627
- }
628
-
629
- if (Array.isArray(value)) {
630
- return value as ReactNode;
631
- }
632
-
633
- return null;
634
- }
1
+ import {
2
+ isValidElement,
3
+ lazy,
4
+ Suspense,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import { createPortal } from 'react-dom';
13
+ import { IconButton, ScrollArea } from '@workbench-kit/react/primitives';
14
+ import { SplitView } from '@workbench-kit/react/workbench/split-view';
15
+ import type { WorkspaceEditorTheme } from '@workbench-kit/react/workbench/workspace/editor';
16
+ import {
17
+ EDITOR_SAVE_COMMAND_ID,
18
+ type EditorHost,
19
+ type EditorTabState,
20
+ } from '@workbench-kit/workbench-core';
21
+
22
+ import { CommandInspectorSurface } from '../commands/inspector-surface.js';
23
+ import { resolveEditorDocumentViews, type EditorDocumentViewProvider } from './view-providers.js';
24
+ import { mimeTypeForResource, pathForResource } from './resource.js';
25
+ import { FieldRemapEditorSurface } from '../field-remap/editor-surface.js';
26
+ import {
27
+ editorViewModeToPaneVisibility,
28
+ getVisibleEditorPaneKinds,
29
+ paneVisibilityToPrimaryViewMode,
30
+ resolveDefaultEditorPaneVisibility,
31
+ resolveEffectiveEditorPaneVisibility,
32
+ type EditorPaneKind,
33
+ type EditorPaneVisibility,
34
+ type EditorViewMode,
35
+ } from './pane-visibility.js';
36
+ import { useWorkbench } from '../shell/provider.js';
37
+ import { useEditorHost, useEditorService } from './use-editor.js';
38
+
39
+ export type { EditorViewMode } from './pane-visibility.js';
40
+
41
+ const WorkspaceEditor = lazy(async () => {
42
+ const module = await import('@workbench-kit/react/workbench/workspace/editor');
43
+ return { default: module.WorkspaceEditor };
44
+ });
45
+
46
+ export interface EditorHostSurfaceProps {
47
+ activeTab: EditorTabState | undefined;
48
+ defaultViewModeForResource: ((resourceUri: string) => EditorViewMode | undefined) | undefined;
49
+ modeToolbarHost: HTMLDivElement | null;
50
+ onModeToolbarVisibleChange: (visible: boolean) => void;
51
+ theme: WorkspaceEditorTheme | undefined;
52
+ viewProviders: readonly EditorDocumentViewProvider[];
53
+ }
54
+
55
+ export function EditorHostSurface({
56
+ activeTab,
57
+ defaultViewModeForResource,
58
+ modeToolbarHost,
59
+ onModeToolbarVisibleChange,
60
+ theme,
61
+ viewProviders,
62
+ }: EditorHostSurfaceProps) {
63
+ const editorService = useEditorService();
64
+ const host = useEditorHost(activeTab?.id);
65
+ const hostFrameRef = useRef<HTMLDivElement>(null);
66
+
67
+ useEffect(() => {
68
+ if (!host || !activeTab) {
69
+ return undefined;
70
+ }
71
+
72
+ host.onDidChangeDirty = (dirty) => {
73
+ editorService.setDirty(activeTab.id, dirty);
74
+ };
75
+
76
+ return () => {
77
+ host.onDidChangeDirty = undefined;
78
+ };
79
+ }, [activeTab, editorService, host]);
80
+
81
+ if (!activeTab || !host) {
82
+ return null;
83
+ }
84
+
85
+ const rendered = host.render();
86
+
87
+ if (isMissingResourceEditorRenderPayload(rendered)) {
88
+ return (
89
+ <MissingResourceEditorSurface
90
+ message={rendered.message}
91
+ resourceUri={rendered.resourceUri}
92
+ tabId={activeTab.id}
93
+ title={host.title ?? activeTab.title}
94
+ />
95
+ );
96
+ }
97
+
98
+ if (isCommandInspectorEditorRenderPayload(rendered)) {
99
+ return (
100
+ <CommandInspectorSurface
101
+ commandId={rendered.commandId}
102
+ resourceUri={rendered.resourceUri}
103
+ tabId={activeTab.id}
104
+ />
105
+ );
106
+ }
107
+
108
+ if (isFieldRemapEditorRenderPayload(rendered)) {
109
+ return (
110
+ <FieldRemapEditorSurface
111
+ resourceUri={rendered.resourceUri}
112
+ surfaceId={rendered.surfaceId}
113
+ tabId={activeTab.id}
114
+ />
115
+ );
116
+ }
117
+
118
+ if (isTextEditorRenderPayload(rendered)) {
119
+ return (
120
+ <TextEditorSurface
121
+ defaultViewModeForResource={defaultViewModeForResource}
122
+ host={host as TextEditorHostLike}
123
+ initialContent={rendered.initialContent}
124
+ modeToolbarHost={modeToolbarHost}
125
+ mimeType={rendered.mimeType}
126
+ onModeToolbarVisibleChange={onModeToolbarVisibleChange}
127
+ resourceUri={rendered.resourceUri}
128
+ tabId={activeTab.id}
129
+ theme={theme}
130
+ viewProviders={viewProviders}
131
+ />
132
+ );
133
+ }
134
+
135
+ return (
136
+ <div
137
+ ref={hostFrameRef}
138
+ aria-label={host.title ?? activeTab.title ?? 'Editor'}
139
+ className="workbench-editor-area__host"
140
+ data-editor-host-id={activeTab.id}
141
+ >
142
+ {toReactNode(rendered)}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ interface TextEditorHostLike extends EditorHost {
148
+ getContent?(): string;
149
+ markDirty?(): void;
150
+ setContent?(content: string): void;
151
+ setDirty?(dirty: boolean): void;
152
+ }
153
+
154
+ interface TextEditorRenderPayload {
155
+ initialContent: string;
156
+ kind: 'workbench-kit.builtin.editor/text';
157
+ mimeType?: string | undefined;
158
+ resourceUri: string;
159
+ }
160
+
161
+ function TextEditorSurface({
162
+ defaultViewModeForResource,
163
+ host,
164
+ initialContent,
165
+ modeToolbarHost,
166
+ mimeType,
167
+ onModeToolbarVisibleChange,
168
+ resourceUri,
169
+ tabId,
170
+ theme,
171
+ viewProviders,
172
+ }: {
173
+ defaultViewModeForResource: ((resourceUri: string) => EditorViewMode | undefined) | undefined;
174
+ host: TextEditorHostLike;
175
+ initialContent: string;
176
+ modeToolbarHost: HTMLDivElement | null;
177
+ mimeType?: string | undefined;
178
+ onModeToolbarVisibleChange: (visible: boolean) => void;
179
+ resourceUri: string;
180
+ tabId: string;
181
+ theme: WorkspaceEditorTheme | undefined;
182
+ viewProviders: readonly EditorDocumentViewProvider[];
183
+ }) {
184
+ const editorService = useEditorService();
185
+ const { executeCommand, workspaceHostPort } = useWorkbench();
186
+ const [content, setContent] = useState(initialContent);
187
+ const previousResourceUriRef = useRef<string | undefined>(undefined);
188
+ const splitSizesRef = useRef<Record<string, number>>({});
189
+ const editorDocument = useMemo(
190
+ () => ({
191
+ content,
192
+ mimeType: mimeType ?? mimeTypeForResource(resourceUri),
193
+ path: pathForResource(resourceUri),
194
+ resourceUri,
195
+ }),
196
+ [content, mimeType, resourceUri],
197
+ );
198
+ const { formProvider, previewProvider } = useMemo(
199
+ () => resolveEditorDocumentViews(editorDocument, viewProviders),
200
+ [editorDocument, viewProviders],
201
+ );
202
+ const formEligible = Boolean(formProvider);
203
+ const previewEligible = Boolean(previewProvider);
204
+ const [panePreference, setPanePreference] = useState<EditorPaneVisibility>(() =>
205
+ resolveDefaultEditorPaneVisibility(defaultViewModeForResource?.(resourceUri), {
206
+ formEligible,
207
+ }),
208
+ );
209
+ const paneVisibility = useMemo(
210
+ () => resolveEffectiveEditorPaneVisibility(panePreference, { formEligible, previewEligible }),
211
+ [formEligible, panePreference, previewEligible],
212
+ );
213
+ const codeIcon = isJsonSourceDocument(editorDocument) ? 'codicon-json' : 'codicon-code';
214
+ const modeToolbarVisible = formEligible || previewEligible;
215
+
216
+ useEffect(() => {
217
+ const resourceChanged = previousResourceUriRef.current !== resourceUri;
218
+ previousResourceUriRef.current = resourceUri;
219
+
220
+ if (resourceChanged) {
221
+ setContent(initialContent);
222
+ setPanePreference(
223
+ resolveDefaultEditorPaneVisibility(defaultViewModeForResource?.(resourceUri), {
224
+ formEligible,
225
+ }),
226
+ );
227
+ return;
228
+ }
229
+
230
+ setContent((current) => (current === initialContent ? current : initialContent));
231
+ }, [defaultViewModeForResource, formEligible, initialContent, resourceUri]);
232
+
233
+ useEffect(() => {
234
+ onModeToolbarVisibleChange(modeToolbarVisible);
235
+ }, [modeToolbarVisible, onModeToolbarVisibleChange]);
236
+
237
+ useEffect(() => {
238
+ return () => {
239
+ onModeToolbarVisibleChange(false);
240
+ };
241
+ }, [onModeToolbarVisibleChange]);
242
+
243
+ const getSplitSize = useCallback((key: string, defaultPercent: number) => {
244
+ return splitSizesRef.current[key] ?? defaultPercent;
245
+ }, []);
246
+
247
+ const setSplitSize = useCallback((key: string, percent: number) => {
248
+ splitSizesRef.current[key] = percent;
249
+ }, []);
250
+
251
+ const handleChange = useCallback(
252
+ (nextContent: string) => {
253
+ setContent(nextContent);
254
+ host.setContent?.(nextContent);
255
+ host.markDirty?.();
256
+ editorService.promotePreviewOnEdit(tabId);
257
+ },
258
+ [editorService, host, tabId],
259
+ );
260
+
261
+ const handleSave = useCallback(
262
+ (nextContent: string) => {
263
+ if (nextContent !== content) {
264
+ handleChange(nextContent);
265
+ }
266
+
267
+ void executeCommand(EDITOR_SAVE_COMMAND_ID);
268
+ },
269
+ [content, executeCommand, handleChange],
270
+ );
271
+
272
+ const sourcePane = (
273
+ <div className="workbench-editor-area__source-pane">
274
+ <Suspense
275
+ fallback={
276
+ <div className="workbench-editor-area__editor-loading" role="status">
277
+ Loading editor...
278
+ </div>
279
+ }
280
+ >
281
+ <WorkspaceEditor
282
+ file={editorDocument}
283
+ showFileBar
284
+ showHeader={false}
285
+ theme={theme}
286
+ value={content}
287
+ onChange={handleChange}
288
+ onSave={workspaceHostPort ? handleSave : undefined}
289
+ />
290
+ </Suspense>
291
+ </div>
292
+ );
293
+
294
+ const previewPane = previewProvider ? (
295
+ <ScrollArea
296
+ aria-label="Preview"
297
+ as="section"
298
+ className="workbench-editor-area__preview-pane"
299
+ orientation="vertical"
300
+ >
301
+ {toReactNode(
302
+ previewProvider.render({ document: editorDocument, onContentChange: handleChange }),
303
+ )}
304
+ </ScrollArea>
305
+ ) : null;
306
+ const formPane = formProvider
307
+ ? toReactNode(formProvider.render({ document: editorDocument, onContentChange: handleChange }))
308
+ : null;
309
+ const paneByKind: Record<EditorPaneKind, ReactNode | null> = {
310
+ code: sourcePane,
311
+ form: formPane,
312
+ preview: previewPane,
313
+ };
314
+ const visiblePaneEntries = getVisibleEditorPaneKinds(paneVisibility)
315
+ .map((kind) => ({ kind, node: paneByKind[kind] }))
316
+ .filter((entry): entry is { kind: EditorPaneKind; node: ReactNode } => entry.node !== null);
317
+ const editorBody = composeEditorPaneLayout(visiblePaneEntries, getSplitSize, setSplitSize);
318
+ const modeToolbar =
319
+ modeToolbarVisible && modeToolbarHost
320
+ ? createPortal(
321
+ <EditorViewModeIconSelect
322
+ codeIcon={codeIcon}
323
+ formEligible={formEligible}
324
+ previewEligible={previewEligible}
325
+ value={paneVisibilityToPrimaryViewMode(paneVisibility)}
326
+ onValueChange={(mode) => {
327
+ setPanePreference(editorViewModeToPaneVisibility(mode));
328
+ }}
329
+ />,
330
+ modeToolbarHost,
331
+ )
332
+ : null;
333
+
334
+ return (
335
+ <>
336
+ {modeToolbar}
337
+ <section
338
+ aria-label={host.title ?? resourceUri}
339
+ className="workbench-editor-area__text-editor"
340
+ data-resource-uri={resourceUri}
341
+ >
342
+ <div className="workbench-editor-area__text-editor-body">{editorBody}</div>
343
+ </section>
344
+ </>
345
+ );
346
+ }
347
+
348
+ function composeEditorPaneLayout(
349
+ panes: readonly { kind: EditorPaneKind; node: ReactNode }[],
350
+ getSplitSize: (key: string, defaultPercent: number) => number,
351
+ onSplitSizeChange: (key: string, percent: number) => void,
352
+ ): ReactNode {
353
+ if (panes.length === 0) {
354
+ return null;
355
+ }
356
+
357
+ if (panes.length === 1) {
358
+ return panes[0]?.node ?? null;
359
+ }
360
+
361
+ const layoutSignature = panes.map((pane) => pane.kind).join('|');
362
+
363
+ return panes.slice(1).reduce<ReactNode>((primary, entry, splitIndex) => {
364
+ const primaryKind = panes[0]?.kind;
365
+ const primaryLabel = primaryKind ? editorPaneKindLabel(primaryKind) : 'Editor pane';
366
+ const splitKey = `${layoutSignature}#${splitIndex}`;
367
+
368
+ return (
369
+ <SplitView
370
+ key={splitKey}
371
+ className="workbench-editor-area__split"
372
+ defaultPrimarySizePercent={50}
373
+ minPrimarySizePercent={25}
374
+ primarySizePercent={getSplitSize(splitKey, 50)}
375
+ onPrimarySizePercentChange={(percent) => {
376
+ onSplitSizeChange(splitKey, percent);
377
+ }}
378
+ primary={
379
+ <section aria-label={primaryLabel} className="workbench-editor-area__split-pane">
380
+ {primary}
381
+ </section>
382
+ }
383
+ secondary={
384
+ <section
385
+ aria-label={editorPaneKindLabel(entry.kind)}
386
+ className="workbench-editor-area__split-pane"
387
+ >
388
+ {entry.node}
389
+ </section>
390
+ }
391
+ />
392
+ );
393
+ }, panes[0]?.node ?? null);
394
+ }
395
+
396
+ function editorPaneKindLabel(kind: EditorPaneKind): string {
397
+ switch (kind) {
398
+ case 'code':
399
+ return 'Code';
400
+ case 'form':
401
+ return 'Form';
402
+ case 'preview':
403
+ return 'Preview';
404
+ }
405
+ }
406
+
407
+ function EditorViewModeIconSelect({
408
+ codeIcon,
409
+ formEligible,
410
+ onValueChange,
411
+ previewEligible,
412
+ value,
413
+ }: {
414
+ codeIcon: string;
415
+ formEligible: boolean;
416
+ onValueChange: (mode: EditorViewMode) => void;
417
+ previewEligible: boolean;
418
+ value: EditorViewMode;
419
+ }) {
420
+ const resolvedValue =
421
+ (value === 'form' && !formEligible) || (value === 'preview' && !previewEligible)
422
+ ? 'code'
423
+ : value;
424
+
425
+ return (
426
+ <div
427
+ aria-label="Editor view mode"
428
+ className="workbench-editor-area__view-toolbar"
429
+ data-testid="editor-view-mode-select"
430
+ role="radiogroup"
431
+ >
432
+ <EditorViewModeIconButton
433
+ checked={resolvedValue === 'code'}
434
+ icon={codeIcon}
435
+ label="Code"
436
+ onSelect={() => {
437
+ onValueChange('code');
438
+ }}
439
+ />
440
+ {formEligible ? (
441
+ <EditorViewModeIconButton
442
+ checked={resolvedValue === 'form'}
443
+ icon="codicon-symbol-field"
444
+ label="Form"
445
+ onSelect={() => {
446
+ onValueChange('form');
447
+ }}
448
+ />
449
+ ) : null}
450
+ {previewEligible ? (
451
+ <EditorViewModeIconButton
452
+ checked={resolvedValue === 'preview'}
453
+ icon="codicon-preview"
454
+ label="Preview"
455
+ onSelect={() => {
456
+ onValueChange('preview');
457
+ }}
458
+ />
459
+ ) : null}
460
+ </div>
461
+ );
462
+ }
463
+
464
+ function EditorViewModeIconButton({
465
+ checked,
466
+ icon,
467
+ label,
468
+ onSelect,
469
+ }: {
470
+ checked: boolean;
471
+ icon: string;
472
+ label: string;
473
+ onSelect: () => void;
474
+ }) {
475
+ return (
476
+ <IconButton
477
+ aria-checked={checked}
478
+ className={[
479
+ 'workbench-editor-area__view-button',
480
+ checked && 'workbench-editor-area__view-button--active',
481
+ ]
482
+ .filter(Boolean)
483
+ .join(' ')}
484
+ compact
485
+ icon={icon}
486
+ label={label}
487
+ role="radio"
488
+ onClick={onSelect}
489
+ />
490
+ );
491
+ }
492
+
493
+ function isJsonSourceDocument(document: { mimeType?: string | undefined; path: string }): boolean {
494
+ const mimeType = document.mimeType?.toLowerCase();
495
+ return document.path.toLowerCase().endsWith('.json') || Boolean(mimeType?.includes('json'));
496
+ }
497
+
498
+ interface CommandInspectorEditorRenderPayload {
499
+ readonly commandId: string;
500
+ readonly kind: 'workbench-kit.builtin.commands/inspector';
501
+ readonly resourceUri: string;
502
+ }
503
+
504
+ function isCommandInspectorEditorRenderPayload(
505
+ value: unknown,
506
+ ): value is CommandInspectorEditorRenderPayload {
507
+ if (typeof value !== 'object' || value === null) {
508
+ return false;
509
+ }
510
+
511
+ const candidate = value as Partial<CommandInspectorEditorRenderPayload>;
512
+ return (
513
+ candidate.kind === 'workbench-kit.builtin.commands/inspector' &&
514
+ typeof candidate.commandId === 'string' &&
515
+ typeof candidate.resourceUri === 'string'
516
+ );
517
+ }
518
+
519
+ interface FieldRemapEditorRenderPayload {
520
+ readonly kind: 'workbench-kit.samples.field-remap/editor';
521
+ readonly resourceUri: string;
522
+ readonly surfaceId: string;
523
+ }
524
+
525
+ function isFieldRemapEditorRenderPayload(value: unknown): value is FieldRemapEditorRenderPayload {
526
+ if (typeof value !== 'object' || value === null) {
527
+ return false;
528
+ }
529
+
530
+ const candidate = value as Partial<FieldRemapEditorRenderPayload>;
531
+ return (
532
+ candidate.kind === 'workbench-kit.samples.field-remap/editor' &&
533
+ typeof candidate.resourceUri === 'string' &&
534
+ typeof candidate.surfaceId === 'string' &&
535
+ candidate.surfaceId.length > 0
536
+ );
537
+ }
538
+
539
+ function isTextEditorRenderPayload(value: unknown): value is TextEditorRenderPayload {
540
+ if (typeof value !== 'object' || value === null) {
541
+ return false;
542
+ }
543
+
544
+ const candidate = value as Partial<TextEditorRenderPayload>;
545
+ return (
546
+ candidate.kind === 'workbench-kit.builtin.editor/text' &&
547
+ typeof candidate.resourceUri === 'string' &&
548
+ typeof candidate.initialContent === 'string'
549
+ );
550
+ }
551
+
552
+ interface MissingResourceEditorRenderPayload {
553
+ readonly kind: 'workbench-kit.builtin.editor/missing-resource';
554
+ readonly message: string;
555
+ readonly resourceUri: string;
556
+ }
557
+
558
+ function isMissingResourceEditorRenderPayload(
559
+ value: unknown,
560
+ ): value is MissingResourceEditorRenderPayload {
561
+ if (typeof value !== 'object' || value === null) {
562
+ return false;
563
+ }
564
+
565
+ const candidate = value as Partial<MissingResourceEditorRenderPayload>;
566
+ return (
567
+ candidate.kind === 'workbench-kit.builtin.editor/missing-resource' &&
568
+ typeof candidate.resourceUri === 'string' &&
569
+ typeof candidate.message === 'string'
570
+ );
571
+ }
572
+
573
+ function MissingResourceEditorSurface({
574
+ message,
575
+ resourceUri,
576
+ tabId,
577
+ title,
578
+ }: {
579
+ message: string;
580
+ resourceUri: string;
581
+ tabId: string;
582
+ title?: string | undefined;
583
+ }) {
584
+ const editorService = useEditorService();
585
+ const path = pathForResource(resourceUri);
586
+
587
+ return (
588
+ <section
589
+ aria-label={title ?? path}
590
+ className="workbench-editor-area__missing-resource"
591
+ data-resource-uri={resourceUri}
592
+ >
593
+ <div className="workbench-editor-area__missing-resource-body" role="alert">
594
+ <p className="workbench-editor-area__missing-resource-title">Unable to open editor</p>
595
+ <p className="workbench-editor-area__missing-resource-message">{message}</p>
596
+ <p className="workbench-editor-area__missing-resource-path">{path}</p>
597
+ <button
598
+ className="workbench-editor-area__missing-resource-close"
599
+ type="button"
600
+ onClick={() => {
601
+ editorService.closeEditor(tabId);
602
+ }}
603
+ >
604
+ Close tab
605
+ </button>
606
+ </div>
607
+ </section>
608
+ );
609
+ }
610
+
611
+ function toReactNode(value: unknown): ReactNode {
612
+ if (
613
+ value === undefined ||
614
+ value === null ||
615
+ typeof value === 'boolean' ||
616
+ typeof value === 'symbol' ||
617
+ typeof value === 'function'
618
+ ) {
619
+ return null;
620
+ }
621
+
622
+ if (typeof value === 'string' || typeof value === 'number' || isValidElement(value)) {
623
+ return value;
624
+ }
625
+
626
+ if (Array.isArray(value)) {
627
+ return value as ReactNode;
628
+ }
629
+
630
+ return null;
631
+ }