From 5b8099cd98357d4a14d9495133a29afd1abdd1a4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:18:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=B7=BB=E5=8A=A0=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E5=86=85=E5=AE=B9=E9=85=8D=E7=BD=AE=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=92=8CVS=20Code=E6=8F=92=E4=BB=B6=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现基于YAML的配置源文件和JSON Schema结构验证 - 提供运行时只读查询和Source Generator代码生成 - 添加VS Code插件实现配置浏览、编辑和轻量校验功能 - 支持开发期热重载和跨表引用校验 - 实现批量编辑和表单预览功能 --- docs/zh-CN/game/config-system.md | 3 +- tools/vscode-config-extension/README.md | 3 +- tools/vscode-config-extension/package.json | 10 ++ .../src/configValidation.js | 67 +++++++ .../vscode-config-extension/src/extension.js | 163 ++++++++++++++++++ .../test/configValidation.test.js | 39 +++++ 6 files changed, 283 insertions(+), 2 deletions(-) diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index f327480..e0950e1 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -169,8 +169,9 @@ var hotReload = loader.EnableHotReload( - 打开匹配的 schema 文件 - 对必填字段、未知顶层字段、基础标量类型和标量数组元素做轻量校验 - 对顶层标量字段和顶层标量数组提供轻量表单入口 +- 对同一配置域内的多份 YAML 文件执行批量字段更新 -当前仍建议把复杂数组、嵌套对象和批量修改放在 raw YAML 中完成。 +当前批量编辑入口适合对同域文件统一改动顶层标量字段和顶层标量数组;复杂数组、嵌套对象仍建议放在 raw YAML 中完成。 ## 当前限制 diff --git a/tools/vscode-config-extension/README.md b/tools/vscode-config-extension/README.md index c1208e3..eb29ce9 100644 --- a/tools/vscode-config-extension/README.md +++ b/tools/vscode-config-extension/README.md @@ -9,6 +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 required fields, unknown top-level fields, scalar types, and scalar array items - Open a lightweight form preview for top-level scalar fields and top-level scalar arrays +- Batch edit one config domain across multiple files for top-level scalar and scalar-array fields ## Validation Coverage @@ -32,7 +33,7 @@ node --test ./test/*.test.js - Multi-root workspaces use the first workspace folder - Validation only covers a minimal subset of JSON Schema -- Form editing currently supports top-level scalar fields and top-level scalar arrays +- Form and batch editing currently support top-level scalar fields and top-level scalar arrays - Nested objects and complex arrays should still be edited in raw YAML ## Workspace Settings diff --git a/tools/vscode-config-extension/package.json b/tools/vscode-config-extension/package.json index 33b7832..4ff3cf8 100644 --- a/tools/vscode-config-extension/package.json +++ b/tools/vscode-config-extension/package.json @@ -17,6 +17,7 @@ "onCommand:gframeworkConfig.openRaw", "onCommand:gframeworkConfig.openSchema", "onCommand:gframeworkConfig.openFormPreview", + "onCommand:gframeworkConfig.batchEditDomain", "onCommand:gframeworkConfig.validateAll" ], "main": "./src/extension.js", @@ -49,6 +50,10 @@ "command": "gframeworkConfig.openFormPreview", "title": "GFramework Config: Open Form Preview" }, + { + "command": "gframeworkConfig.batchEditDomain", + "title": "GFramework Config: Batch Edit Domain" + }, { "command": "gframeworkConfig.validateAll", "title": "GFramework Config: Validate All" @@ -82,6 +87,11 @@ "command": "gframeworkConfig.openFormPreview", "when": "view == gframeworkConfigExplorer && viewItem == gframeworkConfigFile", "group": "navigation" + }, + { + "command": "gframeworkConfig.batchEditDomain", + "when": "view == gframeworkConfigExplorer && viewItem == domain", + "group": "navigation" } ] }, diff --git a/tools/vscode-config-extension/src/configValidation.js b/tools/vscode-config-extension/src/configValidation.js index 6182210..36255b7 100644 --- a/tools/vscode-config-extension/src/configValidation.js +++ b/tools/vscode-config-extension/src/configValidation.js @@ -41,6 +41,44 @@ function parseSchemaContent(content) { }; } +/** + * Collect top-level schema fields that the current tooling can edit in bulk. + * The bulk editor intentionally stays aligned with the lightweight form editor: + * top-level scalars and scalar arrays are supported, while nested objects and + * complex array items remain raw-YAML-only. + * + * @param {{required: string[], properties: Record}} schemaInfo Parsed schema info. + * @returns {Array<{key: string, type: string, itemType?: string, inputKind: "scalar" | "array", required: boolean}>} Editable field descriptors. + */ +function getEditableSchemaFields(schemaInfo) { + const editableFields = []; + const requiredSet = new Set(Array.isArray(schemaInfo.required) ? schemaInfo.required : []); + + for (const [key, property] of Object.entries(schemaInfo.properties || {})) { + if (isEditableScalarType(property.type)) { + editableFields.push({ + key, + type: property.type, + inputKind: "scalar", + required: requiredSet.has(key) + }); + continue; + } + + if (property.type === "array" && isEditableScalarType(property.itemType || "")) { + editableFields.push({ + key, + type: property.type, + itemType: property.itemType, + inputKind: "array", + required: requiredSet.has(key) + }); + } + } + + return editableFields.sort((left, right) => left.key.localeCompare(right.key)); +} + /** * Parse a minimal top-level YAML structure for config validation and form * preview. This parser intentionally focuses on the repository's current @@ -203,6 +241,20 @@ function validateParsedConfig(schemaInfo, parsedYaml) { return diagnostics; } +/** + * Determine whether the current schema type can be edited through the + * lightweight form or batch-edit tooling. + * + * @param {string} schemaType Schema type. + * @returns {boolean} True when the type is supported by the lightweight editors. + */ +function isEditableScalarType(schemaType) { + return schemaType === "string" || + schemaType === "integer" || + schemaType === "number" || + schemaType === "boolean"; +} + /** * Determine whether a scalar value matches a minimal schema type. * @@ -308,6 +360,19 @@ function applyScalarUpdates(originalYaml, updates) { return applyFormUpdates(originalYaml, {scalars: updates}); } +/** + * Parse the batch editor's comma-separated array input. + * + * @param {string} value Raw input value. + * @returns {string[]} Parsed array items. + */ +function parseBatchArrayValue(value) { + return String(value) + .split(",") + .map((item) => item.trim()) + .filter((item) => item.length > 0); +} + /** * Format a scalar value for YAML output. * @@ -455,7 +520,9 @@ module.exports = { applyScalarUpdates, findTopLevelBlocks, formatYamlScalar, + getEditableSchemaFields, isScalarCompatible, + parseBatchArrayValue, parseSchemaContent, parseTopLevelYaml, unquoteScalar, diff --git a/tools/vscode-config-extension/src/extension.js b/tools/vscode-config-extension/src/extension.js index 49ad2c2..6984048 100644 --- a/tools/vscode-config-extension/src/extension.js +++ b/tools/vscode-config-extension/src/extension.js @@ -3,6 +3,8 @@ const path = require("path"); const vscode = require("vscode"); const { applyFormUpdates, + getEditableSchemaFields, + parseBatchArrayValue, parseSchemaContent, parseTopLevelYaml, unquoteScalar, @@ -36,6 +38,9 @@ function activate(context) { vscode.commands.registerCommand("gframeworkConfig.openFormPreview", async (item) => { await openFormPreview(item, diagnostics); }), + vscode.commands.registerCommand("gframeworkConfig.batchEditDomain", async (item) => { + await openBatchEdit(item, diagnostics, provider); + }), vscode.commands.registerCommand("gframeworkConfig.validateAll", async () => { await validateAllConfigs(diagnostics); }), @@ -364,6 +369,142 @@ async function validateConfigFile(configUri, diagnostics) { diagnostics.set(configUri, fileDiagnostics); } +/** + * Open a minimal batch editor for one config domain. + * The workflow intentionally focuses on one schema-bound directory at a time + * so designers can apply the same top-level scalar or scalar-array values + * across multiple files without dropping down to repetitive raw-YAML edits. + * + * @param {ConfigTreeItem | { kind?: string, resourceUri?: vscode.Uri }} item Tree item. + * @param {vscode.DiagnosticCollection} diagnostics Diagnostic collection. + * @param {ConfigTreeDataProvider} provider Tree provider. + * @returns {Promise} Async task. + */ +async function openBatchEdit(item, diagnostics, provider) { + const workspaceRoot = getWorkspaceRoot(); + const domainUri = item && item.resourceUri; + if (!workspaceRoot || !domainUri || item.kind !== "domain") { + return; + } + + const fileItems = fs.readdirSync(domainUri.fsPath, {withFileTypes: true}) + .filter((entry) => entry.isFile() && isYamlPath(entry.name)) + .sort((left, right) => left.name.localeCompare(right.name)) + .map((entry) => { + const fileUri = vscode.Uri.joinPath(domainUri, entry.name); + return { + label: entry.name, + description: path.relative(workspaceRoot.uri.fsPath, fileUri.fsPath), + fileUri, + picked: true + }; + }); + + if (fileItems.length === 0) { + void vscode.window.showWarningMessage("No YAML config files were found in the selected domain."); + return; + } + + const selectedFiles = await vscode.window.showQuickPick(fileItems, { + canPickMany: true, + title: `Batch Edit: ${path.basename(domainUri.fsPath)}`, + placeHolder: "Select the config files to update." + }); + if (!selectedFiles || selectedFiles.length === 0) { + return; + } + + const schemaInfo = await loadSchemaInfoForConfig(selectedFiles[0].fileUri, workspaceRoot); + if (!schemaInfo.exists) { + void vscode.window.showWarningMessage("Batch edit requires a matching schema file for the selected domain."); + return; + } + + const editableFields = getEditableSchemaFields(schemaInfo); + if (editableFields.length === 0) { + void vscode.window.showWarningMessage( + "No top-level scalar or scalar-array fields were found in the matching schema."); + return; + } + + const selectedFields = await vscode.window.showQuickPick( + editableFields.map((field) => ({ + label: field.key, + description: field.inputKind === "array" + ? `array<${field.itemType}>` + : field.type, + detail: field.required ? "required" : undefined, + field + })), + { + canPickMany: true, + title: `Batch Edit Fields: ${path.basename(domainUri.fsPath)}`, + placeHolder: "Select the fields to apply across the chosen files." + }); + if (!selectedFields || selectedFields.length === 0) { + return; + } + + const updates = { + scalars: {}, + arrays: {} + }; + + for (const selectedField of selectedFields) { + const field = selectedField.field; + const rawValue = await promptBatchFieldValue(field); + if (rawValue === undefined) { + return; + } + + if (field.inputKind === "array") { + updates.arrays[field.key] = parseBatchArrayValue(rawValue); + continue; + } + + updates.scalars[field.key] = rawValue; + } + + const edit = new vscode.WorkspaceEdit(); + const touchedDocuments = []; + let changedFileCount = 0; + + for (const fileItem of selectedFiles) { + const document = await vscode.workspace.openTextDocument(fileItem.fileUri); + const originalYaml = document.getText(); + const updatedYaml = applyFormUpdates(originalYaml, updates); + if (updatedYaml === originalYaml) { + continue; + } + + const fullRange = new vscode.Range( + document.positionAt(0), + document.positionAt(originalYaml.length)); + edit.replace(fileItem.fileUri, fullRange, updatedYaml); + touchedDocuments.push(document); + changedFileCount += 1; + } + + if (changedFileCount === 0) { + void vscode.window.showInformationMessage("Batch edit did not change any selected config files."); + return; + } + + const applied = await vscode.workspace.applyEdit(edit); + if (!applied) { + throw new Error("VS Code rejected the batch edit workspace update."); + } + + for (const document of touchedDocuments) { + await document.save(); + await validateConfigFile(document.uri, diagnostics); + } + + provider.refresh(); + void vscode.window.showInformationMessage( + `Batch updated ${changedFileCount} config file(s) in '${path.basename(domainUri.fsPath)}'.`); +} + /** * Load schema info for a config file. * @@ -582,6 +723,28 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) { `; } +/** + * Prompt for one batch-edit field value. + * + * @param {{key: string, type: string, itemType?: string, inputKind: "scalar" | "array", required: boolean}} field Editable field descriptor. + * @returns {Promise} User input, or undefined when cancelled. + */ +async function promptBatchFieldValue(field) { + if (field.inputKind === "array") { + return vscode.window.showInputBox({ + title: `Batch Edit Array: ${field.key}`, + prompt: `Enter comma-separated items for '${field.key}' (expected array<${field.itemType}>). Leave empty to clear the array.`, + ignoreFocusOut: true + }); + } + + return vscode.window.showInputBox({ + title: `Batch Edit Field: ${field.key}`, + prompt: `Enter the new value for '${field.key}' (expected ${field.type}).`, + ignoreFocusOut: true + }); +} + /** * Enumerate all YAML files recursively. * diff --git a/tools/vscode-config-extension/test/configValidation.test.js b/tools/vscode-config-extension/test/configValidation.test.js index c13b58d..60ce73e 100644 --- a/tools/vscode-config-extension/test/configValidation.test.js +++ b/tools/vscode-config-extension/test/configValidation.test.js @@ -3,6 +3,8 @@ const assert = require("node:assert/strict"); const { applyFormUpdates, applyScalarUpdates, + getEditableSchemaFields, + parseBatchArrayValue, parseSchemaContent, parseTopLevelYaml, validateParsedConfig @@ -138,3 +140,40 @@ test("applyFormUpdates should replace top-level scalar arrays and preserve unrel assert.match(updated, /^reward:$/mu); assert.match(updated, /^ gold: 10$/mu); }); + +test("getEditableSchemaFields should expose only scalar and scalar-array properties", () => { + const schema = parseSchemaContent(` + { + "type": "object", + "required": ["id", "dropItems"], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "reward": { "type": "object" }, + "dropItems": { + "type": "array", + "items": { "type": "string" } + }, + "waypoints": { + "type": "array", + "items": { "type": "object" } + } + } + } + `); + + assert.deepEqual(getEditableSchemaFields(schema), [ + {key: "dropItems", type: "array", itemType: "string", inputKind: "array", required: true}, + {key: "id", type: "integer", inputKind: "scalar", required: true}, + {key: "name", type: "string", inputKind: "scalar", required: false} + ]); +}); + +test("parseBatchArrayValue should split comma-separated items and drop empty segments", () => { + assert.deepEqual(parseBatchArrayValue(" potion, hi potion , ,bomb "), [ + "potion", + "hi potion", + "bomb" + ]); + assert.deepEqual(parseBatchArrayValue(""), []); +});