@moloodev/editor-sdk 0.0.0-stage → 0.1.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.
Files changed (110) hide show
  1. package/package.json +62 -3
  2. package/src/channel.ts +287 -0
  3. package/src/chrome-manifest.ts +111 -0
  4. package/src/commands/clipboard/router.ts +99 -0
  5. package/src/commands/clipboard/types.ts +20 -0
  6. package/src/commands/history/stack.ts +53 -0
  7. package/src/commands/icons/catalog.ts +172 -0
  8. package/src/commands/index.ts +100 -0
  9. package/src/commands/keys/display.ts +16 -0
  10. package/src/commands/keys/normalize.ts +55 -0
  11. package/src/commands/keys/reserved.ts +62 -0
  12. package/src/commands/keys/validate.ts +77 -0
  13. package/src/commands/manifest/configuration.ts +59 -0
  14. package/src/commands/manifest/contribution-manifest-schema.ts +56 -0
  15. package/src/commands/manifest/editor-document.ts +501 -0
  16. package/src/commands/manifest/ids.ts +2 -0
  17. package/src/commands/manifest/merge.ts +215 -0
  18. package/src/commands/manifest/types.ts +113 -0
  19. package/src/commands/manifest/validate-manifest.ts +45 -0
  20. package/src/commands/react/context.tsx +249 -0
  21. package/src/commands/react/hotkey-defaults.ts +11 -0
  22. package/src/commands/react/scope-context.tsx +31 -0
  23. package/src/commands/react/scope-provider.tsx +123 -0
  24. package/src/commands/react/store.ts +182 -0
  25. package/src/commands/registry/create.ts +55 -0
  26. package/src/commands/registry/types.ts +12 -0
  27. package/src/commands/when/evaluate.ts +80 -0
  28. package/src/commands/when/index.ts +19 -0
  29. package/src/commands/when/parse.ts +276 -0
  30. package/src/commands/when/types.ts +24 -0
  31. package/src/commands/when/validate.ts +20 -0
  32. package/src/crdt/awareness.ts +29 -0
  33. package/src/crdt/bytes.ts +26 -0
  34. package/src/crdt/connect.ts +285 -0
  35. package/src/crdt/diff.ts +86 -0
  36. package/src/crdt/doc.ts +67 -0
  37. package/src/crdt/front-matter.ts +45 -0
  38. package/src/crdt/index.ts +76 -0
  39. package/src/crdt/persist.ts +40 -0
  40. package/src/crdt/protocol.ts +119 -0
  41. package/src/crdt/shared.ts +104 -0
  42. package/src/crdt/text-body.ts +125 -0
  43. package/src/crdt/undo.ts +59 -0
  44. package/src/crdt/vite-plugin.ts +42 -0
  45. package/src/crdt/yjs.ts +16 -0
  46. package/src/editor-common.ts +160 -0
  47. package/src/editor-id.ts +56 -0
  48. package/src/frame-attributes.ts +9 -0
  49. package/src/index.ts +229 -0
  50. package/src/json-schema.ts +13 -0
  51. package/src/layout/bind-toolbar.ts +60 -0
  52. package/src/layout/catalog.ts +82 -0
  53. package/src/layout/chrome/command-affordances.ts +47 -0
  54. package/src/layout/chrome/command-layout-state.ts +42 -0
  55. package/src/layout/chrome/editor-menu.tsx +358 -0
  56. package/src/layout/chrome/editor-shortcut-keys.tsx +27 -0
  57. package/src/layout/chrome/menu-styles.ts +3 -0
  58. package/src/layout/chrome/mid-menu-nodes.ts +27 -0
  59. package/src/layout/chrome/use-editor-command-affordances.ts +33 -0
  60. package/src/layout/chrome/visible-menu-leaves.ts +15 -0
  61. package/src/layout/content-error-notice.tsx +31 -0
  62. package/src/layout/define-editor-layout.ts +60 -0
  63. package/src/layout/define-editor-layout.types.ts +57 -0
  64. package/src/layout/doc-undo-bridge.tsx +22 -0
  65. package/src/layout/dock-zones.tsx +61 -0
  66. package/src/layout/drop.ts +39 -0
  67. package/src/layout/editor-layout.tsx +287 -0
  68. package/src/layout/editor-loading.tsx +23 -0
  69. package/src/layout/frame-layout-binding.tsx +74 -0
  70. package/src/layout/frame.ts +23 -0
  71. package/src/layout/hide.ts +103 -0
  72. package/src/layout/index.ts +14 -0
  73. package/src/layout/layout-binding.tsx +42 -0
  74. package/src/layout/layout-storage-key.ts +6 -0
  75. package/src/layout/layout-values.ts +48 -0
  76. package/src/layout/mutate.ts +486 -0
  77. package/src/layout/nesting-context.tsx +20 -0
  78. package/src/layout/panel-header-action.tsx +29 -0
  79. package/src/layout/panel-header.tsx +168 -0
  80. package/src/layout/panel-hide-menu.tsx +6 -0
  81. package/src/layout/panel-menu.tsx +85 -0
  82. package/src/layout/panel-section.tsx +114 -0
  83. package/src/layout/panel-size.ts +94 -0
  84. package/src/layout/panel-title.tsx +5 -0
  85. package/src/layout/registry.tsx +333 -0
  86. package/src/layout/restore-panel.ts +26 -0
  87. package/src/layout/slots-context.tsx +189 -0
  88. package/src/layout/tab-button.tsx +60 -0
  89. package/src/layout/toolbar-scroll.ts +22 -0
  90. package/src/layout/toolbar.tsx +168 -0
  91. package/src/layout/types.ts +41 -0
  92. package/src/layout/use-collapsed-sections.ts +50 -0
  93. package/src/layout/validate-layout-spec.ts +143 -0
  94. package/src/message-samples.ts +377 -0
  95. package/src/origin-headers.ts +57 -0
  96. package/src/print/index.ts +10 -0
  97. package/src/print/inspect.ts +16 -0
  98. package/src/print/schema.ts +122 -0
  99. package/src/protocol-entry.ts +19 -0
  100. package/src/protocol.ts +471 -0
  101. package/src/render-result.ts +25 -0
  102. package/src/view/editor-chrome.ts +28 -0
  103. package/src/view/hooks.ts +260 -0
  104. package/src/view/mount-editor-view.ts +36 -0
  105. package/src/view/port-binding.ts +127 -0
  106. package/src/view/port-commands.ts +36 -0
  107. package/src/view/port.ts +35 -0
  108. package/src/view/theme-class.ts +11 -0
  109. package/src/view.ts +313 -0
  110. package/README.md +0 -3
@@ -0,0 +1,172 @@
1
+ import type { ComponentType } from "react";
2
+ import { parseTablerIconUri } from "../manifest/editor-document";
3
+ import {
4
+ IconArrowBackUp,
5
+ IconArrowForwardUp,
6
+ IconBold,
7
+ IconClick,
8
+ IconClipboard,
9
+ IconCode,
10
+ IconColumnInsertLeft,
11
+ IconColumnInsertRight,
12
+ IconCopy,
13
+ IconDownload,
14
+ IconEraser,
15
+ IconFileImport,
16
+ IconFilePlus,
17
+ IconFileText,
18
+ IconFolderOpen,
19
+ IconH1,
20
+ IconH2,
21
+ IconH3,
22
+ IconHighlight,
23
+ IconHandMove,
24
+ IconHierarchy,
25
+ IconLayout,
26
+ IconHistory,
27
+ IconItalic,
28
+ IconLayersSubtract,
29
+ IconLayoutDashboard,
30
+ IconLayoutGrid,
31
+ IconLink,
32
+ IconList,
33
+ IconListCheck,
34
+ IconListNumbers,
35
+ IconLock,
36
+ IconLockOpen,
37
+ IconMarkdown,
38
+ IconMaximize,
39
+ IconMenu2,
40
+ IconMessageCircle,
41
+ IconMessages,
42
+ IconMinus,
43
+ IconMusic,
44
+ IconPackage,
45
+ IconPencil,
46
+ IconPhoto,
47
+ IconPlus,
48
+ IconPointer,
49
+ IconQuote,
50
+ IconRestore,
51
+ IconRowInsertBottom,
52
+ IconRowInsertTop,
53
+ IconScissors,
54
+ IconShape,
55
+ IconSparkles,
56
+ IconStrikethrough,
57
+ IconTable,
58
+ IconTimelineEvent,
59
+ IconTrash,
60
+ IconWorld,
61
+ IconWorldOff,
62
+ IconX,
63
+ IconZoomIn,
64
+ IconZoomOut,
65
+ type IconProps,
66
+ } from "@tabler/icons-react";
67
+
68
+ export type EditorIconComponent = ComponentType<IconProps>;
69
+
70
+ /** Host-resolved Tabler icon names. */
71
+ export const EDITOR_ICON_CATALOG = {
72
+ menu: IconMenu2,
73
+ "arrow-back-up": IconArrowBackUp,
74
+ restore: IconRestore,
75
+ "arrow-forward-up": IconArrowForwardUp,
76
+ scissors: IconScissors,
77
+ copy: IconCopy,
78
+ clipboard: IconClipboard,
79
+ "file-plus": IconFilePlus,
80
+ "file-text": IconFileText,
81
+ plus: IconPlus,
82
+ "folder-open": IconFolderOpen,
83
+ "file-import": IconFileImport,
84
+ download: IconDownload,
85
+ trash: IconTrash,
86
+ x: IconX,
87
+ history: IconHistory,
88
+ lock: IconLock,
89
+ "lock-open": IconLockOpen,
90
+ pointer: IconPointer,
91
+ "hand-move": IconHandMove,
92
+ click: IconClick,
93
+ "layers-subtract": IconLayersSubtract,
94
+ maximize: IconMaximize,
95
+ "zoom-in": IconZoomIn,
96
+ "zoom-out": IconZoomOut,
97
+ hierarchy: IconHierarchy,
98
+ layout: IconLayout,
99
+ table: IconTable,
100
+ "row-insert-top": IconRowInsertTop,
101
+ "row-insert-bottom": IconRowInsertBottom,
102
+ "column-insert-left": IconColumnInsertLeft,
103
+ "column-insert-right": IconColumnInsertRight,
104
+ eraser: IconEraser,
105
+ markdown: IconMarkdown,
106
+ bold: IconBold,
107
+ italic: IconItalic,
108
+ strikethrough: IconStrikethrough,
109
+ highlight: IconHighlight,
110
+ heading: IconH1,
111
+ heading2: IconH2,
112
+ heading3: IconH3,
113
+ list: IconList,
114
+ "list-numbers": IconListNumbers,
115
+ "list-check": IconListCheck,
116
+ quote: IconQuote,
117
+ code: IconCode,
118
+ link: IconLink,
119
+ photo: IconPhoto,
120
+ minus: IconMinus,
121
+ shape: IconShape,
122
+ "layout-grid": IconLayoutGrid,
123
+ "timeline-event": IconTimelineEvent,
124
+ music: IconMusic,
125
+ pencil: IconPencil,
126
+ messages: IconMessages,
127
+ sparkles: IconSparkles,
128
+ "message-circle": IconMessageCircle,
129
+ "layout-dashboard": IconLayoutDashboard,
130
+ package: IconPackage,
131
+ world: IconWorld,
132
+ "world-off": IconWorldOff,
133
+ } as const satisfies Record<string, EditorIconComponent>;
134
+
135
+ export type EditorIconName = keyof typeof EDITOR_ICON_CATALOG;
136
+
137
+ const COMMAND_ICON_FALLBACK: Partial<Record<string, EditorIconName>> = {
138
+ "editor.undo": "arrow-back-up",
139
+ "editor.redo": "arrow-forward-up",
140
+ "editor.cut": "scissors",
141
+ "editor.copy": "copy",
142
+ "editor.paste": "clipboard",
143
+ "editor.delete": "trash",
144
+ "editor.new": "file-plus",
145
+ "editor.open": "folder-open",
146
+ "editor.import": "file-import",
147
+ "editor.download": "download",
148
+ "bundle.add": "plus",
149
+ "bundle.restore": "restore",
150
+ "bundle.delete": "x",
151
+ "bundle.access": "world",
152
+ "bundle.confirm": "package",
153
+ };
154
+
155
+ function isEditorIconName(name: string): name is EditorIconName {
156
+ return Object.hasOwn(EDITOR_ICON_CATALOG, name);
157
+ }
158
+
159
+ export function resolveEditorIcon(
160
+ name?: string,
161
+ commandId?: string,
162
+ ): EditorIconComponent | null {
163
+ const parsed = name ? parseTablerIconUri(name) : null;
164
+ if (parsed && isEditorIconName(parsed)) {
165
+ return EDITOR_ICON_CATALOG[parsed];
166
+ }
167
+ if (commandId) {
168
+ const fallback = COMMAND_ICON_FALLBACK[commandId];
169
+ if (fallback) return EDITOR_ICON_CATALOG[fallback];
170
+ }
171
+ return null;
172
+ }
@@ -0,0 +1,100 @@
1
+ export type {
2
+ CommandDefinition,
3
+ CommandHandler,
4
+ CommandId,
5
+ } from "./registry/types";
6
+ export {
7
+ createCommandRegistry,
8
+ type CommandRegistry,
9
+ } from "./registry/create";
10
+
11
+ export type {
12
+ ConfigurationGroup,
13
+ ConfigurationProperty,
14
+ ContributionManifest,
15
+ KeybindingContribution,
16
+ ManifestActivation,
17
+ MenuContribution,
18
+ MenuNode,
19
+ MergedConfiguration,
20
+ MergedContributions,
21
+ } from "./manifest/types";
22
+ export { contributionManifestSchema } from "./manifest/contribution-manifest-schema";
23
+ export { validateManifest } from "./manifest/validate-manifest";
24
+ export {
25
+ DEFAULT_MENU_GROUP,
26
+ mergeActiveContributions,
27
+ mergeManifests,
28
+ selectManifests,
29
+ } from "./manifest/merge";
30
+ export {
31
+ mergeActiveConfiguration,
32
+ mergeConfiguration,
33
+ } from "./manifest/configuration";
34
+ export { SHELL_SERVICE_MANIFEST_ID } from "./manifest/ids";
35
+
36
+ export {
37
+ normalizeKey,
38
+ keyToKbdTokens,
39
+ } from "./keys/normalize";
40
+ export { displayShortcutsByCommand } from "./keys/display";
41
+ export {
42
+ BROWSER_RESERVED_KEYS,
43
+ CLIPBOARD_FORBIDDEN_KEYS,
44
+ isBrowserReservedKey,
45
+ isClipboardForbiddenKey,
46
+ } from "./keys/reserved";
47
+ export {
48
+ validateKeybindings,
49
+ claimKeybindings,
50
+ type KeybindingConflict,
51
+ } from "./keys/validate";
52
+
53
+ export {
54
+ createUndoStack,
55
+ type UndoCommand,
56
+ type UndoStack,
57
+ } from "./history/stack";
58
+
59
+ export {
60
+ createClipboardRouter,
61
+ type ClipboardRouter,
62
+ } from "./clipboard/router";
63
+ export type {
64
+ ClipboardAdapter,
65
+ ClipboardPayload,
66
+ ClipboardSelection,
67
+ } from "./clipboard/types";
68
+
69
+ export {
70
+ CommandsProvider,
71
+ useCommands,
72
+ useMergedConfiguration,
73
+ useMergedContributions,
74
+ type CommandsProviderProps,
75
+ } from "./react/context";
76
+ export {
77
+ ScopeProvider,
78
+ useOptionalScope,
79
+ useScope,
80
+ type ScopeContextValue,
81
+ type ScopeProviderProps,
82
+ } from "./react/scope-provider";
83
+ export { commandsHotkeyDefaults } from "./react/hotkey-defaults";
84
+
85
+ export {
86
+ parseWhen,
87
+ evaluateWhen,
88
+ validateWhenClause,
89
+ isWhenEnabled,
90
+ WhenParseError,
91
+ type ContextKeys,
92
+ type ContextValue,
93
+ type WhenExpr,
94
+ type WhenValidationIssue,
95
+ } from "./when";
96
+
97
+ export { HotkeysProvider, useHotkeyRegistrations } from "@tanstack/react-hotkeys";
98
+
99
+ export { defineEditorManifest, parseTablerIconUri, type EditorManifest, type EditorManifestInput, type EditorTemplate } from "./manifest/editor-document";
100
+ export { EDITOR_ICON_CATALOG, resolveEditorIcon, type EditorIconComponent, type EditorIconName } from "./icons/catalog";
@@ -0,0 +1,16 @@
1
+ import type { CommandId } from "../registry/types";
2
+
3
+ /** Display chord per command: registered keybindings win over menu-only hints. */
4
+ export function displayShortcutsByCommand(
5
+ keybindings: readonly { commandId: CommandId; key: string }[],
6
+ menus: readonly { commandId: CommandId; shortcut?: string }[],
7
+ ): Map<CommandId, string> {
8
+ const map = new Map<CommandId, string>();
9
+ for (const menu of menus) {
10
+ if (menu.shortcut) map.set(menu.commandId, menu.shortcut);
11
+ }
12
+ for (const binding of keybindings) {
13
+ map.set(binding.commandId, binding.key);
14
+ }
15
+ return map;
16
+ }
@@ -0,0 +1,55 @@
1
+ /** Normalize Mod-style chords for comparison (case-insensitive, sorted mods). */
2
+ export function normalizeKey(key: string): string {
3
+ const parts = key
4
+ .split("+")
5
+ .map((part) => part.trim())
6
+ .filter(Boolean);
7
+ if (parts.length === 0) {
8
+ return "";
9
+ }
10
+ const mods = new Set<string>();
11
+ let main = "";
12
+ for (const part of parts) {
13
+ const lower = part.toLowerCase();
14
+ if (
15
+ lower === "mod" ||
16
+ lower === "meta" ||
17
+ lower === "cmd" ||
18
+ lower === "command" ||
19
+ lower === "ctrl" ||
20
+ lower === "control" ||
21
+ lower === "alt" ||
22
+ lower === "option" ||
23
+ lower === "shift"
24
+ ) {
25
+ if (lower === "meta" || lower === "cmd" || lower === "command") {
26
+ mods.add("mod");
27
+ } else if (lower === "control") {
28
+ mods.add("ctrl");
29
+ } else if (lower === "option") {
30
+ mods.add("alt");
31
+ } else {
32
+ mods.add(lower === "mod" ? "mod" : lower);
33
+ }
34
+ } else {
35
+ main = lower.length === 1 ? lower.toUpperCase() : lower;
36
+ }
37
+ }
38
+ const orderedMods = ["mod", "ctrl", "alt", "shift"].filter((m) =>
39
+ mods.has(m),
40
+ );
41
+ return [...orderedMods, main].filter(Boolean).join("+");
42
+ }
43
+
44
+ /** Convert `Mod+K` to UI kbd tokens like `["cmd","K"]`. */
45
+ export function keyToKbdTokens(key: string): string[] {
46
+ const normalized = normalizeKey(key);
47
+ if (!normalized) return [];
48
+ return normalized.split("+").map((part) => {
49
+ if (part === "mod") return "cmd";
50
+ if (part === "ctrl") return "ctrl";
51
+ if (part === "alt") return "alt";
52
+ if (part === "shift") return "shift";
53
+ return part;
54
+ });
55
+ }
@@ -0,0 +1,62 @@
1
+ import { normalizeKey } from "./normalize";
2
+
3
+ /**
4
+ * Browser / OS escape-hatch chords. Nothing in moloo may register these
5
+ * as command keybindings.
6
+ */
7
+ const BROWSER_RESERVED_RAW = [
8
+ // Zoom (a11y)
9
+ "Mod+=",
10
+ "Mod+-",
11
+ "Mod+0",
12
+ "Mod+Plus",
13
+ "Mod+Minus",
14
+ // Address bar / chrome focus
15
+ "Mod+L",
16
+ "F6",
17
+ "Alt+D",
18
+ // Tabs / windows
19
+ "Mod+W",
20
+ "Mod+Shift+W",
21
+ "Mod+T",
22
+ "Mod+N",
23
+ "Mod+Shift+T",
24
+ "Mod+1",
25
+ "Mod+2",
26
+ "Mod+3",
27
+ "Mod+4",
28
+ "Mod+5",
29
+ "Mod+6",
30
+ "Mod+7",
31
+ "Mod+8",
32
+ "Mod+9",
33
+ // Reload
34
+ "Mod+R",
35
+ "Mod+Shift+R",
36
+ "F5",
37
+ // Quit
38
+ "Mod+Q",
39
+ // Open local file (browser)
40
+ "Mod+O",
41
+ // Save page / some browser sidebar toggles — auto-save policy; leave alone
42
+ "Mod+S",
43
+ ] as const;
44
+
45
+ /** Clipboard chords — not keybindings; handled via native clipboard events. */
46
+ const CLIPBOARD_FORBIDDEN_RAW = ["Mod+C", "Mod+X", "Mod+V"] as const;
47
+
48
+ export const BROWSER_RESERVED_KEYS = new Set(
49
+ BROWSER_RESERVED_RAW.map(normalizeKey),
50
+ );
51
+
52
+ export const CLIPBOARD_FORBIDDEN_KEYS = new Set(
53
+ CLIPBOARD_FORBIDDEN_RAW.map(normalizeKey),
54
+ );
55
+
56
+ export function isBrowserReservedKey(key: string): boolean {
57
+ return BROWSER_RESERVED_KEYS.has(normalizeKey(key));
58
+ }
59
+
60
+ export function isClipboardForbiddenKey(key: string): boolean {
61
+ return CLIPBOARD_FORBIDDEN_KEYS.has(normalizeKey(key));
62
+ }
@@ -0,0 +1,77 @@
1
+ import type { KeybindingContribution } from "../manifest/types";
2
+ import { normalizeKey } from "./normalize";
3
+ import {
4
+ isBrowserReservedKey,
5
+ isClipboardForbiddenKey,
6
+ } from "./reserved";
7
+
8
+ export type KeybindingConflict = {
9
+ key: string;
10
+ commandId: string;
11
+ reason: string;
12
+ };
13
+
14
+ /**
15
+ * Validate keybindings against browser reserved, clipboard-forbidden,
16
+ * and duplicate chords. Higher-layer bindings (already claimed) cannot
17
+ * be overridden by later manifests.
18
+ */
19
+ export function validateKeybindings(
20
+ keybindings: KeybindingContribution[],
21
+ claimed: ReadonlyMap<string, string> = new Map(),
22
+ ): { ok: true } | { ok: false; conflicts: KeybindingConflict[] } {
23
+ const conflicts: KeybindingConflict[] = [];
24
+ const local = new Map<string, string>();
25
+
26
+ for (const binding of keybindings) {
27
+ const key = normalizeKey(binding.key);
28
+ if (!key) {
29
+ conflicts.push({
30
+ key: binding.key,
31
+ commandId: binding.commandId,
32
+ reason: "Empty keybinding",
33
+ });
34
+ continue;
35
+ }
36
+ if (isBrowserReservedKey(key)) {
37
+ conflicts.push({
38
+ key: binding.key,
39
+ commandId: binding.commandId,
40
+ reason: "Browser-reserved key",
41
+ });
42
+ continue;
43
+ }
44
+ if (isClipboardForbiddenKey(key)) {
45
+ conflicts.push({
46
+ key: binding.key,
47
+ commandId: binding.commandId,
48
+ reason: "Clipboard keys use native events, not keybindings",
49
+ });
50
+ continue;
51
+ }
52
+ const prior = claimed.get(key) ?? local.get(key);
53
+ if (prior && prior !== binding.commandId) {
54
+ conflicts.push({
55
+ key: binding.key,
56
+ commandId: binding.commandId,
57
+ reason: `Key already bound to "${prior}"`,
58
+ });
59
+ continue;
60
+ }
61
+ local.set(key, binding.commandId);
62
+ }
63
+
64
+ if (conflicts.length > 0) {
65
+ return { ok: false, conflicts };
66
+ }
67
+ return { ok: true };
68
+ }
69
+
70
+ export function claimKeybindings(
71
+ keybindings: KeybindingContribution[],
72
+ claimed: Map<string, string>,
73
+ ): void {
74
+ for (const binding of keybindings) {
75
+ claimed.set(normalizeKey(binding.key), binding.commandId);
76
+ }
77
+ }
@@ -0,0 +1,59 @@
1
+ import type {
2
+ ConfigurationGroup,
3
+ ConfigurationProperty,
4
+ ContributionManifest,
5
+ MergedConfiguration,
6
+ } from "./types";
7
+ import { selectManifests } from "./merge";
8
+
9
+ /**
10
+ * Merge configuration groups from selected manifests.
11
+ * Property keys must be globally unique across all groups.
12
+ */
13
+ export function mergeConfiguration(
14
+ manifests: readonly ContributionManifest[],
15
+ ): MergedConfiguration {
16
+ const groups: ConfigurationGroup[] = [];
17
+ const properties = new Map<string, ConfigurationProperty>();
18
+ const defaults: Record<string, boolean | number | string> = {};
19
+ const seenKeys = new Map<string, string>();
20
+
21
+ for (const manifest of manifests) {
22
+ for (const group of manifest.configuration?.groups ?? []) {
23
+ const nextProperties: Record<string, ConfigurationProperty> = {};
24
+ for (const [key, property] of Object.entries(group.properties)) {
25
+ const prior = seenKeys.get(key);
26
+ if (prior) {
27
+ throw new Error(
28
+ `Configuration property "${key}" redeclared by "${manifest.id}" (already from "${prior}")`,
29
+ );
30
+ }
31
+ seenKeys.set(key, manifest.id);
32
+ nextProperties[key] = property;
33
+ properties.set(key, property);
34
+ defaults[key] = property.default;
35
+ }
36
+ groups.push({
37
+ id: group.id,
38
+ title: group.title,
39
+ order: group.order,
40
+ properties: nextProperties,
41
+ });
42
+ }
43
+ }
44
+
45
+ groups.sort(
46
+ (a, b) =>
47
+ (a.order ?? 0) - (b.order ?? 0) || a.title.localeCompare(b.title),
48
+ );
49
+
50
+ return { groups, defaults, properties };
51
+ }
52
+
53
+ /** Configuration for the active scope (any + exact matches). */
54
+ export function mergeActiveConfiguration(
55
+ manifests: readonly ContributionManifest[],
56
+ activeScopeId: string | null,
57
+ ): MergedConfiguration {
58
+ return mergeConfiguration(selectManifests(manifests, activeScopeId));
59
+ }
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+
3
+ const menuContributionSchema = z.object({
4
+ path: z.array(z.string()).min(1),
5
+ commandId: z.string().min(1),
6
+ order: z.number().optional(),
7
+ group: z.string().optional(),
8
+ label: z.string().optional(),
9
+ shortcut: z.string().optional(),
10
+ when: z.string().min(1).optional(),
11
+ });
12
+
13
+ const keybindingContributionSchema = z.object({
14
+ commandId: z.string().min(1),
15
+ key: z.string().min(1),
16
+ });
17
+
18
+ const configurationPropertySchema = z.object({
19
+ type: z.enum(["boolean", "number", "string"]),
20
+ default: z.union([z.boolean(), z.number(), z.string()]),
21
+ title: z.string(),
22
+ description: z.string().optional(),
23
+ enum: z.array(z.string()).optional(),
24
+ minimum: z.number().optional(),
25
+ maximum: z.number().optional(),
26
+ order: z.number().optional(),
27
+ });
28
+
29
+ const configurationGroupSchema = z.object({
30
+ id: z.string().min(1),
31
+ title: z.string(),
32
+ order: z.number().optional(),
33
+ properties: z.record(z.string(), configurationPropertySchema),
34
+ });
35
+
36
+ export const contributionManifestSchema = z.object({
37
+ id: z.string().min(1),
38
+ activation: z.enum(["always", "any", "exact"]).optional(),
39
+ commands: z
40
+ .array(
41
+ z.object({
42
+ id: z.string().min(1),
43
+ title: z.string(),
44
+ group: z.string().optional(),
45
+ when: z.string().min(1).optional(),
46
+ }),
47
+ )
48
+ .optional(),
49
+ menus: z.array(menuContributionSchema).optional(),
50
+ keybindings: z.array(keybindingContributionSchema).optional(),
51
+ configuration: z
52
+ .object({
53
+ groups: z.array(configurationGroupSchema),
54
+ })
55
+ .optional(),
56
+ });