From c87e51053ce2f3eacd147f829cfd131dd58e6e28 Mon Sep 17 00:00:00 2001 From: IDK Date: Thu, 27 Nov 2025 13:32:01 +0300 Subject: [PATCH] Initial empty commit --- .eslintrc.json | 14 + .gitignore | 6 + .prettierignore | 2 + .prettierrc | 7 + package-lock.json | 1747 +++++++++++++++++ package.json | 68 + scripts/build_webview.js | 26 + scripts/copy_python_files.js | 42 + src/extension.ts | 142 ++ src/generator/CodeGenerator.ts | 116 ++ src/generator/eventHelpers.ts | 74 + src/generator/types.ts | 28 + src/generator/utils.ts | 49 + src/generator/widgetHelpers.ts | 73 + src/parser/CodeParser.ts | 137 ++ .../tkinter_ast_parser.cpython-313.pyc | Bin 0 -> 42386 bytes src/parser/astConverter.ts | 127 ++ src/parser/pythonRunner.ts | 80 + src/parser/tk_ast/__init__.py | 3 + src/parser/tk_ast/analyzer/__init__.py | 1 + src/parser/tk_ast/analyzer/base.py | 109 + src/parser/tk_ast/analyzer/calls.py | 44 + src/parser/tk_ast/analyzer/connections.py | 93 + src/parser/tk_ast/analyzer/context.py | 13 + src/parser/tk_ast/analyzer/events.py | 77 + src/parser/tk_ast/analyzer/extractors.py | 28 + src/parser/tk_ast/analyzer/imports.py | 14 + src/parser/tk_ast/analyzer/placements.py | 39 + src/parser/tk_ast/analyzer/values.py | 149 ++ src/parser/tk_ast/analyzer/widget_creation.py | 96 + src/parser/tk_ast/grid_layout.py | 55 + src/parser/tk_ast/parser.py | 54 + src/parser/tkinter_ast_parser.py | 21 + src/parser/utils.ts | 21 + src/webview/TkinterDesignerProvider.ts | 237 +++ src/webview/preview.html | 13 + src/webview/react/App.tsx | 27 + src/webview/react/components/Canvas.tsx | 95 + src/webview/react/components/EventsPanel.tsx | 130 ++ src/webview/react/components/Palette.tsx | 37 + .../react/components/PropertiesPanel.tsx | 112 ++ src/webview/react/components/Toolbar.tsx | 162 ++ src/webview/react/index.tsx | 25 + src/webview/react/state.tsx | 242 +++ src/webview/react/types.ts | 32 + src/webview/react/useMessaging.ts | 35 + src/webview/style.css | 593 ++++++ tsconfig.json | 15 + 48 files changed, 5310 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/build_webview.js create mode 100644 scripts/copy_python_files.js create mode 100644 src/extension.ts create mode 100644 src/generator/CodeGenerator.ts create mode 100644 src/generator/eventHelpers.ts create mode 100644 src/generator/types.ts create mode 100644 src/generator/utils.ts create mode 100644 src/generator/widgetHelpers.ts create mode 100644 src/parser/CodeParser.ts create mode 100644 src/parser/__pycache__/tkinter_ast_parser.cpython-313.pyc create mode 100644 src/parser/astConverter.ts create mode 100644 src/parser/pythonRunner.ts create mode 100644 src/parser/tk_ast/__init__.py create mode 100644 src/parser/tk_ast/analyzer/__init__.py create mode 100644 src/parser/tk_ast/analyzer/base.py create mode 100644 src/parser/tk_ast/analyzer/calls.py create mode 100644 src/parser/tk_ast/analyzer/connections.py create mode 100644 src/parser/tk_ast/analyzer/context.py create mode 100644 src/parser/tk_ast/analyzer/events.py create mode 100644 src/parser/tk_ast/analyzer/extractors.py create mode 100644 src/parser/tk_ast/analyzer/imports.py create mode 100644 src/parser/tk_ast/analyzer/placements.py create mode 100644 src/parser/tk_ast/analyzer/values.py create mode 100644 src/parser/tk_ast/analyzer/widget_creation.py create mode 100644 src/parser/tk_ast/grid_layout.py create mode 100644 src/parser/tk_ast/parser.py create mode 100644 src/parser/tkinter_ast_parser.py create mode 100644 src/parser/utils.ts create mode 100644 src/webview/TkinterDesignerProvider.ts create mode 100644 src/webview/preview.html create mode 100644 src/webview/react/App.tsx create mode 100644 src/webview/react/components/Canvas.tsx create mode 100644 src/webview/react/components/EventsPanel.tsx create mode 100644 src/webview/react/components/Palette.tsx create mode 100644 src/webview/react/components/PropertiesPanel.tsx create mode 100644 src/webview/react/components/Toolbar.tsx create mode 100644 src/webview/react/index.tsx create mode 100644 src/webview/react/state.tsx create mode 100644 src/webview/react/types.ts create mode 100644 src/webview/react/useMessaging.ts create mode 100644 src/webview/style.css create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..f5e0e66 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { "ecmaVersion": 2020, "sourceType": "module" }, + "plugins": ["@typescript-eslint", "prettier"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "rules": { + "prettier/prettier": ["error", { "useTabs": false, "tabWidth": 4 }] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a2ea14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode/ +out/ +docs/ +node_modules/ +README.md +examples/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..07d2252 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +out \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f545925 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "useTabs": false, + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ec9970e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1747 @@ +{ + "name": "tkinter-designer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tkinter-designer", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "16.x", + "@types/react": "^18.3.26", + "@types/react-dom": "^18.3.7", + "@types/vscode": "^1.74.0", + "esbuild": "^0.21.5", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "prettier": "^3.6.2", + "typescript": "^4.9.5" + }, + "engines": { + "vscode": "^1.74.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.103.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.103.0.tgz", + "integrity": "sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..881b592 --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "tkinter-designer", + "displayName": "Tkinter Visual Designer", + "description": "Visual drag-and-drop designer for Python tkinter GUI applications", + "version": "0.1.0", + "engines": { + "vscode": "^1.74.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:tkinter-designer.openDesigner" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "tkinter-designer.openDesigner", + "title": "Open Tkinter Designer", + "category": "Tkinter" + }, + { + "command": "tkinter-designer.generateCode", + "title": "Generate Python Code", + "category": "Tkinter" + }, + { + "command": "tkinter-designer.parseCode", + "title": "Parse Tkinter Code", + "category": "Tkinter" + } + ], + "menus": { + "explorer/context": [ + { + "command": "tkinter-designer.openDesigner", + "when": "resourceExtname == .py", + "group": "navigation" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./ && npm run copy-python-files && npm run build:webview", + "copy-python-files": "node scripts/copy_python_files.js", + "build:webview": "node scripts/build_webview.js", + "watch": "tsc -watch -p ./", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\"" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "16.x", + "@types/react": "^18.3.26", + "@types/react-dom": "^18.3.7", + "@types/vscode": "^1.74.0", + "esbuild": "^0.21.5", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "prettier": "^3.6.2", + "typescript": "^4.9.5" + } +} diff --git a/scripts/build_webview.js b/scripts/build_webview.js new file mode 100644 index 0000000..8ef32ee --- /dev/null +++ b/scripts/build_webview.js @@ -0,0 +1,26 @@ +/* eslint-disable */ +const esbuild = require('esbuild'); +const path = require('path'); + +async function build() { + const entry = path.resolve(__dirname, '../src/webview/react/index.tsx'); + const outFile = path.resolve(__dirname, '../out/webview/react-webview.js'); + try { + await esbuild.build({ + entryPoints: [entry], + outfile: outFile, + bundle: true, + platform: 'browser', + format: 'iife', + sourcemap: true, + minify: false, + loader: { '.ts': 'ts', '.tsx': 'tsx' }, + }); + console.log('Built React webview to', outFile); + } catch (err) { + console.error('Failed to build React webview:', err); + process.exit(1); + } +} + +build(); diff --git a/scripts/copy_python_files.js b/scripts/copy_python_files.js new file mode 100644 index 0000000..b15b3f5 --- /dev/null +++ b/scripts/copy_python_files.js @@ -0,0 +1,42 @@ +const fs = require('fs'); +const path = require('path'); + +function copyFile(src, dst) { + fs.mkdirSync(path.dirname(dst), { recursive: true }); + fs.copyFileSync(src, dst); +} + +function copyDir(src, dst) { + if (!fs.existsSync(src)) return; + fs.mkdirSync(dst, { recursive: true }); + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const dstPath = path.join(dst, entry.name); + if (entry.isDirectory()) { + copyDir(srcPath, dstPath); + } else { + fs.copyFileSync(srcPath, dstPath); + } + } +} + +function main() { + const projectRoot = process.cwd(); + const srcParserPath = path.join(projectRoot, 'src', 'parser'); + const outParserPath = path.join(projectRoot, 'out', 'parser'); + + copyFile( + path.join(srcParserPath, 'tkinter_ast_parser.py'), + path.join(outParserPath, 'tkinter_ast_parser.py') + ); + + copyDir( + path.join(srcParserPath, 'tk_ast'), + path.join(outParserPath, 'tk_ast') + ); + + console.log('Copied Python files to out/parser.'); +} + +main(); diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..0519eeb --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,142 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { CodeGenerator } from './generator/CodeGenerator'; +import { CodeParser } from './parser/CodeParser'; +import { TkinterDesignerProvider } from './webview/TkinterDesignerProvider'; + +export function activate(context: vscode.ExtensionContext) { + const provider = new TkinterDesignerProvider(context.extensionUri); + + TkinterDesignerProvider._instance = provider; + + const openDesignerCommand = vscode.commands.registerCommand( + 'tkinter-designer.openDesigner', + () => { + TkinterDesignerProvider.createOrShow(context.extensionUri); + } + ); + + const generateCodeCommand = vscode.commands.registerCommand( + 'tkinter-designer.generateCode', + async () => { + console.log('[GenerateCode] Command invoked'); + const generator = new CodeGenerator(); + const designData = await provider.getDesignData(); + if ( + !designData || + !designData.widgets || + designData.widgets.length === 0 + ) { + console.log('[GenerateCode] No design data'); + vscode.window.showWarningMessage( + 'No design data found. Please open the designer and create some widgets first.' + ); + return; + } + + const pythonCode = generator.generateTkinterCode(designData); + const activeEditor = vscode.window.activeTextEditor; + + if (activeEditor && activeEditor.document.languageId === 'python') { + console.log('[GenerateCode] Writing into active Python file'); + const doc = activeEditor.document; + const start = new vscode.Position(0, 0); + const end = doc.lineCount + ? doc.lineAt(doc.lineCount - 1).range.end + : start; + const fullRange = new vscode.Range(start, end); + await activeEditor.edit((editBuilder) => { + editBuilder.replace(fullRange, pythonCode); + }); + await doc.save(); + vscode.window.showInformationMessage( + 'Python code generated into the active file' + ); + } else { + console.log('[GenerateCode] Creating new Python file'); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage( + 'No workspace folder is open. Please open a folder first.' + ); + return; + } + const fileName = `app_${Date.now()}.py`; + const filePath = path.join( + workspaceFolder.uri.fsPath, + fileName + ); + const fileUri = vscode.Uri.file(filePath); + const encoder = new TextEncoder(); + const fileBytes = encoder.encode(pythonCode); + await vscode.workspace.fs.writeFile(fileUri, fileBytes); + const doc = await vscode.workspace.openTextDocument(fileUri); + await vscode.window.showTextDocument(doc, { preview: false }); + vscode.window.showInformationMessage( + `Python file created: ${fileName}` + ); + } + console.log('[GenerateCode] Done'); + } + ); + + const parseCodeCommand = vscode.commands.registerCommand( + 'tkinter-designer.parseCode', + async () => { + console.log('[ParseCode] Command invoked'); + const activeEditor = vscode.window.activeTextEditor; + + if (activeEditor && activeEditor.document.languageId === 'python') { + const parser = new CodeParser(); + const code = activeEditor.document.getText(); + console.log('[ParseCode] Code length:', code.length); + + try { + const designData = await parser.parseCodeToDesign(code); + if ( + designData && + designData.widgets && + designData.widgets.length > 0 + ) { + console.log('[ParseCode] Widgets found:', designData.widgets.length); + const designerInstance = + TkinterDesignerProvider.createOrShow( + context.extensionUri + ); + if (designerInstance) { + designerInstance.loadDesignData(designData); + } else { + } + + vscode.window.showInformationMessage( + `Code parsed successfully! Found ${designData.widgets.length} widgets.` + ); + } else { + console.log('[ParseCode] No widgets found'); + vscode.window.showWarningMessage( + 'No tkinter widgets found in the code. Make sure your code contains tkinter widget creation statements like tk.Label(), tk.Button(), etc.' + ); + } + } catch (error) { + console.error('[ParseCode] Error:', error); + vscode.window.showErrorMessage( + `Error parsing code: ${error}` + ); + } + } else { + console.log('[ParseCode] No active Python editor'); + vscode.window.showErrorMessage( + 'Please open a Python file with tkinter code' + ); + } + } + ); + + context.subscriptions.push( + openDesignerCommand, + generateCodeCommand, + parseCodeCommand + ); +} + +export function deactivate() {} diff --git a/src/generator/CodeGenerator.ts b/src/generator/CodeGenerator.ts new file mode 100644 index 0000000..014cfbc --- /dev/null +++ b/src/generator/CodeGenerator.ts @@ -0,0 +1,116 @@ +import { DesignData, WidgetData } from './types'; +import { + getVariableName, + generateVariableNames, + getWidgetTypeForGeneration, + indentText, +} from './utils'; +import { + getWidgetParameters, + getPlaceParameters, + generateWidgetContent, +} from './widgetHelpers'; +import { generateEventHandlers, getWidgetEventBindings } from './eventHelpers'; + +export class CodeGenerator { + private indentLevel = 0; + private designData: DesignData | null = null; + + public generateTkinterCode(designData: DesignData): string { + this.designData = designData; + console.log('[Generator] Start, widgets:', designData.widgets.length, 'events:', designData.events?.length || 0); + const lines: string[] = []; + const nameMap = generateVariableNames(designData.widgets); + + lines.push('import tkinter as tk'); + lines.push(''); + + lines.push('class Application:'); + this.indentLevel = 1; + + lines.push(this.indent('def __init__(self):')); + this.indentLevel = 2; + lines.push(this.indent('self.root = tk.Tk()')); + lines.push(this.indent(`self.root.title("${designData.form.title}")`)); + lines.push( + this.indent( + `self.root.geometry("${designData.form.size.width}x${designData.form.size.height}")` + ) + ); + lines.push(this.indent('self.create_widgets()')); + lines.push(''); + + this.indentLevel = 1; + lines.push(this.indent('def create_widgets(self):')); + this.indentLevel = 2; + + designData.widgets.forEach((widget) => { + console.log('[Generator] Widget:', widget.id, widget.type); + lines.push(...this.generateWidgetCode(widget, nameMap)); + lines.push(''); + }); + + this.indentLevel = 1; + lines.push(this.indent('def run(self):')); + this.indentLevel = 2; + lines.push(this.indent('self.root.mainloop()')); + lines.push(''); + + const hasEvents = designData.events && designData.events.length > 0; + + if (hasEvents) { + console.log('[Generator] Generating event handlers'); + lines.push( + ...generateEventHandlers( + designData, + (text) => indentText(1, text), + (text) => indentText(2, text) + ) + ); + } + + this.indentLevel = 0; + lines.push('if __name__ == "__main__":'); + this.indentLevel = 1; + lines.push(this.indent('app = Application()')); + lines.push(this.indent('app.run()')); + + return lines.join('\n'); + } + + private generateWidgetCode( + widget: WidgetData, + nameMap: Map + ): string[] { + const lines: string[] = []; + const varName = getVariableName(widget, nameMap); + + const widgetType = getWidgetTypeForGeneration(widget.type); + lines.push( + this.indent( + `self.${varName} = ${widgetType}(self.root${getWidgetParameters(widget)})` + ) + ); + + const placeParams = getPlaceParameters(widget); + lines.push(this.indent(`self.${varName}.place(${placeParams})`)); + + const contentLines = generateWidgetContent(widget, varName); + contentLines.forEach((line) => lines.push(this.indent(line))); + + lines.push( + ...getWidgetEventBindings( + this.designData, + widget, + varName, + (text) => this.indent(text) + ) + ); + + return lines; + } + + private indent(text: string): string { + return indentText(this.indentLevel, text); + } +} diff --git a/src/generator/eventHelpers.ts b/src/generator/eventHelpers.ts new file mode 100644 index 0000000..fc10496 --- /dev/null +++ b/src/generator/eventHelpers.ts @@ -0,0 +1,74 @@ +import { DesignData, Event, WidgetData } from './types'; + +export function generateEventHandlers( + designData: DesignData, + indentDef: (text: string) => string, + indentBody: (text: string) => string +): string[] { + const lines: string[] = []; + + if (designData.events && designData.events.length > 0) { + designData.events.forEach((event: Event) => { + const handlerName = event.name; + const isBindEvent = + event.type.startsWith('<') && event.type.endsWith('>'); + + let hasCode = false; + const widget = (designData.widgets || []).find( + (w) => w.id === event.widget + ); + if (isBindEvent) { + lines.push(indentDef(`def ${handlerName}(self, event):`)); + } else { + lines.push(indentDef(`def ${handlerName}(self):`)); + } + + const codeLines = (event.code || '').split('\n'); + for (const line of codeLines) { + if (line.trim()) { + lines.push(indentBody(line)); + hasCode = true; + } + } + + if (!hasCode) { + lines.push(indentBody('pass')); + } + }); + } + + return lines; +} + +export function getWidgetEventBindings( + designData: DesignData | null, + widget: WidgetData, + varName: string, + indentLine: (text: string) => string +): string[] { + const lines: string[] = []; + + if (designData && designData.events) { + const widgetEvents = designData.events.filter( + (event) => event.widget === widget.id + ); + + widgetEvents.forEach((event) => { + if (event.type === 'command') { + lines.push( + indentLine( + `self.${varName}.config(command=self.${event.name})` + ) + ); + } else { + lines.push( + indentLine( + `self.${varName}.bind("${event.type}", self.${event.name})` + ) + ); + } + }); + } + + return lines; +} diff --git a/src/generator/types.ts b/src/generator/types.ts new file mode 100644 index 0000000..3f67540 --- /dev/null +++ b/src/generator/types.ts @@ -0,0 +1,28 @@ +export interface Event { + widget: string; + type: string; + name: string; + code: string; +} + +export interface WidgetData { + id: string; + type: string; + x: number; + y: number; + width: number; + height: number; + properties: { [key: string]: any }; +} + +export interface DesignData { + form: { + title: string; + size: { + width: number; + height: number; + }; + }; + widgets: WidgetData[]; + events?: Event[]; +} diff --git a/src/generator/utils.ts b/src/generator/utils.ts new file mode 100644 index 0000000..7d8796e --- /dev/null +++ b/src/generator/utils.ts @@ -0,0 +1,49 @@ +import { WidgetData } from './types'; + +export function escapeString(str: string): string { + return str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n'); +} + +export function getVariableName( + widget: WidgetData, + nameMap?: Map +): string { + if (nameMap && nameMap.has(widget.id)) { + return nameMap.get(widget.id)!; + } + return widget.id.toLowerCase().replace(/[^a-z0-9_]/g, '_'); +} + +export function generateVariableNames( + widgets: WidgetData[] +): Map { + const counts = new Map(); + const names = new Map(); + + widgets.forEach((widget) => { + let baseName = widget.type.toLowerCase(); + // Handle special cases or short forms if desired, e.g. 'button' -> 'btn' + if (baseName === 'button') baseName = 'btn'; + if (baseName === 'checkbutton') baseName = 'chk'; + if (baseName === 'radiobutton') baseName = 'radio'; + if (baseName === 'label') baseName = 'lbl'; + + + const count = (counts.get(baseName) || 0) + 1; + counts.set(baseName, count); + names.set(widget.id, `${baseName}${count}`); + }); + + return names; +} + +export function getWidgetTypeForGeneration(widgetType: string): string { + return `tk.${widgetType}`; +} + +export function indentText(level: number, text: string): string { + return ' '.repeat(level) + text; +} diff --git a/src/generator/widgetHelpers.ts b/src/generator/widgetHelpers.ts new file mode 100644 index 0000000..1314002 --- /dev/null +++ b/src/generator/widgetHelpers.ts @@ -0,0 +1,73 @@ +import { WidgetData } from './types'; +import { escapeString } from './utils'; + +export function getWidgetParameters(widget: WidgetData): string { + const params: string[] = []; + const props = widget.properties; + + switch (widget.type) { + case 'Label': + case 'Button': + case 'Checkbutton': + case 'Radiobutton': + if (props.text) { + params.push(`text="${escapeString(props.text)}"`); + } + break; + case 'Text': + if (props.text) { + } + break; + } + + if (widget.width) { + if (widget.type === 'Text') { + const charWidth = Math.floor(widget.width / 8); + params.push(`width=${charWidth}`); + } else { + params.push(`width=${widget.width}`); + } + } + + if (widget.height && widget.type === 'Text') { + if (widget.type === 'Text') { + const lineHeight = Math.floor(widget.height / 20); + params.push(`height=${lineHeight}`); + } + } + + return params.length > 0 ? ', ' + params.join(', ') : ''; +} + +export function getPlaceParameters(widget: WidgetData): string { + const params = [`x=${widget.x}`, `y=${widget.y}`]; + + if (widget.width) { + params.push(`width=${widget.width}`); + } + if (widget.height) { + params.push(`height=${widget.height}`); + } + + return params.join(', '); +} + +export function generateWidgetContent( + widget: WidgetData, + varName: string +): string[] { + const lines: string[] = []; + const props = widget.properties; + + switch (widget.type) { + case 'Text': + if (props.text) { + lines.push( + `self.${varName}.insert(tk.END, "${escapeString(props.text)}")` + ); + } + break; + } + + return lines; +} diff --git a/src/parser/CodeParser.ts b/src/parser/CodeParser.ts new file mode 100644 index 0000000..8f2ac7f --- /dev/null +++ b/src/parser/CodeParser.ts @@ -0,0 +1,137 @@ +import { DesignData, WidgetData, Event } from '../generator/types'; +import { runPythonAst } from './pythonRunner'; +import { convertASTResultToDesignData } from './astConverter'; + +export class CodeParser { + public async parseCodeToDesign( + pythonCode: string + ): Promise { + console.log( + '[Parser] parseCodeToDesign start, code length:', + pythonCode.length + ); + const astRaw = await runPythonAst(pythonCode); + const astDesign = convertASTResultToDesignData(astRaw); + if (astDesign && astDesign.widgets && astDesign.widgets.length > 0) { + console.log( + '[Parser] AST parsed widgets:', + astDesign.widgets.length + ); + return astDesign; + } + console.log('[Parser] AST returned no widgets, using regex fallback'); + const regexDesign = this.parseWithRegexInline(pythonCode); + console.log( + '[Parser] Regex parsed widgets:', + regexDesign?.widgets?.length || 0 + ); + return regexDesign; + } + + private parseWithRegexInline(code: string): DesignData | null { + const widgetRegex = + /(self\.)?(\w+)\s*=\s*tk\.(Label|Button|Text|Checkbutton|Radiobutton)\s*\(([^)]*)\)/g; + const placeRegex = /(self\.)?(\w+)\.place\s*\(([^)]*)\)/g; + const titleRegex = /\.title\s*\(\s*(["'])(.*?)\1\s*\)/; + const geometryRegex = /\.geometry\s*\(\s*(["'])(\d+)x(\d+)\1\s*\)/; + + const widgets: WidgetData[] = []; + const widgetMap = new Map(); + + let formTitle = 'App'; + let formWidth = 800; + let formHeight = 600; + + const tMatch = code.match(titleRegex); + if (tMatch) formTitle = tMatch[2]; + const gMatch = code.match(geometryRegex); + if (gMatch) { + formWidth = parseInt(gMatch[2], 10) || 800; + formHeight = parseInt(gMatch[3], 10) || 600; + } + + let m: RegExpExecArray | null; + while ((m = widgetRegex.exec(code)) !== null) { + const varName = m[2]; + const type = m[3]; + const paramStr = m[4] || ''; + const id = varName; + const w: WidgetData = { + id, + type, + x: 0, + y: 0, + width: 100, + height: 25, + properties: {}, + }; + const textMatch = paramStr.match(/text\s*=\s*(["'])(.*?)\1/); + if (textMatch) { + w.properties.text = textMatch[2]; + } + const widthMatch = paramStr.match(/width\s*=\s*(\d+)/); + if (widthMatch) { + const wv = parseInt(widthMatch[1], 10); + if (!isNaN(wv)) w.width = wv; + } + const heightMatch = paramStr.match(/height\s*=\s*(\d+)/); + if (heightMatch) { + const hv = parseInt(heightMatch[1], 10); + if (!isNaN(hv)) w.height = hv; + } + widgets.push(w); + widgetMap.set(varName, w); + } + + let p: RegExpExecArray | null; + while ((p = placeRegex.exec(code)) !== null) { + const varName = p[2]; + const params = p[3]; + const w = widgetMap.get(varName); + if (!w) continue; + const getNum = (key: string) => { + const r = new RegExp(key + '\\s*[:=]\\s*(\d+)'); + const mm = params.match(r); + return mm ? parseInt(mm[1], 10) : undefined; + }; + const x = getNum('x'); + const y = getNum('y'); + const width = getNum('width'); + const height = getNum('height'); + if (typeof x === 'number') w.x = x; + if (typeof y === 'number') w.y = y; + if (typeof width === 'number') w.width = width; + if (typeof height === 'number') w.height = height; + } + + const events: Event[] = []; + const configRegex = + /(self\.)?(\w+)\.config\s*\(\s*command\s*=\s*(self\.)?(\w+)\s*\)/g; + let c: RegExpExecArray | null; + while ((c = configRegex.exec(code)) !== null) { + const varName = c[2]; + const handler = c[4]; + if (widgetMap.has(varName)) { + events.push({ + widget: varName, + type: 'command', + name: handler, + code: '', + }); + } + } + + if (widgets.length === 0) { + console.log('[Parser] Regex found 0 widgets'); + return null; + } + return { + form: { + title: formTitle, + size: { width: formWidth, height: formHeight }, + }, + widgets, + events, + }; + } +} diff --git a/src/parser/__pycache__/tkinter_ast_parser.cpython-313.pyc b/src/parser/__pycache__/tkinter_ast_parser.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbc79380e8555b6a51e637d460830bad71ec0668 GIT binary patch literal 42386 zcmdVD33OY>nI?L%kstvOAVCr!xPd#tRT3#tE45Ifsof$(Nw#Rw5D8JVNl?BZwK;a& zNjgJi6V*vZ zIq$viuZxR~7g~BbXI_aa+^YI(ulnn+|NpD%bCW5XgZbyHe{%f)`WDCiM{>wUAN~0J zpQ{rdJ*{_DzuwN~&#jj?kZbt^6u_Keu+@a_7JF@t!9oc-g zle2S`oLlSQ+`4{MZ=sw?{CA~B`sR%cG6RsAE7egylUv`P zuF=Twl5vc)yp=4onAr7FYJ5J$GoDbMD4B- z|AeU7>^mds_nw#-8TWZcMcw|%6Qkb2#JXoj9iPu*V0@qJ=P>Lz8gdwJY78}vgX48> zZGT2@PGU6ayIt+pv7w-65P6Ca7-ASc*$hHqKq8Gy24Q3n4#hO|3?hd?lqo`FF$fET zs8ocIhH>5_APg)GYr0(A#{PT^eiMT#aOa>+6mp6}nHf~!Bhs?<+xv_99nv(5PBjaQ zRs4vw6XKvOxeUI9<+65Ztx*;Spz;`0*&|Xd0+f|Ol|Ldag34!5PIp0nQQGth`-|E$ z+%^VT@hC{7XJ=5AkAW&;P*rXR+FZGw$?a0i;H%vwfZw6mE>xFN236}WOPeY|l{2V1 zxAT!u6%49AU4LVgS275fyQ;q-wdc`K$>pzRu#N5-w6ZknYMD=yyN+)ju6Maaa}*hE z#=Jgr*2{lN({pkeEW%E}D{-{602~3-;shpnPdFb(MGQFw76zmEnKhFZHmwk!e*@O{ zxmK=^8)X4x(fZ@oxV0@B_{L%_Bbew4!g0(MU1Jb~!YEB81;&}QzJtY$Hk1}-jU(s6Rs>V6CT6f3BPDM zHR2l@KQ%Bo?mIRzEM|_3ofzlgpLNp1k9dxbdI2&x$@5;{#K7RF$L|+&WapR{2z*G) z@t&l>;~wA8sF(MPxr5_lV{i-%dPYZ&dIn$ii-x1fWPri>uc}0?-#dEDPmSXg_jaxA zKf#Z`2@e#;oHYg+K%9u!JrO^_bE*?KV#ms2m z(L$^9dB!k=wDcopc>MIs=OZ!EH0(vUjtmZ*9Py7#T-ER(dN{s(Hl#ss*)*IaYf})GhfA9IJZDDi%`LpNF&OCS5TpMm^z1DQK zDb&!l=$h7EG+r>yJQuXoJq5liHVo@xH80yvGJivAhK!% z@y)x{;aaeAM_P2+G?36<8Rf2J`uK=hn@*f`+PaQRyYe z4;<6FaO1LT>#wd4HEvs6`N7(oYop_|@6z+L&j&5_pJ_Np^?g(B`NQW9&+HDGTu&PD z0gI?NK5~fD#H0V8)3Syk&M6@*3|_^dowyhxW^5@#Qc;uGZHJukrE^&2Qcn^BQ0PKn zoA*G%{|Sit_XBV5acOuWlclbqUV%p(e`Jl1V)dcLD!HF>q<-q6kgeWhVwrL;$>FqT z@{@%=%D0E|YlZyUsVuApm!6w_PFfM-4S7fJ442m4)jO7AjQ* zCN-xmeRF10b4rVSa(m`V?b$u)8>Gz>HED%%lO}+)C$wroQK+C^D5#&x4wqG48krrD z*45-zEvvt)FJ9WJG_cqp{UPux8>GO;sU|+ur=Jl@${xdubY28$QO`pgC-k!}>IbV;jp}i|iZ!m3b4fG6Q2FZH28S z%0!4P#q?>ExFBrJc!uNBl*68q2%M?qVD!rInpkseTtV)F`E;RFeei>Dp7b zjKXG{walFu{Y3OhiiwXU;*NlR+6w?BwDtiAszt`BSinpKmjbgAGtybl5Zw)IMCz@3W<#`?R2nfOnmkP{ka@{;-ey0M{psjDDHQY!=NDnIH8f9brp8(Rv|Em0+ow+j+;b48)!(H)LEc z7?&?p-!XQ;Z7aSw`n}Pq?oZ4`GqrD4U8%oZA2K%z=EiW-vTHl8?!Y>nb0KHu@vXOXI z9UqB)qRHhUmmMVwuw4}$%IjAb!k$GGe;cefxZfEHriU($To`%NdZqMoX~@tj7+S+d zOUPIy7^~*C-Z8qu@kLB=8xdQ+j^pbex*vxyDuz|cBSLA@fCVp2pDJx$da)@;tQ##nPsxRBPqs)UBljVp#RmrBvYjK6NP-mgPs665~Ye&R}i-ztXiptnL5n zKT_Lck$u{>_i6V&^N-TPk1LBp3!^NmVU;`cFo;FT^l9Z8L93Jp%%(CTOoP%%T|x4QD#8peZB#Ko5|ek;Yw1?@F(GRlP=V2m>=yqFD23>?~1 zBVg!iL819YeHS>UAkQXv^d2W_+!MU0@{1Zjes%s6qazS1ZQ#_(st zN#q_3+aG;`~cwm7PK?gUr=Y&mt3>1H{DlWxcoxM+$NaY!p6M&g$`Pk zFZwR{rVL@)T{a2@jq_WtZNIwx+TN>sg9YoSvO(n;3quB{U~taaLI#&$0QaP7Zu8uh znX;)J_YH+JbwNYL+|ZSgDw_LiK{Ne%`Uzao5;&-(;WZ4w@FLWBV?)GqTv9fy9m=Ips<$G&rKUBr8h9HU=OKIs zE@78U8-{nGJaC@&3r$Y^LO(!qfh~C6N%{rOfbxF&T~Y>kAG?m&b~^CW&wMDSD%uif9d-ZJ4x;9{3 z`-#amqrH?pn|;YVYktdm&3@G$GOZ9yD*%0Q?S-`$H(uBnvepRJnt;nISiJ$G_r5uw zcT#z?s8$2%1z*4Z|MOh?`csW(g}qTaPPvk{2-^v<@jnF0@Q_n!e)JN)OEI!C52H4t zJ1c1x(aws#{@&^7VzV-1@(2=qidh-k@+-~5=mc3ZAJw9I;$@yFQJ0w?!4LluSuc}C zLIZw`EFUZ}lN5Q5j-M8FZj3~)nB^Yi$4U6iiy2)W-$@T7kh{IUNimyjY4(bSUeD0T zc+`>UJ~85>6!iPYq5gByI|@0q1|lI>c52>~BmXL83%YCU=s3kPJj;&{^IpFnS@I`Y z*nZyYJvrh%CF=K#Pk4`xk5f{_ut+8L82Z>m0SqG$?I%e{GZ=r(FLTg%1c1t7(kY3` zlG$k!XHIziS_H?5suKiUn@UtEqoWzX`hNyXLM5(CEwe2kTAHsmhs>)5^J<`yheR_? zGr}rV*dP=(MA=&morE1#6eZ+n5gf3W3y$SeJ7b>Bf}?qAC+HeWaiFXiZ;Z`)8=HVmgXQT>H4(mCBq@q9^Hrp0p)WXb+{f z_v!kev>00&o>i2l5OY7pq%JjXU7t3GYg2;>viWiKe#$yRD4C|!X>O+&#)N^^RWFG$ zQ%s*mCV_n!fGA1{hBE;ovrpZp?aS!PbW>^=o2drABy3JG?u?d9pT19D&ABospG9C` z1{CL$DX}Jyrz8V<7hLi8Q%Tf!8IOVOxcBfx`TbFg^{K^`DVk0uatTB&tp{QTRH8g% ze)dcI`mFdC-^66&r1CXEq-{=g=N@%qwPnlH24o@eBsS3(Tbzih<=KM8mZ=@&WSS9Q z!GmW8!SJGR%y9oMq(tag_l^By5`AQCy1WjrUYpJ_XZ~|Y(p1EFO1GDXH@d(%cnzz zWrAT@xWsvB*X*wO+}XXMk`|$)<=k$GMmEkGvF#=rx%6sj$kZX2I;3rS(`?h+E3>U3 zN2}myeaCvke%&52uNBN|7biaW(#@|ZH+sU(>MO>} z#`%4h%^_#I;B3EOx^F86(G8u0i#smtn9536d_m!lT^OF6SS;C|?@>O-dGf@ygy>qf!Rcy1>Oi}gLPFqGRY z|HP_zq8)+CD*II_3DuAfrVe3OC9ErN;h7X04F5Da>y$JD9`P zi(cY%zCfXM>85iEiuoG=``>^CS#Yj(Dg$%I=A6sAl5;udUSaFG?y1dTvt@eoH&6b? zVh>l;geuyEinfKWV8!aG?&V;Q>mMUlt+8ZKz&QdsKT1tjez1WcbBL>@% z6hibc{3N5K(z%4(#L^Cehf+*2$()^pR23G=G^r|(L1|M}ATiRUszAD>O;v$RTaqf6 z?NDr0RBoZ^nKo?&wU;Gn1IDe|uS`J16k;Eli-K%uNiI#S4;9D?D|bNcSGR&61Qdd@ z_|zy1xB#gpu4MGAdN)uAO?bcBm%i?0sVOW~)$u^abhtu>>*Ly?#tvzv$fbQU$;FxR1hXh^lkrQ$CF z99r}0mSv)LWWqbfkXBUb?F~j{h*|4NaryKKzEfP54mmM2*s^|f99YWV*%FJPMzQ=P z&HKZrKvV&H&#jS#p_%+pQKL`3p9dF`az;~j~B!3wmtfDW< zav5q1vGkEu{EQ;$QK>&fbs~PyMI=*Hs3P$V1IAKDHvM4m((vqX$W$wsYQqRxbz#+% zL{L34(E zXhLIQs45--kVym2S1}g}ni*wSdESAH`5xrUFEN}VM@jgVw&Ng@V&qMq`fzOhP$DCi z;44~l)Jo(C=Dc#MPqI2lnnntmB5~{n2>_|i?M8JJum%$eyaVOAuK;hnCwvQAs9Q{R z4nObXs33XY1VVw#AceIUrDgc>@d=k+B49M5USGmY+k`ArW|KTwh=38Zqx%?=zEVel zY>~Dge@3D8>E=x8?k59-!9R>Bq`;PczVm$N4+iI4S6VK&1k08$Yz~&K3>Q^iDw{2v zR|!Q8Q#(j0Et$!0tGM{uh1UX>YLfl!0Euq8Y6|795^`6CZN<^K@KVsWCSX}}->kqQ zi;c@4uyL74N+-||1eIbGN69}tWs=I$QlL?uvYa(+rYNMNL{;TTw1&cX1{I*GCgNRb z1S_ib$}{$HX&xq(0cPS07KnNEByGwAmoT>96TXVYGIlEVrzHIPfW|rF8_**9GeF%` zT1H!BnH5`5HX+4?VZNE1TghU}2Q3jx>2RLgsbszNRe?e73KzA*I6_Jj;If}w`6 zf5(N6sXa21jiGQF;MJE>C0UL#^19SK+dNY~ubQvE=DO;dFAmsN1}rP@lb+`?!Le*1 z|AynbBj{KkDB7^N4;$_?H_t4-9I)*OSa!tGG^5UWFV!JK;(81w4sJSQ!qD`aAj@&i zT;S-S@%4q9tZvul;W70 z%r`)eYWgyicBXEvTPIJ!G!Pk&g@~ycrAa|A57{)~S&s?NeoVOGG2uoz+-+*f>eIS& zXom~MBaPe4+ULt?A9I?rSsq*a+{Z%Xxvh_B!s=pPx~`T zqe;tqM^B4n|sFxQ}sl!Y((geiYL7X8Qc6^qNiL(X@}`UzP#$RbR^?}sI7`0-Pu z9W^>R<`XmF@SpJbJ_R4)h1EQdzAwNMHNal7DifgwiStM)i5U{tP*j}~EiwKn9X17Lb*UzkhoUUu zSfo+3DkTg_IN=SBvC5`0!np+(OTJeku`0?1bH&{AcOl3xa!hCZ#!>**L|)ndgrutw zicW}8;tUmc2*n+XS;68h0lodcA$R&<&`^S);>vJAO{l;n6u9QA=3faGEMMX-a)pYT zg`(#9;b76Ku%kg@8qS}FvV7QHAF?+I_NMs*L3?|OtE4GZvP>vhw%`bstPhtqhf0?V zrOOu@gQcAhtl3pL;HIqm*jOGZ*o=)|F6*g?&ML#}aqE0*iTA10LjN#ft*9 zp)!W|2V*of`iW>@hUzQP1xdm|#RIr8RHRyqf#X4(VbYn_A(a#ZS~V5 zseF|OsR~M>{p-RvL3u9<=Rc)J*uzi<{~kVFI{xSQ5q0boxTq)o9UTtm&%>9WCI~(M zBl;$eBHOTvmYCY142krn<|fWjg6Xc{;jj>9L} zaqY&X^Y|!5SfY_A7bYIFo2KwN027guGj-*rn%8p`H!gN91dMl33g?(DZ;+jDi# z;&P#BL$IiGDkp3$x!8H3bFMww65#v09a$@JHPD9Qd!?+{eeWkG*CvFNr+$fKFjS~GF z8}C|-S0bgJe6*@tv>4@_lAu&^w8k`}!Pq$-W$e(AU}Bo2xqwDeW|c0W!Id&tK;Ho; zydH5*?3!6?DY2P_WU`y0JU75*po~PXOp}pN1kl|z!1qp^qi~#~?)F6~8&-bNgqbdwunJ%l3rs63^ zPFb;>(j2Ohr^OTGser7PJ3t>A;SOa!F(vC}|KR^b}aysb)!CTnxe3QsG#y5yh5bdcng@H3_ZDb_{#VfJUA; z#njk{i#|;?@vS-9!OU7a{kS2)SBhCuZ#VkTBKINkq@8Gr!MIgodQN?mW4UZtD}YT=f&A3R1_NubgUB z_@Fr^iy`pkDwtf4RMRa9Q6eEp`bqbUEi(1uQP0@XA&;0j>3i8Xe#*C}8+#AjCo$>; z5%Nu>%I+f&v!G@dg)~UN14qZPPa&=$bcmToL>5?w5TW{T(+;*Zl*H+gi8Epzon@p0 zyaT-Vm>0SdAYbAMTcU~@u`n@1N`F5eqTGK&R+y~c!V*of#EACcMe@rVg{CB}1Sfd! zap|rdwuVsw#JnNgik2McvObzEDrW3rZ6Z-^w9Mgv3sqLT4I+~GG311HU`wD_Bri_U zka#2T<5YtkWL>26^rYO`g%XQ;y0wkIWR=Ju0nrl47T6RONLp@}Uc@11E0cpS(P~%^ zs)udE3d+Ly_Ha>IxVSv*fQmwK*ii|cgre*z)5CmHfgHn$lS7veg$yl%q2+${>Z#o` zwSuuCT)jM0-7ZwON8OQKD$Y_G(3d>iuHtg+Gp4BewP~ebT6w=3l8c z-s%uK`-FxAfNR1b3W&rlj+y3=qe*Zyy)}64_*JNicL?T=aBEwr^%k4|Rwg0xs(c{vJ@+4nE)PR(ch?*Z@d?oEM6%|#UPC0G}gnZKC zi#$?ILW)m7ybNLSeTo!;cvH<1l#j`kat`*0kB97eiKQ}~usLfT2IWMGlMhct+< zJ~T2p#{Uf*E`zjs{0`reDj{vhB=Pl|^tFksF0u&ei@Gf%zP%^JOv3Td6m&&T?EhDU zB^{Fa&jjpB6|>2xeaVV)nQ%PCk%SR1hYD+j!rJI@KBmI{Q^)O{p{@Ict^4mBcrJ9{ z1>wL8p*2T@HAn6o9Sw|~4voDgjJ@`d<2I?g)W4^nXVed*AGiipJ>;UFu(gg?Knq1_E`egksS0WYVhTJP8s(Iho3j0&*Z>8VTYO(N;n&o@DZ;)3EaS2k)l0L{~`z7Kcq?q^)C2jh|LQ64A(*H^N^m~=|HPM!0N1haR zD@i-er`m`3v|vxElrED+n!4ET6=-a{ueUm-T5!rcbf$MnY%HA;6vh*=bt{&}SJvi~ zcZ(zr#-3@jv}f{V_;iQ5n~{hQy5vX`_ks4(1}eq$#UyZm(Y0yQ_LN-sg!oH(3gX%{ zC08lk2rZW!8tcmSRrNL{WEcI6#mYjg)}`U-p=C>bW3jR!&(x!?8GYHz&SLe>Ok3|2 z3TZ_N2)Ri-|LX5tZ&h#m()2N!46)HfBXmJ=G{j_=@tv~I=rf@Hs}$4BPD*o5vF&2= z(u6ckzF3>NGnB^A;Wcu8hdzj->DQ9Zlw!fTq~<97-)&x`or9Yca+Ptch5ED4@<@f$Eei2dXV9ZC$5w^- za{jq#OWpm5Sb1q<^*kb$6__IrJ(Ss(3tQi3WxbUibL>z^E4AL6&|9kBT}vS`;__o7 z4lA}VKTVnQ<*|b?ht-;9ldr&+@6Jkt?+avjls@|rbjzNlWm+=UF0j%+yA%?oFH9zr!j62-9Cx7l2-NQGmqu3zda=j_W# zq(yu8FNI8?PY)~&T~dzPN0dV$^+!?RKF8kTG`*%$Dru}vj=mhUXjY$tjob8e%AqHy z5AY~bN=I|h{xHSJr9j>}Y2)>$M1s?8P87MblBZwSux6#2B|U+%xs7Q{@M3EGxVRsA z8`8#m#8~=!*VxpS9zz~gC~qo;{69I!-+aaCFhb8f=4#@2&q)~DPqKrgv0dCtBF_|) zu&>N{)QFbH286ynNzALnl03;Cvou{v`f`*+&vp>drenBIgs|ZT7w-`^A4zS}S4WCGr?u zOfqALePu_#7*!hc&53>G$G!?I}}#G2J>mL<2?{?r;@Vm7;2c zsH#7!ZgPq$m#A`{)jCIxInQP|y`z3F{0@t%7tX5J;_FpW_1amT6PL6)na<2v^|4C+ zHY#y8e|=OmGC+q;aK!|qLu*A9!w(zz9c| zy|}dOcLb9$4sAj_AEbqY|CeMD*GkmlxFBAqa8Dlf5Aq`?P$>WC2=1lTZTAg%PxJqU zU^F=MB4)Gq1z0ioPnj=bidj2QC+`q$NMl+TVovusKZeU{y+iD7G$w4LCnV^)wSfUw ziF9fa+&x@!L*g{)9yXG5Ne2hiFGE}LdUL8 zgFK`jwf@5TfU#T-+HiS8^x!ewT;vjPYS|UEwg-&uGDIx$eS1mB-Xhp>==j>}S6>g> zJEyjXH*|k+f#Z!lQkgZHh2_(S3HXZZvTMF4Sh+G#v1-8+u&xdmSIcExb9qh3)GU~q**#2+g0*p;DToEF z>!!9s{unLbbJqs04g~Wz;PDFR)#SQ_T-2H#t_bFKOl@Jup{9?`Y@RtfvwtpkZslCv zd~U$n7%(=<`QTpRkf~KLwJPS*y*Jc-_~Y)wK}SFA#i3Afn^4@gu=QT?8ln5})XspT zKfJc!2ze{-;a{a;byUbiz?unYErXL=~wIefO}?+&Q&P$ln+)pbN&bX0zsM zg7*4=%{8AFur>vZO>)^{vz)!Qz={)}19UXRb*Xi>^{uvRYp$+|-ogka=#qA!qsY4RY4&FRzbEr=>%L zt%41@$Tv*aO+i~{pm5{jK(EoC(QDvyeb*IQRt@{KQmwTby(0{orLxi-<89c4+krj-^*X| zNkRFX4!R7dE}se&vupPNBAQaWYuDEl|^a`)HtI z`@Q@fxaQo^05N=F5pb(*);3r0=VbwV{k%E}cZI4}3RNpZrd5Jz)qNT_u_i3PULLe> z4%oKbt_)bW1&rJ77r5T4oM>aULxIDStNl2q*i)ztHWzz}wRh~r9;f!MHs4c|LB!2c z@tX~4Gx6I)*mQx#VR%SJS@(88l>kA+P>OM@lOV8cry92=2||IicWaX%6gEt5T@nOX z%BjYkkp!V2dUIzcL0D2#z=lMh1fd{Sb7v(%fH{|H+}TMGV9TW%w;>6_o*GUYqa+B2 z0)#0Ef+(;Q3(``{yj~$ZKOww9 zA-o_VTtQ^yE=&k-QHXC#2w$cUZchkT5D2-862jXR;wR1@1>uZaoe+PuLVU+E!!{K? z3Ddi7Y3Sh%vR`})d>BGV$;AES?Ke(x7PA)!mz?CpesU4VvglQW_wcT1^h9U$T|&A7 zp;1&di>d}u)heoL&Z-+4&Z^h1KdbKS6jfgmRYycs?OAne>sj@N4QJIA*mojq{RaMj zp(Fs>fEPOaXER3pPT%+hzI|si@ZlWs;ffY)ztp%9T0q3sp&?u)J$V#Y zCr)BNrP(@iQq=6mgMXT5>4?@Y_wf&()z9GL8T#1n#}3YqV0_}{ReazFFSDX(NL51t+CD0gwvURG zvE6sl%TMqhqIQykB!n2T3k~Q49nrxBg*uZZ>dqv$p4>g>dZq^>S>(=+ha1RkjJr+b&XL{wBW7}2$i3s- zju~wvm)v=Ax0T%a#>4B#T^|eo1P#LyrVmQ-^im(W@%myx zNj|LIC@A(pEund^S%s{ojFGL-iYy74aW1a>P3_#lzc9V!U+DPJmxAW?0sVTW@sf1o z0x71jb#_1ckS+&#^pzko;)@>ARn*U8I6z?q#$n+90Y)gDo>5wT*|U1G+QH#0l#3ZK zRh8CRc24YYY<*Qy=9Y1ZTxJi}r$?oxq|A@Y@_L@SEDG!KQhx8(c8>Xd`G^ss<*fCxjD+aEnKmvW>?w6ceqyh8&XsA@-_dvaOWz0ne5 zX$Fy{14j_Ga6Bzgir4sUrGQ_PMKqFhs*V(il1|lCkOxoJpTiR68)w;T1pl7A1z3KX zY*Fr$#A{klz)P;jkrni!SMkZEmk%cSn+Vgs(sa3L{*}wEp~`llvi-tN z+RA98ANFRUf{jAK#w6HPLglIpJO3uX4-6U7u_%l3u_%{( zEQ*~(PQqP8J2TvM8WJpeU|9aQ3~c|F3{tLw;1ox8mVu=X?4ze6J8tPB0?mMWH}DD* zMU%=@y6X!-%dqPU7{2jk92z3xhG8j)TKh5%b(8SS&Ca9Zno#zh4t_Jp7r&V-RNta@ zXW^V4l$_kz`aj%6Jr%6J4PxotAw}sq}6GD~;Z5jQ&cc(+nToLbkZns>)iP*vFkI?e$cyn{J9HT5=D5+qq(cywzMUWDUYGFwB{XL z8)HVdqZ#TGNJZ@~PMdSI&Ey&FE+NR6tCU>svZVTKlFFCx2xRPbw*8k51g6w>Wk31X(_TjO0I%DRa#7mF-8R@!@pQEzURw_VW=gFFeKo*6JZ?wVcq%Ut2A7NKh0Vz*G$6)vxv&l1X4+^=xW zw+a<&!<7wz#&tsF`Ug6Vv+zNdzC3T*5Xs|mO9CZrf~7rVStnT5E$+N$>3LAZ6}3jn zxZ?7Vqv4JNcdmsU)pLjNu0fitO6%q`h0hhMdSQlp<;cwvMJEKL8$DM zvNU9|Ec02Gs{~6&$kHiTIu~EPXW99nm@96?R@`3v-N`9qn29f?N}3^C70i`$t3u`m z!Q2qW_SfPREY3Omyi2HPUpRn_?pZeA_F0r!pIBzPCr&We%pD7vn+0=oJf>rQhfvY6 zm?2bbyl2^jn8l5sXLF^lsa=0qVo#`gTd;cD{gV1{Nj3WEL8jK3 zH*Eyp9(U(DSKV>0n(hhP%IC5KTf_V+!M0*y^sa47*j_#NoM3Ma*;fhnRpHu}`O~;B zH(cF}R#@%$pPZ(s7+F?yAepMVQ00m{l`E!qgl!db4RmMYF~PQSvE;68TWT`P7VJXp z+HiG4xTbX>Q>a;u(ok|*mRwaEuELftF z?&c|K1v{p!V$SyG{h!p}{_McYU4mxY|0e)x476+%DmMko zHiyd_KdGv{-Vv-Ddb?xZ6Y4x9bRG&k_dIR}7oIyB>KweLy51qw4F#*bm?O=YBZ2&S zLGQxgW%px$@!2UgXWRJ2XD8HLy;tQ2$;5rfykmv-SDht0I`V#9mI0fg-=zHo?Jd~; zVhcWKf8kc`1Bdfa9_;}(sA8)ql5-Mg0SG76#NYD-M^iKDMmR2^qSu$+B|RDWWu~<6 zG$mX(C0$ho#T>Rw-lFSczyufI%Ez)7-BlXZ%$FOhW) z7Orw7yenVY$X2V|MBK}sOiAVndQnZWCKb0S;_M$2KBL9!4em3orZhXCFQV(8i}CJK z$h>c1U8rrl(6;@f+F;whfPNoiPT}G`S{S5tBEI3alOOR!*fYw~rBqfp_qba4d@4}( zz(DlLmjTf@F!0KxXH?Qn5zPYw$Dj`~F$#qi-}u0Qm^m;oG(I>mz>};&V&Z0OA3HG) zK4}Ad#B9mY&5w`q#E8fHHKQKCAJ4Y)E68c+jz8qi6Q^9vkRDp*pMgVC7TQf;J1Gpu zyvIi+w!;bXB-R&G(UJ6!h?L~ll63(VSh?=~(k--qMt%p$@{o0utUAQ1baiuWS0ERraGV>~>0EPz zSH>@oliL-^vBu-$Gt!{dRm}J=ot{1Yki%!hxI(vf&VL1`o*#1fj2u@Pbp`OiN$v=T z&xN6f^b;wuk1dxD+3V@pNm7c+dk6VUVYQ|kerdqJYDHr)s?!-brBAq^MeoRCsLn^oAnl8SO|_$u6u@m?s`Z+5yt_Qu3I(lzIO2H!3f73i`IwIN92UM zTDM`o?OMmxjtGa(1=I;YID}t9#bBvD30ipNBhs-n5tI5#yvYOEEV*c+r`jti*jV&6vF+*%HGM_&S>-$_k)W6&yO5G}6 z<5u%px5mNocygsbqd&7>-=EdaI5sp<+`6TH?OJyRZs6AP+3rli)mPjvgvq$DB z5D#)MV8cRu%J+ot02dP;#*N%`r|$be(713H+KotF_-4|L+$e>#r0if-Zv~5I6kn3T z=}nHNZq$VncB2H(485^3N{dn4rIlU7oJ+gs(;+xNr8Vi(Y*;RxXrx-nG&`=(@PYsa zm5d7r4eVRGf)Ed$fczfu4G)}-f1crsk%phj61_QzaHUwhv|(EYK&y{T3=DjLqWWp0 z_y!j)DVx&X(HDgsrBm8(nZw2SxueH@5yf!ku%B<|NMnPjd43pH(!xsvgzEaa9X2hz z5-YC<)I1X(#M%fjQR9wnw{b08{4P-xYBz0gwU1t$rJF<MT`hQPJJqz2cvPyj9cqKN>)y0j)4J_u}1CFB|u~MVyl0q!~FjqsswpXhzP>R1%Kf@{$a(En=zYSt>L@QcNrrT~aEvS}{X4>G=@t zp9MCZV*4+P=}s@{0SulrJz$LA>Irt5axa+@U<`*Al=R%G^l4fLrS};R%_(W8M`J0q z2E{TVMZHw}L_19>bKC}5$V2J+=nO_oJQa_hCbl^xwi&k^8T(9qIelh#Zi}uj%dMih zwfh&*afgxS%99u`%7T?7j0PTyL~_bZmK%66fY>xogU6)HN3Ww4>;}>#v_{jV4gWu1 z1}ov20W`pPuA8GdA>FVZeNq6V2JxDP)$BuVQ^hl^K)>poqf`U2LBws z;u-}kB#TTK%99Ye!n*<=V`}(qNP|5qP}ueMD0a}r(1P^dvo*av`o-roX}*$QsA3!v zmrBeW@*eX*s{apP*_+5osx1F4)OblP`ESEFm|WiOTO|vdS$U7&v$ftT`GWrrfOz6i z@I<`vza;BDvZyVkt6hIeU*Mi{yc-r+O{@od$>YalS;)$zpiD_kfHpUxX3TS%|0k6C zpOQ62*4JS{f>uHi7~cLG`C>#$_tJ`)h%-Rf(y|X+LMs|i;lj)DQ_)q&6m?1Kj>M^= za*7(jN;l{xm&i|Li^|JlKN9=Js?gzL?UN0Re@m_UPf>Qd*blqj-{#tJm#n?$l5SQv z^K#JMFtz=!&GxA?GtXUmVfKaj)xpyC#f-b9os2zDC0OtV_B~52-7{N6_so`rt&XWZ zP;#&q3WZ(QM}vi3AMF$J_D*Gfl2;%UcHb<4{&V12c=b+YV*6t+rj2&-lGm+7KYUpz>z&&At-awWl(Tt3wXpTZp6h#rWm^R2mX9(6 zz3yKbg!27UTkjZ4KF3{{RZx?)m(km_bL$q=L3`WO_P?^0&`Umf#TT0|G|%bgUJ2&8 zrn+Ree!e1@*EH1yrcrJ_y?!fLYUZ`qa<1kGmQ`pIi&eS;1XmT#=U*$kS_YQP)Mh{z z6kXhVVei}sy|T5i>vl!3cB_!T6^fFiyIUg^)XZnm75fV#xR$oQODO1?>L$($?%OS- zKU^H;a6<4WT|OH$mOrRK=_AWHi)~6G3@}bevWQUO zC&*%3I-13)Tda!|`X#am!SGH3!LrKnUnVD6L?QZD!;7{*uYRW0=}ICwD|EK`_66;N zd$DTa(DfXlabpDg6x;2sALV~E7;qo>==iV7gsq2uPo7euloXw081hPV4V*DUlrW*n z&wmcz?{j63QEIHjnkKXCeX2O!AX5}f(Sm5D6cbmca&mFyg;*?7z%ZmlffH1Z!g>|U zUyE=h(SkMxp#ucX*c1cB1GUnPhwxQUa)2-G`cn*i?I~Vxg0fM$)h&8l*PBtzRd9!i zbHGd{=K+QS6{YFRq;ucwZcIEHRE|3pe3_eZ+E4mDraDaMEICf*EG!A7URsPYHk<1CsxI(se&Q_Et|Hdo(!~9500-AxTdT|dojLFPnQxq#$^Yh;r?kM!`Bg!>KRB$noXdjroN4AvY9=qrK1 zY~`WCCZVt?Y%2p-H)N|5Y<1v&N>2rbD_o(9bwUMh>8_G)=?*t+#d#N8vYnTWSN#gv zyJvs+ouTk^us*Wy+iK=KbM8QaD{LzP=Bs7gqB5MUf7+s6RG> zCtPr7D~X~ZDxcw{7tx#yFX2}*eIVTEj@dODHC~JYlat_5OboNoJi|Yk4hHri%8!|Y zG0`*LoQ>@&l;~?h630@z)^3mH(%AUk^v!w%TL#c&|fssRqsJ~l0b^sZ}aT5hdRyBTM|rQc(!7}hXO zZbOqmu{DYnNL>0guoG^QLMuphQf&BggnmU- z@n?0D6UUlYgILw$8q9Ip$?su=AWi-ZAQgJl)QBrACwcFH$3HkSa@Nv~K|l(wI7d0@ z8(Qn+e~6X^FL(hzXY*MoG^Qx27^27&wNMnrg8~%5)3n36NB=lj-3n1H_m2R{$XPa}iPZ)tMVrJtDGG(7nm2(QIB^AmMQliH%g z5x3mw`kAizx{pmO7I1%8K0S=}-PKcDf3M+k+AyEP_ROpW5wAdK*Rfn8cX?Sn2+VL z=@^gEIKO$mEr2r}Y~Ciupc!2Lw=_S?|L9+X!hglE+0K1CcXOuZhxPdSFf)5|LFR|K zne=V1+0v}}aAOwipX(i4>NP*FRMB_64mO>w9K+ME;8HVso+$fno=zOP91^8Nc$@z* zJRn2GNl*1Sp0EYC9(^W}K8y&WMwU3vL?(!aJs?XC5PBcy8M2qK{0js{UCLTsBCePu zmfo?X^OQV90yscAff2#KO>l(OrM`TJzLH5QQNLbd#dq?z5sE*~|1qqHMx|0c&}vj# z$kJ4X-*WiN{w2-9Uwv-%S6`ccC75ad z8@=h=OEcC>_F4N}#cXjfr{=D{7QJq]/g, '').replace(/-/g, '_')}`; + events.push({ + widget: bindEvent.widget, + type: bindEvent.event, + name: cleanName, + code: bindEvent.callback?.lambda_body || '', + }); + } + } + + const keptIds = new Set(widgets.map((w) => w.id)); + const filteredEvents = events.filter((e) => keptIds.has(e.widget)); + + const result: DesignData = { + form: { + title: formTitle, + size: { width: formWidth, height: formHeight }, + }, + widgets, + events: filteredEvents.length ? filteredEvents : [], + }; + return result; +} + +function extractWidgetPropertiesFromAST(w: any): any { + const props: any = {}; + if (!w) return props; + const p = w.properties || w.params || {}; + if (p.text) props.text = p.text; + if (p.command) props.command = p.command; + if (p.variable) props.variable = p.variable; + if (p.orient) props.orient = p.orient; + if (p.from_ !== undefined) props.from_ = p.from_; + if (p.to !== undefined) props.to = p.to; + if (p.width !== undefined) props.width = p.width; + if (p.height !== undefined) props.height = p.height; + return props; +} diff --git a/src/parser/pythonRunner.ts b/src/parser/pythonRunner.ts new file mode 100644 index 0000000..c9160a3 --- /dev/null +++ b/src/parser/pythonRunner.ts @@ -0,0 +1,80 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import { spawn } from 'child_process'; + +async function executePythonScript( + pythonScriptPath: string, + pythonFilePath: string +): Promise { + return await new Promise((resolve, reject) => { + const pythonCommand = getPythonCommand(); + const start = Date.now(); + console.log('[PythonRunner] Spawning:', pythonCommand, pythonScriptPath, pythonFilePath); + const process = spawn(pythonCommand, [ + pythonScriptPath, + pythonFilePath, + ]); + let result = ''; + let errorOutput = ''; + + process.stdout.on('data', (data) => { + result += data.toString(); + }); + + process.stderr.on('data', (data) => { + errorOutput += data.toString(); + }); + + process.on('close', (code) => { + const ms = Date.now() - start; + console.log('[PythonRunner] Exit code:', code, 'time(ms):', ms); + if (code === 0) { + resolve(result); + } else { + reject( + new Error( + `Python script failed with code ${code}: ${errorOutput}` + ) + ); + } + }); + }); +} + +function getPythonCommand(): string { + return process.platform === 'win32' ? 'python' : 'python3'; +} + +function createTempPythonFile(pythonCode: string): string { + const tempDir = os.tmpdir(); + const tempFilePath = path.join(tempDir, `tk_ast_${Date.now()}.py`); + fs.writeFileSync(tempFilePath, pythonCode, 'utf8'); + return tempFilePath; +} + +function cleanupTempFile(tempFile: string): void { + try { + fs.unlinkSync(tempFile); + } catch {} +} + +export async function runPythonAst(pythonCode: string): Promise { + const tempFilePath = createTempPythonFile(pythonCode); + try { + const pythonScriptPath = path.join(__dirname, 'tkinter_ast_parser.py'); + const output = await executePythonScript( + pythonScriptPath, + tempFilePath + ); + console.log('[PythonRunner] Received AST JSON length:', output.length); + const parsed = JSON.parse(output); + return parsed; + } catch (err) { + console.error('[PythonRunner] Error running Python AST:', err); + return null; + } finally { + cleanupTempFile(tempFilePath); + console.log('[PythonRunner] Temp file cleaned:', tempFilePath); + } +} diff --git a/src/parser/tk_ast/__init__.py b/src/parser/tk_ast/__init__.py new file mode 100644 index 0000000..73db8eb --- /dev/null +++ b/src/parser/tk_ast/__init__.py @@ -0,0 +1,3 @@ +from .analyzer import TkinterAnalyzer +from .grid_layout import GridLayoutAnalyzer +from .parser import parse_tkinter_code, parse_file \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/__init__.py b/src/parser/tk_ast/analyzer/__init__.py new file mode 100644 index 0000000..d1361c8 --- /dev/null +++ b/src/parser/tk_ast/analyzer/__init__.py @@ -0,0 +1 @@ +from .base import TkinterAnalyzer \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/base.py b/src/parser/tk_ast/analyzer/base.py new file mode 100644 index 0000000..ec39738 --- /dev/null +++ b/src/parser/tk_ast/analyzer/base.py @@ -0,0 +1,109 @@ +import ast +from typing import Dict, List, Any, Optional + +from .imports import handle_import, handle_import_from +from .context import enter_class, exit_class, enter_function, exit_function +from .calls import handle_method_call +from .widget_creation import is_widget_creation, extract_widget_info, analyze_widget_creation_commands +from .extractors import extract_call_parameters, extract_parent_container +from .values import extract_value, get_variable_name, analyze_lambda_complexity, extract_lambda_body, get_operator_symbol +from .placements import update_widget_placement +from .events import analyze_bind_event, analyze_config_command, analyze_callback, is_interactive_widget +from .connections import create_widget_handler_connections + + +class TkinterAnalyzer(ast.NodeVisitor): + + def __init__(self): + self.widgets: List[Dict[str, Any]] = [] + self.window_config = {'title': 'App', 'width': 800, 'height': 600} + self.imports: Dict[str, str] = {} + self.variables: Dict[str, Any] = {} + self.current_class: Optional[str] = None + self.current_method: Optional[str] = None + self.event_handlers: List[Dict[str, Any]] = [] + self.command_callbacks: List[Dict[str, Any]] = [] + self.bind_events: List[Dict[str, Any]] = [] + + def visit_Import(self, node: ast.Import): + handle_import(self, node) + self.generic_visit(node) + + def visit_ImportFrom(self, node: ast.ImportFrom): + handle_import_from(self, node) + self.generic_visit(node) + + def visit_ClassDef(self, node: ast.ClassDef): + prev = self.current_class + enter_class(self, node) + self.generic_visit(node) + exit_class(self, prev) + + def visit_FunctionDef(self, node: ast.FunctionDef): + prev = self.current_method + enter_function(self, node) + self.generic_visit(node) + exit_function(self, prev) + + def visit_Assign(self, node: ast.Assign): + if is_widget_creation(self, node): + info = extract_widget_info(self, node) + if info: + self.widgets.append(info) + analyze_widget_creation_commands(self, node) + + for target in node.targets: + if isinstance(target, ast.Name): + self.variables[target.id] = node.value + elif isinstance(target, ast.Attribute): + if isinstance(target.value, ast.Name) and target.value.id == 'self': + self.variables[target.attr] = node.value + + self.generic_visit(node) + + def visit_Call(self, node: ast.Call): + if isinstance(node.func, ast.Attribute): + handle_method_call(self, node) + self.generic_visit(node) + + def extract_call_parameters(self, call_node: ast.Call) -> Dict[str, Any]: + return extract_call_parameters(self, call_node) + + def extract_value(self, node: ast.AST) -> Any: + return extract_value(node) + + def extract_parent_container(self, call_node: ast.Call) -> str: + return extract_parent_container(self, call_node) + + def get_variable_name(self, node: ast.AST) -> str: + return get_variable_name(node) + + def update_widget_placement(self, target_var: str, call_node: ast.Call, method: str): + return update_widget_placement(self, target_var, call_node, method) + + def analyze_bind_event(self, target_var: str, call_node: ast.Call): + return analyze_bind_event(self, target_var, call_node) + + def analyze_config_command(self, target_var: str, call_node: ast.Call): + return analyze_config_command(self, target_var, call_node) + + def analyze_callback(self, callback_node: ast.AST) -> Dict[str, Any]: + return analyze_callback(self, callback_node) + + def analyze_lambda_complexity(self, lambda_node: ast.Lambda) -> str: + return analyze_lambda_complexity(lambda_node) + + def extract_lambda_body(self, body_node: ast.AST) -> str: + return extract_lambda_body(body_node) + + def get_operator_symbol(self, op_node: ast.AST) -> str: + return get_operator_symbol(op_node) + + def analyze_widget_creation_commands(self, node: ast.Assign): + return analyze_widget_creation_commands(self, node) + + def create_widget_handler_connections(self, widgets: List[Dict[str, Any]]) -> Dict[str, Any]: + return create_widget_handler_connections(self, widgets) + + def is_interactive_widget(self, widget_type: str) -> bool: + return is_interactive_widget(widget_type) \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/calls.py b/src/parser/tk_ast/analyzer/calls.py new file mode 100644 index 0000000..d305cd5 --- /dev/null +++ b/src/parser/tk_ast/analyzer/calls.py @@ -0,0 +1,44 @@ +import ast +from .values import get_variable_name + +def handle_method_call(analyzer, node: ast.Call): + if not isinstance(node.func, ast.Attribute): + return + + method_name = node.func.attr + target_var = get_variable_name(node.func.value) + + if target_var.startswith('self.'): + target_var = target_var[5:] + + if method_name == 'title' and node.args: + arg0 = node.args[0] + if isinstance(arg0, ast.Constant): + analyzer.window_config['title'] = arg0.value + elif isinstance(arg0, ast.Str): + analyzer.window_config['title'] = arg0.s + + elif method_name == 'geometry' and node.args: + arg0 = node.args[0] + if isinstance(arg0, ast.Constant): + geometry = arg0.value + elif isinstance(arg0, ast.Str): + geometry = arg0.s + else: + return + if 'x' in str(geometry): + try: + width, height = str(geometry).split('x') + analyzer.window_config['width'] = int(width) + analyzer.window_config['height'] = int(height) + except ValueError: + pass + + elif method_name == 'place': + analyzer.update_widget_placement(target_var, node, 'place') + elif method_name == 'grid': + analyzer.update_widget_placement(target_var, node, 'grid') + elif method_name == 'bind': + analyzer.analyze_bind_event(target_var, node) + elif method_name == 'config': + analyzer.analyze_config_command(target_var, node) \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/connections.py b/src/parser/tk_ast/analyzer/connections.py new file mode 100644 index 0000000..778f448 --- /dev/null +++ b/src/parser/tk_ast/analyzer/connections.py @@ -0,0 +1,93 @@ +from typing import Dict, Any, List + +from .events import is_interactive_widget + +def create_widget_handler_connections(analyzer, widgets: List[Dict[str, Any]]) -> Dict[str, Any]: + connections: Dict[str, Any] = { + 'widgets_with_commands': [], + 'widgets_with_bind_events': [], + 'handler_methods': [], + 'lambda_handlers': [], + 'unused_handlers': [], + 'connection_summary': {}, + } + + widget_dict = {w['variable_name']: w for w in widgets} + + for callback in analyzer.command_callbacks: + widget_name = callback['widget'] + if widget_name in widget_dict: + connections['widgets_with_commands'].append({ + 'widget': widget_name, + 'widget_type': widget_dict[widget_name]['type'], + 'handler': callback['command'], + 'context': { + 'class': callback['class_context'], + 'method': callback['method_context'], + }, + }) + + for bind_event in analyzer.bind_events: + widget_name = bind_event['widget'] + if widget_name in widget_dict: + connections['widgets_with_bind_events'].append({ + 'widget': widget_name, + 'widget_type': widget_dict[widget_name]['type'], + 'event': bind_event['event'], + 'handler': bind_event['callback'], + 'context': { + 'class': bind_event['class_context'], + 'method': bind_event['method_context'], + }, + }) + + all_handlers = set() + for callback in analyzer.command_callbacks: + if callback['command']['name']: + all_handlers.add(callback['command']['name']) + for bind_event in analyzer.bind_events: + if bind_event['callback']['name']: + all_handlers.add(bind_event['callback']['name']) + + for handler_name in all_handlers: + if handler_name and not handler_name.startswith('lambda'): + handler_info = { + 'name': handler_name, + 'type': 'method', + 'used_by': [], + } + for callback in analyzer.command_callbacks: + if callback['command']['name'] == handler_name: + handler_info['used_by'].append({ + 'widget': callback['widget'], + 'type': 'command', + }) + for bind_event in analyzer.bind_events: + if bind_event['callback']['name'] == handler_name: + handler_info['used_by'].append({ + 'widget': bind_event['widget'], + 'type': 'bind', + 'event': bind_event['event'], + }) + connections['handler_methods'].append(handler_info) + + lambda_count = 0 + for callback in analyzer.command_callbacks: + if callback['command']['is_lambda']: + lambda_count += 1 + connections['lambda_handlers'].append({ + 'widget': callback['widget'], + 'complexity': callback['command']['complexity'], + 'body': callback['command']['lambda_body'], + }) + + connections['connection_summary'] = { + 'total_widgets': len(widgets), + 'widgets_with_commands': len(connections['widgets_with_commands']), + 'widgets_with_bind_events': len(connections['widgets_with_bind_events']), + 'total_handlers': len(connections['handler_methods']), + 'lambda_handlers': lambda_count, + 'interactive_widgets': len([w for w in widgets if is_interactive_widget(w['type'])]), + } + + return connections \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/context.py b/src/parser/tk_ast/analyzer/context.py new file mode 100644 index 0000000..59fe91e --- /dev/null +++ b/src/parser/tk_ast/analyzer/context.py @@ -0,0 +1,13 @@ +import ast + +def enter_class(analyzer, node: ast.ClassDef): + analyzer.current_class = node.name + +def exit_class(analyzer, prev): + analyzer.current_class = prev + +def enter_function(analyzer, node: ast.FunctionDef): + analyzer.current_method = node.name + +def exit_function(analyzer, prev): + analyzer.current_method = prev \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/events.py b/src/parser/tk_ast/analyzer/events.py new file mode 100644 index 0000000..fd124dc --- /dev/null +++ b/src/parser/tk_ast/analyzer/events.py @@ -0,0 +1,77 @@ +import ast +from typing import Dict, Any + +from .values import extract_value, get_variable_name, analyze_lambda_complexity, extract_lambda_body + +def analyze_bind_event(analyzer, target_var: str, call_node: ast.Call): + if len(call_node.args) < 2: + return + + event_sequence = extract_value(call_node.args[0]) + callback = call_node.args[1] + + callback_info = analyze_callback(analyzer, callback) + + analyzer.bind_events.append({ + 'widget': target_var, + 'event': event_sequence, + 'callback': callback_info, + 'class_context': analyzer.current_class, + 'method_context': analyzer.current_method, + }) + +def analyze_config_command(analyzer, target_var: str, call_node: ast.Call): + for keyword in call_node.keywords: + if keyword.arg == 'command': + callback_info = analyze_callback(analyzer, keyword.value) + analyzer.command_callbacks.append({ + 'widget': target_var, + 'command': callback_info, + 'class_context': analyzer.current_class, + 'method_context': analyzer.current_method, + }) + +def analyze_callback(analyzer, callback_node: ast.AST) -> Dict[str, Any]: + info: Dict[str, Any] = { + 'type': 'unknown', + 'name': None, + 'is_lambda': False, + 'lambda_body': None, + 'parameters': [], + 'arguments': [], + 'complexity': 'simple', + } + + if isinstance(callback_node, ast.Name): + info['type'] = 'function_reference' + info['name'] = callback_node.id + elif isinstance(callback_node, ast.Attribute): + info['type'] = 'method_reference' + info['name'] = get_variable_name(callback_node) + elif isinstance(callback_node, ast.Lambda): + info['type'] = 'lambda' + info['is_lambda'] = True + info['parameters'] = [arg.arg for arg in callback_node.args.args] + if isinstance(callback_node.body, ast.Expr): + info['lambda_body'] = extract_lambda_body(callback_node.body.value) + else: + info['lambda_body'] = extract_lambda_body(callback_node.body) + info['complexity'] = analyze_lambda_complexity(callback_node) + elif isinstance(callback_node, ast.Call): + info['type'] = 'function_call' + info['name'] = get_variable_name(callback_node.func) + info['arguments'] = [extract_value(arg) for arg in callback_node.args] + elif isinstance(callback_node, ast.ListComp): + info['type'] = 'list_comprehension' + info['complexity'] = 'complex' + elif isinstance(callback_node, ast.DictComp): + info['type'] = 'dict_comprehension' + info['complexity'] = 'complex' + + return info + +def is_interactive_widget(widget_type: str) -> bool: + interactive_types = [ + 'Button', 'Checkbutton', 'Radiobutton', 'Text', + ] + return widget_type in interactive_types diff --git a/src/parser/tk_ast/analyzer/extractors.py b/src/parser/tk_ast/analyzer/extractors.py new file mode 100644 index 0000000..1089568 --- /dev/null +++ b/src/parser/tk_ast/analyzer/extractors.py @@ -0,0 +1,28 @@ +import ast +from typing import Dict, Any + +from .values import extract_value, get_variable_name + +def extract_call_parameters(analyzer, call_node: ast.Call) -> Dict[str, Any]: + params: Dict[str, Any] = {} + + for i, arg in enumerate(call_node.args): + if i == 0: + continue + params[f'arg_{i}'] = extract_value(arg) + + for keyword in call_node.keywords: + if keyword.arg: + params[keyword.arg] = extract_value(keyword.value) + + return params + +def extract_parent_container(analyzer, call_node: ast.Call) -> str: + if call_node.args and len(call_node.args) > 0: + parent_arg = call_node.args[0] + if isinstance(parent_arg, ast.Name): + return parent_arg.id + elif isinstance(parent_arg, ast.Attribute): + return get_variable_name(parent_arg) + + return 'root' \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/imports.py b/src/parser/tk_ast/analyzer/imports.py new file mode 100644 index 0000000..0741af0 --- /dev/null +++ b/src/parser/tk_ast/analyzer/imports.py @@ -0,0 +1,14 @@ +import ast + +def handle_import(analyzer, node: ast.Import): + for alias in node.names: + if alias.name == 'tkinter': + analyzer.imports['tkinter'] = alias.asname or 'tkinter' + +def handle_import_from(analyzer, node: ast.ImportFrom): + if node.module == 'tkinter': + for alias in node.names: + analyzer.imports[alias.name] = alias.asname or alias.name + elif node.module == 'tkinter.ttk': + for alias in node.names: + analyzer.imports[alias.name] = alias.asname or alias.name \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/placements.py b/src/parser/tk_ast/analyzer/placements.py new file mode 100644 index 0000000..1e1c755 --- /dev/null +++ b/src/parser/tk_ast/analyzer/placements.py @@ -0,0 +1,39 @@ +import ast + +from .extractors import extract_call_parameters + +def update_widget_placement(analyzer, target_var: str, call_node: ast.Call, method: str): + widget = None + for w in analyzer.widgets: + if w['variable_name'] == target_var: + widget = w + break + + if not widget: + return + + placement_params = extract_call_parameters(analyzer, call_node) + + if method == 'place': + widget['placement'] = { + 'method': 'place', + 'x': placement_params.get('x', 0), + 'y': placement_params.get('y', 0), + 'width': placement_params.get('width'), + 'height': placement_params.get('height'), + 'relx': placement_params.get('relx'), + 'rely': placement_params.get('rely'), + 'relwidth': placement_params.get('relwidth'), + 'relheight': placement_params.get('relheight'), + } + elif method == 'grid': + widget['placement'] = { + 'method': 'grid', + 'row': placement_params.get('row', 0), + 'column': placement_params.get('column', 0), + 'rowspan': placement_params.get('rowspan', 1), + 'columnspan': placement_params.get('columnspan', 1), + 'padx': placement_params.get('padx', 0), + 'pady': placement_params.get('pady', 0), + 'sticky': placement_params.get('sticky', ''), + } \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/values.py b/src/parser/tk_ast/analyzer/values.py new file mode 100644 index 0000000..e16491b --- /dev/null +++ b/src/parser/tk_ast/analyzer/values.py @@ -0,0 +1,149 @@ +import ast +from typing import Any + +def get_variable_name(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + elif isinstance(node, ast.Attribute): + if isinstance(node.value, ast.Name): + return f"{node.value.id}.{node.attr}" + else: + return f"{get_variable_name(node.value)}.{node.attr}" + else: + return str(node) + +def extract_value(node: ast.AST) -> Any: + if isinstance(node, ast.Constant): + return node.value + elif isinstance(node, ast.Str): + return node.s + elif isinstance(node, ast.Num): + return node.n + elif isinstance(node, ast.Name): + return f"${node.id}" + elif isinstance(node, ast.Attribute): + return f"${get_variable_name(node)}" + elif isinstance(node, ast.List): + return [extract_value(item) for item in node.elts] + elif isinstance(node, ast.Tuple): + return tuple(extract_value(item) for item in node.elts) + else: + return str(node) + +def get_operator_symbol(op_node: ast.AST) -> str: + operator_map = { + ast.Add: '+', + ast.Sub: '-', + ast.Mult: '*', + ast.Div: '/', + ast.Mod: '%', + ast.Pow: '**', + ast.LShift: '<<', + ast.RShift: '>>', + ast.BitOr: '|', + ast.BitXor: '^', + ast.BitAnd: '&', + ast.FloorDiv: '//', + ast.Eq: '==', + ast.NotEq: '!=', + ast.Lt: '<', + ast.LtE: '<=', + ast.Gt: '>', + ast.GtE: '>=', + ast.Is: 'is', + ast.IsNot: 'is not', + ast.In: 'in', + ast.NotIn: 'not in', + ast.And: 'and', + ast.Or: 'or', + ast.Not: 'not', + ast.UAdd: '+', + ast.USub: '-', + ast.Invert: '~', + } + return operator_map.get(type(op_node), str(op_node)) + +def analyze_lambda_complexity(lambda_node: ast.Lambda) -> str: + body = lambda_node.body + if isinstance(body, (ast.Constant, ast.Str, ast.Num)): + return 'simple' + elif isinstance(body, (ast.Name, ast.Attribute)): + return 'simple' + elif isinstance(body, (ast.Call, ast.BinOp, ast.Compare)): + return 'medium' + else: + return 'complex' + +def extract_lambda_body(body_node: ast.AST) -> str: + if isinstance(body_node, ast.Constant): + return str(body_node.value) + elif isinstance(body_node, ast.Str): + return f'"{body_node.s}"' + elif isinstance(body_node, ast.Num): + return str(body_node.n) + elif isinstance(body_node, ast.Name): + return body_node.id + elif isinstance(body_node, ast.Attribute): + return get_variable_name(body_node) + elif isinstance(body_node, ast.Call): + func_name = get_variable_name(body_node.func) + args = [extract_lambda_body(arg) for arg in body_node.args] + kwargs = [f"{kw.arg}={extract_lambda_body(kw.value)}" for kw in body_node.keywords if kw.arg] + all_args = args + kwargs + return f"{func_name}({', '.join(all_args)})" + elif isinstance(body_node, ast.BinOp): + left = extract_lambda_body(body_node.left) + right = extract_lambda_body(body_node.right) + op = get_operator_symbol(body_node.op) + return f"({left} {op} {right})" + elif isinstance(body_node, ast.Compare): + left = extract_lambda_body(body_node.left) + comparators = [extract_lambda_body(comp) for comp in body_node.comparators] + ops = [get_operator_symbol(op) for op in body_node.ops] + return f"({left} {' '.join([f'{op} {comp}' for op, comp in zip(ops, comparators)])})" + elif isinstance(body_node, ast.BoolOp): + op = get_operator_symbol(body_node.op) + values = [extract_lambda_body(value) for value in body_node.values] + return f"({f' {op} '.join(values)})" + elif isinstance(body_node, ast.UnaryOp): + op = get_operator_symbol(body_node.op) + operand = extract_lambda_body(body_node.operand) + return f"{op}{operand}" + elif isinstance(body_node, ast.IfExp): + test = extract_lambda_body(body_node.test) + body = extract_lambda_body(body_node.body) + orelse = extract_lambda_body(body_node.orelse) + return f"({body} if {test} else {orelse})" + elif isinstance(body_node, ast.List): + elements = [extract_lambda_body(el) for el in body_node.elts] + return f"[{', '.join(elements)}]" + elif isinstance(body_node, ast.Dict): + keys = [extract_lambda_body(k) for k in body_node.keys] + values = [extract_lambda_body(v) for v in body_node.values] + pairs = [f"{k}: {v}" for k, v in zip(keys, values)] + return f"{{{', '.join(pairs)}}}" + elif isinstance(body_node, ast.Subscript): + value = extract_lambda_body(body_node.value) + if hasattr(ast, 'Index') and isinstance(body_node.slice, ast.Index): + slice_val = extract_lambda_body(body_node.slice.value) + else: + slice_val = extract_lambda_body(body_node.slice) + return f"{value}[{slice_val}]" + elif isinstance(body_node, ast.ListComp): + return "" + elif isinstance(body_node, ast.DictComp): + return "" + elif isinstance(body_node, ast.JoinedStr): + parts = [] + for value in body_node.values: + if isinstance(value, ast.Constant): + parts.append(str(value.value)) + elif isinstance(value, ast.Str): + parts.append(value.s) + else: + parts.append(f"{{{extract_lambda_body(value)}}}") + return f"f\"{''.join(parts)}\"" + elif isinstance(body_node, ast.FormattedValue): + return f"{{{extract_lambda_body(body_node.value)}}}" + else: + return f"" \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/widget_creation.py b/src/parser/tk_ast/analyzer/widget_creation.py new file mode 100644 index 0000000..9b52f08 --- /dev/null +++ b/src/parser/tk_ast/analyzer/widget_creation.py @@ -0,0 +1,96 @@ +import ast +from typing import Dict, Any, Optional + +from .extractors import extract_call_parameters, extract_parent_container +from .values import get_variable_name + +def is_tkinter_widget_call(analyzer, call_node: ast.Call) -> bool: + if not isinstance(call_node.func, ast.Attribute): + return False + + module_name = None + if isinstance(call_node.func.value, ast.Name): + module_name = call_node.func.value.id + elif isinstance(call_node.func.value, ast.Attribute): + full = get_variable_name(call_node.func.value) + parts = full.split('.') + module_name = parts[-1] + + widget_type = call_node.func.attr + + if module_name in ['tk', 'tkinter'] or module_name in analyzer.imports.values(): + return widget_type in ['Label', 'Button', 'Text', 'Checkbutton', 'Radiobutton'] + if module_name in ['ttk'] or 'ttk' in analyzer.imports.values(): + return False + return widget_type in ['Label', 'Button', 'Text', 'Checkbutton', 'Radiobutton'] + +def is_widget_creation(analyzer, node: ast.Assign) -> bool: + if not isinstance(node.value, ast.Call): + return False + + if isinstance(node.value.func, ast.Attribute): + return is_tkinter_widget_call(analyzer, node.value) + elif isinstance(node.value.func, ast.Name): + return node.value.func.id in ['Label', 'Button', 'Text', 'Checkbutton'] + return False + +def extract_widget_info(analyzer, node: ast.Assign) -> Optional[Dict[str, Any]]: + if isinstance(node.targets[0], ast.Name): + variable_name = node.targets[0].id + elif isinstance(node.targets[0], ast.Attribute): + if isinstance(node.targets[0].value, ast.Name) and node.targets[0].value.id == 'self': + variable_name = node.targets[0].attr + else: + return None + else: + return None + + call_node = node.value + + if isinstance(call_node.func, ast.Attribute): + widget_type = call_node.func.attr + if isinstance(call_node.func.value, ast.Name): + module_name = call_node.func.value.id + if module_name in ['ttk'] or 'ttk' in analyzer.imports.values(): + return None + elif isinstance(call_node.func, ast.Name): + widget_type = call_node.func.id + else: + return None + + params = extract_call_parameters(analyzer, call_node) + parent = extract_parent_container(analyzer, call_node) + + return { + 'variable_name': variable_name, + 'type': widget_type, + 'params': params, + 'parent': parent, + 'placement': None, + 'class_context': analyzer.current_class, + 'method_context': analyzer.current_method, + } + +def analyze_widget_creation_commands(analyzer, node: ast.Assign): + if not is_widget_creation(analyzer, node): + return + + call_node = node.value + + for keyword in call_node.keywords: + if keyword.arg == 'command': + if isinstance(node.targets[0], ast.Name): + widget_name = node.targets[0].id + elif isinstance(node.targets[0], ast.Attribute): + widget_name = node.targets[0].attr + else: + continue + + callback_info = analyzer.analyze_callback(keyword.value) + analyzer.command_callbacks.append({ + 'widget': widget_name, + 'command': callback_info, + 'class_context': analyzer.current_class, + 'method_context': analyzer.current_method, + 'created_at_creation': True, + }) diff --git a/src/parser/tk_ast/grid_layout.py b/src/parser/tk_ast/grid_layout.py new file mode 100644 index 0000000..26e6987 --- /dev/null +++ b/src/parser/tk_ast/grid_layout.py @@ -0,0 +1,55 @@ +from typing import Dict, List, Any + + +class GridLayoutAnalyzer: + + def __init__(self): + self.grid_widgets = [] + self.cell_width = 100 + self.cell_height = 40 + self.padding_x = 10 + self.padding_y = 10 + + def analyze_grid_layout(self, widgets: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + grid_widgets = [] + for w in widgets: + placement = w.get('placement') + if placement and placement.get('method') == 'grid': + grid_widgets.append(w) + + if not grid_widgets: + return widgets + + max_row = max((w['placement']['row'] for w in grid_widgets), default=0) + max_col = max((w['placement']['column'] for w in grid_widgets), default=0) + + window_width = 800 + window_height = 600 + + if max_col > 0: + self.cell_width = (window_width - 2 * self.padding_x) // (max_col + 1) + if max_row > 0: + self.cell_height = (window_height - 2 * self.padding_y) // (max_row + 1) + + for widget in grid_widgets: + placement = widget.get('placement', {}) + if not placement: + continue + row = placement.get('row', 0) + col = placement.get('column', 0) + + x = col * self.cell_width + self.padding_x + y = row * self.cell_height + self.padding_y + + width = self.cell_width * placement.get('columnspan', 1) + height = self.cell_height * placement.get('rowspan', 1) + + widget['placement'] = { + 'method': 'place', + 'x': x, + 'y': y, + 'width': width, + 'height': height + } + + return widgets \ No newline at end of file diff --git a/src/parser/tk_ast/parser.py b/src/parser/tk_ast/parser.py new file mode 100644 index 0000000..3e7dc3b --- /dev/null +++ b/src/parser/tk_ast/parser.py @@ -0,0 +1,54 @@ +import ast +import json +from typing import Dict, Any, List + +from tk_ast.analyzer import TkinterAnalyzer +from tk_ast.grid_layout import GridLayoutAnalyzer + + +def parse_tkinter_code(code: str) -> Dict[str, Any]: + try: + tree = ast.parse(code) + analyzer = TkinterAnalyzer() + analyzer.visit(tree) + grid_analyzer = GridLayoutAnalyzer() + widgets = grid_analyzer.analyze_grid_layout(analyzer.widgets) + result: Dict[str, Any] = { + 'window': analyzer.window_config, + 'widgets': widgets, + 'command_callbacks': analyzer.command_callbacks, + 'bind_events': analyzer.bind_events, + 'success': True + } + + return result + + except SyntaxError as e: + return { + 'error': f'Syntax error: {e}', + 'success': False + } + except Exception as e: + return { + 'error': f'Parsing error: {e}', + 'success': False + } + + +def parse_file(filename: str) -> str: + try: + with open(filename, 'r', encoding='utf-8') as f: + code = f.read() + result = parse_tkinter_code(code) + return json.dumps(result, indent=2, ensure_ascii=False) + + except FileNotFoundError: + return json.dumps({ + 'error': f'File not found: {filename}', + 'success': False + }, ensure_ascii=False) + except Exception as e: + return json.dumps({ + 'error': f'File reading error: {e}', + 'success': False + }, ensure_ascii=False) diff --git a/src/parser/tkinter_ast_parser.py b/src/parser/tkinter_ast_parser.py new file mode 100644 index 0000000..e14db7f --- /dev/null +++ b/src/parser/tkinter_ast_parser.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + + +import sys +import json + +try: + from tk_ast.parser import parse_tkinter_code, parse_file +except Exception as e: + raise RuntimeError( + f"Failed to import tk_ast package: {e}. Ensure 'tk_ast' exists next to this script." + ) + + +if __name__ == '__main__': + if len(sys.argv) > 1: + print(parse_file(sys.argv[1])) + else: + code = sys.stdin.read() + result = parse_tkinter_code(code) + print(json.dumps(result, indent=2, ensure_ascii=False)) \ No newline at end of file diff --git a/src/parser/utils.ts b/src/parser/utils.ts new file mode 100644 index 0000000..912f731 --- /dev/null +++ b/src/parser/utils.ts @@ -0,0 +1,21 @@ +export function getDefaultWidth(type: string): number { + const defaults: { [key: string]: number } = { + Label: 100, + Button: 80, + Text: 200, + Checkbutton: 100, + Radiobutton: 100, + }; + return defaults[type] || 100; +} + +export function getDefaultHeight(type: string): number { + const defaults: { [key: string]: number } = { + Label: 25, + Button: 30, + Text: 100, + Checkbutton: 25, + Radiobutton: 25, + }; + return defaults[type] || 25; +} diff --git a/src/webview/TkinterDesignerProvider.ts b/src/webview/TkinterDesignerProvider.ts new file mode 100644 index 0000000..1118f89 --- /dev/null +++ b/src/webview/TkinterDesignerProvider.ts @@ -0,0 +1,237 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { Uri } from 'vscode'; + +export class TkinterDesignerProvider implements vscode.WebviewViewProvider { + public static readonly viewType = 'tkinter-designer'; + public static _instance: TkinterDesignerProvider | undefined; + private _view?: vscode.WebviewPanel; + private _designData: any = { + widgets: [], + events: [], + form: { title: 'My App', size: { width: 800, height: 600 } }, + }; + + constructor(private readonly _extensionUri: vscode.Uri) {} + + public static createOrShow(extensionUri: vscode.Uri) { + const column = vscode.window.activeTextEditor + ? vscode.window.activeTextEditor.viewColumn + : undefined; + if (TkinterDesignerProvider._instance?._view) { + console.log('[Webview] Revealing existing panel'); + TkinterDesignerProvider._instance._view.reveal(column); + return TkinterDesignerProvider._instance; + } + + console.log('[Webview] Creating new panel'); + const panel = vscode.window.createWebviewPanel( + TkinterDesignerProvider.viewType, + 'Tkinter Designer', + column || vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [ + extensionUri, + vscode.Uri.joinPath(extensionUri, 'media'), + vscode.Uri.joinPath(extensionUri, 'src', 'webview'), + vscode.Uri.joinPath(extensionUri, 'out', 'webview'), + ], + } + ); + if (!TkinterDesignerProvider._instance) { + TkinterDesignerProvider._instance = new TkinterDesignerProvider( + extensionUri + ); + } + + TkinterDesignerProvider._instance._view = panel; + TkinterDesignerProvider._instance._setWebviewContent(panel.webview); + TkinterDesignerProvider._instance._setupMessageHandling(panel.webview); + + panel.onDidDispose(() => { + console.log('[Webview] Panel disposed'); + if (TkinterDesignerProvider._instance) { + TkinterDesignerProvider._instance._view = undefined; + } + }); + + return TkinterDesignerProvider._instance; + } + + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken + ) { + this._view = webviewView as any; + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [ + this._extensionUri, + vscode.Uri.joinPath(this._extensionUri, 'src', 'webview'), + vscode.Uri.joinPath(this._extensionUri, 'out', 'webview'), + vscode.Uri.joinPath(this._extensionUri, 'media'), + ], + }; + + this._setWebviewContent(webviewView.webview); + this._setupMessageHandling(webviewView.webview); + } + + private _setWebviewContent(webview: vscode.Webview) { + webview.html = this._getHtmlForWebview(webview); + } + + private _setupMessageHandling(webview: vscode.Webview) { + webview.onDidReceiveMessage((message) => { + console.log('[Webview] Message:', message.type); + switch (message.type) { + case 'designUpdated': + this._designData = message.data; + break; + case 'getDesignData': + webview.postMessage({ + type: 'designData', + data: this._designData, + }); + break; + case 'webviewReady': + if ( + this._designData && + this._designData.widgets && + this._designData.widgets.length > 0 + ) { + webview.postMessage({ + type: 'loadDesign', + data: this._designData, + }); + console.log('[Webview] Sent loadDesign'); + } + break; + case 'generateCode': + console.log('[Webview] Generating code from webview'); + this.handleGenerateCode(message.data); + break; + case 'showInfo': + vscode.window.showInformationMessage(message.text); + break; + case 'showError': + vscode.window.showErrorMessage(message.text); + break; + } + }, undefined); + } + + public async getDesignData(): Promise { + return this._designData; + } + + public loadDesignData(data: any): void { + this._designData = data; + if (this._view) { + const webview = (this._view as any).webview || this._view; + webview.postMessage({ + type: 'loadDesign', + data: this._designData, + }); + console.log('[Webview] loadDesign posted'); + } else { + } + } + + private async handleGenerateCode(designData: any): Promise { + try { + console.log('[GenerateCode] Start'); + const { CodeGenerator } = await import( + '../generator/CodeGenerator' + ); + const generator = new CodeGenerator(); + const pythonCode = generator.generateTkinterCode(designData); + + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor && activeEditor.document.languageId === 'python') { + console.log('[GenerateCode] Writing into active editor'); + const doc = activeEditor.document; + const start = new vscode.Position(0, 0); + const end = doc.lineCount + ? doc.lineAt(doc.lineCount - 1).range.end + : start; + const fullRange = new vscode.Range(start, end); + await activeEditor.edit((editBuilder) => { + editBuilder.replace(fullRange, pythonCode); + }); + await doc.save(); + vscode.window.showInformationMessage( + 'Python code generated into the active file' + ); + } else { + console.log('[GenerateCode] Creating new file'); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage( + 'No workspace folder is open. Please open a folder first.' + ); + return; + } + const fileName = `app_${Date.now()}.py`; + const filePath = path.join( + workspaceFolder.uri.fsPath, + fileName + ); + const fileUri = Uri.file(filePath); + const encoder = new TextEncoder(); + const fileBytes = encoder.encode(pythonCode); + await vscode.workspace.fs.writeFile(fileUri, fileBytes); + + const doc = await vscode.workspace.openTextDocument(fileUri); + await vscode.window.showTextDocument(doc, { + preview: false, + }); + vscode.window.showInformationMessage( + `Python file created: ${fileName}` + ); + } + console.log('[GenerateCode] Done'); + } catch (error) { + console.error('[GenerateCode] Error:', error); + vscode.window.showErrorMessage(`Error generating code: ${error}`); + } + } + + private _getHtmlForWebview(webview: vscode.Webview): string { + const styleUri = webview.asWebviewUri( + vscode.Uri.joinPath( + this._extensionUri, + 'src', + 'webview', + 'style.css' + ) + ); + const reactBundleUri = webview.asWebviewUri( + vscode.Uri.joinPath( + this._extensionUri, + 'out', + 'webview', + 'react-webview.js' + ) + ); + const csp = `default-src 'none'; img-src ${webview.cspSource} https:; style-src ${webview.cspSource} 'unsafe-inline'; script-src ${webview.cspSource};`; + + return ` + + + + + + + Tkinter Designer + + +
+ + + `; + } +} diff --git a/src/webview/preview.html b/src/webview/preview.html new file mode 100644 index 0000000..49979e3 --- /dev/null +++ b/src/webview/preview.html @@ -0,0 +1,13 @@ + + + + + + + Tkinter Designer Preview + + +
+ + + \ No newline at end of file diff --git a/src/webview/react/App.tsx b/src/webview/react/App.tsx new file mode 100644 index 0000000..d205fc2 --- /dev/null +++ b/src/webview/react/App.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Toolbar } from './components/Toolbar'; +import { Palette } from './components/Palette'; +import { PropertiesPanel } from './components/PropertiesPanel'; +import { EventsPanel } from './components/EventsPanel'; +import { Canvas } from './components/Canvas'; +import { useMessaging } from './useMessaging'; + +export function App() { + useMessaging(); + return ( +
+ +
+
+

Widgets

+ + + +
+
+ +
+
+
+ ); +} diff --git a/src/webview/react/components/Canvas.tsx b/src/webview/react/components/Canvas.tsx new file mode 100644 index 0000000..c4e0baf --- /dev/null +++ b/src/webview/react/components/Canvas.tsx @@ -0,0 +1,95 @@ +import React, { useRef } from 'react'; +import { useAppDispatch, useAppState } from '../state'; +import type { WidgetType } from '../types'; + +export function Canvas() { + const dispatch = useAppDispatch(); + const { design, selectedWidgetId } = useAppState(); + const containerRef = useRef(null); + + const onDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const onDrop = (e: React.DragEvent) => { + e.preventDefault(); + const type = e.dataTransfer.getData('text/plain') as WidgetType; + const rect = containerRef.current?.getBoundingClientRect(); + const x = e.clientX - (rect?.left || 0); + const y = e.clientY - (rect?.top || 0); + console.log('[Canvas] Drop widget', type, 'at', x, y); + if (type) dispatch({ type: 'addWidget', payload: { type, x, y } }); + }; + + const onSelect = (id: string | null) => { + console.log('[Canvas] Select widget', id); + dispatch({ type: 'selectWidget', payload: { id } }); + }; + + const onMouseDown = (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const w = design.widgets.find((w) => w.id === id); + if (!w) return; + const initX = w.x; + const initY = w.y; + console.log('[Canvas] Drag start', id, 'at', initX, initY); + const onMove = (ev: MouseEvent) => { + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + dispatch({ + type: 'updateWidget', + payload: { id, patch: { x: initX + dx, y: initY + dy } }, + }); + }; + const onUp = () => { + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + console.log('[Canvas] Drag end', id); + }; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }; + + return ( +
+
onSelect(null)} + > + {design.widgets.length === 0 && ( +
+

Drag widgets here to start designing

+
+ )} + {design.widgets.map((w) => ( +
{ + e.stopPropagation(); + onSelect(w.id); + }} + onMouseDown={(e) => onMouseDown(e, w.id)} + > +
+ {w.properties?.text || w.type} +
+
+ ))} +
+
+ ); +} diff --git a/src/webview/react/components/EventsPanel.tsx b/src/webview/react/components/EventsPanel.tsx new file mode 100644 index 0000000..0701ce2 --- /dev/null +++ b/src/webview/react/components/EventsPanel.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { useAppDispatch, useAppState } from '../state'; + +export function EventsPanel() { + const dispatch = useAppDispatch(); + const { design, selectedWidgetId, vscode } = useAppState(); + const [eventType, setEventType] = useState('command'); + const [eventName, setEventName] = useState('onClick'); + const [eventCode, setEventCode] = useState('print("clicked")'); + + const w = design.widgets.find((x) => x.id === selectedWidgetId); + const widgetEvents = (id: string | undefined) => + (design.events || []).filter((e) => e.widget === id); + + const add = () => { + if (!w) { + vscode.postMessage({ + type: 'showError', + text: 'Select a widget to add events to', + }); + return; + } + if (!eventType || !eventName || !eventCode) { + vscode.postMessage({ + type: 'showError', + text: 'Please fill in all fields: event type, name, and code', + }); + return; + } + dispatch({ + type: 'addEvent', + payload: { + widget: w.id, + type: eventType, + name: eventName, + code: eventCode, + }, + }); + vscode.postMessage({ + type: 'showInfo', + text: `Event added: ${eventType} -> ${eventName}`, + }); + }; + + const remove = (type: string) => { + if (!w) return; + dispatch({ type: 'removeEvent', payload: { widget: w.id, type } }); + vscode.postMessage({ type: 'showInfo', text: 'Event removed' }); + }; + + return ( +
+

Events & Commands

+
+ {!w ? ( +

Select a widget to configure events

+ ) : ( +
+
+ + setEventType(e.target.value)} + /> + + setEventName(e.target.value)} + /> + +