diff --git a/package-lock.json b/package-lock.json index 0d9f2e54d..cddb57685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1891,12 +1891,14 @@ } }, "node_modules/esbuild-loader": { - "version": "3.0.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-3.2.0.tgz", + "integrity": "sha512-lnIdRMQpk50alCa0QoW0ozc0D3rjJXl02mtMsk9INIcW25RPZhDja332bu85ixwVNbhQ7VfBRcQyZ/qza8mWiA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.17.6", - "get-tsconfig": "^4.4.0", + "esbuild": "^0.19.0", + "get-tsconfig": "^4.6.2", "loader-utils": "^2.0.4", "webpack-sources": "^1.4.3" }, @@ -1907,14 +1909,32 @@ "webpack": "^4.40.0 || ^5.0.0" } }, + "node_modules/esbuild-loader/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/esbuild-loader/node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1924,13 +1944,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1940,13 +1961,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1956,13 +1978,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1972,13 +1995,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1988,13 +2012,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2004,13 +2029,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2020,13 +2046,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2036,13 +2063,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2052,13 +2080,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2068,13 +2097,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2084,13 +2114,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2100,13 +2131,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2116,13 +2148,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2132,13 +2165,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2148,7 +2182,9 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-x64": { - "version": "0.17.19", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "cpu": [ "x64" ], @@ -2163,13 +2199,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -2179,13 +2216,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2195,13 +2233,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -2211,13 +2250,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2227,13 +2267,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2243,13 +2284,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2259,7 +2301,9 @@ } }, "node_modules/esbuild-loader/node_modules/esbuild": { - "version": "0.17.19", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2270,28 +2314,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" } }, "node_modules/esbuild-loader/node_modules/webpack-sources": { @@ -3050,9 +3095,14 @@ "license": "MIT" }, "node_modules/get-tsconfig": { - "version": "4.4.0", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, "funding": { "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } @@ -3869,6 +3919,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rimraf": { "version": "3.0.2", "license": "ISC", @@ -4508,9 +4568,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.17", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz", - "integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==", + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c9b87452b..ba31aa7ad 100644 --- a/package.json +++ b/package.json @@ -2651,7 +2651,7 @@ }, { "command": "code-for-ibmi.copyFilter", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "4_filters@2" }, { @@ -2661,27 +2661,27 @@ }, { "command": "code-for-ibmi.moveFilterUp", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "inline" }, { "command": "code-for-ibmi.moveFilterDown", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "inline" }, { "command": "code-for-ibmi.moveFilterToTop", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "5_filters@1" }, { "command": "code-for-ibmi.moveFilterToBottom", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "5_filters@2" }, { "command": "code-for-ibmi.createMember", - "when": "view == objectBrowser && viewItem == SPF", + "when": "view == objectBrowser && !listMultiSelection && viewItem == SPF", "group": "3_sourceFileStuff@1" }, { @@ -2721,7 +2721,7 @@ }, { "submenu": "code-for-ibmi.debug.group", - "when": "view == objectBrowser && viewItem =~ /^object.(pgm|srvpgm).*/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^object.(pgm|srvpgm).*/", "group": "2_debug@1" }, { @@ -2731,17 +2731,17 @@ }, { "command": "code-for-ibmi.setCurrentLibrary", - "when": "!code-for-ibmi:libraryListDisabled && view == objectBrowser && viewItem =~ /library/", + "when": "!code-for-ibmi:libraryListDisabled && !listMultiSelection && view == objectBrowser && viewItem =~ /library/", "group": "2_library@2" }, { "command": "code-for-ibmi.updateMemberText", - "when": "view == objectBrowser && viewItem == member", + "when": "view == objectBrowser && !listMultiSelection && viewItem == member", "group": "2_memberStuff@1" }, { "command": "code-for-ibmi.copyMember", - "when": "view == objectBrowser && viewItem =~ /^member.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^member.*$/", "group": "2_memberStuff@2" }, { @@ -2751,7 +2751,7 @@ }, { "command": "code-for-ibmi.renameMember", - "when": "view == objectBrowser && viewItem == member", + "when": "view == objectBrowser && !listMultiSelection && viewItem == member", "group": "2_memberStuff@3" }, { @@ -2761,7 +2761,7 @@ }, { "command": "code-for-ibmi.uploadAndReplaceMemberAsFile", - "when": "view == objectBrowser && viewItem == member", + "when": "view == objectBrowser && !listMultiSelection && viewItem == member", "group": "3_memberTransfer@2" }, { @@ -2781,12 +2781,12 @@ }, { "submenu": "code-for-ibmi.openIFSFile", - "when": "view == ifsBrowser && !listMultiSelection && viewItem == streamfile", + "when": "view == ifsBrowser && viewItem == streamfile", "group": "0_open@1" }, { "command": "code-for-ibmi.runAction", - "when": "view == ifsBrowser && !listMultiSelection && viewItem == streamfile", + "when": "view == ifsBrowser && viewItem == streamfile", "group": "1_workspace@1" }, { @@ -2821,12 +2821,12 @@ }, { "command": "code-for-ibmi.searchIFS", - "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^directory.*$/", + "when": "view == ifsBrowser && viewItem =~ /^directory.*$/", "group": "3_ifsStuff@1" }, { "command": "code-for-ibmi.ifs.find", - "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^(directory|shortcut).*$/", + "when": "view == ifsBrowser && viewItem =~ /^(directory|shortcut).*$/", "group": "3_ifsStuff@2" }, { @@ -2916,12 +2916,12 @@ }, { "command": "code-for-ibmi.changeObjectDesc", - "when": "view == objectBrowser && (viewItem =~ /^object/ || viewItem == SPF)", + "when": "view == objectBrowser && !listMultiSelection && (viewItem =~ /^object/ || viewItem == SPF)", "group": "1_objActions@1" }, { "command": "code-for-ibmi.copyObject", - "when": "view == objectBrowser && (viewItem =~ /^object/ || viewItem == SPF)", + "when": "view == objectBrowser && !listMultiSelection && (viewItem =~ /^object/ || viewItem == SPF)", "group": "1_objActions@2" }, { @@ -2931,17 +2931,17 @@ }, { "command": "code-for-ibmi.renameObject", - "when": "view == objectBrowser && viewItem =~ /^object/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^object/", "group": "1_objActions@3" }, { "command": "code-for-ibmi.moveObject", - "when": "view == objectBrowser && viewItem =~ /^object(?!.lib)/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^object(?!.lib)/", "group": "1_objActions@4" }, { "command": "code-for-ibmi.createSourceFile", - "when": "view == objectBrowser && viewItem =~ /library/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /library/", "group": "1_LibActions@1" }, { @@ -2988,12 +2988,17 @@ "command": "code-for-ibmi.generateBinderSource", "when": "view == objectBrowser && viewItem =~ /^object.(module|srvpgm).*/", "group": "1_objActions@6" - } + } ], "explorer/context": [ { "submenu": "code-for-ibmi.compareWithLocal", "group": "3_compare@40" + }, + { + "command": "code-for-ibmi.runAction", + "when": "code-for-ibmi:connected && !explorerResourceIsFolder && code-for-ibmi:hasLocalActions", + "group": "1_localactions@01" } ] }, @@ -3041,7 +3046,7 @@ }, "dependencies": { "@ibm/ibmi-eventf-parser": "^1.0.2", - "@vscode-elements/elements": "^1.9.1", + "@vscode-elements/elements": "^1.9.1", "adm-zip": "^0.5.14", "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", "csv": "^6.2.1", diff --git a/src/Instance.ts b/src/Instance.ts index 46eeb9755..baf86ba40 100644 --- a/src/Instance.ts +++ b/src/Instance.ts @@ -1,15 +1,16 @@ +import { EventEmitter } from "stream"; import * as vscode from "vscode"; -import { ConnectionConfig, ConnectionData, IBMiEvent } from "./typings"; +import { ILELibrarySettings } from "./api/CompileTools"; import IBMi, { ConnectionResult } from "./api/IBMi"; import { CodeForIStorage } from "./api/configuration/storage/CodeForIStorage"; -import { handleConnectionResults, messageCallback } from "./ui/connection"; -import { VsStorage } from "./config/Storage"; -import { VsCodeConfig } from "./config/Configuration"; -import { EventEmitter } from "stream"; import { ConnectionStorage } from "./api/configuration/storage/ConnectionStorage"; -import { VscodeTools } from "./ui/Tools"; -import { ILELibrarySettings } from "./api/CompileTools"; +import { VsCodeConfig } from "./config/Configuration"; +import { VsStorage } from "./config/Storage"; import { getEnvConfig } from "./filesystems/local/env"; +import { ConnectionConfig, ConnectionData, IBMiEvent } from "./typings"; +import { VscodeTools } from "./ui/Tools"; +import { handleConnectionResults, messageCallback } from "./ui/connection"; + type IBMiEventSubscription = { func: Function, @@ -19,9 +20,9 @@ type IBMiEventSubscription = { type SubscriptionMap = Map export interface ConnectionOptions { - data: ConnectionData, - reconnecting?: boolean, - reloadServerSettings?: boolean, + data: ConnectionData, + reconnecting?: boolean, + reloadServerSettings?: boolean, onConnectedOperations?: Function[] } @@ -99,15 +100,15 @@ export default class Instance { this.disconnect(); if (reconnect) { - await this.connect({...options, reconnecting: true}); + await this.connect({ ...options, reconnecting: true }); } } }; return VscodeTools.withContext("code-for-ibmi:connecting", async () => { while (true) { - let customError: string|undefined; - await vscode.window.withProgress({location: vscode.ProgressLocation.Notification, title: options.data.name, cancellable: true}, async (p, cancelToken) => { + let customError: string | undefined; + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: options.data.name, cancellable: true }, async (p, cancelToken) => { try { const cancelEmitter = new EventEmitter(); @@ -116,16 +117,16 @@ export default class Instance { }); result = await connection.connect( - options.data, + options.data, { timeoutCallback: timeoutHandler, onConnectedOperations: options.onConnectedOperations || [], uiErrorHandler: handleConnectionResults, - progress: (message) => {p.report(message)}, + progress: (message) => { p.report(message) }, message: messageCallback, cancelEmitter - }, - options.reconnecting, + }, + options.reconnecting, options.reloadServerSettings, ); } catch (e: any) { @@ -144,7 +145,7 @@ export default class Instance { modal: true, detail: `Reconnection has failed. Would you like to try again?\n\n${customError || `No error provided.`}` }, `Yes`)) { - + options.reconnecting = true; continue; @@ -164,7 +165,7 @@ export default class Instance { async disconnect() { await this.setConnection(); - + await Promise.all([ vscode.commands.executeCommand("code-for-ibmi.refreshObjectBrowser"), vscode.commands.executeCommand("code-for-ibmi.refreshLibraryListView"), diff --git a/src/api/Search.ts b/src/api/Search.ts index 68987bf83..990c842bb 100644 --- a/src/api/Search.ts +++ b/src/api/Search.ts @@ -1,8 +1,8 @@ import * as path from 'path'; import { GetMemberInfo } from './components/getMemberInfo'; -import { IBMiMember, SearchHit, SearchResults } from './types'; -import { Tools } from './Tools'; import IBMi from './IBMi'; +import { Tools } from './Tools'; +import { IBMiMember, SearchHit, SearchResults } from './types'; export namespace Search { export async function searchMembers(connection: IBMi, library: string, sourceFile: string, searchTerm: string, members: string|IBMiMember[], readOnly?: boolean,): Promise { diff --git a/src/api/variables.ts b/src/api/variables.ts index b5e2d4da6..a6ce1ede2 100644 --- a/src/api/variables.ts +++ b/src/api/variables.ts @@ -51,7 +51,10 @@ export class Variables extends Map { toPaseVariables(): Record { const variables: Record = {}; for (const [key, value] of this.entries()) { - variables[key.startsWith('&') ? key.substring(1) : key] = value; + //Env variables can't contain * or {}; keeping only &xxx variables + if(key.startsWith('&')){ + variables[key.substring(1)] = value; + } } return variables; } diff --git a/src/commands/actions.ts b/src/commands/actions.ts index 008357249..e6ee88818 100644 --- a/src/commands/actions.ts +++ b/src/commands/actions.ts @@ -1,67 +1,82 @@ import path from "path"; -import { commands, TreeItem, Uri, WorkspaceFolder, window, Disposable } from "vscode"; -import { refreshDiagnosticsFromServer } from "../ui/diagnostics"; +import { commands, Disposable, l10n, TreeItem, Uri, window, WorkspaceFolder } from "vscode"; +import IBMi from "../api/IBMi"; +import { Tools } from "../api/Tools"; import Instance from "../Instance"; import { Action, DeploymentMethod } from "../typings"; import { runAction } from "../ui/actions"; -import IBMi from "../api/IBMi"; +import { refreshDiagnosticsFromServer } from "../ui/diagnostics"; import { BrowserItem } from "../ui/types"; export function registerActionsCommands(instance: Instance): Disposable[] { return [ - commands.registerCommand(`code-for-ibmi.runAction`, async (target: TreeItem | BrowserItem | Uri, group?: any, action?: Action, method?: DeploymentMethod, workspaceFolder?: WorkspaceFolder) => { + commands.registerCommand(`code-for-ibmi.runAction`, async (item?: (TreeItem | BrowserItem | Uri), items?: (TreeItem | BrowserItem | Uri)[], action?: Action, method?: DeploymentMethod, workspaceFolder?: WorkspaceFolder) => { const connection = instance.getConnection()!; - const editor = window.activeTextEditor; - let uri; - let browserItem; - if (target) { - if ("fsPath" in target) { - uri = target; + if (connection) { + const editor = window.activeTextEditor; + const browserItems: BrowserItem[] = []; + const uris: Uri[] = []; + if (!item) { + if (editor?.document.uri) { + uris.push(editor?.document.uri); + } } else { - uri = target?.resourceUri; - if ("refresh" in target) { - browserItem = target; + for (const target of (Array.isArray(items) ? items : [item])) { + if (target instanceof Uri) { + uris.push(target); + } + else if (target.resourceUri) { + uris.push(target.resourceUri); + if (target instanceof BrowserItem) { + browserItems.push(target); + } + } } } - } - uri = uri || editor?.document.uri; + const scheme = uris[0]?.scheme; + if (scheme) { + if (!uris.every(uri => uri.scheme === scheme)) { + window.showWarningMessage(l10n.t("Actions can't be run on multiple items of different natures. ({0})", uris.map(uri => uri.scheme).filter(Tools.distinct).join(", "))); + return false; + } - if (uri) { - if (connection) { - const config = connection.getConfig(); - let canRun = true; - if (editor && uri.path === editor.document.uri.path && editor.document.isDirty) { - if (config.autoSaveBeforeAction) { - await editor.document.save(); - } else { - const result = await window.showWarningMessage(`The file must be saved to run Actions.`, `Save`, `Save automatically`, `Cancel`); - switch (result) { - case `Save`: - await editor.document.save(); - canRun = true; - break; - case `Save automatically`: - config.autoSaveBeforeAction = true; - await IBMi.connectionManager.update(config); - await editor.document.save(); - canRun = true; - break; - default: - canRun = false; - break; + const config = connection.getConfig(); + + for (const openedEditor of window.visibleTextEditors) { + const path = openedEditor.document.uri.path; + if (uris.some(uri => uri.path === path) && openedEditor.document.isDirty) { + if (config.autoSaveBeforeAction) { + await openedEditor.document.save(); + } else { + const result = await window.showWarningMessage(`File ${path} must be saved to run Actions.`, `Save`, `Save automatically`, `Cancel`); + switch (result) { + case `Save`: + await openedEditor.document.save(); + break; + + case `Save automatically`: + config.autoSaveBeforeAction = true; + await IBMi.connectionManager.update(config); + await openedEditor.document.save(); + break; + + default: + return; + } } } } - if (canRun && [`member`, `streamfile`, `file`, 'object'].includes(uri.scheme)) { - return await runAction(instance, uri, action, method, browserItem, workspaceFolder); + if ([`member`, `streamfile`, `file`, 'object'].includes(scheme)) { + return await runAction(instance, uris, action, method, browserItems, workspaceFolder); } } - else { - window.showErrorMessage('Please connect to an IBM i first'); - } + + } + else { + window.showErrorMessage('Please connect to an IBM i first'); } return false; @@ -94,7 +109,7 @@ export function registerActionsCommands(instance: Instance): Disposable[] { let initialPath = ``; const editor = window.activeTextEditor; const connection = instance.getConnection(); - + if (editor && connection) { const config = connection.getConfig(); const uri = editor.document.uri; diff --git a/src/commands/open.ts b/src/commands/open.ts index ad412086d..d2f04fd3a 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -47,10 +47,10 @@ export function registerOpenCommands(instance: Instance): Disposable[] { } try { - if(options.position){ + if (options.position) { await commands.executeCommand(`vscode.openWith`, uri, 'default', { selection: options.position } as TextDocumentShowOptions); } - else{ + else { await commands.executeCommand(`vscode.open`, uri); } @@ -71,17 +71,17 @@ export function registerOpenCommands(instance: Instance): Disposable[] { } }), - commands.registerCommand("code-for-ibmi.browse", (item: WithPath | MemberItem) => { - return commands.executeCommand("code-for-ibmi.openWithDefaultMode", item, "browse" as DefaultOpenMode); + commands.registerCommand("code-for-ibmi.browse", (item: WithPath | MemberItem, items?: WithPath | MemberItem) => { + return commands.executeCommand("code-for-ibmi.openWithDefaultMode", items || item, "browse" as DefaultOpenMode); }), - commands.registerCommand("code-for-ibmi.edit", (item: WithPath | MemberItem) => { - return commands.executeCommand("code-for-ibmi.openWithDefaultMode", item, "edit" as DefaultOpenMode); + commands.registerCommand("code-for-ibmi.edit", (item: WithPath | MemberItem, items?: WithPath | MemberItem) => { + return commands.executeCommand("code-for-ibmi.openWithDefaultMode", items || item, "edit" as DefaultOpenMode); }), - commands.registerCommand("code-for-ibmi.openWithDefaultMode", (item: WithPath, overrideMode?: DefaultOpenMode, position?: Range) => { + commands.registerCommand("code-for-ibmi.openWithDefaultMode", (items: WithPath | WithPath[], overrideMode?: DefaultOpenMode, position?: Range) => { const readonly = (overrideMode || IBMi.connectionManager.get("defaultOpenMode")) === "browse"; - commands.executeCommand(`code-for-ibmi.openEditable`, item.path, { readonly, position } as OpenEditableOptions); + (Array.isArray(items) ? items : [items]).forEach(item => commands.executeCommand(`code-for-ibmi.openEditable`, item.path, { readonly, position } as OpenEditableOptions)); }), @@ -121,7 +121,7 @@ export function registerOpenCommands(instance: Instance): Disposable[] { const LOADING_LABEL = `Please wait`; const connection = instance.getConnection(); if (!connection) return; - + const storage = instance.getStorage(); const content = connection?.getContent(); let starRemoved: boolean = false; @@ -367,11 +367,11 @@ export function registerOpenCommands(instance: Instance): Disposable[] { window.showInformationMessage(`Cleared cached files.`); } else { const selectionSplit = connection!.upperCaseName(selection).split('/') - if ([3,4].includes(selectionSplit.length) || selection.startsWith(`/`)) { + if ([3, 4].includes(selectionSplit.length) || selection.startsWith(`/`)) { // When selection is QSYS path if (!selection.startsWith(`/`) && connection) { - if(selectionSplit.length === 4){ + if (selectionSplit.length === 4) { //Remove the iASP part selectionSplit.shift(); } diff --git a/src/filesystems/local/actions.ts b/src/filesystems/local/actions.ts index 512139d8d..80c26a315 100644 --- a/src/filesystems/local/actions.ts +++ b/src/filesystems/local/actions.ts @@ -1,12 +1,15 @@ import { RelativePattern, window, workspace, WorkspaceFolder } from "vscode"; import { Action } from "../../api/types"; +export async function getLocalActionsFiles(currentWorkspace?: WorkspaceFolder) { + return currentWorkspace ? await workspace.findFiles(new RelativePattern(currentWorkspace, `**/.vscode/actions.json`)) : []; +} + export async function getLocalActions(currentWorkspace: WorkspaceFolder) { const actions: Action[] = []; if (currentWorkspace) { - const relativeSearch = new RelativePattern(currentWorkspace, `**/.vscode/actions.json`); - const actionsFiles = await workspace.findFiles(relativeSearch); + const actionsFiles = await getLocalActionsFiles(currentWorkspace); for (const file of actionsFiles) { const actionsContent = await workspace.fs.readFile(file); diff --git a/src/filesystems/local/deployment.ts b/src/filesystems/local/deployment.ts index 0dc1190b2..1a01606ea 100644 --- a/src/filesystems/local/deployment.ts +++ b/src/filesystems/local/deployment.ts @@ -3,13 +3,13 @@ import path from 'path'; import tar from 'tar'; import tmp from 'tmp'; import vscode from 'vscode'; -import { instance } from '../../instantiate'; import IBMi from '../../api/IBMi'; +import IBMiContent from '../../api/IBMiContent'; import { Tools } from '../../api/Tools'; -import { getLocalActions } from './actions'; -import { DeployTools } from './deployTools'; +import { instance } from '../../instantiate'; import { DeploymentParameters } from '../../typings'; -import IBMiContent from '../../api/IBMiContent'; +import { getLocalActions, getLocalActionsFiles } from './actions'; +import { DeployTools } from './deployTools'; export namespace Deployment { export interface MD5Entry { @@ -43,7 +43,7 @@ export namespace Deployment { const workspaces = vscode.workspace.workspaceFolders; if (workspaces && workspaces.length > 0) { - buildWatcher().then(context.subscriptions.push); + workspaceWatcher().then(context.subscriptions.push); } instance.subscribe( @@ -125,7 +125,7 @@ export namespace Deployment { }); } - async function buildWatcher() { + async function workspaceWatcher() { const invalidFs = [`member`, `streamfile`]; const watcher = vscode.workspace.createFileSystemWatcher(`**`); @@ -143,19 +143,40 @@ export namespace Deployment { } } + const checkLocalActionsFiles = async (uri: vscode.Uri | vscode.WorkspaceFolder) => { + let workspace: vscode.WorkspaceFolder | undefined; + if (uri instanceof vscode.Uri) { + if (uri.path.endsWith('/.vscode/actions.json')) { + workspace = vscode.workspace.getWorkspaceFolder(uri); + } + else { + return; + } + } + else { + workspace = uri; + } + + vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasLocalActions`, (await getLocalActionsFiles(workspace)).length > 0); + }; + watcher.onDidChange(uri => { getChangesMap(uri)?.set(uri.fsPath, uri); }); watcher.onDidCreate(async uri => { + checkLocalActionsFiles(uri); const fileStat = await vscode.workspace.fs.stat(uri); if (fileStat.type === vscode.FileType.File) { getChangesMap(uri)?.set(uri.fsPath, uri); } }); watcher.onDidDelete(uri => { + checkLocalActionsFiles(uri); getChangesMap(uri)?.delete(uri.fsPath); }); + vscode.workspace.workspaceFolders?.forEach(checkLocalActionsFiles); + return watcher; } diff --git a/src/testing/action.ts b/src/testing/action.ts index fc5ca379f..c2ff65746 100644 --- a/src/testing/action.ts +++ b/src/testing/action.ts @@ -161,6 +161,120 @@ export const ActionSuite: TestSuite = { const success = await runAction(instance, uri, action, `all`); assert.strictEqual(success, false); } + }, + { + name: "Fail to run action on uri with different schemes", test: async () => { + const uris = [ + vscode.Uri.parse("streamfile:///home/someone/meh.txt"), + vscode.Uri.parse("member:///QTEMP/SOMETHING.RPGLE"), + vscode.Uri.parse("file://loca/youwish.txt") + ]; + assert.strictEqual(await runAction(instance, uris, { + name: "It won't run", + command: "wont run", + environment: "ile", + }), false); + } + }, + { + name: "Run multiple objects action", test: async () => { + const dtaaras = [1, 2, 3, 4, 5].map(num => vscode.Uri.parse(`object:/QTEMP/DATAAREA${num}.DTAARA`)); + const result = await runAction(instance, dtaaras, { + name: "Create Data Area", + command: "CRTDTAARA DTAARA(&LIBRARY/&NAME) TYPE(*CHAR) LEN(10) VALUE('&NAME')", + type: "object", + environment: "ile" + }); + + assert.ok(result); + } + }, + { + name: "Run multiple members action", test: async () => { + const connection = instance.getConnection()!; + const config = connection.getConfig(); + const content = connection.getContent(); + const testlib = config.tempLibrary; + const file = "XX" + Tools.makeid(6); + try { + await connection.runCommand({ command: `CRTSRCPF FILE(${testlib}/${file}) RCDLEN(112)`, noLibList: true }); + + const members = []; + for (let i = 1; i < 6; i++) { + const member = `MEMBER${i}`; + const addpfm = await connection.runCommand({ command: `ADDPFM FILE(${testlib}/${file}) MBR(${member}) SRCTYPE(CLLE)` }); + if (addpfm.code !== 0) { + throw new Error(`Failed to add member: ${addpfm.stderr}`) + } + + await content.uploadMemberContent(testlib, file, member, ['PGM', ` DLYJOB ${i}`, `ENDPGM`].join('\n')); + members.push(vscode.Uri.parse(`member:/${config.tempLibrary}/${file}/MEMBER${i}.CLLE`)); + } + + const result = await runAction(instance, members, { + name: "Create Bound CL Program", + command: "CRTBNDCL PGM(QTEMP/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF)", + type: "member", + environment: "ile" + }); + + assert.ok(result); + } + finally { + await connection.runCommand({ command: `DLTF FILE(${testlib}/${file})`, noLibList: true }); + } + } + }, + { + name: "Run multiple streamfiles action", test: async () => { + const connection = instance.getConnection()!; + const config = connection.getConfig(); + const content = connection.getContent(); + const testLib = config.tempLibrary; + const table = "YY" + Tools.makeid(6); + + try { + await connection.runSQL(`Create or replace table ${testLib}.${table} (key varchar(10), value varchar(100));`); + + await connection.withTempDirectory(async directory => { + const files = []; + for (let i = 1; i < 6; i++) { + const file = `statement_${i}.sql`; + await content.writeStreamfileRaw(`${directory}/${file}`, `insert into ${testLib}.${table} values ('hello_${i}', 'world_${i}');`); + files.push(vscode.Uri.parse(`streamfile:${directory}/${file}`)); + } + + const result = await runAction(instance, files, { + name: "Run SQL statement", + command: "RUNSQLSTM SRCSTMF('&FULLPATH') COMMIT(*NONE) NAMING(*SQL)", + type: "streamfile", + environment: "ile" + }); + + assert.ok(result); + + const rows = await connection.runSQL(`Select key, value from ${testLib}.${table}`); + assert.strictEqual(rows.length, 5); + }) + } + finally { + await connection.runCommand({ command: `DLTF FILE(${testLib}/${table})`, noLibList: true }); + } + } + }, + { + name: "Run multiple local files action", test: async () => { + const uris = helloWorldProject.files!.map(file => file.localPath!); + const action: Action = { + command: `[ -e "&FULLPATH" ] && attr "&FULLPATH" CCSID`, + environment: `pase`, + type: `file`, + name: `Check file CCSID`, + }; + + const success = await runAction(instance, uris, action, `compare`); + assert.ok(success); + } } ] }; diff --git a/src/ui/actions.ts b/src/ui/actions.ts index fcc426df8..780e0ceae 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -1,5 +1,9 @@ - import path from 'path'; +import vscode, { CustomExecution, Pseudoterminal, TaskGroup, TaskRevealKind, WorkspaceFolder, commands, l10n, tasks } from 'vscode'; +import { CompileTools } from '../api/CompileTools'; +import IBMi from '../api/IBMi'; +import { Tools } from '../api/Tools'; +import { Variables } from '../api/variables'; import { getLocalActions } from '../filesystems/local/actions'; import { DeployTools } from '../filesystems/local/deployTools'; import { getBranchLibraryName, getEnvConfig } from '../filesystems/local/env'; @@ -7,21 +11,28 @@ import { getGitBranch } from '../filesystems/local/git'; import { parseFSOptions } from '../filesystems/qsys/QSysFs'; import Instance from '../Instance'; import { Action, DeploymentMethod } from '../typings'; +import { CustomUI, TreeListItem } from '../webviews/CustomUI'; import { EvfEventInfo, refreshDiagnosticsFromLocal, refreshDiagnosticsFromServer, registerDiagnostics } from './diagnostics'; -import vscode, { CustomExecution, Pseudoterminal, TaskGroup, TaskRevealKind, WorkspaceFolder, commands, tasks } from 'vscode'; -import { CompileTools } from '../api/CompileTools'; -import IBMi from '../api/IBMi'; -import { Tools } from '../api/Tools'; -import { Variables } from '../api/variables'; -import { CustomUI } from '../webviews/CustomUI'; import { BrowserItem } from './types'; -interface CommandObject { +type CommandObject = { object: string library?: string } +type ActionTarget = { + uri: vscode.Uri + extension: string + fragment: string + protected: boolean + workspaceFolder: vscode.WorkspaceFolder + executionOK: boolean, + hasRun: boolean, + processed: boolean, + output: string[] +} + const actionUsed: Map = new Map; const PARM_REGEX = /(PNLGRP|OBJ|PGM|MODULE)\((?.+?)\)/; @@ -31,22 +42,38 @@ export function registerActionTools(context: vscode.ExtensionContext) { ); } -export async function runAction(instance: Instance, uri: vscode.Uri, customAction?: Action, method?: DeploymentMethod, browserItem?: BrowserItem, workspaceFolder?: WorkspaceFolder): Promise { - const connection = instance.getConnection(); +export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Uri[], customAction?: Action, method?: DeploymentMethod, browserItems?: BrowserItem[], workspaceFolder?: WorkspaceFolder): Promise { + uris = Array.isArray(uris) ? uris : [uris]; + //Global scheme: all URIs share the same + const scheme = uris[0].scheme; + if (!uris.every(uri => uri.scheme === scheme)) { + vscode.window.showWarningMessage(l10n.t("Actions can't be run on multiple items of different natures. ({0})", uris.map(uri => uri.scheme).filter(Tools.distinct).join(", "))); + return false; + } - const uriOptions = parseFSOptions(uri); + const connection = instance.getConnection(); if (connection) { const config = connection.getConfig(); const content = connection.getContent(); - const extension = uri.path.substring(uri.path.lastIndexOf(`.`) + 1).toUpperCase(); - const fragment = uri.fragment.toUpperCase(); - - const isProtected = uriOptions.readonly || config?.readOnlyMode; - - if (!workspaceFolder) { - workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + const targets = uris.map(uri => ({ + uri, + extension: uri.path.substring(uri.path.lastIndexOf(`.`) + 1).toUpperCase(), + fragment: uri.fragment.toUpperCase(), + protected: parseFSOptions(uri).readonly || config?.readOnlyMode, + workspaceFolder: workspaceFolder || vscode.workspace.getWorkspaceFolder(uri), + executionOK: false, + hasRun: false, + processed: false, + output: [] + }) as ActionTarget); + + workspaceFolder = targets[0].workspaceFolder; + if (!targets.every(target => target.workspaceFolder === workspaceFolder)) { + vscode.window.showErrorMessage(l10n.t("Actions can only be run on files from the same workspace")); + return false; } + let remoteCwd = config?.homeDirectory || `.`; let availableActions: { label: string; action: Action; }[] = []; @@ -56,8 +83,8 @@ export async function runAction(instance: Instance, uri: vscode.Uri, customActio // Then, if we're being called from a local file // we fetch the Actions defined from the workspace. - if (workspaceFolder && uri.scheme === `file`) { - const localActions = await getLocalActions(workspaceFolder); + if (targets[0].workspaceFolder && scheme === `file`) { + const localActions = await getLocalActions(targets[0].workspaceFolder); allActions.push(...localActions); } @@ -69,7 +96,9 @@ export async function runAction(instance: Instance, uri: vscode.Uri, customActio }); // Then we get all the available Actions for the current context - availableActions = allActions.filter(action => action.type === uri.scheme && (!action.extensions || action.extensions.includes(extension) || action.extensions.includes(fragment) || action.extensions.includes(`GLOBAL`)) && (!isProtected || action.runOnProtected)) + availableActions = allActions.filter(action => action.type === scheme) + .filter(action => !action.extensions || action.extensions.every(e => !e) || targets.every(t => action.extensions!.includes(t.extension) || action.extensions!.includes(t.fragment)) || action.extensions.includes(`GLOBAL`)) + .filter(action => action.runOnProtected || !targets.some(t => t.protected)) .sort((a, b) => (actionUsed.get(b.name) || 0) - (actionUsed.get(a.name) || 0)) .map(action => ({ label: action.name, @@ -109,457 +138,447 @@ export async function runAction(instance: Instance, uri: vscode.Uri, customActio } } - let fromWorkspace: WorkspaceFolder | undefined; + const fromWorkspace = (chosenAction.type === `file` && vscode.workspace.workspaceFolders) ? vscode.workspace.workspaceFolders[workspaceId || 0] : undefined; + const envFileVars = workspaceFolder ? await getEnvConfig(workspaceFolder) : {}; + + const commandConfirm = async (commandString: string): Promise => { + const commands = commandString.split(`\n`).filter(command => command.trim().length > 0); + const promptedCommands = []; + for (let command of commands) { + if (command.startsWith(`?`)) { + command = await vscode.window.showInputBox({ prompt: `Run Command`, value: command.substring(1) }) || ''; + } else { + command = await showCustomInputs(`Run Command`, command, chosenAction.name || `Command`); + } + promptedCommands.push(command); + if (!command) break; + } - if (chosenAction.type === `file` && vscode.workspace.workspaceFolders) { - fromWorkspace = vscode.workspace.workspaceFolders[workspaceId || 0]; + return !promptedCommands.includes(``) ? promptedCommands.join(`\n`) : ``; } - const variables = new Variables(connection); - const evfeventInfo: EvfEventInfo = { - object: '', - library: '', - extension, - workspace: fromWorkspace - }; + let cancelled = false; - if (workspaceFolder) { - for (const [key, value] of Object.entries(await getEnvConfig(workspaceFolder))) { - variables.set(`&${key}`, value) - } - } + //Prompt once now in case of multiple targets + const promptOnce = targets.length > 1; + const command = promptOnce ? chosenAction.command : await commandConfirm(chosenAction.command); - switch (chosenAction.type) { - case `member`: - const memberDetail = connection.parserMemberPath(uri.path); - evfeventInfo.library = memberDetail.library; - evfeventInfo.object = memberDetail.name; - evfeventInfo.extension = memberDetail.extension; - evfeventInfo.asp = memberDetail.asp; - - variables.set(`&OPENLIBL`, memberDetail.library.toLowerCase()) - .set(`&OPENLIB`, memberDetail.library) - - .set(`&OPENSPFL`, memberDetail.file.toLowerCase()) - .set(`&OPENSPF`, memberDetail.file) - - .set(`&OPENMBRL`, memberDetail.name.toLowerCase()) - .set(`&OPENMBR`, memberDetail.name) - - .set(`&EXTL`, memberDetail.extension.toLowerCase()) - .set(`&EXT`, memberDetail.extension); - break; - - case `file`: - case `streamfile`: - const pathData = path.parse(uri.path); - const basename = pathData.base; - const ext = pathData.ext ? (pathData.ext.startsWith(`.`) ? pathData.ext.substring(1) : pathData.ext) : ``; - const parent = path.parse(pathData.dir).base; - let name = pathData.name; - - // Logic to handle second extension, caused by bob. - const bobTypes = [`.PGM`, `.SRVPGM`]; - const secondName = path.parse(name); - if (secondName.ext && bobTypes.includes(secondName.ext.toUpperCase())) { - name = secondName.name; + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: true, title: l10n.t("Running action {0} on {1} item(s)", chosenAction.name, targets.length) }, async (task, canceled) => { + const increment = 100 / targets.length; + let done = 1; + for (const target of targets) { + if (canceled.isCancellationRequested) { + cancelled = true; + return; } - - // Remove bob text convention - if (name.includes(`-`)) { - name = name.substring(0, name.indexOf(`-`)); + task.report({ message: `${done++}/${targets.length}`, increment }) + target.processed = true; + const variables = new Variables(connection); + if (target.workspaceFolder) { + for (const [key, value] of Object.entries(await getEnvConfig(target.workspaceFolder))) { + variables.set(`&${key}`, value) + } } + Object.entries(envFileVars).forEach(([key, value]) => variables.set(`&${key}`, value)); + const evfeventInfo: EvfEventInfo = { + object: '', + library: '', + extension: target.extension, + workspace: fromWorkspace + }; - evfeventInfo.library = connection.upperCaseName(variables.get(`&CURLIB`) || config.currentLibrary); - evfeventInfo.object = connection.upperCaseName(name); - evfeventInfo.extension = ext; + switch (chosenAction.type) { + case `member`: + const memberDetail = connection.parserMemberPath(target.uri.path); + evfeventInfo.library = memberDetail.library; + evfeventInfo.object = memberDetail.name; + evfeventInfo.extension = memberDetail.extension; + evfeventInfo.asp = memberDetail.asp; - if (chosenAction.command.includes(`&SRCFILE`)) { - variables.set(`&SRCLIB`, evfeventInfo.library) - .set(`&SRCPF`, `QTMPSRC`) - .set(`&SRCFILE`, `${evfeventInfo.library}/QTMPSRC`); - } + variables.set(`&OPENLIBL`, memberDetail.library.toLowerCase()) + .set(`&OPENLIB`, memberDetail.library) - switch (chosenAction.type) { - case `file`: - variables.set(`&LOCALPATH`, uri.fsPath); - if (fromWorkspace) { - const relativePath = path.relative(fromWorkspace.uri.path, uri.path).split(path.sep).join(path.posix.sep); - // We need to make sure the remote path is posix - const fullPath = path.posix.join(remoteCwd, relativePath); - variables.set(`&RELATIVEPATH`, relativePath) - .set(`&FULLPATH`, fullPath) - .set(`{path}`, fullPath) - .set(`&WORKDIR`, remoteCwd) - .set(`&FILEDIR`, path.posix.parse(fullPath).dir); - - const branch = getGitBranch(fromWorkspace); - if (branch) { - variables.set(`&BRANCHLIB`, getBranchLibraryName(branch)) - .set(`&BRANCH`, branch) - .set(`{branch}`, branch); - } - } + .set(`&OPENSPFL`, memberDetail.file.toLowerCase()) + .set(`&OPENSPF`, memberDetail.file) + + .set(`&OPENMBRL`, memberDetail.name.toLowerCase()) + .set(`&OPENMBR`, memberDetail.name) + + .set(`&EXTL`, memberDetail.extension.toLowerCase()) + .set(`&EXT`, memberDetail.extension); break; + case `file`: case `streamfile`: - const relativePath = path.posix.relative(remoteCwd, uri.path); - const fullName = uri.path; - variables.set(`&RELATIVEPATH`, relativePath) - .set(`&FULLPATH`, fullName) - .set(`&FILEDIR`, path.parse(fullName).dir); - break; - } + const pathData = path.parse(target.uri.path); + const basename = pathData.base; + const ext = pathData.ext ? (pathData.ext.startsWith(`.`) ? pathData.ext.substring(1) : pathData.ext) : ``; + const parent = path.parse(pathData.dir).base; + let name = pathData.name; + + // Logic to handle second extension, caused by bob. + const bobTypes = [`.PGM`, `.SRVPGM`]; + const secondName = path.parse(name); + if (secondName.ext && bobTypes.includes(secondName.ext.toUpperCase())) { + name = secondName.name; + } - variables.set(`&PARENT`, parent) - .set(`&BASENAME`, basename) - .set(`{filename}`, basename) + // Remove bob text convention + if (name.includes(`-`)) { + name = name.substring(0, name.indexOf(`-`)); + } - .set(`&NAMEL`, name.toLowerCase()) - .set(`&NAME`, name) + evfeventInfo.library = connection.upperCaseName(variables.get(`&CURLIB`) || config.currentLibrary); + evfeventInfo.object = connection.upperCaseName(name); + evfeventInfo.extension = ext; - .set(`&EXTL`, extension.toLowerCase()) - .set(`&EXT`, extension); - break; + if (chosenAction.command.includes(`&SRCFILE`)) { + variables.set(`&SRCLIB`, evfeventInfo.library) + .set(`&SRCPF`, `QTMPSRC`) + .set(`&SRCFILE`, `${evfeventInfo.library}/QTMPSRC`); + } - case `object`: - const [_, library, fullName] = uri.path.toUpperCase().split(`/`); - const object = fullName.substring(0, fullName.lastIndexOf(`.`)); + switch (chosenAction.type) { + case `file`: + variables.set(`&LOCALPATH`, target.uri.fsPath); + if (fromWorkspace) { + const relativePath = path.relative(fromWorkspace.uri.path, target.uri.path).split(path.sep).join(path.posix.sep); + // We need to make sure the remote path is posix + const fullPath = path.posix.join(remoteCwd, relativePath); + variables.set(`&RELATIVEPATH`, relativePath) + .set(`&FULLPATH`, fullPath) + .set(`{path}`, fullPath) + .set(`&WORKDIR`, remoteCwd) + .set(`&FILEDIR`, path.posix.parse(fullPath).dir); + + const branch = getGitBranch(fromWorkspace); + if (branch) { + variables.set(`&BRANCHLIB`, getBranchLibraryName(branch)) + .set(`&BRANCH`, branch) + .set(`{branch}`, branch); + } + } + break; + + case `streamfile`: + const relativePath = path.posix.relative(remoteCwd, target.uri.path); + const fullName = target.uri.path; + variables.set(`&RELATIVEPATH`, relativePath) + .set(`&FULLPATH`, fullName) + .set(`&FILEDIR`, path.parse(fullName).dir); + break; + } - evfeventInfo.library = library; - evfeventInfo.object = object; + variables.set(`&PARENT`, parent) + .set(`&BASENAME`, basename) + .set(`{filename}`, basename) - variables.set(`&LIBRARYL`, library.toLowerCase()) - .set(`&LIBRARY`, library) + .set(`&NAMEL`, name.toLowerCase()) + .set(`&NAME`, name) - .set(`&NAMEL`, object.toLowerCase()) - .set(`&NAME`, object) + .set(`&EXTL`, target.extension.toLowerCase()) + .set(`&EXT`, target.extension); + break; - .set(`&TYPEL`, extension.toLowerCase()) - .set(`&TYPE`, extension) + case `object`: + const [_, library, fullName] = connection.upperCaseName(target.uri.path).split(`/`); + const object = fullName.substring(0, fullName.lastIndexOf(`.`)); - .set(`&EXTL`, extension.toLowerCase()) - .set(`&EXT`, extension); - break; - } + evfeventInfo.library = library; + evfeventInfo.object = object; - const viewControl = IBMi.connectionManager.get(`postActionView`) || "none"; - const outputBuffer: string[] = []; - let actionName = chosenAction.name; - let hasRun = false; + variables.set(`&LIBRARYL`, library.toLowerCase()) + .set(`&LIBRARY`, library) - const commandConfirm = async (commandString: string): Promise => { - const commands = commandString.split(`\n`).filter(command => command.trim().length > 0); - const promptedCommands = []; - for (let command of commands) { - if (command.startsWith(`?`)) { - command = await vscode.window.showInputBox({ prompt: `Run Command`, value: command.substring(1) }) || ''; - } else { - command = await showCustomInputs(`Run Command`, command, chosenAction.name || `Command`); - } - promptedCommands.push(command); - if (!command) break; - } + .set(`&NAMEL`, object.toLowerCase()) + .set(`&NAME`, object) - return !promptedCommands.includes(``) ? promptedCommands.join(`\n`) : ``; - } + .set(`&TYPEL`, target.extension.toLowerCase()) + .set(`&TYPE`, target.extension) - const exitCode = await new Promise(resolve => - tasks.executeTask({ - isBackground: true, - name: chosenAction.name, - definition: { type: `ibmi` }, - scope: workspaceFolder, - source: 'IBM i', - presentationOptions: { - showReuseMessage: true, - clear: IBMi.connectionManager.get(`clearOutputEveryTime`), - focus: false, - reveal: (viewControl === `task` ? TaskRevealKind.Always : TaskRevealKind.Never), - }, - problemMatchers: [], - runOptions: {}, - group: TaskGroup.Build, - execution: new CustomExecution(async (e) => { - const writeEmitter = new vscode.EventEmitter(); - const closeEmitter = new vscode.EventEmitter(); - - writeEmitter.event(s => outputBuffer.push(s)); - closeEmitter.event(resolve); - - const term: Pseudoterminal = { - onDidWrite: writeEmitter.event, - onDidClose: closeEmitter.event, - open: async (initialDimensions: vscode.TerminalDimensions | undefined) => { - let successful = false; - let problemsFetched = false; - - try { - writeEmitter.fire(`Running Action: ${chosenAction.name} (${new Date().toLocaleTimeString()})` + CompileTools.NEWLINE); - - // If &SRCFILE is set, we need to copy the file to a temporary source file from the IFS - if (variables.has(`&FULLPATH`) && variables.has(`&SRCFILE`) && evfeventInfo.object) { - const [lib, srcpf] = variables.get(`&SRCFILE`)!.split(`/`); - - const createSourceFile = content.toCl(`CRTSRCPF`, { - rcdlen: 112, //NICE: this configurable in a VS Code setting? - file: `${lib}/${srcpf}`, - }); - - const copyFromStreamfile = content.toCl(`CPYFRMSTMF`, { - fromstmf: variables.get(`&FULLPATH`), - tombr: `'${Tools.qualifyPath(lib, srcpf, evfeventInfo.object)}'`, - mbropt: `*REPLACE`, - dbfccsid: `*FILE`, - stmfccsid: 1208, - }); - - // We don't care if this fails. Usually it's because the source file already exists. - await CompileTools.runCommand(connection, { command: createSourceFile, environment: `ile`, noLibList: true }); - - // Attempt to copy to member - const copyResult = await CompileTools.runCommand(connection, { command: copyFromStreamfile, environment: `ile`, noLibList: true }); - - if (copyResult.code !== 0) { - writeEmitter.fire(`Failed to copy file to a temporary member.\n\t${copyResult.stderr}\n\n`); - closeEmitter.fire(copyResult.code || 1); - } - } + .set(`&EXTL`, target.extension.toLowerCase()) + .set(`&EXT`, target.extension); + break; + } - const commandResult = await CompileTools.runCommand(connection, - { - title: chosenAction.name, - environment, - command: chosenAction.command, - cwd: remoteCwd, - env: variables, - }, { - writeEvent: (content) => writeEmitter.fire(content), - commandConfirm - } - ); + const viewControl = IBMi.connectionManager.get(`postActionView`) || "none"; + let actionName = chosenAction.name; + + const exitCode = await new Promise(resolve => + tasks.executeTask({ + isBackground: true, + name: chosenAction.name, + definition: { type: `ibmi` }, + scope: workspaceFolder, + source: 'IBM i', + presentationOptions: { + showReuseMessage: true, + clear: IBMi.connectionManager.get(`clearOutputEveryTime`), + focus: false, + reveal: (viewControl === `task` ? TaskRevealKind.Always : TaskRevealKind.Never), + }, + problemMatchers: [], + runOptions: {}, + group: TaskGroup.Build, + execution: new CustomExecution(async (e) => { + const writeEmitter = new vscode.EventEmitter(); + const closeEmitter = new vscode.EventEmitter(); + + writeEmitter.event(s => target.output.push(s)); + closeEmitter.event(resolve); + + const term: Pseudoterminal = { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: async () => { + let successful = false; + let problemsFetched = false; + + try { + writeEmitter.fire(`Running Action: ${chosenAction.name} (${new Date().toLocaleTimeString()})` + CompileTools.NEWLINE); + + // If &SRCFILE is set, we need to copy the file to a temporary source file from the IFS + const fullPath = variables.get(`&FULLPATH`); + const srcFile = variables.get(`&SRCFILE`); + if (fullPath && srcFile && evfeventInfo.object) { + const [lib, srcpf] = srcFile.split(`/`); + + const createSourceFile = content.toCl(`CRTSRCPF`, { + rcdlen: 112, //NICE: this configurable in a VS Code setting? + file: `${lib}/${srcpf}`, + }); + + const copyFromStreamfile = content.toCl(`CPYFRMSTMF`, { + fromstmf: fullPath, + tombr: `'${Tools.qualifyPath(lib, srcpf, evfeventInfo.object)}'`, + mbropt: `*REPLACE`, + dbfccsid: `*FILE`, + stmfccsid: 1208, + }); + + // We don't care if this fails. Usually it's because the source file already exists. + await CompileTools.runCommand(connection, { command: createSourceFile, environment: `ile`, noLibList: true }); + + // Attempt to copy to member + const copyResult = await CompileTools.runCommand(connection, { command: copyFromStreamfile, environment: `ile`, noLibList: true }); + + if (copyResult.code !== 0) { + writeEmitter.fire(`Failed to copy file to a temporary member.\n\t${copyResult.stderr}\n\n`); + closeEmitter.fire(copyResult.code || 1); + } + } - if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { - hasRun = true; - const isIleCommand = environment === `ile`; + const commandResult = await CompileTools.runCommand(connection, + { + title: chosenAction.name, + environment, + command, + cwd: remoteCwd, + env: variables, + }, { + writeEvent: (content) => writeEmitter.fire(content), + commandConfirm: promptOnce ? commandConfirm : undefined + } + ); - const useLocalEvfevent = - fromWorkspace && chosenAction.postDownload && - (chosenAction.postDownload.includes(`.evfevent`) || chosenAction.postDownload.includes(`.evfevent/`)); + if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { + target.hasRun = true; + const isIleCommand = environment === `ile`; - const possibleObject = getObjectFromCommand(commandResult.command); - if (isIleCommand && possibleObject) { - Object.assign(evfeventInfo, possibleObject); - } + const useLocalEvfevent = + fromWorkspace && chosenAction.postDownload && + (chosenAction.postDownload.includes(`.evfevent`) || chosenAction.postDownload.includes(`.evfevent/`)); - actionName = (isIleCommand && possibleObject ? `${chosenAction.name} for ${evfeventInfo.library}/${evfeventInfo.object}` : actionName); - successful = (commandResult.code === 0 || commandResult.code === null); + const possibleObject = getObjectFromCommand(commandResult.command); + if (isIleCommand && possibleObject) { + Object.assign(evfeventInfo, possibleObject); + } - writeEmitter.fire(CompileTools.NEWLINE); + actionName = (isIleCommand && possibleObject ? `${chosenAction.name} for ${evfeventInfo.library}/${evfeventInfo.object}` : actionName); + successful = (commandResult.code === 0 || commandResult.code === null); - if (useLocalEvfevent) { - writeEmitter.fire(`Fetching errors from .evfevent.${CompileTools.NEWLINE}`); + writeEmitter.fire(CompileTools.NEWLINE); - } - else if (evfeventInfo.object && evfeventInfo.library) { - if (chosenAction.command.includes(`*EVENTF`)) { - writeEmitter.fire(`Fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); - refreshDiagnosticsFromServer(instance, evfeventInfo); - problemsFetched = true; - } else if (chosenAction.command.trimStart().toUpperCase().startsWith(`CRT`)) { - writeEmitter.fire(`*EVENTF not found in command string. Not fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); - } - } + if (useLocalEvfevent) { + writeEmitter.fire(`Fetching errors from .evfevent.${CompileTools.NEWLINE}`); - if (chosenAction.type === `file` && chosenAction.postDownload?.length) { - if (fromWorkspace) { - const remoteDir = remoteCwd; - const localDir = fromWorkspace.uri; - - const postDownloads: { type: vscode.FileType, localPath: string, remotePath: string }[] = []; - const downloadDirectories = new Set(); - for (const download of chosenAction.postDownload) { - const remotePath = path.posix.join(remoteDir, download); - const localPath = vscode.Uri.joinPath(localDir, download).path; - - let type: vscode.FileType; - if (await content.isDirectory(remotePath)) { - downloadDirectories.add(vscode.Uri.joinPath(localDir, download)); - type = vscode.FileType.Directory; - } - else { - const directory = path.parse(download).dir; - if (directory) { - downloadDirectories.add(vscode.Uri.joinPath(localDir, directory)); - } - type = vscode.FileType.File; + } + else if (evfeventInfo.object && evfeventInfo.library) { + if (chosenAction.command.includes(`*EVENTF`)) { + writeEmitter.fire(`Fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); + refreshDiagnosticsFromServer(instance, evfeventInfo); + problemsFetched = true; + } else if (chosenAction.command.trimStart().toUpperCase().startsWith(`CRT`)) { + writeEmitter.fire(`*EVENTF not found in command string. Not fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); } - - postDownloads.push({ remotePath, localPath, type }) } - //Clear and create every local download directories - for (const downloadPath of downloadDirectories) { - try { - const stat = await vscode.workspace.fs.stat(downloadPath); //Check if target exists - if (stat.type !== vscode.FileType.Directory) { - if (await vscode.window.showWarningMessage(`${downloadPath} exists but is a file.`, "Delete and create directory")) { - await vscode.workspace.fs.delete(downloadPath); - throw new Error("Create directory"); + if (chosenAction.type === `file` && chosenAction.postDownload?.length) { + if (fromWorkspace) { + const remoteDir = remoteCwd; + const localDir = fromWorkspace.uri; + + const postDownloads: { type: vscode.FileType, localPath: string, remotePath: string }[] = []; + const downloadDirectories = new Set(); + for (const download of chosenAction.postDownload) { + const remotePath = path.posix.join(remoteDir, download); + const localPath = vscode.Uri.joinPath(localDir, download).path; + + let type: vscode.FileType; + if (await content.isDirectory(remotePath)) { + downloadDirectories.add(vscode.Uri.joinPath(localDir, download)); + type = vscode.FileType.Directory; } + else { + const directory = path.parse(download).dir; + if (directory) { + downloadDirectories.add(vscode.Uri.joinPath(localDir, directory)); + } + type = vscode.FileType.File; + } + + postDownloads.push({ remotePath, localPath, type }) } - else if (stat.type === vscode.FileType.Directory) { - await vscode.workspace.fs.delete(downloadPath, { recursive: true }); - throw new Error("Create directory"); - } - } - catch (e) { - //Either fs.stat did not find the folder or it wasn't a folder and it's been deleted above - try { - await vscode.workspace.fs.createDirectory(downloadPath) - } - catch (error) { - vscode.window.showWarningMessage(`Failed to create download path ${downloadPath}: ${error}`); - console.log(error); - closeEmitter.fire(1); - } - } - } - // Then we download the files that is specified. - const downloads = postDownloads.map( - async (postDownload) => { - const content = connection.getContent(); - if (postDownload.type === vscode.FileType.Directory) { - return content.downloadDirectory(postDownload.localPath, postDownload.remotePath, { recursive: true, concurrency: 5 }); - } else { - return content.downloadFile(postDownload.localPath, postDownload.remotePath); + //Clear and create every local download directories + for (const downloadPath of downloadDirectories) { + try { + const stat = await vscode.workspace.fs.stat(downloadPath); //Check if target exists + if (stat.type !== vscode.FileType.Directory) { + if (await vscode.window.showWarningMessage(`${downloadPath} exists but is a file.`, "Delete and create directory")) { + await vscode.workspace.fs.delete(downloadPath); + throw new Error("Create directory"); + } + } + else if (stat.type === vscode.FileType.Directory) { + await vscode.workspace.fs.delete(downloadPath, { recursive: true }); + throw new Error("Create directory"); + } + } + catch (e) { + //Either fs.stat did not find the folder or it wasn't a folder and it's been deleted above + try { + await vscode.workspace.fs.createDirectory(downloadPath) + } + catch (error) { + vscode.window.showWarningMessage(`Failed to create download path ${downloadPath}: ${error}`); + console.log(error); + closeEmitter.fire(1); + } + } } + + // Then we download the files that is specified. + const downloads = postDownloads.map( + async (postDownload) => { + const content = connection.getContent(); + if (postDownload.type === vscode.FileType.Directory) { + return content.downloadDirectory(postDownload.localPath, postDownload.remotePath, { recursive: true, concurrency: 5 }); + } else { + return content.downloadFile(postDownload.localPath, postDownload.remotePath); + } + } + ); + + await Promise.all(downloads) + .then(async () => { + // Done! + writeEmitter.fire(`Downloaded files as part of Action: ${chosenAction.postDownload!.join(`, `)}\n`); + + // Process locally downloaded evfevent files: + if (useLocalEvfevent) { + refreshDiagnosticsFromLocal(instance, evfeventInfo); + problemsFetched = true; + } + }) + .catch(error => { + vscode.window.showErrorMessage(`Failed to download files as part of Action.`); + writeEmitter.fire(`Failed to download a file after Action: ${error.message}\n`); + closeEmitter.fire(1); + }); } - ); + } - await Promise.all(downloads) - .then(async result => { - // Done! - writeEmitter.fire(`Downloaded files as part of Action: ${chosenAction.postDownload!.join(`, `)}\n`); + if (problemsFetched && viewControl === `problems`) { + commands.executeCommand(`workbench.action.problems.focus`); + } - // Process locally downloaded evfevent files: - if (useLocalEvfevent) { - refreshDiagnosticsFromLocal(instance, evfeventInfo); - problemsFetched = true; - } - }) - .catch(error => { - vscode.window.showErrorMessage(`Failed to download files as part of Action.`); - writeEmitter.fire(`Failed to download a file after Action: ${error.message}\n`); - closeEmitter.fire(1); - }); + if (chosenAction.outputToFile) { + await outputToFile(connection, chosenAction.outputToFile, variables, target.output); + } + } else { + writeEmitter.fire(`Command did not run.` + CompileTools.NEWLINE); } - } - if (problemsFetched && viewControl === `problems`) { - commands.executeCommand(`workbench.action.problems.focus`); + } catch (e) { + writeEmitter.fire(`${e}\n`); + vscode.window.showErrorMessage(`Action ${chosenAction} for ${evfeventInfo.library}/${evfeventInfo.object} failed. (internal error).`); + successful = false; } - } else { - writeEmitter.fire(`Command did not run.` + CompileTools.NEWLINE); - } - } catch (e) { - writeEmitter.fire(`${e}\n`); - vscode.window.showErrorMessage(`Action ${chosenAction} for ${evfeventInfo.library}/${evfeventInfo.object} failed. (internal error).`); - successful = false; - } + closeEmitter.fire(successful ? 0 : 1); + }, + close: function (): void { } + }; - closeEmitter.fire(successful ? 0 : 1); - }, - close: function (): void { } - }; - - return term; - }) - }) - ); - - const executionOK = (exitCode === 0); - if (hasRun) { - if (executionOK && browserItem) { - switch (chosenAction.refresh) { - case 'browser': - if (chosenAction.type === 'streamfile') { - vscode.commands.executeCommand("code-for-ibmi.refreshIFSBrowser"); - } - else if (chosenAction.type !== 'file') { - vscode.commands.executeCommand("code-for-ibmi.refreshObjectBrowser"); - } - break; + return term; + }) + }) + ); - case 'filter': - //Filter is a top level item so it has no parent (like Batman) - let filter: BrowserItem = browserItem; - while (filter.parent) { - filter = filter.parent; - } - filter.refresh?.(); - break; - - case 'parent': - browserItem.parent?.refresh?.(); - break; + target.executionOK = (exitCode === 0); - default: - //No refresh + if (target.hasRun && target.executionOK && target.executionOK) { + doRefresh(chosenAction, browserItems?.find(item => item.resourceUri?.path === target.uri.path)); } } + }); - const openOutputAction = "Open output"; //TODO: will be translated in the future - const uiPromise = executionOK ? - vscode.window.showInformationMessage(`Action ${actionName} was successful.`, openOutputAction) : - vscode.window.showErrorMessage(`Action ${actionName} was not successful.`, openOutputAction); - - uiPromise.then(openOutput => { - if (openOutput) { - const now = new Date(); - new CustomUI() - .addParagraph(`
${outputBuffer.join("")}
`) - .setOptions({ fullWidth: true }) - .loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); - } - }) + const openOutputAction = l10n.t("Open output(s)"); + let uiPromise; + if (cancelled) { + uiPromise = vscode.window.showWarningMessage(l10n.t(`Action {0} was cancelled; ({1} processed).`, chosenAction.name, targets.filter(target => target.processed).length), openOutputAction); + } + else if (targets.every(target => target.executionOK)) { + uiPromise = vscode.window.showInformationMessage(l10n.t(`Action {0} was successful.`, chosenAction.name), openOutputAction); + } + else { + uiPromise = vscode.window.showErrorMessage(l10n.t(`Action {0} was not successful ({1}/{2} failed).`, chosenAction.name, targets.filter(target => !target.executionOK).length, targets.length), openOutputAction); } - if (chosenAction.outputToFile) { - const outputPath = variables.expand(chosenAction.outputToFile); - let actualPath; - if (outputPath.includes('&i')) { - //Rolling output - let count = 0; - const generatePath = () => outputPath.replace("&i", `_${String(count++).padStart(3, "0")}`); - while (await connection.getContent().testStreamFile((actualPath = generatePath()), "e")); + uiPromise.then(openOutput => { + if (openOutput) { + const now = new Date(); + const resultsPanel = new CustomUI(); + if (targets.length === 1) { + resultsPanel.addParagraph(`
${targets[0].output.join("")}
`) + .setOptions({ fullPage: true }); + } + else { + resultsPanel.addBrowser("results", targets.filter(target => target.processed).map(target => ({ label: `${getTargetResultIcon(target)} ${path.basename(target.uri.path)}`, value: `
${target.output.join("")}
` } as TreeListItem))) + .setOptions({ + fullPage: true, + css: /* css */ ` + body{ + margin: 0; + padding: 0; + overflow: hidden; + } + pre { + margin: 1em; + } + ` + }); + } + resultsPanel.loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); } - else { - //Overwrite if output exists - actualPath = outputPath; - } - //Replace ~ if needed - if (actualPath.includes('~')) { - actualPath = (await connection.sendCommand({ command: `echo ${actualPath}` })).stdout; - } - await connection.getContent().writeStreamfileRaw(actualPath, outputBuffer.join("")); - } - - return executionOK; - } - else { - return false; + }) } - } else if (isProtected) { - //when a member is protected(read only) - vscode.window.showErrorMessage(`Action cannot be applied on a read only member.`); - return false; - } else { - //No compile commands - vscode.window.showErrorMessage(`No compile commands found for ${uri.scheme}-${extension}.`); + return targets.every(target => target.executionOK); + } + else { + vscode.window.showErrorMessage(l10n.t(`No suitable actions found for {0} - {1}`, scheme, targets.map(t => t.extension).filter(Tools.distinct).join(", "))); return false; } } @@ -568,6 +587,7 @@ export async function runAction(instance: Instance, uri: vscode.Uri, customActio } } + function getObjectFromCommand(baseCommand?: string): CommandObject | undefined { if (baseCommand) { const regex = PARM_REGEX.exec(baseCommand.toUpperCase()); @@ -668,4 +688,65 @@ async function showCustomInputs(name: string, command: string, title?: string): } return command; +} + +function doRefresh(chosenAction: Action, browserItem?: BrowserItem) { + if (browserItem) { + switch (chosenAction.refresh) { + case 'browser': + if (chosenAction.type === 'streamfile') { + vscode.commands.executeCommand("code-for-ibmi.refreshIFSBrowser"); + } + else if (chosenAction.type !== 'file') { + vscode.commands.executeCommand("code-for-ibmi.refreshObjectBrowser"); + } + break; + + case 'filter': + //Filter is a top level item so it has no parent (like Batman) + let filter: BrowserItem = browserItem; + while (filter.parent) { + filter = filter.parent; + } + filter.refresh?.(); + break; + + case 'parent': + browserItem.parent?.refresh?.(); + break; + + default: + //No refresh + } + } +} + +function getTargetResultIcon(target: ActionTarget) { + if (target.hasRun) { + return target.executionOK ? '✔️' : '❌'; + } + else { + return '❔'; + } +} + +async function outputToFile(connection: IBMi, outputPattern: string, variables: Variables, output: string[]) { + const outputPath = variables.expand(outputPattern); + let actualPath; + if (outputPath.includes('&i')) { + //Rolling output + let count = 0; + const generatePath = () => outputPath.replace("&i", `_${String(count++).padStart(3, "0")}`); + while (await connection.getContent().testStreamFile((actualPath = generatePath()), "e")); + + } + else { + //Overwrite if output exists + actualPath = outputPath; + } + //Replace ~ if needed + if (actualPath.includes('~')) { + actualPath = (await connection.sendCommand({ command: `echo ${actualPath}` })).stdout; + } + await connection.getContent().writeStreamfileRaw(actualPath, output.join("")); } \ No newline at end of file diff --git a/src/ui/views/ifsBrowser.ts b/src/ui/views/ifsBrowser.ts index ddd50de5c..1a0c99db2 100644 --- a/src/ui/views/ifsBrowser.ts +++ b/src/ui/views/ifsBrowser.ts @@ -158,7 +158,8 @@ class IFSDirectoryItem extends IFSItem { async getChildren(): Promise { const connection = instance.getConnection(); - if (connection) {; + if (connection) { + ; const content = connection.getContent(); const config = connection.getConfig(); try { @@ -718,18 +719,28 @@ Please type "{0}" to confirm deletion.`, dirName); } }), - vscode.commands.registerCommand(`code-for-ibmi.searchIFS`, async (node?: IFSItem) => { + vscode.commands.registerCommand(`code-for-ibmi.searchIFS`, async (node?: IFSItem, nodes?: IFSItem[]) => { const connection = instance.getConnection(); if (connection && connection.remoteFeatures.grep) { const config = connection.getConfig(); - const searchPath = node?.path || await vscode.window.showInputBox({ - value: config.homeDirectory, - prompt: l10n.t(`Enter IFS directory to search`), - title: l10n.t(`Search directory`) - }); + const searchPaths: string[] = []; + if (node) { + (nodes || [node]).forEach(n => searchPaths.push(n.path)); + } + else { + const path = await vscode.window.showInputBox({ + value: config.homeDirectory, + prompt: l10n.t(`Enter IFS directory to search`), + title: l10n.t(`Search directory`) + }); - if (searchPath) { + if (path) { + searchPaths.push(path); + } + } + + if (searchPaths.length) { const list = IBMi.GlobalStorage.getPreviousSearchTerms(); const items: vscode.QuickPickItem[] = list.map(term => ({ label: term })); const listHeader = [ @@ -741,7 +752,7 @@ Please type "{0}" to confirm deletion.`, dirName); const quickPick = vscode.window.createQuickPick(); quickPick.items = items.length ? [...items, ...clearListArray] : []; quickPick.placeholder = items.length ? l10n.t(`Enter search term or select one of the previous search terms.`) : l10n.t("Enter search term."); - quickPick.title = l10n.t(`Search {0}`, searchPath); + quickPick.title = l10n.t(`Search`); quickPick.onDidChangeValue(() => { if (!quickPick.value) { @@ -765,7 +776,7 @@ Please type "{0}" to confirm deletion.`, dirName); } else { quickPick.hide(); IBMi.GlobalStorage.addPreviousSearchTerm(searchTerm); - await doSearchInStreamfiles(searchTerm, searchPath); + await doSearchInStreamfiles(searchTerm, searchPaths); } } }); @@ -778,18 +789,27 @@ Please type "{0}" to confirm deletion.`, dirName); } }), - vscode.commands.registerCommand(`code-for-ibmi.ifs.find`, async (node?: IFSItem) => { + vscode.commands.registerCommand(`code-for-ibmi.ifs.find`, async (node?: IFSItem, nodes?: IFSItem[]) => { const connection = instance.getConnection(); if (connection && connection.remoteFeatures.find) { const config = connection.getConfig(); - const findPath = node?.path || await vscode.window.showInputBox({ - value: config.homeDirectory, - prompt: l10n.t(`Enter IFS directory to find files in`), - title: l10n.t(`Find in directory`) - }); + const findPaths: string[] = []; + if (node) { + (nodes || [node]).forEach(n => findPaths.push(n.path)); + } + else { + const path = await vscode.window.showInputBox({ + value: config.homeDirectory, + prompt: l10n.t(`Enter IFS directory to find files in`), + title: l10n.t(`Find in directory`) + }); + if (path) { + findPaths.push(path); + } + } - if (findPath) { + if (findPaths.length) { const list = IBMi.GlobalStorage.getPreviousFindTerms(); const items: vscode.QuickPickItem[] = list.map(term => ({ label: term })); const listHeader = [ @@ -801,7 +821,7 @@ Please type "{0}" to confirm deletion.`, dirName); const quickPick = vscode.window.createQuickPick(); quickPick.items = items.length ? [...items, ...clearListArray] : []; quickPick.placeholder = items.length ? l10n.t(`Enter find term or select one of the previous find terms.`) : l10n.t("Enter find term."); - quickPick.title = l10n.t(`Find {0}`, findPath); + quickPick.title = l10n.t(`Find {0}`, findPaths); quickPick.onDidChangeValue(() => { if (!quickPick.value) { @@ -825,7 +845,7 @@ Please type "{0}" to confirm deletion.`, dirName); } else { quickPick.hide(); IBMi.GlobalStorage.addPreviousFindTerm(findTerm); - await doFindStreamfiles(findTerm, findPath); + await doFindStreamfiles(findTerm, findPaths); } } }); @@ -942,55 +962,83 @@ function storeIFSList(path: string, list: string[]) { } } -async function doSearchInStreamfiles(searchTerm: string, searchPath: string) { +async function doSearchInStreamfiles(searchTerm: string, searchPaths: string[]) { try { - await vscode.window.withProgress({ + const total = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, - title: l10n.t(`Searching`), - }, async progress => { - progress.report({ - message: l10n.t(`"{0}" in {1}.`, searchTerm, searchPath) - }); - const results = await Search.searchIFS(instance.getConnection()!, searchPath, searchTerm); - if (results?.hits.length) { - openIFSSearchResults(searchPath, results); - } else { - vscode.window.showInformationMessage(l10n.t(`No results found searching for "{0}" in {1}.`, searchTerm, searchPath)); + title: l10n.t(`Searching "{0}" in `, searchTerm), + }, async (progress, cancel) => { + const increment = 100 / searchPaths.length; + let total = 0; + let append = false; + for (const searchPath of searchPaths) { + if (cancel.isCancellationRequested) { + return total; + } + progress.report({ + message: searchPath, + increment + }); + const results = await Search.searchIFS(instance.getConnection()!, searchPath, searchTerm); + if (results) { + total += results.hits.length; + openIFSSearchResults(searchPath, results, append); + append = true; + } } + + return total; }); + if (!total) { + vscode.window.showInformationMessage(l10n.t(`No results found searching for "{0}".`, searchTerm)); + } } catch (e) { vscode.window.showErrorMessage(l10n.t(`Error searching streamfiles.`)); } } -async function doFindStreamfiles(findTerm: string, findPath: string) { +async function doFindStreamfiles(findTerm: string, findPaths: string[]) { try { - await vscode.window.withProgress({ + const total = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, - title: l10n.t(`Finding`), - }, async progress => { - progress.report({ - message: l10n.t(`Finding filenames with "{0}" in {1}.`, findTerm, findPath) - }); - const results = (await Search.findIFS(instance.getConnection()!, findPath, findTerm)); - if (results?.hits.length) { - openIFSSearchResults(findPath, results); - } else { - vscode.window.showInformationMessage(l10n.t(`No results found finding filenames with "{0}" in {1}.`, findTerm, findPath)); + title: l10n.t(`Finding filenames with "{0}" in`, findTerm), + }, async (progress, cancel) => { + const increment = 100 / findPaths.length; + let total = 0; + let append = false; + for (const findPath of findPaths) { + if (cancel.isCancellationRequested) { + return total; + } + progress.report({ + message: findPath, + increment + }); + const results = (await Search.findIFS(instance.getConnection()!, findPath, findTerm)); + if (results) { + total += results.hits.length; + openIFSSearchResults(findPath, results, append); + append = true; + } } + + return total; }); + if (!total) { + vscode.window.showInformationMessage(l10n.t(`No results found finding filenames with "{0}".`, findTerm)); + } } catch (e) { vscode.window.showErrorMessage(l10n.t(`Error finding filenames.`)); } } -function openIFSSearchResults(searchPath: string, searchResults: SearchResults) { +function openIFSSearchResults(searchPath: string, searchResults: SearchResults, appendResults: boolean) { searchResults.hits = searchResults.hits.map(a => ({ ...a, label: path.posix.relative(searchPath, a.path) }) as SearchHit) .sort((a, b) => a.path.localeCompare(b.path)); - vscode.commands.executeCommand(`code-for-ibmi.setSearchResults`, searchResults); + vscode.commands.executeCommand(`code-for-ibmi.setSearchResults`, searchResults, appendResults); } async function showOpenDialog() { diff --git a/src/ui/views/objectBrowser.ts b/src/ui/views/objectBrowser.ts index d9d207327..ab8930d5a 100644 --- a/src/ui/views/objectBrowser.ts +++ b/src/ui/views/objectBrowser.ts @@ -1,7 +1,7 @@ import fs, { existsSync } from "fs"; import os from "os"; import path, { basename, dirname } from "path"; -import vscode from "vscode"; +import vscode, { l10n } from "vscode"; import { parseFilter, singleGenericName } from "../../api/Filter"; import IBMi, { MemberParts } from "../../api/IBMi"; import { SortOptions, SortOrder } from "../../api/IBMiContent"; @@ -9,13 +9,11 @@ import { Search } from "../../api/Search"; import { Tools } from "../../api/Tools"; import { getMemberUri } from "../../filesystems/qsys/QSysFs"; import { instance } from "../../instantiate"; -import { CommandResult, DefaultOpenMode, FilteredItem, FocusOptions, IBMiMember, IBMiObject, MemberItem, ModuleExport, ObjectFilters, ObjectItem, ProgramExportImportInfo, WithLibrary } from "../../typings"; +import { CommandResult, DefaultOpenMode, FilteredItem, FocusOptions, IBMiMember, IBMiObject, MemberItem, ObjectFilters, ObjectItem, WithLibrary } from "../../typings"; import { editFilter } from "../../webviews/filters"; import { VscodeTools } from "../Tools"; import { BrowserItem, BrowserItemParameters } from "../types"; -const URI_LIST_SEPARATOR = "\r\n"; - const objectNamesLower = () => IBMi.connectionManager.get(`ObjectBrowser.showNamesInLowercase`); const objectSortOrder = () => IBMi.connectionManager.get(`ObjectBrowser.sortObjectsByName`) ? `name` : `type`; @@ -47,6 +45,11 @@ const objectIcons = { '': `circle-large-outline` } +type SearchParameters = { + path: string + fillter?: ObjectFilters +} + abstract class ObjectBrowserItem extends BrowserItem { constructor(readonly filter: ObjectFilters, label: string, params?: BrowserItemParameters) { super(label, params); @@ -512,9 +515,14 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { objectBrowser.refresh(); }), - vscode.commands.registerCommand(`code-for-ibmi.maintainFilter`, async (node?: FilteredItem) => { - await editFilter(node?.filter); - objectBrowser.refresh(); + vscode.commands.registerCommand(`code-for-ibmi.maintainFilter`, async (node?: FilteredItem, nodes?: FilteredItem[]) => { + if (node) { + (nodes || [node]).map(n => n.filter).forEach(filter => editFilter(filter).then(() => objectBrowser.refresh())); + } + else { + await editFilter(); + objectBrowser.refresh(); + } }), vscode.commands.registerCommand(`code-for-ibmi.moveFilterUp`, (node: ObjectBrowserFilterItem) => objectBrowser.moveFilterInList(node, `UP`)), @@ -539,25 +547,36 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { objectTreeViewer.reveal(item, options); }), - vscode.commands.registerCommand(`code-for-ibmi.generateBinderSource`, async (node: ObjectBrowserObjectItem) => { + vscode.commands.registerCommand(`code-for-ibmi.generateBinderSource`, async (node: ObjectBrowserObjectItem, nodes: ObjectBrowserObjectItem[]) => { + nodes = (nodes || [node]); const contentApi = getContent(); - let exports: ProgramExportImportInfo[] | ModuleExport[] = []; - if (node.object.type === '*MODULE') { - exports = (await contentApi.getModuleExports(node.object.library, node.object.name)) - .filter(exp => exp.symbolType === 'PROCEDURE'); - } else { - exports = (await contentApi.getProgramExportImportInfo(node.object.library, node.object.name, node.object.type)) - .filter(info => info.symbolUsage === '*PROCEXP'); - } - const content = [ - `/* Binder source generated from ${node} */`, - ``, - `STRPGMEXP PGMLVL(*CURRENT) /* SIGNATURE("") */`, - ...exports.map(info => ` EXPORT SYMBOL("${info.symbolName}")`), - `ENDPGMEXP`, - ].join("\n"); - const textDoc = await vscode.workspace.openTextDocument({ language: 'bnd', content }); - await vscode.window.showTextDocument(textDoc); + const increment = 100 / nodes.length; + vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: true, title: l10n.t("Generating binder source") }, async (progress, cancel) => { + for (const node of nodes) { + if (cancel.isCancellationRequested) { + return; + } + progress.report({ message: node.toString(), increment }); + + const exports = []; + if (node.object.type === '*MODULE') { + exports.push(...(await contentApi.getModuleExports(node.object.library, node.object.name)) + .filter(exp => exp.symbolType === 'PROCEDURE')); + } else { + exports.push(...(await contentApi.getProgramExportImportInfo(node.object.library, node.object.name, node.object.type)) + .filter(info => info.symbolUsage === '*PROCEXP')); + } + const content = [ + `/* Binder source generated from ${node} */`, + ``, + `STRPGMEXP PGMLVL(*CURRENT) /* SIGNATURE("") */`, + ...exports.map(info => ` EXPORT SYMBOL("${info.symbolName}")`), + `ENDPGMEXP`, + ].join("\n"); + const textDoc = await vscode.workspace.openTextDocument({ language: 'bnd', content }); + await vscode.window.showTextDocument(textDoc); + } + }); }), vscode.commands.registerCommand(`code-for-ibmi.createMember`, async (node: ObjectBrowserSourcePhysicalFileItem, fullName?: string) => { @@ -934,13 +953,13 @@ Do you want to replace it?`, item.name), { modal: true }, skipAllLabel, overwrit } }), - vscode.commands.registerCommand(`code-for-ibmi.searchSourceFile`, async (node?: ObjectItem) => { - const parameters = { - path: node?.path || ``, - filter: node?.filter - } + vscode.commands.registerCommand(`code-for-ibmi.searchSourceFile`, async (node?: ObjectItem, nodes?: ObjectItem[]) => { + const parameters: SearchParameters[] = []; - if (!parameters.path) { + if (node) { + (nodes || [node]).forEach(n => parameters.push({ path: n.path, fillter: n.filter })); + } + else { const connection = getConnection(); const input = await vscode.window.showInputBox({ prompt: vscode.l10n.t(`Enter LIB/SPF/member.ext to search (member.ext is optional and can contain wildcards)`), @@ -972,15 +991,14 @@ Do you want to replace it?`, item.name), { modal: true }, skipAllLabel, overwrit if (input) { const path = connection.upperCaseName(input.trim()).split(`/`); - parameters.path = [path[0], path[1]].join('/'); + parameters.push({ path: [path[0], path[1]].join('/') }); } } - if (parameters.path) { + if (parameters.length) { const connection = getConnection(); - const pathParts = parameters.path.split(`/`); - if (pathParts[1] !== `*ALL`) { + if (!parameters.some(p => p.path.split('/')[1] === '*ALL')) { const selectedAsp = connection.getCurrentIAspName(); const aspText = (selectedAsp ? vscode.l10n.t(`(in ASP {0})`, selectedAsp) : ``); @@ -994,7 +1012,7 @@ Do you want to replace it?`, item.name), { modal: true }, skipAllLabel, overwrit const quickPick = vscode.window.createQuickPick(); quickPick.items = list.length > 0 ? listHeader.concat(list.map(term => ({ label: term }))).concat(clearListArray) : []; quickPick.placeholder = list.length > 0 ? vscode.l10n.t(`Enter search term or select one of the previous search terms.`) : vscode.l10n.t(`Enter search term.`); - quickPick.title = vscode.l10n.t(`Search {0} {1}`, parameters.path, aspText); + quickPick.title = vscode.l10n.t(`Search {0} {1}`, parameters.map(p => p.path).join(", "), aspText); quickPick.onDidChangeValue(() => { if (quickPick.value === ``) { @@ -1017,7 +1035,7 @@ Do you want to replace it?`, item.name), { modal: true }, skipAllLabel, overwrit } else { quickPick.hide(); IBMi.GlobalStorage.addPreviousSearchTerm(searchTerm); - await doSearchInSourceFile(searchTerm, parameters.path, parameters.filter); + await doSearch(searchTerm, parameters); } } }); @@ -1366,72 +1384,88 @@ function storeMemberList(path: string, list: string[]) { } } -async function doSearchInSourceFile(searchTerm: string, path: string, filter?: ObjectFilters) { - const [library, sourceFile] = path.split(`/`); +async function doSearch(searchTerm: string, parameters: SearchParameters[]) { try { - await vscode.window.withProgress({ + const total = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t(`Searching`), - }, async progress => { - progress.report({ - message: vscode.l10n.t(`Fetching member list for {0}.`, path) - }); - - progress.report({ message: vscode.l10n.t(`"{0}" in {1}.`, searchTerm, path) }); - - // NOTE: if more messages are added, lower the timeout interval - const timeoutInternal = 9000; - const searchMessages = [ - // vscode.l10n.t(`This is taking a while because there are {0} members. Searching "{1}" in {2} still.`, members.length, searchTerm, path), - vscode.l10n.t(`What's so special about "{0}" anyway?`, searchTerm), - vscode.l10n.t(`Still searching "{0}" in {1}...`, searchTerm, path), - vscode.l10n.t(`While you wait, why not make some tea?`), - vscode.l10n.t(`Wow. This really is taking a while. Let's hope you get the result you want.`), - vscode.l10n.t(`Why was six afraid of seven?`), - // vscode.l10n.t(`How does one end up with {0} members?`, members.length), - vscode.l10n.t(`"{0}" in {1}.`, searchTerm, path), - ]; - - let currentMessage = 0; - const messageTimeout = setInterval(() => { - if (currentMessage < searchMessages.length) { - progress.report({ - message: searchMessages[currentMessage] - }); - currentMessage++; - } else { - clearInterval(messageTimeout); + cancellable: true + }, async (progress, cancel) => { + let total = 0; + const increment = 100 / parameters.length; + let appendResults = false; + for (const parameter of parameters) { + if (cancel.isCancellationRequested) { + return total; } - }, timeoutInternal); - let memberFilter: string = '*'; - if (filter?.member && filter?.filterType !== "regex" && singleGenericName(filter.member)) { - memberFilter = filter?.member; - } + const path = parameter.path; + const filter = parameter.fillter; + progress.report({ message: vscode.l10n.t(`"{0}" in {1}.`, searchTerm, path), increment }); + + // NOTE: if more messages are added, lower the timeout interval + const timeoutInternal = 9000; + const searchMessages = [ + // vscode.l10n.t(`This is taking a while because there are {0} members. Searching "{1}" in {2} still.`, members.length, searchTerm, path), + vscode.l10n.t(`What's so special about "{0}" anyway?`, searchTerm), + vscode.l10n.t(`Still searching "{0}" in {1}...`, searchTerm, path), + vscode.l10n.t(`While you wait, why not make some tea?`), + vscode.l10n.t(`Wow. This really is taking a while. Let's hope you get the result you want.`), + vscode.l10n.t(`Why was six afraid of seven?`), + // vscode.l10n.t(`How does one end up with {0} members?`, members.length), + vscode.l10n.t(`"{0}" in {1}.`, searchTerm, path), + ]; + + let currentMessage = 0; + const messageTimeout = setInterval(() => { + if (currentMessage < searchMessages.length) { + progress.report({ + message: searchMessages[currentMessage] + }); + currentMessage++; + } else { + clearInterval(messageTimeout); + } + }, timeoutInternal); - const results = await Search.searchMembers(instance.getConnection()!, library, sourceFile, searchTerm, memberFilter, filter?.protected); - clearInterval(messageTimeout) - if (results.hits.length) { - const objectNamesLower = IBMi.connectionManager.get(`ObjectBrowser.showNamesInLowercase`); + let memberFilter: string = '*'; + if (filter?.member && filter?.filterType !== "regex" && singleGenericName(filter.member)) { + memberFilter = filter?.member; + } - // Format result to be lowercase if the setting is enabled - results.hits.forEach(result => { - if (objectNamesLower === true) { - result.path = result.path.toLowerCase(); - } - }); + const [library, sourceFile] = path.split(`/`); + const results = await Search.searchMembers(instance.getConnection()!, library, sourceFile, searchTerm, memberFilter, filter?.protected); + clearInterval(messageTimeout); + if (cancel.isCancellationRequested) { + return; + } + if (results.hits.length) { + const objectNamesLower = IBMi.connectionManager.get(`ObjectBrowser.showNamesInLowercase`); - results.hits = results.hits.sort((a, b) => { - return a.path.localeCompare(b.path); - }); + // Format result to be lowercase if the setting is enabled + results.hits.forEach(result => { + if (objectNamesLower === true) { + result.path = result.path.toLowerCase(); + } + }); - vscode.commands.executeCommand(`code-for-ibmi.setSearchResults`, results); - } else { - vscode.window.showInformationMessage(vscode.l10n.t(`No results found searching for "{0}" in {1}.`, searchTerm, path)); + results.hits = results.hits.sort((a, b) => { + return a.path.localeCompare(b.path); + }); + + vscode.commands.executeCommand(`code-for-ibmi.setSearchResults`, results, appendResults); + appendResults = true; + } + total += results.hits.length; } + return total; }); + if (!total) { + vscode.window.showInformationMessage(vscode.l10n.t(`No results found searching for "{0}".`, searchTerm)); + } + } catch (e: any) { vscode.window.showErrorMessage(vscode.l10n.t(`Error searching source members: {0}`, e)); } diff --git a/src/ui/views/searchView.ts b/src/ui/views/searchView.ts index d5cf072d3..44b427873 100644 --- a/src/ui/views/searchView.ts +++ b/src/ui/views/searchView.ts @@ -16,29 +16,35 @@ export function initializeSearchView(context: vscode.ExtensionContext) { vscode.commands.registerCommand(`code-for-ibmi.refreshSearchView`, async () => searchView.refresh()), vscode.commands.registerCommand(`code-for-ibmi.closeSearchView`, async () => vscode.commands.executeCommand(`setContext`, `code-for-ibmi:searchViewVisible`, false)), vscode.commands.registerCommand(`code-for-ibmi.collapseSearchView`, async () => searchView.collapse()), - vscode.commands.registerCommand(`code-for-ibmi.setSearchResults`, async (searchResults: SearchResults) => { + vscode.commands.registerCommand(`code-for-ibmi.setSearchResults`, async (searchResults: SearchResults, appendResults?: boolean) => { + const hits = appendResults ? searchView.hits + searchResults.hits.length : searchResults.hits.length; if (searchResults.hits.some(hit => hit.lines.length)) { - searchViewViewer.message = vscode.l10n.t(`{0} file(s) contain(s) '{1}'`, searchResults.hits.length, searchResults.term); + searchViewViewer.message = vscode.l10n.t(`{0} file(s) contain(s) '{1}'`, hits, searchResults.term); } else { - searchViewViewer.message = vscode.l10n.t(`{0} file(s) named '{1}'`, searchResults.hits.length, searchResults.term); + searchViewViewer.message = vscode.l10n.t(`{0} file(s) named '{1}'`, hits, searchResults.term); } - searchView.setResults(searchResults); + searchView.setResults(searchResults, appendResults); }) ) } class SearchView implements vscode.TreeDataProvider { - private _results: SearchResults = { term: "", hits: [] }; - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly _results: SearchResults = { term: "", hits: [] }; + private readonly _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; setViewVisible(visible: boolean) { vscode.commands.executeCommand(`setContext`, `code-for-ibmi:searchViewVisible`, visible); } - setResults(results: SearchResults) { - this._results = results; + setResults(results: SearchResults, appendResults?: boolean) { + if(!appendResults){ + this._results.term = results.term; + this._results.hits = []; + } + this._results.hits.push(...results.hits); + this.refresh(); this.setViewVisible(true); @@ -64,6 +70,10 @@ class SearchView implements vscode.TreeDataProvider { return hitSource.getChildren(); } } + + get hits() { + return this._results.hits.length; + } } class HitSource extends vscode.TreeItem implements WithPath { diff --git a/src/webviews/CustomUI.ts b/src/webviews/CustomUI.ts index dfafe79b4..11111106f 100644 --- a/src/webviews/CustomUI.ts +++ b/src/webviews/CustomUI.ts @@ -6,8 +6,12 @@ const vscodeweb = require(`@vscode-elements/elements/dist/bundled`); type PanelOptions = { fullWidth?: boolean + fullPage?: boolean + css?: string }; +type TreeLeafAction = "submit" | "browse"; + export interface Page { panel: vscode.WebviewPanel data?: T @@ -37,7 +41,7 @@ export interface ComplexTab { } interface WebviewMessageRequest { - type: "submit"|"file"; + type: "submit" | "file"; data?: any; } @@ -63,7 +67,7 @@ export class Section { return this; } - addInput(id: string, label: string, description?: string, options?: { default?: string, readonly?: boolean, rows?: number, minlength?: number, maxlength?: number, regexTest?: string, inputType?: InputType, min?:number, max?:number }) { + addInput(id: string, label: string, description?: string, options?: { default?: string, readonly?: boolean, rows?: number, minlength?: number, maxlength?: number, regexTest?: string, inputType?: InputType, min?: number, max?: number }) { const input = Object.assign(new Field('input', id, label, description), options); this.addField(input); return this; @@ -113,8 +117,9 @@ export class Section { return this; } - addTree(id: string, label: string, treeItems: TreeListItem[], description?: string) { + addTree(id: string, label: string, treeItems: TreeListItem[], description?: string, onClick: TreeLeafAction = "submit") { const tree = new Field('tree', id, label, description); + tree.treeLeafAction = onClick; tree.treeList = treeItems; this.addField(tree); return this; @@ -132,6 +137,17 @@ export class Section { this.fields.push(field); return this; } + + addBrowser(id: string, items: TreeListItem[]) { + const browser = new Field('browser', id, ''); + browser.treeList = items; + if (browser.treeList[0]) { + browser.treeList[0].selected = true; + } + browser.treeLeafAction = 'browse'; + this.addField(browser); + return this; + } } const openedWebviews: Map = new Map; @@ -220,8 +236,8 @@ export class CustomUI extends Section { } private getHTML(panel: vscode.WebviewPanel, title: string) { - const notInputFields = [`submit`, `buttons`, `tree`, `hr`, `paragraph`, `tabs`, `complexTabs`]; - const trees = this.fields.filter(field => field.type == `tree`); + const notInputFields = [`submit`, `buttons`, `tree`, `hr`, `paragraph`, `tabs`, `complexTabs`, 'browser'] as FieldType[]; + const trees = this.fields.filter(field => [`tree`, 'browser'].includes(field.type)); const complexTabFields = this.fields.filter(field => field.type === `complexTabs`).map(tabs => tabs.complexTabItems?.map(tab => tab.fields)); const allFields = [...this.fields, ...complexTabFields.flat(2)].filter(cField => cField) as Field[]; @@ -239,13 +255,13 @@ export class CustomUI extends Section { - - ${this.fields.map(field => field.getHTML()).join(``)} - + ${this.options?.fullPage ? + this.fields.map(field => field.getHTML()).join(``) : + /* html */ ` + + ${this.fields.map(field => field.getHTML()).join(``)} + + ` + }