diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index f84314f..7129b70 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -201,11 +201,20 @@ var hotReload = loader.EnableHotReload( - 打开 raw YAML 文件 - 打开匹配的 schema 文件 - 对嵌套对象中的必填字段、未知字段、基础标量类型、标量数组和对象数组元素做轻量校验 -- 对嵌套对象字段、顶层标量字段和顶层标量数组提供轻量表单入口 +- 对嵌套对象字段、对象数组、顶层标量字段和顶层标量数组提供轻量表单入口 - 对同一配置域内的多份 YAML 文件执行批量字段更新 - 在表单和批量编辑入口中显示 `title / description / default / enum / ref-table` 元数据 -当前表单入口适合编辑嵌套对象中的标量字段和标量数组;对象数组仍建议放在 raw YAML 中完成。 +当前表单入口适合编辑嵌套对象中的标量字段、标量数组,以及对象数组中的对象项。 + +对象数组编辑器当前支持: + +- 新增和删除对象项 +- 编辑对象项中的标量字段 +- 编辑对象项中的标量数组 +- 编辑对象项中的嵌套对象字段 + +如果对象数组项内部继续包含对象数组,当前仍建议回退到 raw YAML 完成。 当前批量编辑入口仍刻意限制在“同域文件统一改动顶层标量字段和顶层标量数组”,避免复杂结构批量写回时破坏人工维护的 YAML 排版。 @@ -214,7 +223,7 @@ var hotReload = loader.EnableHotReload( 以下能力尚未完全完成: - 更完整的 JSON Schema 支持 -- VS Code 中对象数组的安全表单编辑器 +- VS Code 中更深层对象数组嵌套的安全表单编辑器 - 更强的复杂数组与更深 schema 关键字支持 因此,现阶段更适合作为你游戏项目的“受控试点配表系统”,而不是完全无约束的大规模内容生产平台。 diff --git a/tools/vscode-config-extension/README.md b/tools/vscode-config-extension/README.md index 96c56ab..d30aa0d 100644 --- a/tools/vscode-config-extension/README.md +++ b/tools/vscode-config-extension/README.md @@ -9,7 +9,7 @@ Minimal VS Code extension scaffold for the GFramework AI-First config workflow. - Open matching schema files from `schemas/` - Run lightweight schema validation for nested required fields, unknown nested fields, scalar types, scalar arrays, and arrays of objects -- Open a lightweight form preview for nested object fields, top-level scalar fields, and scalar arrays +- Open a lightweight form preview for nested object fields, object arrays, top-level scalar fields, and scalar arrays - Batch edit one config domain across multiple files for top-level scalar and scalar-array fields - Surface schema metadata such as `title`, `description`, `default`, `enum`, and `x-gframework-ref-table` in the lightweight editors @@ -25,8 +25,6 @@ The extension currently validates the repository's minimal config-schema subset: - arrays of objects whose items use the same supported subset recursively - scalar `enum` constraints and scalar-array item `enum` constraints -Object-array editing should still be reviewed in raw YAML. - ## Local Testing ```bash @@ -38,7 +36,7 @@ node --test ./test/*.test.js - Multi-root workspaces use the first workspace folder - Validation only covers a minimal subset of JSON Schema -- Form preview supports nested objects and scalar arrays, but object arrays remain raw-YAML-only for edits +- Form preview supports object-array editing, but nested object arrays inside array items still fall back to raw YAML - Batch editing remains limited to top-level scalar fields and top-level scalar arrays ## Workspace Settings diff --git a/tools/vscode-config-extension/src/configValidation.js b/tools/vscode-config-extension/src/configValidation.js index 320e242..301e3d6 100644 --- a/tools/vscode-config-extension/src/configValidation.js +++ b/tools/vscode-config-extension/src/configValidation.js @@ -150,13 +150,14 @@ function isScalarCompatible(expectedType, scalarValue) { * from the parsed structure so nested object edits can be saved safely. * * @param {string} originalYaml Original YAML content. - * @param {{scalars?: Record, arrays?: Record}} updates Updated form values. + * @param {{scalars?: Record, arrays?: Record, objectArrays?: Record>>}} updates Updated form values. * @returns {string} Updated YAML content. */ function applyFormUpdates(originalYaml, updates) { const root = normalizeRootNode(parseTopLevelYaml(originalYaml)); const scalarUpdates = updates.scalars || {}; const arrayUpdates = updates.arrays || {}; + const objectArrayUpdates = updates.objectArrays || {}; for (const [path, value] of Object.entries(scalarUpdates)) { setNodeAtPath(root, path.split("."), createScalarNode(String(value))); @@ -167,6 +168,11 @@ function applyFormUpdates(originalYaml, updates) { (values || []).map((item) => createScalarNode(String(item))))); } + for (const [path, items] of Object.entries(objectArrayUpdates)) { + setNodeAtPath(root, path.split("."), createArrayNode( + (items || []).map((item) => createNodeFromFormValue(item)))); + } + return renderYaml(root).join("\n"); } @@ -687,6 +693,11 @@ function renderObjectNode(node, indent) { continue; } + if (entry.node.kind === "array" && entry.node.items.length === 0) { + lines.push(`${" ".repeat(indent)}${entry.key}: []`); + continue; + } + lines.push(`${" ".repeat(indent)}${entry.key}:`); lines.push(...renderYaml(entry.node, indent + 2)); } @@ -736,6 +747,32 @@ function createArrayNode(items) { return {kind: "array", items}; } +/** + * Convert one structured form value back into a YAML node tree. + * Object-array editors submit plain JavaScript objects so the writer can + * rebuild the full array deterministically instead of patching item paths + * one by one. + * + * @param {unknown} value Structured form value. + * @returns {YamlNode} YAML node. + */ +function createNodeFromFormValue(value) { + if (Array.isArray(value)) { + return createArrayNode(value.map((item) => createNodeFromFormValue(item))); + } + + if (value && typeof value === "object") { + const objectNode = createObjectNode(); + for (const [key, childValue] of Object.entries(value)) { + setObjectEntry(objectNode, key, createNodeFromFormValue(childValue)); + } + + return objectNode; + } + + return createScalarNode(String(value ?? "")); +} + /** * Create an object node. * diff --git a/tools/vscode-config-extension/src/extension.js b/tools/vscode-config-extension/src/extension.js index 32d66a4..22fceb8 100644 --- a/tools/vscode-config-extension/src/extension.js +++ b/tools/vscode-config-extension/src/extension.js @@ -13,8 +13,8 @@ const { /** * Activate the GFramework config extension. - * The initial MVP focuses on workspace file navigation, lightweight validation, - * and a small form-preview entry for top-level scalar values. + * The current tool focuses on workspace file navigation, lightweight + * validation, and a schema-aware form preview for common editing workflows. * * @param {vscode.ExtensionContext} context Extension context. */ @@ -253,8 +253,8 @@ async function openSchemaFile(item) { /** * Open a lightweight form preview for schema-bound config fields. - * The preview now walks nested object structures recursively, while complex - * object-array editing still falls back to raw YAML for safety. + * The preview walks nested object structures recursively and now supports + * object-array editing for the repository's supported schema subset. * * @param {ConfigTreeItem | { resourceUri?: vscode.Uri }} item Tree item. * @param {vscode.DiagnosticCollection} diagnostics Diagnostic collection. @@ -287,7 +287,8 @@ async function openFormPreview(item, diagnostics) { const latestYamlText = await fs.promises.readFile(configUri.fsPath, "utf8"); const updatedYaml = applyFormUpdates(latestYamlText, { scalars: message.scalars || {}, - arrays: parseArrayFieldPayload(message.arrays || {}) + arrays: parseArrayFieldPayload(message.arrays || {}), + objectArrays: message.objectArrays || {} }); await fs.promises.writeFile(configUri.fsPath, updatedYaml, "utf8"); const document = await vscode.workspace.openTextDocument(configUri); @@ -570,54 +571,7 @@ async function loadSchemaInfoForConfig(configUri, workspaceRoot) { function renderFormHtml(fileName, schemaInfo, parsedYaml) { const formModel = buildFormModel(schemaInfo, parsedYaml); const renderedFields = formModel.fields - .map((field) => { - if (field.kind === "section") { - return ` -
-
${escapeHtml(field.label)} ${field.required ? "required" : ""}
-
${escapeHtml(field.path)}
- ${field.description ? `${escapeHtml(field.description)}` : ""} -
- `; - } - - if (field.kind === "array") { - const itemType = field.itemType - ? `array<${escapeHtml(field.itemType)}>` - : "array"; - return ` - - `; - } - - const enumValues = Array.isArray(field.schema.enumValues) ? field.schema.enumValues : []; - const inputControl = enumValues.length > 0 - ? ` - - ` - : ``; - - return ` - - `; - }) + .map((field) => renderFormField(field)) .join("\n"); const unsupportedFields = formModel.unsupported @@ -664,6 +618,10 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) { padding: 8px 12px; cursor: pointer; } + .secondary-button { + background: transparent; + color: var(--vscode-button-foreground); + } .meta { margin-bottom: 16px; color: var(--vscode-descriptionForeground); @@ -734,6 +692,34 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) { margin-bottom: 10px; color: var(--vscode-descriptionForeground); } + .object-array { + margin-bottom: 18px; + padding: 12px; + border: 1px solid var(--vscode-panel-border, transparent); + border-radius: 6px; + } + .object-array-items { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 12px; + } + .object-array-item { + padding: 12px; + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 6px; + background: color-mix(in srgb, var(--vscode-editor-background) 88%, var(--vscode-panel-border, transparent)); + } + .object-array-item-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 8px; + } + .object-array-item-title { + font-weight: 700; + } .depth-1 { margin-left: 12px; } @@ -743,6 +729,12 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) { .depth-3 { margin-left: 36px; } + .depth-4 { + margin-left: 48px; + } + .depth-5 { + margin-left: 60px; + } @@ -757,16 +749,96 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) {
${emptyState}