diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index 71dfc7c2a..28695ef36 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -13,16 +13,16 @@ "devDependencies": { "@types/node": "^20.11.17", "@types/vscode": "^1.81.0", - "esbuild": "^0.20.0" + "esbuild": "0.20.2" }, "engines": { "vscode": "^1.81.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", - "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -36,9 +36,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", - "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -52,9 +52,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", - "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -68,9 +68,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", - "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -84,9 +84,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", - "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -100,9 +100,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", - "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -116,9 +116,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", - "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -132,9 +132,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", - "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -148,9 +148,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", - "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -164,9 +164,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", - "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -180,9 +180,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", - "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -196,9 +196,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", - "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -212,9 +212,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", - "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -228,9 +228,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", - "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -244,9 +244,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", - "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -260,9 +260,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", - "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -276,9 +276,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", - "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -292,9 +292,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", - "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", - "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", - "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -340,9 +340,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", - "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -356,9 +356,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", - "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -372,9 +372,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", - "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -408,9 +408,9 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/esbuild": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", - "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -420,29 +420,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.0", - "@esbuild/android-arm": "0.20.0", - "@esbuild/android-arm64": "0.20.0", - "@esbuild/android-x64": "0.20.0", - "@esbuild/darwin-arm64": "0.20.0", - "@esbuild/darwin-x64": "0.20.0", - "@esbuild/freebsd-arm64": "0.20.0", - "@esbuild/freebsd-x64": "0.20.0", - "@esbuild/linux-arm": "0.20.0", - "@esbuild/linux-arm64": "0.20.0", - "@esbuild/linux-ia32": "0.20.0", - "@esbuild/linux-loong64": "0.20.0", - "@esbuild/linux-mips64el": "0.20.0", - "@esbuild/linux-ppc64": "0.20.0", - "@esbuild/linux-riscv64": "0.20.0", - "@esbuild/linux-s390x": "0.20.0", - "@esbuild/linux-x64": "0.20.0", - "@esbuild/netbsd-x64": "0.20.0", - "@esbuild/openbsd-x64": "0.20.0", - "@esbuild/sunos-x64": "0.20.0", - "@esbuild/win32-arm64": "0.20.0", - "@esbuild/win32-ia32": "0.20.0", - "@esbuild/win32-x64": "0.20.0" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/lru-cache": { diff --git a/editors/vscode/package.json b/editors/vscode/package.json index e04b51e98..4d77f7bde 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -36,6 +36,12 @@ "default": 25564, "title": "Language server port", "description": "Port to connect to GroovyScript mod" + }, + "groovyscript.enableIcons": { + "type": "boolean", + "default": true, + "title": "Enable Inline Icons", + "description": "Enables preview icons of some global methods like item()" } } }, @@ -62,6 +68,6 @@ "devDependencies": { "@types/node": "^20.11.17", "@types/vscode": "^1.81.0", - "esbuild": "^0.20.0" + "esbuild": "0.20.2" } } diff --git a/editors/vscode/src/features/TextureDecoration.ts b/editors/vscode/src/features/TextureDecoration.ts new file mode 100644 index 000000000..7e65c71f6 --- /dev/null +++ b/editors/vscode/src/features/TextureDecoration.ts @@ -0,0 +1,91 @@ +import { CancellationToken, Disposable, ProviderResult, TextDocument, Range as VRange } from 'vscode'; +import { ClientCapabilities, DocumentColorOptions, DocumentSelector, ensure, FeatureClient, MessageDirection, PartialResultParams, ProtocolRequestType, RequestHandler, ServerCapabilities, StaticRegistrationOptions, TextDocumentIdentifier, TextDocumentLanguageFeature, TextDocumentRegistrationOptions, WorkDoneProgressOptions, WorkDoneProgressParams } from 'vscode-languageclient'; +import { registerTextureDecorationProvider } from '../languageProviders/TextureDecorationLanguageProvider'; + +export interface TextureDecorationParams extends WorkDoneProgressParams, PartialResultParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; +} + +export interface TextureDecorationInformation { + range: VRange; + textureUri: string; + tooltips: string[]; +} + +export interface TextureDecorationOptions extends WorkDoneProgressOptions { +} + +export interface TextureDecorationRegistrationOptions extends TextDocumentRegistrationOptions, StaticRegistrationOptions, DocumentColorOptions { +} + +export interface ProvideTextureDecorationsSignature { + (document: TextDocument, token: CancellationToken): ProviderResult; +} + +export interface TextureDecorationProvider { + provideTextureDecoration(document: TextDocument, token: CancellationToken): ProviderResult; +} + +export interface TextureDecorationMiddleware { + provideTextureDecorations?: (this: void, document: TextDocument, token: CancellationToken, next: ProvideTextureDecorationsSignature) => ProviderResult; +} + +export namespace TextureDecorationRequest { + export const method: 'groovyScript/textureDecoration' = 'groovyScript/textureDecoration'; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); + export type HandlerSignature = RequestHandler; +} + +export class TextureDecorationFeature extends TextDocumentLanguageFeature { + constructor(client: FeatureClient) { + super(client, TextureDecorationRequest.type); + } + fillClientCapabilities(capabilities: ClientCapabilities): void { + ensure(ensure(capabilities, 'experimental')!, 'textureDecorationProvider')!.dynamicRegistration = true; + } + initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { + const [id, options] = this.getRegistration(documentSelector, capabilities.experimental.textureDecorationProvider); + if (!id || !options) { + return; + } + this.register({ id: id, registerOptions: options }); + } + protected registerLanguageProvider(options: TextureDecorationRegistrationOptions, id: string): [Disposable, TextureDecorationProvider] { + const selector = options.documentSelector!; + + const provider: TextureDecorationProvider = { + provideTextureDecoration: (document, token) => { + const client = this._client; + const provideTextureDecorations: ProvideTextureDecorationsSignature = (document, token) => { + const requestParams: TextureDecorationParams = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), + }; + + return client.sendRequest(TextureDecorationRequest.type, requestParams, token).then((result) => { + if (token.isCancellationRequested) { + return null; + } + return result.map(decoration => ({ + range: decoration.range, + textureUri: client.protocol2CodeConverter.asUri(decoration.textureUri).toString(true), + tooltips: decoration.tooltips + })); + }, (error) => { + return client.handleFailedRequest(TextureDecorationRequest.type, token, error, null); + }); + }; + const middleware = client.middleware; + return middleware.provideTextureDecorations + ? middleware.provideTextureDecorations(document, token, provideTextureDecorations) + : provideTextureDecorations(document, token); + }, + }; + + return [registerTextureDecorationProvider(this._client.protocol2CodeConverter.asDocumentSelector(selector), provider), provider]; + } + +} \ No newline at end of file diff --git a/editors/vscode/src/languageProviders/TextureDecorationLanguageProvider.ts b/editors/vscode/src/languageProviders/TextureDecorationLanguageProvider.ts new file mode 100644 index 000000000..5b73ff90d --- /dev/null +++ b/editors/vscode/src/languageProviders/TextureDecorationLanguageProvider.ts @@ -0,0 +1,66 @@ +import * as vscode from "vscode"; +import { DocumentSelector, Disposable, window as vWindow, workspace as vWorkspace, CancellationTokenSource, TextEditor, languages, DecorationOptions, Uri } from "vscode"; +import { TextureDecorationInformation, TextureDecorationProvider } from "../features/TextureDecoration"; + +export function registerTextureDecorationProvider(selector: DocumentSelector, provider: TextureDecorationProvider): Disposable { + let cancellationSource = new CancellationTokenSource(); + + function cancel() { + cancellationSource.cancel(); + cancellationSource.dispose(); + cancellationSource = new CancellationTokenSource(); + } + + async function doDecorate(editor: TextEditor): Promise { + const configuration = vscode.workspace.getConfiguration("groovyscript"); + if (configuration.get("enableIcons", true)) { + const result = await provider.provideTextureDecoration(editor.document, cancellationSource.token); + if (result) { + decorate(editor, result); + } + return; + } + removeDecoration(editor) + return; + } + + const editorChangedHandler = async (editor: TextEditor | undefined): Promise => { + if (editor && languages.match(selector, editor.document)) { + cancel(); + await doDecorate(editor) + } + }; + const changedActiveTextEditor = vWindow.onDidChangeActiveTextEditor(editorChangedHandler); + + editorChangedHandler(vWindow.activeTextEditor); + + const changedDocumentText = vWorkspace.onDidChangeTextDocument(async event => { + if (vWindow.activeTextEditor?.document === event.document && languages.match(selector, event.document)) { + cancel(); + await doDecorate(vWindow.activeTextEditor) + } + }) + + return new Disposable(() => { + changedActiveTextEditor.dispose(); + changedDocumentText.dispose(); + }); +} + +function removeDecoration(textEditor: TextEditor) { + textEditor.setDecorations(decorationStyle, []) +} + +function decorate(textEditor: TextEditor, decorations: TextureDecorationInformation[]) { + textEditor.setDecorations(decorationStyle, decorations.map(decoration => ({ + range: decoration.range, + hoverMessage: decoration.tooltips, + renderOptions: { + before: { + contentIconPath: Uri.parse(decoration.textureUri, true), + } + } + }))) +} + +const decorationStyle = vWindow.createTextEditorDecorationType({}) \ No newline at end of file diff --git a/editors/vscode/src/main.ts b/editors/vscode/src/main.ts index 323e17564..a044cff31 100755 --- a/editors/vscode/src/main.ts +++ b/editors/vscode/src/main.ts @@ -2,6 +2,8 @@ import * as net from "net"; import * as lc from "vscode-languageclient/node"; import * as vscode from "vscode"; import { extensionStatusBar } from "./gui/extensionStatusBarProvider"; +import { TextureDecorationFeature, TextureDecorationMiddleware } from "./features/TextureDecoration"; +import { FeatureClient } from "vscode-languageclient/node"; let client: lc.LanguageClient; let outputChannel = vscode.window.createOutputChannel("GroovyScript Language Server"); @@ -37,7 +39,9 @@ async function startClient() { traceOutputChannel, }; - client = new lc.LanguageClient("groovyscript", "GroovyScript", serverOptions, clientOptions) + client = new lc.LanguageClient("groovyscript", "GroovyScript", serverOptions, clientOptions); + + registerFeatures(); try { await client.start(); @@ -51,6 +55,10 @@ async function startClient() { outputChannel.appendLine("Connected to GroovyScript Language Server"); } +function registerFeatures() { + client.registerFeature(new TextureDecorationFeature(>client)); +} + async function stopClient() { if (!client || !client.isRunning()) return; try { diff --git a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java index 84a11cfb9..2ea8084a8 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -23,7 +23,7 @@ import com.cleanroommc.groovyscript.sandbox.*; import com.cleanroommc.groovyscript.sandbox.mapper.GroovyDeobfMapper; import com.cleanroommc.groovyscript.sandbox.meta.GrSMetaClassCreationHandle; -import com.cleanroommc.groovyscript.server.GroovyScriptLanguageServer; +import com.cleanroommc.groovyscript.server.GroovyScriptLanguageServerImpl; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import groovy.lang.GroovySystem; @@ -328,7 +328,7 @@ public static void postScriptRunResult(ICommandSender sender, boolean onlyLogFai public static boolean runLanguageServer() { if (languageServerThread != null) return false; - languageServerThread = new Thread(() -> GroovyScriptLanguageServer.listen(getSandbox().getScriptRoot())); + languageServerThread = new Thread(() -> GroovyScriptLanguageServerImpl.listen(getSandbox().getScriptRoot())); languageServerThread.start(); return true; } diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/mods/ModSupport.java b/src/main/java/com/cleanroommc/groovyscript/compat/mods/ModSupport.java index 701c4bd63..fa98883e4 100644 --- a/src/main/java/com/cleanroommc/groovyscript/compat/mods/ModSupport.java +++ b/src/main/java/com/cleanroommc/groovyscript/compat/mods/ModSupport.java @@ -136,9 +136,9 @@ public static Collection> get return Collections.unmodifiableList(containerList); } - private ModSupport() { - } + private ModSupport() {} + @GroovyBlacklist @ApiStatus.Internal public void setup(ASMDataTable dataTable) { for (ASMDataTable.ASMData data : dataTable.getAll(GroovyPlugin.class.getName().replace('.', '/'))) { @@ -155,6 +155,7 @@ public void setup(ASMDataTable dataTable) { } } + @GroovyBlacklist private void registerContainer(GroovyPlugin container) { if (container instanceof GroovyContainer) { GroovyScript.LOGGER.error("GroovyPlugin must not extend {}", GroovyContainer.class.getSimpleName()); @@ -185,6 +186,7 @@ private void registerContainer(GroovyPlugin container) { externalPluginClasses.add(container.getClass()); } + @GroovyBlacklist void registerContainer(GroovyContainer container) { if (containerList.contains(container) || containers.containsKey(container.getModId())) { throw new IllegalStateException("Container already present!"); diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/ItemStackMixinExpansion.java b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/ItemStackMixinExpansion.java index 88e43240a..b6f2d8736 100644 --- a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/ItemStackMixinExpansion.java +++ b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/ItemStackMixinExpansion.java @@ -1,5 +1,6 @@ package com.cleanroommc.groovyscript.compat.vanilla; +import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.api.IIngredient; import com.cleanroommc.groovyscript.api.INBTResourceStack; import com.cleanroommc.groovyscript.api.INbtIngredient; @@ -35,8 +36,10 @@ static ItemStackMixinExpansion of(ItemStack stack) { void grs$setTransformer(ItemStackTransformer transformer); + @GroovyBlacklist void grs$setNbtMatcher(Predicate matcher); + @GroovyBlacklist void grs$setMatcher(Predicate matcher); @Nullable @@ -44,6 +47,7 @@ static ItemStackMixinExpansion of(ItemStack stack) { void grs$setMark(String mark); + @GroovyBlacklist default boolean grs$isEmpty() { return grs$getItemStack().isEmpty(); } diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/ItemStackMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/ItemStackMixin.java index b9996f9f6..54c836a43 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/ItemStackMixin.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/ItemStackMixin.java @@ -1,5 +1,6 @@ package com.cleanroommc.groovyscript.core.mixin; +import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.compat.vanilla.ItemStackMixinExpansion; import com.cleanroommc.groovyscript.compat.vanilla.ItemStackTransformer; import net.minecraft.item.ItemStack; @@ -22,26 +23,31 @@ public abstract class ItemStackMixin implements ItemStackMixinExpansion { @Unique protected String groovyScript$mark; + @GroovyBlacklist @Override public ItemStack grs$getItemStack() { return (ItemStack) (Object) this; } + @GroovyBlacklist @Override public ItemStackTransformer grs$getTransformer() { return groovyScript$transformer; } + @GroovyBlacklist @Override public Predicate grs$getMatcher() { return groovyScript$matchCondition; } + @GroovyBlacklist @Override public Predicate grs$getNbtMatcher() { return groovyScript$nbtMatcher; } + @GroovyBlacklist @Override public void grs$setTransformer(ItemStackTransformer transformer) { if (grs$getItemStack() != ItemStack.EMPTY) { @@ -49,6 +55,7 @@ public abstract class ItemStackMixin implements ItemStackMixinExpansion { } } + @GroovyBlacklist @Override public void grs$setMatcher(Predicate matcher) { if (grs$getItemStack() != ItemStack.EMPTY) { @@ -56,6 +63,7 @@ public abstract class ItemStackMixin implements ItemStackMixinExpansion { } } + @GroovyBlacklist @Override public void grs$setNbtMatcher(Predicate nbtMatcher) { if (grs$getItemStack() != ItemStack.EMPTY) { @@ -63,12 +71,14 @@ public abstract class ItemStackMixin implements ItemStackMixinExpansion { } } + @GroovyBlacklist @Nullable @Override public String grs$getMark() { return groovyScript$mark; } + @GroovyBlacklist @Override public void grs$setMark(String mark) { this.groovyScript$mark = mark; diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java index 09ad6e0ee..db086edf9 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java @@ -26,12 +26,12 @@ public abstract class ModuleNodeMixin { public void init(SourceUnit context, CallbackInfo ci) { // auto set package name String script = context.getName(); - if (!RunConfig.isGroovyFile(script)) { + String rel; + if (!RunConfig.isGroovyFile(script) || (rel = FileUtil.relativizeNullable(GroovyScript.getScriptPath(), script)) == null) { // probably not a script file // can happen with traits return; } - String rel = FileUtil.relativize(GroovyScript.getScriptPath(), script); int i = rel.lastIndexOf(File.separatorChar); if (i >= 0) { // inject correct package declaration into script @@ -43,7 +43,8 @@ public void init(SourceUnit context, CallbackInfo ci) { @Inject(method = "setPackage", at = @At("HEAD"), cancellable = true) public void setPackage(PackageNode packageNode, CallbackInfo ci) { if (this.packageNode == null || this.context == null) return; - if (!RunConfig.isGroovyFile(this.context.getName())) { + String rel; + if (!RunConfig.isGroovyFile(this.context.getName()) || (rel = FileUtil.relativizeNullable(GroovyScript.getScriptPath(), this.context.getName())) == null) { // probably not a script file // can happen with traits return; @@ -52,7 +53,6 @@ public void setPackage(PackageNode packageNode, CallbackInfo ci) { String cur = this.packageNode.getName(); String newName = packageNode.getName(); if (!cur.equals(newName)) { - String rel = FileUtil.relativize(GroovyScript.getScriptPath(), this.context.getName()); GroovyLog.get().error("Expected package {} but got {} in script {}", cur, newName, rel); } if (this.packageNode.getAnnotations() != null) { diff --git a/src/main/java/com/cleanroommc/groovyscript/gameobjects/GameObjectHandlerManager.java b/src/main/java/com/cleanroommc/groovyscript/gameobjects/GameObjectHandlerManager.java index fde76dc38..15d7e3e69 100644 --- a/src/main/java/com/cleanroommc/groovyscript/gameobjects/GameObjectHandlerManager.java +++ b/src/main/java/com/cleanroommc/groovyscript/gameobjects/GameObjectHandlerManager.java @@ -20,7 +20,7 @@ public static void init() { @Nullable public static Object getGameObject(String name, String mainArg, Object... args) { - return ObjectMapperManager.getGameObject(name, mainArg, args); + return ObjectMapperManager.getGameObject(false, name, mainArg, args); } public static boolean hasGameObjectHandler(String key) { diff --git a/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapper.java b/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapper.java index 5f0d1c557..782d195d1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapper.java +++ b/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapper.java @@ -18,6 +18,7 @@ import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; import java.lang.reflect.Modifier; import java.util.*; @@ -53,9 +54,10 @@ public static Builder builder(String name, Class returnType) { private final List[]> paramTypes; private final Completer completer; private final String documentation; + private final TextureBinder textureBinder; private List methodNodes; - private ObjectMapper(String name, GroovyContainer mod, IObjectParser handler, Supplier> defaultValue, Class returnType, List[]> paramTypes, Completer completer, String documentation) { + private ObjectMapper(String name, GroovyContainer mod, IObjectParser handler, Supplier> defaultValue, Class returnType, List[]> paramTypes, Completer completer, String documentation, TextureBinder textureBinder) { super(null); this.name = name; this.mod = mod; @@ -65,26 +67,34 @@ private ObjectMapper(String name, GroovyContainer mod, IObjectParser handl this.paramTypes = paramTypes; this.completer = completer; this.documentation = documentation; + this.textureBinder = textureBinder; } - T invoke(String s, Object... args) { + @Nullable + public T invoke(boolean silent, String s, Object... args) { Result t = Objects.requireNonNull(handler.parse(s, args), "Object mapper must return a non null result!"); if (t.hasError()) { - if (this.mod == null) { - GroovyLog.get().error("Can't find {} for name {}!", name, s); - } else { - GroovyLog.get().error("Can't find {} {} for name {}!", mod, name, s); - } - if (t.getError() != null && !t.getError().isEmpty()) { - GroovyLog.get().error(" - reason: {}", t.getError()); + if (!silent) { + if (this.mod == null) { + GroovyLog.get().error("Can't find {} for name {}!", name, s); + } else { + GroovyLog.get().error("Can't find {} {} for name {}!", mod, name, s); + } + if (t.getError() != null && !t.getError().isEmpty()) { + GroovyLog.get().error(" - reason: {}", t.getError()); + } } - t = this.defaultValue.get(); - return t == null || t.hasError() ? null : t.getValue(); + return null; } return Objects.requireNonNull(t.getValue(), "Object mapper result must contain a non-null value!"); } - T invokeDefault() { + public T invokeWithDefault(boolean silent, String s, Object... args) { + T t = invoke(silent, s, args); + return t != null ? t : invokeDefault(); + } + + public T invokeDefault() { Result t = this.defaultValue.get(); return t == null || t.hasError() ? null : t.getValue(); } @@ -116,7 +126,7 @@ public Completer getCompleter() { } public T doCall(String s, Object... args) { - return invoke(s, args); + return invokeWithDefault(false, s, args); } public T doCall() { @@ -146,6 +156,11 @@ public List getMethodNodes() { return methodNodes; } + @ApiStatus.Experimental + public TextureBinder getTextureBinder() { + return textureBinder; + } + /** * A helper class to create {@link ObjectMapper}s. * @@ -161,6 +176,7 @@ public static class Builder { private final List[]> paramTypes = new ArrayList<>(); private Completer completer; private String documentation; + private TextureBinder textureBinder; @ApiStatus.Internal public Builder(String name, Class returnType) { @@ -323,6 +339,12 @@ public Builder docOfType(String type) { return documentation("returns a " + mod + type); } + @ApiStatus.Experimental + public Builder textureBinder(TextureBinder textureBinder) { + this.textureBinder = textureBinder; + return this; + } + /** * Registers the mapper. * @@ -338,7 +360,7 @@ public void register() { if (this.defaultValue == null) this.defaultValue = () -> null; this.documentation = IDocumented.toJavaDoc(this.documentation); ObjectMapper goh = new ObjectMapper<>(this.name, this.mod, this.handler, this.defaultValue, - this.returnType, this.paramTypes, this.completer, this.documentation); + this.returnType, this.paramTypes, this.completer, this.documentation, this.textureBinder); ObjectMapperManager.registerObjectMapper(this.mod, goh); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapperManager.java b/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapperManager.java index 5c9b591ab..2cb545716 100644 --- a/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapperManager.java +++ b/src/main/java/com/cleanroommc/groovyscript/mapper/ObjectMapperManager.java @@ -19,10 +19,12 @@ import net.minecraft.creativetab.CreativeTabs; import net.minecraft.enchantment.Enchantment; import net.minecraft.init.Blocks; +import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; import net.minecraft.potion.PotionType; +import net.minecraft.potion.PotionUtils; import net.minecraft.util.ResourceLocation; import net.minecraft.util.SoundEvent; import net.minecraft.util.text.TextFormatting; @@ -85,6 +87,7 @@ public static void init() { .parser((s, args) -> s.contains(WILDCARD) ? Result.some(OreDictWildcardIngredient.of(s)) : Result.some(new OreDictIngredient(s))) .completerOfNames(OreDictionaryAccessor::getIdToName) .docOfType("ore dict entry") + .textureBinder(TextureBinder.of(i -> Arrays.asList(i.getMatchingStacks()), TextureBinder.ofItem(), i -> String.format("![](${item('%s')}) %s", i.getItem().getRegistryName(), i.getDisplayName()))) .register(); ObjectMapper.builder("item", ItemStack.class) .parser(ObjectMappers::parseItemStack) @@ -93,21 +96,25 @@ public static void init() { .defaultValue(() -> ItemStack.EMPTY) .completer(ForgeRegistries.ITEMS) .docOfType("item stack") + .textureBinder(TextureBinder.ofItem()) .register(); ObjectMapper.builder("liquid", FluidStack.class) .parser(ObjectMappers::parseFluidStack) .completerOfNames(FluidRegistry.getRegisteredFluids()::keySet) .docOfType("fluid stack") + .textureBinder(TextureBinder.ofFluid()) .register(); ObjectMapper.builder("fluid", FluidStack.class) .parser(ObjectMappers::parseFluidStack) .completerOfNames(FluidRegistry.getRegisteredFluids()::keySet) + .textureBinder(TextureBinder.ofFluid()) .register(); ObjectMapper.builder("block", Block.class) .parser(IObjectParser.wrapForgeRegistry(ForgeRegistries.BLOCKS)) .completer(ForgeRegistries.BLOCKS) .defaultValue(() -> Blocks.AIR) .docOfType("block") + .textureBinder(TextureBinder.of(ItemStack::new, TextureBinder.ofItem())) .register(); ObjectMapper.builder("blockstate", IBlockState.class) .parser(ObjectMappers::parseBlockState) @@ -127,6 +134,7 @@ public static void init() { .parser(IObjectParser.wrapForgeRegistry(ForgeRegistries.POTIONS)) .completer(ForgeRegistries.POTIONS) .docOfType("potion") + .textureBinder(TextureBinder.of(potion -> PotionUtils.addPotionToItemStack(new ItemStack(Items.POTIONITEM), PotionType.REGISTRY.getObject(potion.getRegistryName())), TextureBinder.ofItem())) .register(); ObjectMapper.builder("potionType", PotionType.class) .parser(IObjectParser.wrapForgeRegistry(ForgeRegistries.POTION_TYPES)) @@ -203,9 +211,23 @@ public static void init() { */ @Nullable public static Object getGameObject(String name, String mainArg, Object... args) { + return getGameObject(false, name, mainArg, args); + } + + /** + * Finds the game object handle and invokes it. Called by injected calls via the groovy script transformer. + * + * @param name game object handler name (method name) + * @param mainArg main argument + * @param args extra arguments + * @param silent if error messages should be logged + * @return game object or null + */ + @Nullable + public static Object getGameObject(boolean silent, String name, String mainArg, Object... args) { ObjectMapper objectMapper = handlers.get(name); if (objectMapper != null) { - return objectMapper.invoke(mainArg, args); + return objectMapper.invokeWithDefault(silent, mainArg, args); } return null; } diff --git a/src/main/java/com/cleanroommc/groovyscript/mapper/TextureBinder.java b/src/main/java/com/cleanroommc/groovyscript/mapper/TextureBinder.java new file mode 100644 index 000000000..114572ec7 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/mapper/TextureBinder.java @@ -0,0 +1,103 @@ +package com.cleanroommc.groovyscript.mapper; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * This interface draws objects, so they can be rendered as icons in an ide. + * + * This is marked as experimental. This class is likely to change and may even be removed in a future update. + */ +@ApiStatus.Experimental +public interface TextureBinder extends Function> { + + static TextureBinder of(Function mapper, TextureBinder binder) { + return o -> binder.apply(mapper.apply(o)); + } + + static TextureBinder of(Function> mapper, TextureBinder binder, Function tooltipMapper) { + return o -> { + var list = mapper.apply(o); + binder.apply(list.get(0)); + return list.stream().map(tooltipMapper).collect(Collectors.toList()); + }; + } + + static TextureBinder ofItem() { + return item -> { + if (Minecraft.getMinecraft().isCallingFromMinecraftThread()) { + GlStateManager.enableDepth(); + RenderHelper.enableGUIStandardItemLighting(); + var mc = Minecraft.getMinecraft(); + var fontRenderer = item.getItem().getFontRenderer(item); + if (fontRenderer == null) + fontRenderer = mc.fontRenderer; + mc.getRenderItem().renderItemAndEffectIntoGUI(null, item, 0, 0); + mc.getRenderItem().renderItemOverlayIntoGUI(fontRenderer, item, 0, 0, null); + GlStateManager.disableBlend(); + RenderHelper.disableStandardItemLighting(); + GlStateManager.enableAlpha(); + GlStateManager.disableDepth(); + } + + return Collections.singletonList(item.getDisplayName()); + }; + } + + static TextureBinder ofFluid() { + return fluid -> { + if (Minecraft.getMinecraft().isCallingFromMinecraftThread()) { + GlStateManager.enableBlend(); + GlStateManager.enableAlpha(); + + Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE); + + var texture = Minecraft.getMinecraft().getTextureMapBlocks().getTextureExtry(fluid.getFluid().getStill(fluid).toString()); + + if (texture == null) { + texture = Minecraft.getMinecraft().getTextureMapBlocks().getMissingSprite(); + } + + var color = fluid.getFluid().getColor(fluid); + GlStateManager.color((color >> 16 & 0xFF) / 255.0F, (color >> 8 & 0xFF) / 255.0F, (color & 0xFF) / 255.0F, (color >> 24 & 0xFF) / 255.0F); + + drawSprite(texture); + + GlStateManager.disableAlpha(); + GlStateManager.disableBlend(); + } + + return Collections.singletonList(fluid.getLocalizedName()); + }; + } + + static void drawSprite(TextureAtlasSprite textureSprite) { + double uMin = textureSprite.getMinU(); + double uMax = textureSprite.getMaxU(); + double vMin = textureSprite.getMinV(); + double vMax = textureSprite.getMaxV(); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder bufferBuilder = tessellator.getBuffer(); + bufferBuilder.begin(7, DefaultVertexFormats.POSITION_TEX); + bufferBuilder.pos(0, 16, 0).tex(uMin, vMax).endVertex(); + bufferBuilder.pos(16, 16, 0).tex(uMax, vMax).endVertex(); + bufferBuilder.pos(16, 0, 0).tex(uMax, vMin).endVertex(); + bufferBuilder.pos(0, 0, 0).tex(uMin, vMin).endVertex(); + tessellator.draw(); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/mapper/TextureTooltip.java b/src/main/java/com/cleanroommc/groovyscript/mapper/TextureTooltip.java new file mode 100644 index 000000000..20a4e2abe --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/mapper/TextureTooltip.java @@ -0,0 +1,91 @@ +package com.cleanroommc.groovyscript.mapper; + +import org.apache.commons.codec.digest.DigestUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class TextureTooltip { + private static final Pattern embeddingPattern = Pattern.compile("(?>\\$\\{(?\\w+)\\(['\\\"](?[\\w\\s:-]+)['\\\"]\\)})"); + + private final String content; + private final List embeddings; + + public TextureTooltip(String content) { + this.content = content; + this.embeddings = new ArrayList<>(); + + var matcher = embeddingPattern.matcher(content); + + while (matcher.find()) { + var mapper = ObjectMapperManager.getObjectMapper(matcher.group("mapper")); + if (mapper == null) { + continue; + } + + var binder = mapper.getTextureBinder(); + + if (binder == null) { + continue; + } + + var key = matcher.group("key"); + var result = mapper.invoke(true, key); + + if (result == null) { + continue; + } + + embeddings.add(new Embedding(matcher.start(), matcher.end(), result, binder, computeTextureName(mapper.getName(), key))); + } + } + + private static String computeTextureName(String name, String arg) { + return DigestUtils.sha1Hex(name + arg); + } + + public String getContent() { + return content; + } + + public List getEmbeddings() { + return embeddings; + } + + public class Embedding { + private final int start; + private final int end; + private final T context; + private final TextureBinder textureBinder; + private final String textureName; + + private Embedding(int start, int end, T context, TextureBinder textureBinder, String textureName) { + this.start = start; + this.end = end; + this.context = context; + this.textureBinder = textureBinder; + this.textureName = textureName; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public T getContext() { + return context; + } + + public TextureBinder getTextureBinder() { + return textureBinder; + } + + public String getTextureName() { + return textureName; + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/FileUtil.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/FileUtil.java index fa70115a0..5201b19b0 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/FileUtil.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/FileUtil.java @@ -1,45 +1,77 @@ package com.cleanroommc.groovyscript.sandbox; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; import net.minecraftforge.fml.common.Loader; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; public class FileUtil { - public static String relativize(String rootPath, String longerThanRootPath) { - try { - longerThanRootPath = URLDecoder.decode(longerThanRootPath, "UTF-8"); - } catch (UnsupportedEncodingException ignored) { - } + private static final Char2ObjectOpenHashMap encodings = new Char2ObjectOpenHashMap<>(); + static { + encodings.put(' ', "%20"); + encodings.put('!', "%21"); + encodings.put('"', "%22"); + encodings.put('#', "%23"); + encodings.put('$', "%24"); + encodings.put('%', "%25"); + encodings.put('&', "%26"); + encodings.put('\'', "%27"); + encodings.put('(', "%28"); + encodings.put(')', "%29"); + encodings.put('+', "%2B"); + encodings.put(',', "%2C"); + //encodings.put(':', "%3F"); // do not encode : + encodings.put(';', "%3B"); + encodings.put('<', "%3C"); + encodings.put('=', "%3D"); + encodings.put('>', "%3E"); + encodings.put('?', "%3F"); + encodings.put('@', "%40"); + encodings.put('[', "%5B"); + encodings.put(']', "%5D"); + encodings.put('{', "%7B"); + encodings.put('|', "%7C"); + encodings.put('}', "%7D"); if (File.separatorChar != '/') { - longerThanRootPath = longerThanRootPath.replace('/', File.separatorChar); + encodings.put('\\', "/"); } - return relativizeInternal(fixDriveCase(rootPath), fixDriveCase(longerThanRootPath)); } - private static String relativizeInternal(String rootPath, String longerThanRootPath) { + @NotNull + public static String relativize(String rootPath, String longerThanRootPath) { + longerThanRootPath = encodeURI(fixPath(decodeURI(longerThanRootPath))); + rootPath = encodeURI(rootPath); + return relativizeInternal(rootPath, longerThanRootPath, false); + } + + @Nullable + public static String relativizeNullable(String rootPath, String longerThanRootPath) { + longerThanRootPath = encodeURI(fixPath(decodeURI(longerThanRootPath))); + rootPath = encodeURI(rootPath); + return relativizeInternal(rootPath, longerThanRootPath, true); + } + + @Contract("_,_,false -> !null") + private static String relativizeInternal(String rootPath, String longerThanRootPath, boolean nullable) { int index = longerThanRootPath.indexOf(rootPath); if (index < 0) { + if (nullable) return null; throw new IllegalArgumentException("The path '" + longerThanRootPath + "' does not contain the root path '" + rootPath + "'"); } return longerThanRootPath.substring(index + rootPath.length() + 1); } - // sometimes the paths passed to relativize() have a lower case drive letter - public static String fixDriveCase(String path) { - if (path == null || path.length() < 2) return path; - if (Character.isLowerCase(path.charAt(0)) && path.charAt(1) == ':') { - return Character.toUpperCase(path.charAt(0)) + ":" + path.substring(2); - } - return path; - } - public static String getParent(String path) { int i = path.lastIndexOf(File.separatorChar); if (i <= 0) return StringUtils.EMPTY; @@ -98,4 +130,127 @@ public static boolean mkdirsAndFile(File file) { } return b; } + + public static URI fixUri(URI uri) { + String scheme = uri.getScheme(); + return URI.create(scheme + ':' + fixUriString(uri.getRawPath())); + } + + public static URI fixUri(String uri) { + return URI.create(fixUriString(uri)); + } + + public static String fixUriString(String uri) { + return encodeURI(fixPath(decodeURI(uri))); + } + + public static String fixPath(String uri) { + int i = 0, s = 0; + if (uri.startsWith("file:")) i = 5; + while (uri.charAt(i + s) == '/') s++; + i += s; + char c = uri.charAt(i); + if (uri.length() <= i + 1 || !Character.isLowerCase(c)) return uri; + int d = 1; + if (uri.charAt(i + 1) != ':') { + if (uri.length() <= i + 3 || uri.charAt(i + 1) != '%' || uri.charAt(i + 2) != '3' || uri.charAt(i + 3) != 'F') return uri; + d = 3; + } + StringBuilder builder = new StringBuilder(); + s -= Math.min(s, 3); // max 3 '/' after file: + if (i > 0) builder.append(uri, 0, i - s); + builder.append(Character.toUpperCase(uri.charAt(i + d - 1))); + builder.append(uri, i + d, uri.length()); + return builder.toString(); + } + + public static String decodeURI(String s) { + Charset charset = StandardCharsets.UTF_8; + boolean needToChange = false; + int numChars = s.length(); + StringBuilder sb = new StringBuilder(numChars > 500 ? numChars / 2 : numChars); + int i = 0; + + char c; + byte[] bytes = null; + while (i < numChars) { + c = s.charAt(i); + switch (c) { + case '+': + sb.append(' '); + i++; + needToChange = true; + break; + case '%': + /* + * Starting with this instance of %, process all consecutive substrings of the form %xy. Each + * substring %xy will yield a byte. Convert all consecutive bytes obtained this way to whatever + * character(s) they represent in the provided encoding. + */ + + try { + + // (numChars-i)/3 is an upper bound for the number + // of remaining bytes + if (bytes == null) bytes = new byte[(numChars - i) / 3]; + int pos = 0; + + while (((i + 2) < numChars) && (c == '%')) { + int v = parseFromHex(s.charAt(i + 1), s.charAt(i + 2));//Integer.parseInt(s, i + 1, i + 3, 16); + if (v < 0) { + throw new IllegalArgumentException( + "URLDecoder: Illegal hex characters in escape " + "(%) pattern - negative value"); + } + bytes[pos++] = (byte) v; + i += 3; + if (i < numChars) c = s.charAt(i); + } + + // A trailing, incomplete byte encoding such as + // "%x" will cause an exception to be thrown + + if ((i < numChars) && (c == '%')) { + throw new IllegalArgumentException("URLDecoder: Incomplete trailing escape (%) pattern"); + } + + sb.append(new String(bytes, 0, pos, charset)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("URLDecoder: Illegal hex characters in escape (%) pattern - " + e.getMessage()); + } + needToChange = true; + break; + default: + if (c == File.separatorChar && File.separatorChar != '/') { + needToChange = true; + c = '/'; + } + sb.append(c); + i++; + break; + } + } + + return (needToChange ? sb.toString() : s); + } + + private static int parseFromHex(char c0, char c1) { + return Character.digit(c0, 16) * 16 + Character.digit(c1, 16); + } + + public static String encodeURI(String s) { + if (s == null || s.isEmpty()) return s; + boolean needToChange = false; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + String rep = encodings.get(c); + if (rep != null) { + builder.append(rep); + needToChange = true; + } else { + builder.append(c); + } + } + return needToChange ? builder.toString() : s; + } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java index 1a3280932..2b7365e96 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java @@ -12,6 +12,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.codehaus.groovy.runtime.InvokerHelper; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -36,7 +37,7 @@ public abstract class GroovySandbox { private final URL[] scriptEnvironment; private final ThreadLocal running = ThreadLocal.withInitial(() -> false); private final Map bindings = new Object2ObjectOpenHashMap<>(); - private final Set> staticImports = new HashSet<>(); + private final ImportCustomizer importCustomizer = new ImportCustomizer(); private final CachedClassLoader ccl = new CachedClassLoader(); protected GroovySandbox(URL[] scriptEnvironment) { @@ -65,15 +66,6 @@ public void registerBinding(INamed named) { } } - protected void registerStaticImports(Class... classes) { - Objects.requireNonNull(classes); - if (classes.length == 0) { - throw new IllegalArgumentException("Static imports must not be empty!"); - } - - Collections.addAll(staticImports, classes); - } - protected void startRunning() { this.running.set(true); } @@ -86,6 +78,7 @@ protected GroovyScriptEngine createScriptEngine() { GroovyScriptEngine engine = new GroovyScriptEngine(this.scriptEnvironment, this.ccl); CompilerConfiguration config = new CompilerConfiguration(CompilerConfiguration.DEFAULT); config.setSourceEncoding("UTF-8"); + config.addCompilationCustomizers(this.importCustomizer); engine.setConfig(config); initEngine(engine, config); return engine; @@ -214,8 +207,8 @@ public Map getBindings() { return bindings; } - public Set> getStaticImports() { - return staticImports; + public ImportCustomizer getImportCustomizer() { + return importCustomizer; } public String getCurrentScript() { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java index be16abfda..1e9118700 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -60,7 +60,6 @@ public class GroovyScriptSandbox extends GroovySandbox { private final File cacheRoot; private final File scriptRoot; - private final ImportCustomizer importCustomizer = new ImportCustomizer(); private final Map, AtomicInteger> storedExceptions; private final Map index = new Object2ObjectOpenHashMap<>(); @@ -74,9 +73,8 @@ public GroovyScriptSandbox(File scriptRoot, File cacheRoot) throws MalformedURLE registerBinding("Log", GroovyLog.get()); registerBinding("EventManager", GroovyEventManager.INSTANCE); - this.importCustomizer.addStaticStars(GroovyHelper.class.getName(), MathHelper.class.getName()); - registerStaticImports(GroovyHelper.class, MathHelper.class); - this.importCustomizer.addImports("net.minecraft.world.World", + getImportCustomizer().addStaticStars(GroovyHelper.class.getName(), MathHelper.class.getName()); + getImportCustomizer().addImports("net.minecraft.world.World", "net.minecraft.block.state.IBlockState", "net.minecraft.block.Block", "net.minecraft.block.SoundType", @@ -328,7 +326,6 @@ protected void postInitBindings(Binding binding) { protected void initEngine(GroovyScriptEngine engine, CompilerConfiguration config) { config.addCompilationCustomizers(new GroovyScriptCompiler()); config.addCompilationCustomizers(new GroovyScriptEarlyCompiler()); - config.addCompilationCustomizers(this.importCustomizer); } @Override @@ -379,10 +376,6 @@ public LoadStage getCurrentLoader() { return currentLoadStage; } - public ImportCustomizer getImportCustomizer() { - return importCustomizer; - } - public File getScriptRoot() { return scriptRoot; } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java index 77577d253..780570e9e 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java @@ -38,22 +38,47 @@ public static void registerPreprocessor(String name, BiPredicate public static List parsePreprocessors(File file) { List preprocessors = new ArrayList<>(); + parsePreprocessors(file, preprocessors); + return preprocessors.isEmpty() ? Collections.emptyList() : preprocessors; + } + + public static int getImportStartLine(File file) { + return parsePreprocessors(file, new ArrayList<>()); + } + + private static int parsePreprocessors(File file, List preprocessors) { + int lines = 0; + int empty = 0; + boolean lastEmpty = false; try (BufferedReader br = new BufferedReader(new FileReader(file))) { boolean isComment = false; String line; while ((line = br.readLine()) != null) { + lines++; line = line.trim(); - if (line.isEmpty()) continue; + if (!lastEmpty) empty = 0; + if (line.isEmpty()) { + empty++; + lastEmpty = true; + continue; + } + lastEmpty = false; if (line.startsWith("/*")) { - isComment = true; - line = line.substring(2).trim(); + if (line.endsWith("*/")) { + line = line.substring(2, line.length() - 2).trim(); + } else if (line.contains("*/")) { + return preprocessors.isEmpty() ? 0 : lines - empty - 1; + } else { + isComment = true; + line = line.substring(2).trim(); + } if (line.isEmpty()) continue; } if (line.startsWith("//")) { line = line.substring(2).trim(); if (line.isEmpty()) continue; } else if (!isComment) { - return preprocessors.isEmpty() ? Collections.emptyList() : preprocessors; + return preprocessors.isEmpty() ? 0 : lines - empty - 1; } if (isComment && line.endsWith("*/")) { isComment = false; @@ -66,7 +91,7 @@ public static List parsePreprocessors(File file) { } catch (IOException e) { throw new RuntimeException(e); } - return preprocessors.isEmpty() ? Collections.emptyList() : preprocessors; + return preprocessors.isEmpty() ? 0 : lines - empty - 1; } public static boolean validatePreprocessor(File file, List preprocessors) { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java index 8727b64be..2f8a6ff9f 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java @@ -1,6 +1,9 @@ package com.cleanroommc.groovyscript.sandbox.security; import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.IScriptReloadable; +import com.cleanroommc.groovyscript.compat.mods.GroovyPropertyContainer; +import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; import com.cleanroommc.groovyscript.sandbox.GroovyLogImpl; import com.cleanroommc.groovyscript.sandbox.expand.LambdaClosure; import groovy.lang.GroovyClassLoader; @@ -49,13 +52,16 @@ public void initDefaults() { banPackage("groovy.grape"); banPackage("groovy.beans"); banPackage("groovy.cli"); - banPackage("groovyjarjar"); + banPackage("groovyjarjarantlr4."); + banPackage("groovyjarjarasm."); + banPackage("groovyjarjarpicocli."); banPackage("sun."); // sun contains so many classes where some of them seem useful and others can break EVERYTHING, so im just gonna ban all because im lazy banPackage("javax.net"); banPackage("javax.security"); banPackage("javax.script"); banPackage("org.spongepowered"); banPackage("zone.rong.mixinbooter"); + banPackage("net.minecraftforge.gradle"); banClasses(Runtime.class, ClassLoader.class, Scanner.class); banClasses(GroovyScriptEngine.class, Eval.class, GroovyMain.class, GroovySocketServer.class, GroovyShell.class, GroovyClassLoader.class); banMethods(System.class, "exit", "gc", "setSecurityManager"); @@ -69,6 +75,10 @@ public void initDefaults() { banPackage("com.cleanroommc.groovyscript.core"); banPackage("com.cleanroommc.groovyscript.sandbox"); banPackage("com.cleanroommc.groovyscript.server"); + banPackage("net.prominic"); + banMethods(IScriptReloadable.class, "onReload", "afterScriptLoad"); + banMethods(VirtualizedRegistry.class, "createRecipeStorage"); + banMethods(GroovyPropertyContainer.class, "initialize"); } public void unBanClass(Class clazz) { @@ -104,8 +114,7 @@ public void banMethods(Class clazz, Collection method) { } public boolean isValid(Method method) { - return isValidMethod(method.getDeclaringClass(), method.getName()) && - !method.isAnnotationPresent(GroovyBlacklist.class); + return isValidMethod(method.getDeclaringClass(), method.getName()) && !method.isAnnotationPresent(GroovyBlacklist.class); } public boolean isValid(MetaMethod method) { @@ -117,8 +126,7 @@ public boolean isValid(Field field) { } public boolean isValid(Class clazz) { - return this.whiteListedClasses.contains(clazz) || - (isValidClass(clazz) && isValidPackage(clazz)); + return this.whiteListedClasses.contains(clazz) || (isValidClass(clazz) && isValidPackage(clazz)); } public boolean isValidPackage(Class clazz) { @@ -136,8 +144,19 @@ public boolean isValidClass(Class clazz) { } public boolean isValidMethod(Class receiver, String method) { + while (receiver != null && receiver != Object.class) { + if (isMethodBannedFromClass(receiver, method)) return false; + for (Class interf : receiver.getInterfaces()) { + if (isMethodBannedFromClass(interf, method)) return false; + } + receiver = receiver.getSuperclass(); + } + return true; + } + + private boolean isMethodBannedFromClass(Class receiver, String method) { Set methods = bannedMethods.get(receiver); - return methods == null || !methods.contains(method); + return methods != null && methods.contains(method); } public List getBannedPackages() { diff --git a/src/main/java/com/cleanroommc/groovyscript/server/Completions.java b/src/main/java/com/cleanroommc/groovyscript/server/Completions.java index dd8da7420..b5bd55b24 100644 --- a/src/main/java/com/cleanroommc/groovyscript/server/Completions.java +++ b/src/main/java/com/cleanroommc/groovyscript/server/Completions.java @@ -3,6 +3,7 @@ import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -24,7 +25,7 @@ public boolean reachedLimit() { return size() >= this.limit; } - public void addAll(Iterable values, Function toCompletionItem) { + public void addAll(Iterable values, Function toCompletionItem) { for (V v : values) { if (reachedLimit()) break; CompletionItem item = toCompletionItem.apply(v); @@ -34,7 +35,7 @@ public void addAll(Iterable values, Function toComplet } } - public void addAll(V[] values, Function toCompletionItem) { + public void addAll(V[] values, Function toCompletionItem) { for (V v : values) { if (reachedLimit()) break; CompletionItem item = toCompletionItem.apply(v); diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptCapabilities.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptCapabilities.java new file mode 100644 index 000000000..3cd3dfb30 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptCapabilities.java @@ -0,0 +1,14 @@ +package com.cleanroommc.groovyscript.server; + +public class GroovyScriptCapabilities { + + private Boolean textureDecorationProvider; + + public Boolean getTextureDecorationProvider() { + return textureDecorationProvider; + } + + public void setTextureDecorationProvider(Boolean textureDecorationProvider) { + this.textureDecorationProvider = textureDecorationProvider; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptDocumentationProvider.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptDocumentationProvider.java index 758960bc4..1e448516d 100644 --- a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptDocumentationProvider.java +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptDocumentationProvider.java @@ -3,13 +3,14 @@ import com.cleanroommc.groovyscript.api.IGroovyContainer; import com.cleanroommc.groovyscript.api.IScriptReloadable; import com.cleanroommc.groovyscript.api.documentation.annotations.MethodDescription; +import com.cleanroommc.groovyscript.api.documentation.annotations.RegistryDescription; import com.cleanroommc.groovyscript.compat.mods.ModSupport; import com.cleanroommc.groovyscript.documentation.Registry; import net.prominic.groovyls.compiler.ast.ASTContext; import net.prominic.groovyls.compiler.documentation.IDocumentationProvider; import net.prominic.groovyls.compiler.util.GroovyReflectionUtils; import org.codehaus.groovy.ast.AnnotatedNode; -import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.MethodNode; import org.jetbrains.annotations.Nullable; @@ -22,26 +23,32 @@ public class GroovyScriptDocumentationProvider implements IDocumentationProvider public @Nullable String getDocumentation(AnnotatedNode node, ASTContext context) { var builder = new StringBuilder(); - if (node instanceof MethodNode methodNode && methodNode.getDeclaringClass().implementsInterface(new ClassNode(IScriptReloadable.class))) { - ModSupport.getAllContainers().stream() - .filter(IGroovyContainer::isLoaded) - .map(groovyContainer -> { - var methodRegistry = groovyContainer.get().getRegistries().stream() - .filter(registry -> registry.getClass().equals(methodNode.getDeclaringClass().getTypeClass())) - .findFirst(); + if (node instanceof MethodNode methodNode && + methodNode.getDeclaringClass().implementsInterface(ClassHelper.makeCached(IScriptReloadable.class))) { + ModSupport.getAllContainers().stream().filter(IGroovyContainer::isLoaded).map(groovyContainer -> { + var methodRegistry = groovyContainer.get().getRegistries().stream().filter( + registry -> registry.getClass().equals(methodNode.getDeclaringClass().getTypeClass())).findFirst(); - if (methodRegistry.isPresent()) { - var method = GroovyReflectionUtils.resolveMethodFromMethodNode(methodNode, context); + if (methodRegistry.isPresent()) { + var method = GroovyReflectionUtils.resolveMethodFromMethodNode(methodNode, context); - if (method.isPresent() && method.get().isAnnotationPresent(MethodDescription.class)) { - return new Registry(groovyContainer, methodRegistry.get()).documentMethods(Collections.singletonList(method.get()), true); - } - } + if (method != null && method.isAnnotationPresent(MethodDescription.class)) { + return new Registry(groovyContainer, methodRegistry.get()).documentMethods(Collections.singletonList(method), true); + } + } - return null; - }).filter(Objects::nonNull).forEach(builder::append); + return null; + }).filter(Objects::nonNull).forEach(builder::append); } return builder.length() == 0 ? null : builder.toString(); } + + @Override + public @Nullable String getSortText(AnnotatedNode node, ASTContext context) { + return node instanceof MethodNode methodNode && + !methodNode.getDeclaringClass().getAnnotations(ClassHelper.makeCached(RegistryDescription.class)).isEmpty() && + !methodNode.getAnnotations(ClassHelper.makeCached(MethodDescription.class)).isEmpty() ? + "!!!" + methodNode.getName() : null; + } } diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptFeaturesService.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptFeaturesService.java new file mode 100644 index 000000000..379afc95a --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptFeaturesService.java @@ -0,0 +1,18 @@ +package com.cleanroommc.groovyscript.server; + +import com.cleanroommc.groovyscript.server.features.textureDecoration.TextureDecorationInformation; +import com.cleanroommc.groovyscript.server.features.textureDecoration.TextureDecorationParams; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@JsonSegment("groovyScript") +public interface GroovyScriptFeaturesService { + + @JsonRequest + default CompletableFuture> textureDecoration(TextureDecorationParams params) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServer.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServer.java index ad7ba34a3..6a244415f 100644 --- a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServer.java +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServer.java @@ -1,40 +1,12 @@ package com.cleanroommc.groovyscript.server; -import com.cleanroommc.groovyscript.GroovyScript; -import com.cleanroommc.groovyscript.GroovyScriptConfig; -import com.cleanroommc.groovyscript.api.GroovyLog; -import net.prominic.groovyls.GroovyLanguageServer; -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate; +import org.eclipse.lsp4j.services.LanguageServer; -import java.io.File; -import java.net.ServerSocket; +public interface GroovyScriptLanguageServer extends LanguageServer { -public class GroovyScriptLanguageServer extends GroovyLanguageServer { - - @SuppressWarnings("InfiniteLoopStatement") - public static void listen(File root) { - GroovyLog.get().infoMC("Starting Language server"); - var languageServerContext = new GroovyScriptLanguageServerContext(); - - while (true) { - var server = new GroovyScriptLanguageServer(root, languageServerContext); - try (var serverSocket = new ServerSocket(GroovyScriptConfig.languageServerPort); - var socket = serverSocket.accept()) { - - GroovyScript.LOGGER.info("Accepted connection from: {}", socket.getInetAddress()); - - var launcher = Launcher.createLauncher(server, LanguageClient.class, socket.getInputStream(), socket.getOutputStream()); - server.connect(launcher.getRemoteProxy()); - - launcher.startListening().get(); - } catch (Exception e) { - GroovyScript.LOGGER.error("Connection failed", e); - } - } - } - - public GroovyScriptLanguageServer(File root, GroovyScriptLanguageServerContext languageServerContext) { - super(new GroovyScriptCompilationUnitFactory(root, languageServerContext), languageServerContext); + @JsonDelegate + default GroovyScriptFeaturesService getGroovyScriptFeaturesService() { + return null; } } diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServerContext.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServerContext.java index cc21c2a17..1eba0e351 100644 --- a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServerContext.java +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServerContext.java @@ -15,14 +15,16 @@ public class GroovyScriptLanguageServerContext implements ILanguageServerContext private final FileContentsTracker fileContentsTracker = new FileContentsTracker(); - private ScanResult scanResult = new ClassGraph() + private final ScanResult scanResult = new ClassGraph() .enableClassInfo() .enableMethodInfo() + .enableFieldInfo() .enableSystemJarsAndModules() .overrideClassLoaders(Launch.classLoader) .acceptPaths("*") .rejectClasses(GroovySecurityManager.INSTANCE.getBannedClasses().stream().map(Class::getName).toArray(String[]::new)) .rejectPackages(GroovySecurityManager.INSTANCE.getBannedPackages().stream().toArray(String[]::new)) + .rejectPackages("scala.", "akka.") .acceptClasses(GroovySecurityManager.INSTANCE.getWhiteListedClasses().stream().map(Class::getName).toArray(String[]::new)) .scan(); diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServerImpl.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServerImpl.java new file mode 100644 index 000000000..e2e5147e1 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptLanguageServerImpl.java @@ -0,0 +1,67 @@ +package com.cleanroommc.groovyscript.server; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.GroovyScriptConfig; +import com.cleanroommc.groovyscript.api.GroovyLog; +import net.prominic.groovyls.GroovyLanguageServer; +import net.prominic.groovyls.compiler.ILanguageServerContext; +import net.prominic.groovyls.config.ICompilationUnitFactory; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.services.LanguageClient; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.net.ServerSocket; +import java.util.concurrent.CompletableFuture; + +public class GroovyScriptLanguageServerImpl extends GroovyLanguageServer implements GroovyScriptLanguageServer { + + @SuppressWarnings("InfiniteLoopStatement") + public static void listen(File root) { + GroovyLog.get().infoMC("Starting Language server"); + var languageServerContext = new GroovyScriptLanguageServerContext(); + + while (true) { + var server = new GroovyScriptLanguageServerImpl(root, languageServerContext); + try (var serverSocket = new ServerSocket(GroovyScriptConfig.languageServerPort); + var socket = serverSocket.accept()) { + + GroovyScript.LOGGER.info("Accepted connection from: {}", socket.getInetAddress()); + + var launcher = Launcher.createLauncher(server, LanguageClient.class, socket.getInputStream(), socket.getOutputStream()); + server.connect(launcher.getRemoteProxy()); + + launcher.startListening().get(); + } catch (Exception e) { + GroovyScript.LOGGER.error("Connection failed", e); + } + } + } + + public GroovyScriptLanguageServerImpl(File root, GroovyScriptLanguageServerContext languageServerContext) { + super(new GroovyScriptCompilationUnitFactory(root, languageServerContext), languageServerContext); + } + + @Override + public CompletableFuture initialize(InitializeParams params) { + return super.initialize(params).thenApply(initializeResult -> { + var groovyScriptCapabilities = new GroovyScriptCapabilities(); + groovyScriptCapabilities.setTextureDecorationProvider(true); + + initializeResult.getCapabilities().setExperimental(groovyScriptCapabilities); + return initializeResult; + }); + } + + @Override + public GroovyScriptFeaturesService getGroovyScriptFeaturesService() { + return groovyServices; + } + + @Override + protected @NotNull GroovyScriptServices createGroovyServices(ICompilationUnitFactory compilationUnitFactory, ILanguageServerContext languageServerContext) { + return new GroovyScriptServices(compilationUnitFactory, languageServerContext); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptServices.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptServices.java new file mode 100644 index 000000000..196033cb2 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptServices.java @@ -0,0 +1,36 @@ +package com.cleanroommc.groovyscript.server; + +import com.cleanroommc.groovyscript.sandbox.FileUtil; +import com.cleanroommc.groovyscript.server.features.textureDecoration.TextureDecorationInformation; +import com.cleanroommc.groovyscript.server.features.textureDecoration.TextureDecorationParams; +import com.cleanroommc.groovyscript.server.features.textureDecoration.TextureDecorationProvider; +import net.prominic.groovyls.GroovyServices; +import net.prominic.groovyls.compiler.ILanguageServerContext; +import net.prominic.groovyls.compiler.ast.ASTContext; +import net.prominic.groovyls.config.ICompilationUnitFactory; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class GroovyScriptServices extends GroovyServices implements GroovyScriptFeaturesService { + + public GroovyScriptServices(ICompilationUnitFactory factory, ILanguageServerContext languageServerContext) { + super(factory, languageServerContext); + } + + @Override + public CompletableFuture> textureDecoration(TextureDecorationParams params) { + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); + var unit = compilationUnitFactory.create(workspaceRoot, uri); + + var visitor = compileAndVisitAST(unit, uri); + + if (visitor == null) { + return CompletableFuture.completedFuture(null); + } + + var provider = new TextureDecorationProvider(uri, new ASTContext(visitor, languageServerContext)); + return provider.provideTextureDecorations(); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationInformation.java b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationInformation.java new file mode 100644 index 000000000..4c6119280 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationInformation.java @@ -0,0 +1,42 @@ +package com.cleanroommc.groovyscript.server.features.textureDecoration; + +import org.eclipse.lsp4j.Range; + +import java.util.List; + +public class TextureDecorationInformation { + + private Range range; + private String textureUri; + private List tooltips; + + public TextureDecorationInformation(Range range, String textureUri, List tooltips) { + this.range = range; + this.textureUri = textureUri; + this.tooltips = tooltips; + } + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } + + public String getTextureUri() { + return textureUri; + } + + public void setTextureUri(String textureUri) { + this.textureUri = textureUri; + } + + public List getTooltips() { + return tooltips; + } + + public void setTooltips(List tooltips) { + this.tooltips = tooltips; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationParams.java b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationParams.java new file mode 100644 index 000000000..0738a694d --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationParams.java @@ -0,0 +1,48 @@ +package com.cleanroommc.groovyscript.server.features.textureDecoration; + +import org.eclipse.lsp4j.PartialResultParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.WorkDoneProgressParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +public class TextureDecorationParams implements WorkDoneProgressParams, PartialResultParams { + + private TextDocumentIdentifier textDocument; + + private Either partialResultToken; + private Either workDoneToken; + + public TextureDecorationParams(TextDocumentIdentifier textDocument, Either partialResultToken, Either workDoneToken) { + this.textDocument = textDocument; + this.partialResultToken = partialResultToken; + this.workDoneToken = workDoneToken; + } + + public TextDocumentIdentifier getTextDocument() { + return textDocument; + } + + public void setTextDocument(TextDocumentIdentifier textDocument) { + this.textDocument = textDocument; + } + + @Override + public Either getPartialResultToken() { + return partialResultToken; + } + + @Override + public void setPartialResultToken(Either token) { + this.partialResultToken = token; + } + + @Override + public Either getWorkDoneToken() { + return workDoneToken; + } + + @Override + public void setWorkDoneToken(Either token) { + this.workDoneToken = token; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationProvider.java b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationProvider.java new file mode 100644 index 000000000..be9850752 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationProvider.java @@ -0,0 +1,316 @@ +package com.cleanroommc.groovyscript.server.features.textureDecoration; + +import com.cleanroommc.groovyscript.mapper.ObjectMapper; +import com.cleanroommc.groovyscript.mapper.TextureBinder; +import com.cleanroommc.groovyscript.mapper.TextureTooltip; +import com.cleanroommc.groovyscript.sandbox.FileUtil; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.prominic.groovyls.compiler.ast.ASTContext; +import net.prominic.groovyls.compiler.util.GroovyASTUtils; +import net.prominic.groovyls.providers.DocProvider; +import net.prominic.groovyls.util.GroovyLSUtils; +import org.apache.commons.codec.digest.DigestUtils; +import org.codehaus.groovy.GroovyBugError; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.expr.*; +import org.eclipse.lsp4j.Range; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class TextureDecorationProvider extends DocProvider { + + private static final int ICON_W = 16, ICON_H = 16; + private static final int ICON_X = 0, ICON_Y = 0; + private static final Map textures = new Object2ObjectOpenHashMap<>(); + + private final String cacheRoot = FileUtil.makePath(FileUtil.getMinecraftHome(), "cache", "groovy", "textureDecorations"); + + + public TextureDecorationProvider(URI doc, ASTContext context) { + super(doc, context); + } + + public CompletableFuture> provideTextureDecorations() { + List decorationInformations = new ArrayList<>(); + List queueDeco = new ArrayList<>(); + List queueRange = new ArrayList<>(); + Set mappers = new ObjectOpenHashSet<>(); + for (ASTNode node : getNodes()) { + ASTNode start; + MethodCallExpression call; + if (node instanceof PropertyExpression prop) { + if (prop.getObjectExpression() instanceof MethodCallExpression mc) { + start = prop; + call = mc; + } else continue; + } else if (node instanceof MethodCallExpression method) { + if (method.getObjectExpression() instanceof MethodCallExpression mc) { + start = method; + call = mc; + } else { + start = method; + call = method; + } + } else continue; + if (mappers.contains(call) || + !(call.getArguments() instanceof ArgumentListExpression args) || + args.getExpressions().isEmpty()) continue; + ObjectMapper mapper; + try { + mapper = GroovyASTUtils.getMapperOfNode(call, astContext); + } catch (GroovyBugError e) { + continue; + } + if (mapper == null) continue; + mappers.add(call); + + if (!args.getExpressions().stream().allMatch(e -> e instanceof ConstantExpression)) { + continue; + } + + var binder = mapper.getTextureBinder(); + if (binder == null) continue; + + var textureName = computeTextureName(mapper.getName(), args.getExpressions()); + var uri = getURIForDecoration(textureName); + var decoration = textures.get(uri); + + if (decoration == null) { + var bindable = getObjectWithConstArgs(mapper, args); + if (bindable == null) continue; + decoration = new TextureDecoration(textureName, binder, bindable, uri); + textures.put(uri, decoration); + } + + var range = GroovyLSUtils.astNodeToRange(start, call); + if (decoration.isFileExists()) { + var tooltips = new ArrayList<>(decoration.render()); + if (formatTooltips(tooltips, null)) { + decorationInformations.add(new TextureDecorationInformation(range, uri, tooltips)); + continue; + } + } + if (!decoration.queued) { + decoration.queued = true; + queueDeco.add(decoration); + queueRange.add(range); + } + } + + if (queueDeco.isEmpty()) { + return CompletableFuture.completedFuture(decorationInformations); + } + + var future = new CompletableFuture>(); + + Minecraft.getMinecraft() + .addScheduledTask(() -> render(queueDeco, queueRange, decorationInformations)) + .addListener(() -> future.complete(decorationInformations), Runnable::run); + + return future; + } + + private static T getObjectWithConstArgs(ObjectMapper mapper, ArgumentListExpression args) { + var additionalArgs = new Object[args.getExpressions().size() - 1]; + for (int i = 0; i < additionalArgs.length; i++) { + if (args.getExpressions().get(i + 1) instanceof ConstantExpression argExpression) + additionalArgs[i] = argExpression.getValue(); + } + return mapper.invoke(true, args.getExpressions().get(0).getText(), additionalArgs); + } + + private String computeTextureName(String name, List expressions) { + var sb = new StringBuilder(name); + for (Expression expression : expressions) { + sb.append(expression.getText()); + } + return DigestUtils.sha1Hex(sb.toString()); + } + + private void render(List queueDeco, List queueRange, + List decorationInformations) { + var framebuffer = GL30.glGenFramebuffers(); + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer); + + var texture = GL11.glGenTextures(); + GlStateManager.bindTexture(texture); + GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, ICON_W, ICON_H, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); + + GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 0); + + var renderBuffer = GL30.glGenRenderbuffers(); + + GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, renderBuffer); + GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL30.GL_DEPTH_COMPONENT32F, ICON_W, ICON_H); + GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, 0); + + GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, renderBuffer); + + if (GL30.glCheckFramebufferStatus(GL30.GL_FRAMEBUFFER) != GL30.GL_FRAMEBUFFER_COMPLETE) { + throw new IllegalStateException("Framebuffer is not complete!"); + } + + GL11.glDrawBuffer(GL30.GL_COLOR_ATTACHMENT0); + + GlStateManager.viewport(0, 0, ICON_W, ICON_H); + + GL11.glDrawBuffer(GL30.GL_COLOR_ATTACHMENT0); + GlStateManager.matrixMode(GL11.GL_PROJECTION); + GlStateManager.loadIdentity(); + GlStateManager.ortho(0.0D, ICON_W, ICON_H, 0.0D, 1000.0D, 21000.0D); + GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.loadIdentity(); + GlStateManager.translate(ICON_X, ICON_Y, -2000.0F); + GlStateManager.enableDepth(); + GlStateManager.depthMask(true); + GlStateManager.depthFunc(GL11.GL_LEQUAL); + GlStateManager.enableAlpha(); + GlStateManager.alphaFunc(GL11.GL_GREATER, 0.1F); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + + var buffer = BufferUtils.createByteBuffer(ICON_W * ICON_H * 4); + + for (int i = 0; i < queueDeco.size(); i++) { + TextureDecoration decoration = queueDeco.get(i); + Range range = queueRange.get(i); + + var tooltipStrings = new ArrayList<>(render(buffer, decoration.getFile(), decoration::render)); + + formatTooltips(tooltipStrings, buffer); + + decoration.fileExists = true; + decoration.queued = false; + decorationInformations.add(new TextureDecorationInformation(range, decoration.getUri(), tooltipStrings)); + } + + GlStateManager.enableAlpha(); + GlStateManager.disableDepth(); + + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + GlStateManager.deleteTexture(texture); + GL30.glDeleteRenderbuffers(renderBuffer); + GL30.glDeleteFramebuffers(framebuffer); + } + + private boolean formatTooltips(ArrayList tooltipStrings, @Nullable ByteBuffer buffer) { + for (int j = 0; j < tooltipStrings.size(); j++) { + var str = tooltipStrings.get(j); + var tooltip = new TextureTooltip(str); + + for (var embedding : tooltip.getEmbeddings()) { + var embeddingDeco = new TextureDecoration(embedding.getTextureName(), embedding.getTextureBinder(), embedding.getContext(), getURIForDecoration(embedding.getTextureName())); + + if (!embeddingDeco.isFileExists()) { + if (buffer == null) { + return false; + } + render(buffer, embeddingDeco.getFile(), embeddingDeco::render); + } + + var before = embedding.getStart() == 0 ? "" : str.substring(0, embedding.getStart()); + var after = embedding.getEnd() == str.length() - 1 ? "" : str.substring(embedding.getEnd()); + + str = before + embeddingDeco.getUri() + after; + } + + tooltipStrings.set(j, str); + } + + return true; + } + + private static List render(ByteBuffer buffer, File outputFile, Supplier> renderer) { + GlStateManager.clear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + + var tooltips = renderer.get(); + + GL11.glReadPixels(0, 0, ICON_W, ICON_H, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); + + saveImage(outputFile, buffer); + buffer.rewind(); + + return tooltips; + } + + private static void saveImage(File file, ByteBuffer buffer) { + var image = new BufferedImage(ICON_W, ICON_H, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < ICON_H; y++) { + for (int x = 0; x < ICON_W; x++) { + int r = buffer.get() & 0xFF; + int g = buffer.get() & 0xFF; + int b = buffer.get() & 0xFF; + int a = buffer.get() & 0xFF; + int color = (a << 24) | (r << 16) | (g << 8) | b; + image.setRGB(x, ICON_H - 1 - y, color); // Flip the y-coordinate to correct the image orientation + } + } + + try { + ImageIO.write(image, "PNG", file); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String getURIForDecoration(String name) { + return FileUtil.makeFile(cacheRoot, name + ".png").toURI().toString(); + } + + private class TextureDecoration { + + private final String name; + private final String uri; + private final TextureBinder binder; + private final Object bindable; + private boolean fileExists; + private boolean queued; + + private TextureDecoration(String name, TextureBinder binder, Object bindable, String uri) { + this.name = name; + this.binder = binder; + this.bindable = bindable; + this.uri = uri; + File file = getFile(); + this.fileExists = !file.getParentFile().mkdirs() && file.exists(); + } + + public String getUri() { + return uri; + } + + private @NotNull File getFile() { + return FileUtil.makeFile(cacheRoot, name + ".png"); + } + + public List render() { + return (List) ((TextureBinder) binder).apply(bindable); + } + + public boolean isFileExists() { + return fileExists; + } + } +} diff --git a/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java b/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java index 281f5ba43..fb6db21d5 100644 --- a/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java +++ b/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java @@ -19,45 +19,37 @@ //////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.net.ServerSocket; +import com.cleanroommc.groovyscript.sandbox.FileUtil; +import net.prominic.groovyls.compiler.ILanguageServerContext; +import net.prominic.groovyls.config.ICompilationUnitFactory; +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.services.*; +import org.jetbrains.annotations.NotNull; + import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.concurrent.CompletableFuture; -import net.prominic.groovyls.compiler.ILanguageServerContext; -import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.eclipse.lsp4j.launch.LSPLauncher; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageClientAware; -import org.eclipse.lsp4j.services.LanguageServer; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; - -import com.cleanroommc.groovyscript.GroovyScript; -import com.cleanroommc.groovyscript.GroovyScriptConfig; - -import net.prominic.groovyls.config.CompilationUnitFactory; -import net.prominic.groovyls.config.ICompilationUnitFactory; - -public class GroovyLanguageServer implements LanguageServer, LanguageClientAware { +public class GroovyLanguageServer implements LanguageServer, LanguageClientAware { - private final GroovyServices groovyServices; + protected final T groovyServices; public GroovyLanguageServer(ICompilationUnitFactory compilationUnitFactory, ILanguageServerContext languageServerContext) { - this.groovyServices = new GroovyServices(compilationUnitFactory, languageServerContext); + this.groovyServices = createGroovyServices(compilationUnitFactory, languageServerContext); + } + + @NotNull + protected T createGroovyServices(ICompilationUnitFactory compilationUnitFactory, ILanguageServerContext languageServerContext) { + return (T) new GroovyServices(compilationUnitFactory, languageServerContext); } @Override public CompletableFuture initialize(InitializeParams params) { String rootUriString = params.getRootUri(); if (rootUriString != null) { - URI uri = URI.create(params.getRootUri()); + URI uri = URI.create(FileUtil.fixUriString(rootUriString)); Path workspaceRoot = Paths.get(uri); groovyServices.setWorkspaceRoot(workspaceRoot); } @@ -88,7 +80,8 @@ public CompletableFuture shutdown() { } @Override - public void exit() {} + public void exit() { + } @Override public TextDocumentService getTextDocumentService() { diff --git a/src/main/java/net/prominic/groovyls/GroovyServices.java b/src/main/java/net/prominic/groovyls/GroovyServices.java index 1fdd063fc..99e6b7231 100644 --- a/src/main/java/net/prominic/groovyls/GroovyServices.java +++ b/src/main/java/net/prominic/groovyls/GroovyServices.java @@ -20,17 +20,18 @@ package net.prominic.groovyls; import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.sandbox.FileUtil; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.prominic.groovyls.compiler.ILanguageServerContext; import net.prominic.groovyls.compiler.ast.ASTContext; import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; import net.prominic.groovyls.compiler.control.GroovyLSCompilationUnit; import net.prominic.groovyls.config.ICompilationUnitFactory; import net.prominic.groovyls.providers.*; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; -import net.prominic.groovyls.util.URIUtils; -import net.prominic.lsp.utils.Positions; +import net.prominic.groovyls.util.GroovyLSUtils; +import net.prominic.groovyls.util.Positions; import org.codehaus.groovy.GroovyBugError; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.control.ErrorCollector; @@ -60,10 +61,10 @@ public class GroovyServices implements TextDocumentService, WorkspaceService, La private LanguageClient languageClient; - private Path workspaceRoot; - private ICompilationUnitFactory compilationUnitFactory; - private final ILanguageServerContext languageServerContext; - private Map> prevDiagnosticsByFile; + protected Path workspaceRoot; + protected final ICompilationUnitFactory compilationUnitFactory; + protected final ILanguageServerContext languageServerContext; + private Map prevDiagnosticsByFile; public GroovyServices(ICompilationUnitFactory factory, ILanguageServerContext languageServerContext) { compilationUnitFactory = factory; @@ -75,6 +76,10 @@ public void setWorkspaceRoot(Path workspaceRoot) { compilationUnitFactory.invalidateCompilationUnit(); } + public boolean isInGroovyWorkspace(URI uri) { + return FileUtil.relativizeNullable(workspaceRoot.toString(), uri.toString()) != null; + } + @Override public void connect(LanguageClient client) { languageClient = client; @@ -85,7 +90,7 @@ public void connect(LanguageClient client) { @Override public void didOpen(DidOpenTextDocumentParams params) { languageServerContext.getFileContentsTracker().didOpen(params); - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); @@ -95,7 +100,7 @@ public void didOpen(DidOpenTextDocumentParams params) { @Override public void didChange(DidChangeTextDocumentParams params) { languageServerContext.getFileContentsTracker().didChange(params); - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); @@ -105,7 +110,7 @@ public void didChange(DidChangeTextDocumentParams params) { @Override public void didClose(DidCloseTextDocumentParams params) { languageServerContext.getFileContentsTracker().didClose(params); - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); @@ -119,8 +124,8 @@ public void didSave(DidSaveTextDocumentParams params) { @Override public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { - Set urisWithChanges = params.getChanges().stream().map(fileEvent -> URIUtils.toUri(fileEvent.getUri())) - .collect(Collectors.toSet()); + Set urisWithChanges = params.getChanges().stream().map(fileEvent -> FileUtil.fixUri(fileEvent.getUri())).collect( + Collectors.toSet()); for (URI uri : urisWithChanges) { var unit = compilationUnitFactory.create(workspaceRoot, uri); @@ -131,10 +136,9 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { @Override public void didChangeConfiguration(DidChangeConfigurationParams params) { - if (!(params.getSettings() instanceof JsonObject)) { + if (!(params.getSettings() instanceof JsonObject settings)) { return; } - JsonObject settings = (JsonObject) params.getSettings(); this.updateClasspath(settings); } @@ -160,7 +164,7 @@ private void updateClasspath(JsonObject settings) { @Override public CompletableFuture hover(HoverParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); @@ -169,7 +173,7 @@ public CompletableFuture hover(HoverParams params) { return CompletableFuture.completedFuture(null); } - HoverProvider provider = new HoverProvider(new ASTContext(visitor, languageServerContext)); + HoverProvider provider = new HoverProvider(uri, new ASTContext(visitor, languageServerContext)); return provider.provideHover(params.getTextDocument(), params.getPosition()); } @@ -177,25 +181,25 @@ public CompletableFuture hover(HoverParams params) { public CompletableFuture, CompletionList>> completion(CompletionParams params) { TextDocumentIdentifier textDocument = params.getTextDocument(); Position position = params.getPosition(); - URI uri = URIUtils.toUri(textDocument.getUri()); + URI uri = FileUtil.fixUri(textDocument.getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); + if (visitor == null) return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); String originalSource = null; ASTNode offsetNode = visitor.getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter()); if (offsetNode == null) { originalSource = languageServerContext.getFileContentsTracker().getContents(uri); - VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier( - textDocument.getUri(), 1); + VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier(textDocument.getUri(), 1); int offset = Positions.getOffset(originalSource, position); String lineBeforeOffset = originalSource.substring(offset - position.getCharacter(), offset); Matcher matcher = PATTERN_CONSTRUCTOR_CALL.matcher(lineBeforeOffset); TextDocumentContentChangeEvent changeEvent = null; if (matcher.matches()) { - changeEvent = new TextDocumentContentChangeEvent(new Range(position, position), 0, "a()"); + changeEvent = new TextDocumentContentChangeEvent(new Range(position, position), "a()"); } else { - changeEvent = new TextDocumentContentChangeEvent(new Range(position, position), 0, "a"); + changeEvent = new TextDocumentContentChangeEvent(new Range(position, position), "a"); } DidChangeTextDocumentParams didChangeParams = new DidChangeTextDocumentParams(versionedTextDocument, Collections.singletonList(changeEvent)); @@ -212,14 +216,12 @@ public CompletableFuture, CompletionList>> completio CompletableFuture, CompletionList>> result = null; try { - CompletionProvider provider = new CompletionProvider(new ASTContext(visitor, languageServerContext)); - result = provider.provideCompletion(params.getTextDocument(), params.getPosition(), params.getContext()); + CompletionProvider provider = new CompletionProvider(uri, new ASTContext(visitor, languageServerContext)); + result = provider.provideCompletionFuture(params.getTextDocument(), params.getPosition(), params.getContext()); } finally { if (originalSource != null) { - VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier( - textDocument.getUri(), 1); - TextDocumentContentChangeEvent changeEvent = new TextDocumentContentChangeEvent(null, 0, - originalSource); + VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier(textDocument.getUri(), 1); + TextDocumentContentChangeEvent changeEvent = new TextDocumentContentChangeEvent(null, originalSource); DidChangeTextDocumentParams didChangeParams = new DidChangeTextDocumentParams(versionedTextDocument, Collections.singletonList(changeEvent)); didChange(didChangeParams); @@ -230,14 +232,13 @@ public CompletableFuture, CompletionList>> completio } @Override - public CompletableFuture, List>> definition( - DefinitionParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + public CompletableFuture, List>> definition(DefinitionParams params) { + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); - DefinitionProvider provider = new DefinitionProvider(new ASTContext(visitor, languageServerContext)); + DefinitionProvider provider = new DefinitionProvider(uri, new ASTContext(visitor, languageServerContext)); return provider.provideDefinition(params.getTextDocument(), params.getPosition()); } @@ -245,7 +246,7 @@ public CompletableFuture, List signatureHelp(SignatureHelpParams params) { TextDocumentIdentifier textDocument = params.getTextDocument(); Position position = params.getPosition(); - URI uri = URIUtils.toUri(textDocument.getUri()); + URI uri = FileUtil.fixUri(textDocument.getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); @@ -254,10 +255,8 @@ public CompletableFuture signatureHelp(SignatureHelpParams params ASTNode offsetNode = visitor.getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter()); if (offsetNode == null) { originalSource = languageServerContext.getFileContentsTracker().getContents(uri); - VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier( - textDocument.getUri(), 1); - TextDocumentContentChangeEvent changeEvent = new TextDocumentContentChangeEvent( - new Range(position, position), 0, ")"); + VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier(textDocument.getUri(), 1); + TextDocumentContentChangeEvent changeEvent = new TextDocumentContentChangeEvent(new Range(position, position), ")"); DidChangeTextDocumentParams didChangeParams = new DidChangeTextDocumentParams(versionedTextDocument, Collections.singletonList(changeEvent)); // if the offset node is null, there is probably a syntax error. @@ -272,14 +271,12 @@ public CompletableFuture signatureHelp(SignatureHelpParams params } try { - SignatureHelpProvider provider = new SignatureHelpProvider(new ASTContext(visitor, languageServerContext)); + SignatureHelpProvider provider = new SignatureHelpProvider(uri, new ASTContext(visitor, languageServerContext)); return provider.provideSignatureHelp(params.getTextDocument(), params.getPosition()); } finally { if (originalSource != null) { - VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier( - textDocument.getUri(), 1); - TextDocumentContentChangeEvent changeEvent = new TextDocumentContentChangeEvent(null, 0, - originalSource); + VersionedTextDocumentIdentifier versionedTextDocument = new VersionedTextDocumentIdentifier(textDocument.getUri(), 1); + TextDocumentContentChangeEvent changeEvent = new TextDocumentContentChangeEvent(null, originalSource); DidChangeTextDocumentParams didChangeParams = new DidChangeTextDocumentParams(versionedTextDocument, Collections.singletonList(changeEvent)); didChange(didChangeParams); @@ -288,107 +285,111 @@ public CompletableFuture signatureHelp(SignatureHelpParams params } @Override - public CompletableFuture, List>> typeDefinition( - TypeDefinitionParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + public CompletableFuture, List>> typeDefinition(TypeDefinitionParams params) { + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); - TypeDefinitionProvider provider = new TypeDefinitionProvider(new ASTContext(visitor, languageServerContext)); + TypeDefinitionProvider provider = new TypeDefinitionProvider(uri, new ASTContext(visitor, languageServerContext)); return provider.provideTypeDefinition(params.getTextDocument(), params.getPosition()); } @Override public CompletableFuture> references(ReferenceParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); - ReferenceProvider provider = new ReferenceProvider(new ASTContext(visitor, languageServerContext)); + ReferenceProvider provider = new ReferenceProvider(uri, new ASTContext(visitor, languageServerContext)); return provider.provideReferences(params.getTextDocument(), params.getPosition()); } @Override - public CompletableFuture>> documentSymbol( - DocumentSymbolParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + public CompletableFuture>> documentSymbol(DocumentSymbolParams params) { + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); - DocumentSymbolProvider provider = new DocumentSymbolProvider(new ASTContext(visitor, languageServerContext)); - return provider.provideDocumentSymbols(params.getTextDocument()); + DocumentSymbolProvider provider = new DocumentSymbolProvider(uri, new ASTContext(visitor, languageServerContext)); + return provider.provideDocumentSymbolsFuture(params.getTextDocument()); } @Override - public CompletableFuture, List>> symbol(WorkspaceSymbolParams params) { + public CompletableFuture, List>> symbol( + WorkspaceSymbolParams params) { var unit = compilationUnitFactory.create(workspaceRoot, null); var visitor = compileAndVisitAST(unit, null); WorkspaceSymbolProvider provider = new WorkspaceSymbolProvider(new ASTContext(visitor, languageServerContext)); - return provider.provideWorkspaceSymbols(params.getQuery()).thenApply(Either::forLeft); + return provider.provideWorkspaceSymbols(params.getQuery()).thenApply(Either::forRight); } @Override public CompletableFuture rename(RenameParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); var unit = compilationUnitFactory.create(workspaceRoot, uri); var visitor = compileAndVisitAST(unit, uri); - RenameProvider provider = new RenameProvider(new ASTContext(visitor, languageServerContext), languageServerContext.getFileContentsTracker()); + RenameProvider provider = new RenameProvider(uri, new ASTContext(visitor, languageServerContext), + languageServerContext.getFileContentsTracker()); return provider.provideRename(params); } - private @Nullable ASTNodeVisitor compileAndVisitAST(GroovyLSCompilationUnit compilationUnit, URI context) { + @Nullable + protected ASTNodeVisitor compileAndVisitAST(GroovyLSCompilationUnit compilationUnit, URI context) { + if (!isInGroovyWorkspace(context)) { + return null; + } try { return compilationUnit.recompileAndVisitASTIfContextChanged(context); } catch (GroovyBugError | Exception e) { GroovyScript.LOGGER.error("Unexpected exception in language server when compiling Groovy.", e); } finally { - Set diagnostics = handleErrorCollector(compilationUnit.getErrorCollector()); - diagnostics.stream().forEach(languageClient::publishDiagnostics); + for (PublishDiagnosticsParams diag : handleErrorCollector(compilationUnit.getErrorCollector())) { + languageClient.publishDiagnostics(diag); + } } return null; } - private Set handleErrorCollector(ErrorCollector collector) { - Map> diagnosticsByFile = new HashMap<>(); + private Iterable handleErrorCollector(ErrorCollector collector) { + Map diagnosticsByFile = new Object2ObjectOpenHashMap<>(); List errors = collector.getErrors(); if (errors != null) { - errors.stream().filter((Object message) -> message instanceof SyntaxErrorMessage) - .forEach((Object message) -> { - SyntaxErrorMessage syntaxErrorMessage = (SyntaxErrorMessage) message; - SyntaxException cause = syntaxErrorMessage.getCause(); - Range range = GroovyLanguageServerUtils.syntaxExceptionToRange(cause); - Diagnostic diagnostic = new Diagnostic(); - diagnostic.setRange(range); - diagnostic.setSeverity(DiagnosticSeverity.Error); - diagnostic.setMessage(cause.getMessage()); - URI uri = Paths.get(cause.getSourceLocator()).toUri(); - diagnosticsByFile.computeIfAbsent(uri, (key) -> new ArrayList<>()).add(diagnostic); - }); + for (Message m : errors) { + if (m instanceof SyntaxErrorMessage sem) { + SyntaxException cause = sem.getCause(); + Range range = GroovyLSUtils.syntaxExceptionToRange(cause); + if (range == null) continue; + Diagnostic diagnostic = new Diagnostic(); + diagnostic.setRange(range); + diagnostic.setMessage(cause.getOriginalMessage()); + diagnostic.setSeverity(DiagnosticSeverity.Error); // TODO source location + URI uri = Paths.get(cause.getSourceLocator()).toUri(); + diagnosticsByFile.computeIfAbsent(uri, (key) -> new PublishDiagnosticsParams(key.toString(), + new ArrayList<>())).getDiagnostics().add( + diagnostic); + } + } } - Set result = diagnosticsByFile.entrySet().stream() - .map(entry -> new PublishDiagnosticsParams(entry.getKey().toString(), entry.getValue())) - .collect(Collectors.toSet()); - if (prevDiagnosticsByFile != null) { for (URI key : prevDiagnosticsByFile.keySet()) { if (!diagnosticsByFile.containsKey(key)) { // send an empty list of diagnostics for files that had // diagnostics previously or they won't be cleared - result.add(new PublishDiagnosticsParams(key.toString(), new ArrayList<>())); + diagnosticsByFile.put(key, new PublishDiagnosticsParams(key.toString(), Collections.emptyList())); } } } prevDiagnosticsByFile = diagnosticsByFile; - return result; + return diagnosticsByFile.values(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/compiler/ast/ASTNodeVisitor.java b/src/main/java/net/prominic/groovyls/compiler/ast/ASTNodeVisitor.java index c1d974827..044598f17 100644 --- a/src/main/java/net/prominic/groovyls/compiler/ast/ASTNodeVisitor.java +++ b/src/main/java/net/prominic/groovyls/compiler/ast/ASTNodeVisitor.java @@ -20,10 +20,12 @@ package net.prominic.groovyls.compiler.ast; import com.cleanroommc.groovyscript.helper.BetterList; +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; -import net.prominic.lsp.utils.Positions; -import net.prominic.lsp.utils.Ranges; +import net.prominic.groovyls.util.GroovyLSUtils; +import net.prominic.groovyls.util.Positions; +import net.prominic.groovyls.util.Ranges; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.*; @@ -39,31 +41,15 @@ public class ASTNodeVisitor extends ClassCodeVisitorSupport { - private static class ASTLookupKey { - - public ASTLookupKey(ASTNode node) { - this.node = node; - } - - private final ASTNode node; - - @Override - public boolean equals(Object o) { - // some ASTNode subclasses, like ClassNode, override equals() with - // comparisons that are not strict. we need strict. - return o instanceof ASTLookupKey other && node == other.node; - } - - @Override - public int hashCode() { - return node.hashCode(); - } - } - private static class ASTNodeLookupData { public ASTNode parent; public URI uri; + + public ASTNodeLookupData(ASTNode node, URI uri) { + this.parent = node; + this.uri = uri; + } } private SourceUnit sourceUnit; @@ -74,19 +60,29 @@ protected SourceUnit getSourceUnit() { } private final BetterList stack = new BetterList<>(); + private final Map modules = new Object2ObjectOpenHashMap<>(); private final Map> nodesByURI = new Object2ObjectOpenHashMap<>(); private final Map> classNodesByURI = new Object2ObjectOpenHashMap<>(); - private final Map lookup = new Object2ObjectOpenHashMap<>(); + private final Map lookup = new Object2ObjectOpenCustomHashMap<>(new Hash.Strategy<>() { + + @Override + public int hashCode(ASTNode o) { + return o.hashCode(); + } + + @Override + public boolean equals(ASTNode a, ASTNode b) { + return a == b; + } + }); private void pushASTNode(ASTNode node) { if (!(node instanceof AnnotatedNode an && an.isSynthetic())) { URI uri = sourceUnit.getSource().getURI(); nodesByURI.get(uri).add(node); - ASTNodeLookupData data = new ASTNodeLookupData(); - data.uri = uri; - data.parent = stack.peekLast(); - lookup.put(new ASTLookupKey(node), data); + ASTNodeLookupData data = new ASTNodeLookupData(stack.peekLast(), uri); + lookup.put(node, data); } stack.add(node); } @@ -132,7 +128,7 @@ public ASTNode getNodeAtLineAndColumn(URI uri, int line, int column) { // also, do this first because it's the fastest comparison return false; } - Range range = GroovyLanguageServerUtils.astNodeToRange(node); + Range range = GroovyLSUtils.astNodeToRange(node); if (range == null) { return false; } @@ -144,8 +140,7 @@ public ASTNode getNodeAtLineAndColumn(URI uri, int line, int column) { } return result; }).sorted((n1, n2) -> { - int result = Positions.COMPARATOR.reversed().compare(nodeToRange.get(n1).getStart(), - nodeToRange.get(n2).getStart()); + int result = Positions.COMPARATOR.reversed().compare(nodeToRange.get(n1).getStart(), nodeToRange.get(n2).getStart()); if (result != 0) { return result; } @@ -177,7 +172,7 @@ public ASTNode getParent(ASTNode child) { if (child == null) { return null; } - ASTNodeLookupData data = lookup.get(new ASTLookupKey(child)); + ASTNodeLookupData data = lookup.get(child); if (data == null) { return null; } @@ -196,13 +191,21 @@ public boolean contains(ASTNode ancestor, ASTNode descendant) { } public URI getURI(ASTNode node) { - ASTNodeLookupData data = lookup.get(new ASTLookupKey(node)); - if (data == null) { - return null; - } + ASTNodeLookupData data = lookup.get(node); + if (data == null) return null; return data.uri; } + public ModuleNode getModule(ASTNode node) { + ASTNodeLookupData data = lookup.get(node); + if (data == null) return null; + return modules.get(data.uri); + } + + public ModuleNode getModule(URI uri) { + return modules.get(uri); + } + public void visitCompilationUnit(CompilationUnit unit) { nodesByURI.clear(); classNodesByURI.clear(); @@ -215,9 +218,7 @@ public void visitCompilationUnit(CompilationUnit unit, Collection uris) { // clear all old nodes so that they may be replaced List nodes = nodesByURI.remove(uri); if (nodes != null) { - nodes.forEach(node -> { - lookup.remove(new ASTLookupKey(node)); - }); + nodes.forEach(lookup::remove); } classNodesByURI.remove(uri); }); @@ -238,6 +239,7 @@ public void visitSourceUnit(SourceUnit unit) { stack.clear(); ModuleNode moduleNode = unit.getAST(); if (moduleNode != null) { + modules.put(uri, moduleNode); visitModule(moduleNode); } sourceUnit = null; @@ -827,4 +829,4 @@ public void visitBytecodeExpression(BytecodeExpression node) { popASTNode(); } } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/compiler/control/GroovyLSCompilationUnit.java b/src/main/java/net/prominic/groovyls/compiler/control/GroovyLSCompilationUnit.java index 15ef48f85..bf1d2f58b 100644 --- a/src/main/java/net/prominic/groovyls/compiler/control/GroovyLSCompilationUnit.java +++ b/src/main/java/net/prominic/groovyls/compiler/control/GroovyLSCompilationUnit.java @@ -39,13 +39,12 @@ public class GroovyLSCompilationUnit extends CompilationUnit { private final ILanguageServerContext languageServerContext; - @Nullable - private ASTNodeVisitor visitor; + @Nullable private ASTNodeVisitor visitor; - @Nullable - private URI previousContext; + @Nullable private URI previousContext; - public GroovyLSCompilationUnit(CompilerConfiguration config, CodeSource security, GroovyClassLoader loader, ILanguageServerContext languageServerContext) { + public GroovyLSCompilationUnit(CompilerConfiguration config, CodeSource security, GroovyClassLoader loader, + ILanguageServerContext languageServerContext) { super(config, security, loader); this.languageServerContext = languageServerContext; this.errorCollector = new LanguageServerErrorCollector(config); @@ -58,8 +57,8 @@ public void setErrorCollector(LanguageServerErrorCollector errorCollector) { public void removeSources(Collection sourceUnitsToRemove) { for (SourceUnit sourceUnit : sourceUnitsToRemove) { if (sourceUnit.getAST() != null) { - List sourceUnitClassNames = sourceUnit.getAST().getClasses().stream() - .map(classNode -> classNode.getName()).collect(Collectors.toList()); + List sourceUnitClassNames = sourceUnit.getAST().getClasses().stream().map(classNode -> classNode.getName()).collect( + Collectors.toList()); final List generatedClasses = getClasses(); generatedClasses.removeIf(groovyClass -> sourceUnitClassNames.contains(groovyClass.getName())); } diff --git a/src/main/java/net/prominic/groovyls/compiler/control/LanguageServerErrorCollector.java b/src/main/java/net/prominic/groovyls/compiler/control/LanguageServerErrorCollector.java index ec137ebc8..f9cffc349 100644 --- a/src/main/java/net/prominic/groovyls/compiler/control/LanguageServerErrorCollector.java +++ b/src/main/java/net/prominic/groovyls/compiler/control/LanguageServerErrorCollector.java @@ -24,10 +24,10 @@ import org.codehaus.groovy.control.ErrorCollector; /** - * A special ErrorCollector for language servers that can clear all errors and - * does not throw exceptions. + * A special ErrorCollector for language servers that can clear all errors and does not throw exceptions. */ public class LanguageServerErrorCollector extends ErrorCollector { + private static final long serialVersionUID = 1L; public LanguageServerErrorCollector(CompilerConfiguration configuration) { diff --git a/src/main/java/net/prominic/groovyls/compiler/control/io/StringReaderSourceWithURI.java b/src/main/java/net/prominic/groovyls/compiler/control/StringReaderSourceWithURI.java similarity index 78% rename from src/main/java/net/prominic/groovyls/compiler/control/io/StringReaderSourceWithURI.java rename to src/main/java/net/prominic/groovyls/compiler/control/StringReaderSourceWithURI.java index 1b1a96e0e..5ba39d26e 100644 --- a/src/main/java/net/prominic/groovyls/compiler/control/io/StringReaderSourceWithURI.java +++ b/src/main/java/net/prominic/groovyls/compiler/control/StringReaderSourceWithURI.java @@ -17,22 +17,23 @@ // No warranty of merchantability or fitness of any kind. // Use this software at your own risk. //////////////////////////////////////////////////////////////////////////////// -package net.prominic.groovyls.compiler.control.io; - -import java.net.URI; +package net.prominic.groovyls.compiler.control; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.io.StringReaderSource; +import java.net.URI; + public class StringReaderSourceWithURI extends StringReaderSource { - private URI uri; - public StringReaderSourceWithURI(String string, URI uri, CompilerConfiguration configuration) { - super(string, configuration); - this.uri = uri; - } + private final URI uri; + + public StringReaderSourceWithURI(String string, URI uri, CompilerConfiguration configuration) { + super(string, configuration); + this.uri = uri; + } - public URI getURI() { - return uri; - } -} \ No newline at end of file + public URI getURI() { + return uri; + } +} diff --git a/src/main/java/net/prominic/groovyls/compiler/documentation/DocumentationFactory.java b/src/main/java/net/prominic/groovyls/compiler/documentation/DocumentationFactory.java index 31299fac7..8f6a06ba8 100644 --- a/src/main/java/net/prominic/groovyls/compiler/documentation/DocumentationFactory.java +++ b/src/main/java/net/prominic/groovyls/compiler/documentation/DocumentationFactory.java @@ -1,15 +1,30 @@ package net.prominic.groovyls.compiler.documentation; +import com.cleanroommc.groovyscript.compat.mods.GroovyPropertyContainer; +import com.cleanroommc.groovyscript.compat.mods.ModSupport; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.prominic.groovyls.compiler.ast.ASTContext; import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.MethodNode; import org.jetbrains.annotations.Nullable; +import java.util.Set; + public class DocumentationFactory { private final IDocumentationProvider[] providers; + private final Set lastSortedMethods = new ObjectOpenHashSet<>(); public DocumentationFactory(IDocumentationProvider... providers) { this.providers = providers; + this.lastSortedMethods.add("hashCode"); + this.lastSortedMethods.add("equals"); + this.lastSortedMethods.add("wait"); + this.lastSortedMethods.add("getClass"); + this.lastSortedMethods.add("notify"); + this.lastSortedMethods.add("notifyAll"); + this.lastSortedMethods.add("toString"); } public @Nullable String getDocumentation(AnnotatedNode node, ASTContext context) { @@ -21,4 +36,21 @@ public DocumentationFactory(IDocumentationProvider... providers) { } return null; } + + public @Nullable String getSortText(AnnotatedNode node, ASTContext context) { + for (IDocumentationProvider provider : providers) { + String documentation = provider.getSortText(node, context); + if (documentation != null) { + return documentation; + } + } + if (node instanceof MethodNode mn) { + if (node.getDeclaringClass().isDerivedFrom(ClassHelper.makeCached(GroovyPropertyContainer.class)) || + node.getDeclaringClass().isDerivedFrom(ClassHelper.makeCached(ModSupport.class))) { + return "z" + mn.getName(); + } + if (this.lastSortedMethods.contains(mn.getName())) return "zz" + mn.getName(); + } + return null; + } } diff --git a/src/main/java/net/prominic/groovyls/compiler/documentation/GroovydocProvider.java b/src/main/java/net/prominic/groovyls/compiler/documentation/GroovydocProvider.java index fd3a25bc4..1fd00359e 100644 --- a/src/main/java/net/prominic/groovyls/compiler/documentation/GroovydocProvider.java +++ b/src/main/java/net/prominic/groovyls/compiler/documentation/GroovydocProvider.java @@ -68,6 +68,11 @@ public class GroovydocProvider implements IDocumentationProvider { trim(); } + @Override + public @Nullable String getSortText(AnnotatedNode node, ASTContext context) { + return null; + } + private static void appendLine(StringBuilder markdownBuilder, String line) { line = reformatLine(line); if (line.length() == 0) { diff --git a/src/main/java/net/prominic/groovyls/compiler/documentation/IDocumentationProvider.java b/src/main/java/net/prominic/groovyls/compiler/documentation/IDocumentationProvider.java index 85ae10649..ca8aaef0d 100644 --- a/src/main/java/net/prominic/groovyls/compiler/documentation/IDocumentationProvider.java +++ b/src/main/java/net/prominic/groovyls/compiler/documentation/IDocumentationProvider.java @@ -7,4 +7,6 @@ public interface IDocumentationProvider { @Nullable String getDocumentation(AnnotatedNode node, ASTContext context); + + @Nullable String getSortText(AnnotatedNode node, ASTContext context); } diff --git a/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java b/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java index ac116a888..410e54c52 100644 --- a/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java +++ b/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java @@ -23,12 +23,13 @@ import com.cleanroommc.groovyscript.helper.ArrayUtils; import com.cleanroommc.groovyscript.mapper.ObjectMapper; import com.cleanroommc.groovyscript.mapper.ObjectMapperManager; +import com.cleanroommc.groovyscript.sandbox.Preprocessor; import com.cleanroommc.groovyscript.sandbox.expand.IDocumented; import groovy.lang.*; import groovy.lang.groovydoc.Groovydoc; import groovy.lang.groovydoc.GroovydocHolder; import net.prominic.groovyls.compiler.ast.ASTContext; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; +import net.prominic.groovyls.util.GroovyLSUtils; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.ExpressionStatement; @@ -37,7 +38,9 @@ import org.jetbrains.annotations.NotNull; import org.objectweb.asm.Opcodes; +import java.io.File; import java.lang.reflect.Modifier; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -50,12 +53,11 @@ public class GroovyASTUtils { public static final int EXPANSION_MARKER = 0x01000000; public static final int HIDDEN_MARKER = 0x02000000; - public static ASTNode getEnclosingNodeOfType(ASTNode offsetNode, Class nodeType, - ASTContext context) { + public static T getEnclosingNodeOfType(ASTNode offsetNode, Class nodeType, ASTContext context) { ASTNode current = offsetNode; while (current != null) { if (nodeType.isInstance(current)) { - return current; + return (T) current; } current = context.getVisitor().getParent(current); } @@ -67,43 +69,35 @@ public static ASTNode getDefinition(ASTNode node, boolean strict, ASTContext con return null; } ASTNode parentNode = context.getVisitor().getParent(node); - if (node instanceof ExpressionStatement) { - ExpressionStatement statement = (ExpressionStatement) node; + if (node instanceof ExpressionStatement statement) { node = statement.getExpression(); } if (node instanceof ClassNode) { return tryToResolveOriginalClassNode((ClassNode) node, strict, context); - } else if (node instanceof ConstructorCallExpression) { - ConstructorCallExpression callExpression = (ConstructorCallExpression) node; + } else if (node instanceof ConstructorCallExpression callExpression) { return GroovyASTUtils.getMethodFromCallExpression(callExpression, context); - } else if (node instanceof DeclarationExpression) { - DeclarationExpression declExpression = (DeclarationExpression) node; + } else if (node instanceof DeclarationExpression declExpression) { if (!declExpression.isMultipleAssignmentDeclaration()) { ClassNode originType = declExpression.getVariableExpression().getOriginType(); return tryToResolveOriginalClassNode(originType, strict, context); } - } else if (node instanceof ClassExpression) { - ClassExpression classExpression = (ClassExpression) node; + } else if (node instanceof ClassExpression classExpression) { return tryToResolveOriginalClassNode(classExpression.getType(), strict, context); - } else if (node instanceof ImportNode) { - ImportNode importNode = (ImportNode) node; + } else if (node instanceof ImportNode importNode) { return tryToResolveOriginalClassNode(importNode.getType(), strict, context); } else if (node instanceof MethodNode) { return node; } else if (node instanceof ConstantExpression && parentNode != null) { - if (parentNode instanceof MethodCallExpression) { - MethodCallExpression methodCallExpression = (MethodCallExpression) parentNode; + if (parentNode instanceof MethodCallExpression methodCallExpression) { return GroovyASTUtils.getMethodFromCallExpression(methodCallExpression, context); - } else if (parentNode instanceof PropertyExpression) { - PropertyExpression propertyExpression = (PropertyExpression) parentNode; + } else if (parentNode instanceof PropertyExpression propertyExpression) { PropertyNode propNode = GroovyASTUtils.getPropertyFromExpression(propertyExpression, context); if (propNode != null) { return propNode; } return GroovyASTUtils.getFieldFromExpression(propertyExpression, context); } - } else if (node instanceof VariableExpression) { - VariableExpression variableExpression = (VariableExpression) node; + } else if (node instanceof VariableExpression variableExpression) { Variable accessedVariable = variableExpression.getAccessedVariable(); if (accessedVariable instanceof ASTNode) { return (ASTNode) accessedVariable; @@ -131,11 +125,9 @@ public static ASTNode getTypeDefinition(ASTNode node, ASTContext context) { if (definitionNode == null) { return null; } - if (definitionNode instanceof MethodNode) { - MethodNode method = (MethodNode) definitionNode; + if (definitionNode instanceof MethodNode method) { return tryToResolveOriginalClassNode(method.getReturnType(), true, context); - } else if (definitionNode instanceof Variable) { - Variable variable = (Variable) definitionNode; + } else if (definitionNode instanceof Variable variable) { return tryToResolveOriginalClassNode(variable.getOriginType(), true, context); } return null; @@ -211,17 +203,21 @@ public static FieldNode getFieldFromExpression(PropertyExpression node, ASTConte public static List getFieldsForLeftSideOfPropertyExpression(ClassNode classNode, Expression expr, ASTContext context) { boolean statics = expr instanceof ClassExpression; - return collectFields(classNode, new ArrayList<>(), node -> statics == node.isStatic() && (node.getModifiers() & HIDDEN_MARKER) == 0); + return collectFields(classNode, new ArrayList<>(), + node -> statics == node.isStatic() && (node.getModifiers() & HIDDEN_MARKER) == 0); } - public static List getPropertiesForLeftSideOfPropertyExpression(ClassNode classNode, Expression expr, ASTContext context) { + public static List getPropertiesForLeftSideOfPropertyExpression(ClassNode classNode, Expression expr, + ASTContext context) { boolean statics = expr instanceof ClassExpression; - return collectProperties(classNode, new ArrayList<>(), node -> statics == node.isStatic() && (node.getModifiers() & HIDDEN_MARKER) == 0); + return collectProperties(classNode, new ArrayList<>(), + node -> statics == node.isStatic() && (node.getModifiers() & HIDDEN_MARKER) == 0); } public static List getMethodsForLeftSideOfPropertyExpression(ClassNode classNode, Expression expr, ASTContext context) { boolean statics = expr instanceof ClassExpression; - return collectMethods(classNode, new ArrayList<>(), node -> statics == node.isStatic() && (node.getModifiers() & HIDDEN_MARKER) == 0); + return collectMethods(classNode, new ArrayList<>(), + node -> statics == node.isStatic() && (node.getModifiers() & HIDDEN_MARKER) == 0); } public static List collectFields(ClassNode classNode, List nodes, Predicate test) { @@ -264,22 +260,18 @@ public static List collectMethods(ClassNode classNode, List goh = getGohOfNode(expression, context); + } else if (node instanceof MethodCallExpression expression) { + ObjectMapper goh = getMapperOfNode(expression, context); if (goh != null) { return ClassHelper.makeCached(goh.getReturnType()); } @@ -294,33 +286,29 @@ public static ClassNode getTypeOfNode(ASTNode node, ASTContext context) { return methodNode.getReturnType(); } return expr.getType(); - } else if (node instanceof PropertyExpression) { - PropertyExpression expression = (PropertyExpression) node; + } else if (node instanceof PropertyExpression expression) { PropertyNode propNode = GroovyASTUtils.getPropertyFromExpression(expression, context); if (propNode != null) { return getTypeOfNode(propNode, context); } return expression.getType(); - } else if (node instanceof Variable) { - Variable var = (Variable) node; + } else if (node instanceof Variable var) { if (var.getName().equals("this")) { - ClassNode enclosingClass = (ClassNode) getEnclosingNodeOfType(node, ClassNode.class, context); + ClassNode enclosingClass = getEnclosingNodeOfType(node, ClassNode.class, context); if (enclosingClass != null) { return enclosingClass; } } else if (var.isDynamicTyped()) { ASTNode defNode = GroovyASTUtils.getDefinition(node, false, context); - if (defNode instanceof Variable) { - Variable defVar = (Variable) defNode; + if (defNode instanceof Variable defVar) { if (defVar.hasInitialExpression()) { return getTypeOfNode(defVar.getInitialExpression(), context); } else if (!defVar.isDynamicTyped()) { return defVar.getType(); } else { ASTNode declNode = context.getVisitor().getParent(defNode); - if (declNode instanceof DeclarationExpression) { - DeclarationExpression decl = (DeclarationExpression) declNode; + if (declNode instanceof DeclarationExpression decl) { return getTypeOfNode(decl.getRightExpression(), context); } } @@ -330,8 +318,7 @@ public static ClassNode getTypeOfNode(ASTNode node, ASTContext context) { return var.getOriginType(); } } - if (node instanceof Expression) { - Expression expression = (Expression) node; + if (node instanceof Expression expression) { return expression.getType(); } return null; @@ -358,8 +345,8 @@ public static List getMethodOverloadsFromCallExpression(MethodCall n ClassNode constructorType = constructorCallExpr.getType(); if (constructorType != null) { fillClassNode(constructorType); - return constructorType.getDeclaredConstructors().stream().map(constructor -> (MethodNode) constructor) - .collect(Collectors.toList()); + return constructorType.getDeclaredConstructors().stream().map(constructor -> (MethodNode) constructor).collect( + Collectors.toList()); } } else if (node instanceof StaticMethodCallExpression staticMethodCallExpression) { var ownerType = staticMethodCallExpression.getOwnerType(); @@ -377,9 +364,9 @@ public static MethodNode getMethodFromCallExpression(MethodCall node, ASTContext public static MethodNode getMethodFromCallExpression(MethodCall node, ASTContext context, int argIndex) { List possibleMethods = getMethodOverloadsFromCallExpression(node, context); - if (!possibleMethods.isEmpty() && node.getArguments() instanceof ArgumentListExpression) { - ArgumentListExpression actualArguments = (ArgumentListExpression) node.getArguments(); + if (!possibleMethods.isEmpty() && node.getArguments() instanceof ArgumentListExpression actualArguments) { MethodNode foundMethod = possibleMethods.stream().max(new Comparator() { + public int compare(MethodNode m1, MethodNode m2) { Parameter[] p1 = m1.getParameters(); Parameter[] p2 = m2.getParameters(); @@ -432,26 +419,32 @@ private static int calculateArgumentsScore(Parameter[] parameters, ArgumentListE return score; } - public static Range findAddImportRange(ASTNode offsetNode, ASTContext context) { - ModuleNode moduleNode = (ModuleNode) GroovyASTUtils.getEnclosingNodeOfType(offsetNode, ModuleNode.class, - context); + public static Range findAddImportRange(URI uri, ASTNode offsetNode, ASTContext context) { + ModuleNode moduleNode = GroovyASTUtils.getEnclosingNodeOfType(offsetNode, ModuleNode.class, context); if (moduleNode == null) { return new Range(new Position(0, 0), new Position(0, 0)); } ASTNode afterNode = null; - if (afterNode == null) { - List importNodes = moduleNode.getImports(); - if (importNodes.size() > 0) { - afterNode = importNodes.get(importNodes.size() - 1); + List importNodes = moduleNode.getImports(); + if (!importNodes.isEmpty()) { + // auto added imports don't have a line number, + // so we need to iterate all imports and see which one is the most bottom one + for (ImportNode node : importNodes) { + if (node.getLastLineNumber() < 0) continue; + if (afterNode == null || node.getLastLineNumber() > afterNode.getLastLineNumber()) { + afterNode = node; + } } } if (afterNode == null) { afterNode = moduleNode.getPackage(); } if (afterNode == null) { - return new Range(new Position(0, 0), new Position(0, 0)); + int line = Preprocessor.getImportStartLine(new File(uri)); + Position p = new Position(line, 0); + return new Range(p, p); } - Range nodeRange = GroovyLanguageServerUtils.astNodeToRange(afterNode); + Range nodeRange = GroovyLSUtils.astNodeToRange(afterNode); if (nodeRange == null) { return new Range(new Position(0, 0), new Position(0, 0)); } @@ -460,19 +453,16 @@ public static Range findAddImportRange(ASTNode offsetNode, ASTContext context) { } public static MethodNode methodNodeOfClosure(String name, Closure closure) { - Class declarer = closure.getThisObject() == null ? - (closure.getOwner() == null ? Object.class : closure.getOwner().getClass()) : + Class declarer = closure.getThisObject() == null ? (closure.getOwner() == null ? Object.class : closure.getOwner().getClass()) : closure.getThisObject().getClass(); MethodNode method = new MethodNode(name, Modifier.PUBLIC, ClassHelper.OBJECT_TYPE, - ArrayUtils.map(closure.getParameterTypes(), - c -> new Parameter(ClassHelper.makeCached(c), ""), - new Parameter[closure.getParameterTypes().length]), - null, null); + ArrayUtils.map(closure.getParameterTypes(), c -> new Parameter(ClassHelper.makeCached(c), ""), + new Parameter[closure.getParameterTypes().length]), null, null); method.setDeclaringClass(ClassHelper.makeCached(declarer)); return method; } - public static ObjectMapper getGohOfNode(MethodCallExpression expr, ASTContext context) { + public static ObjectMapper getMapperOfNode(MethodCallExpression expr, ASTContext context) { if (expr.isImplicitThis()) { return ObjectMapperManager.getObjectMapper(expr.getMethodAsString()); } @@ -497,8 +487,7 @@ public static void fillClassNode(ClassNode classNode) { if (mm.isPrivate()) continue; int m = mm.getModifiers(); if (mm instanceof Hidden hidden && hidden.isHidden()) m |= HIDDEN_MARKER; - Parameter[] params = ArrayUtils.map(mm.getNativeParameterTypes(), - c -> new Parameter(ClassHelper.makeCached(c), ""), + Parameter[] params = ArrayUtils.map(mm.getNativeParameterTypes(), c -> new Parameter(ClassHelper.makeCached(c), ""), new Parameter[mm.getNativeParameterTypes().length]); MethodNode node = new MethodNode(mm.getName(), m, ClassHelper.makeCached(mm.getReturnType()), params, null, null); node.setDeclaringClass(classNode); @@ -538,4 +527,4 @@ private static PropertyNode makeProperty(ClassNode classNode, FieldNode field, i } return property; } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/compiler/util/GroovyReflectionUtils.java b/src/main/java/net/prominic/groovyls/compiler/util/GroovyReflectionUtils.java index 17b1ee162..0c7b5649a 100644 --- a/src/main/java/net/prominic/groovyls/compiler/util/GroovyReflectionUtils.java +++ b/src/main/java/net/prominic/groovyls/compiler/util/GroovyReflectionUtils.java @@ -5,31 +5,51 @@ import io.github.classgraph.MethodInfo; import net.prominic.groovyls.compiler.ast.ASTContext; import org.codehaus.groovy.ast.MethodNode; +import org.jetbrains.annotations.Nullable; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Optional; +import java.util.function.Function; public class GroovyReflectionUtils { - public static Optional resolveMethodFromMethodNode(MethodNode methodNode, ASTContext context) { - return Arrays.stream(methodNode.getDeclaringClass().getTypeClass().getMethods()) - .filter(GroovySecurityManager.INSTANCE::isValid) - .filter(method -> method.getName().equals(methodNode.getName()) && - method.getParameterTypes().length == methodNode.getParameters().length && - Iterators.elementsEqual(Arrays.stream(method.getParameterTypes()).iterator(), - Arrays.stream(methodNode.getParameters()).map(parameter -> parameter.getType().getTypeClass()).iterator())) - .findFirst(); + @Nullable + public static Method resolveMethodFromMethodNode(MethodNode methodNode, ASTContext context) { + for (Method m : methodNode.getDeclaringClass().getTypeClass().getMethods()) { + if (!GroovySecurityManager.INSTANCE.isValid(m) || + !methodNode.getName().equals(m.getName()) || + methodNode.getParameters().length != m.getParameterCount()) { + continue; + } + if (matchesParams(m.getParameterTypes(), methodNode.getParameters(), p -> p.getType().getTypeClass())) { + return m; + } + } + return null; } - public static Optional resolveMethodFromMethodInfo(MethodInfo methodInfo, ASTContext context) { - return Arrays.stream(methodInfo.getClassInfo().loadClass().getMethods()) - .filter(GroovySecurityManager.INSTANCE::isValid) - .filter(method -> method.getName().equals(methodInfo.getName()) && - method.getParameterTypes().length == methodInfo.getParameterInfo().length && - Iterators.elementsEqual(Arrays.stream(method.getParameterTypes()).iterator(), - Arrays.stream(methodInfo.getParameterInfo()).map(parameter -> context.getLanguageServerContext().getScanResult() - .loadClass(parameter.getTypeSignatureOrTypeDescriptor().toString(), true)).iterator())) - .findFirst(); + @Nullable + public static Method resolveMethodFromMethodInfo(MethodInfo methodInfo, ASTContext context) { + for (Method m : methodInfo.getClassInfo().loadClass().getMethods()) { + if (!GroovySecurityManager.INSTANCE.isValid(m) || + !methodInfo.getName().equals(m.getName()) || + methodInfo.getParameterInfo().length != m.getParameterCount()) { + continue; + } + if (matchesParams(m.getParameterTypes(), methodInfo.getParameterInfo(), + p -> context.getLanguageServerContext().getScanResult().loadClass( + p.getTypeSignatureOrTypeDescriptor().toString(), true))) { + return m; + } + } + return null; + } + + private static boolean matchesParams(Class[] params, T[] otherParams, Function> toClass) { + for (int i = 0, n = params.length; i < n; i++) { + if (!params[i].equals(toClass.apply(otherParams[i]))) return false; + } + return true; } } diff --git a/src/main/java/net/prominic/groovyls/config/CompilationUnitFactoryBase.java b/src/main/java/net/prominic/groovyls/config/CompilationUnitFactoryBase.java index 5b76c4a32..342933bef 100644 --- a/src/main/java/net/prominic/groovyls/config/CompilationUnitFactoryBase.java +++ b/src/main/java/net/prominic/groovyls/config/CompilationUnitFactoryBase.java @@ -2,7 +2,7 @@ import groovy.lang.GroovyClassLoader; import net.prominic.groovyls.compiler.control.GroovyLSCompilationUnit; -import net.prominic.groovyls.compiler.control.io.StringReaderSourceWithURI; +import net.prominic.groovyls.compiler.control.StringReaderSourceWithURI; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.SourceUnit; diff --git a/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java b/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java index 9c6c0c275..2bb6abc1c 100644 --- a/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java @@ -20,18 +20,19 @@ package net.prominic.groovyls.providers; import com.cleanroommc.groovyscript.mapper.ObjectMapper; -import com.cleanroommc.groovyscript.mapper.ObjectMapperManager; import com.cleanroommc.groovyscript.server.Completions; import groovy.lang.Closure; import groovy.lang.DelegatesTo; -import io.github.classgraph.*; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodParameterInfo; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.prominic.groovyls.compiler.ast.ASTContext; import net.prominic.groovyls.compiler.util.GroovyASTUtils; import net.prominic.groovyls.compiler.util.GroovyReflectionUtils; import net.prominic.groovyls.util.CompletionItemFactory; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; -import net.prominic.groovyls.util.URIUtils; +import net.prominic.groovyls.util.GroovyLSUtils; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.BlockStatement; @@ -43,48 +44,49 @@ import java.net.URI; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -public class CompletionProvider { +public class CompletionProvider extends DocProvider { - private final ASTContext astContext; - //private int maxItemCount = 1000; - private boolean isIncomplete = false; + private static final String[] keywords = {"assert", "case", "break", "continue", "transient", "extends ", "implements ", "enum", "try ", "catch", + "finally", "instanceof ", "super", "private", "public", "protected", "abstract", "const", "default", "goto", + "interface", "native", "non-sealed", "package ", "permits", "record", "sealed", "static", "strictfp", + "synchronized", "threadsafe", "throws", "trait", "var", "yields"}; + private static final String[] popularKeywords = {"def ", "else ", "return", "import ", "class", "this", "null", "true", "false", "void", "byte", + "short", "int", "long", "float", "double", "boolean", "char", "throw ", "new", "in ", "as ", "final "}; - public CompletionProvider(ASTContext astContext) { - this.astContext = astContext; + public CompletionProvider(URI doc, ASTContext astContext) { + super(doc, astContext); } - public CompletableFuture, CompletionList>> provideCompletion( - TextDocumentIdentifier textDocument, Position position, CompletionContext context) { - URI uri = URIUtils.toUri(textDocument.getUri()); + public CompletableFuture, CompletionList>> provideCompletionFuture(TextDocumentIdentifier textDocument, + Position position, + CompletionContext context) { + return future(Either.forRight(provideCompletion(textDocument, position, context))); + } + public CompletionList provideCompletion(TextDocumentIdentifier textDocument, Position position, CompletionContext context) { Completions items = new Completions(1000); - ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter()); + ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(doc, position.getLine(), position.getCharacter()); if (offsetNode == null || populateItemsFromNode(position, offsetNode, items)) { populateKeywords(items); } - - return CompletableFuture.completedFuture(items.getResult(this.isIncomplete)); + return new CompletionList(items.reachedLimit(), items); } private void populateKeywords(Completions items) { - items.addAll(new String[]{"def", "assert", "if", "for", "else", "while", "switch", "case", "break", "continue", "return", - "transient", "import", "class", "extends", "implements", "enum", "try", "catch", "finally", "throw", "new", "in", "as", - "instanceof", "super", "this", "null", "true", "false", "void", "byte", "short", "int", "long", "float", "double", "boolean", - "private", "public", "protected"}, - s -> { - var item = new CompletionItem(s); - item.setKind(CompletionItemKind.Keyword); - item.setSortText("zzz" + s); - return item; - }); + items.add(CompletionItemFactory.createKeywordCompletion("if", true, " ($1) $0")); + items.add(CompletionItemFactory.createKeywordCompletion("for", true, " ($1) $0")); + items.add(CompletionItemFactory.createKeywordCompletion("while", true, " ($1) $0")); + items.add(CompletionItemFactory.createKeywordCompletion("do", false, " ($1)")); + items.add(CompletionItemFactory.createKeywordCompletion("switch", true, " ($1) $0")); + items.addAll(popularKeywords, s -> CompletionItemFactory.createKeywordCompletion(s, true)); + items.addAll(keywords, s -> CompletionItemFactory.createKeywordCompletion(s, false)); } private boolean populateItemsFromNode(Position position, ASTNode offsetNode, Completions items) { ASTNode parentNode = astContext.getVisitor().getParent(offsetNode); - + if (parentNode instanceof DeclarationExpression decl && decl.getLeftExpression() == offsetNode) return false; // dont complete definition names if (offsetNode instanceof PropertyExpression) { populateItemsFromPropertyExpression((PropertyExpression) offsetNode, position, items); } else if (parentNode instanceof PropertyExpression) { @@ -118,8 +120,11 @@ private boolean populateItemsFromNode(Position position, ASTNode offsetNode, Com private boolean populateItemsFromConstantExpression(ConstantExpression node, ASTNode parent, Completions items) { if (node.getType().getTypeClass() == String.class) { ASTNode parentParent = astContext.getVisitor().getParent(parent); - if (parentParent instanceof MethodCallExpression expr && expr.getArguments() instanceof ArgumentListExpression args && !args.getExpressions().isEmpty()) { - ObjectMapper goh = GroovyASTUtils.getGohOfNode(expr, astContext); + if (parentParent instanceof MethodCallExpression expr && + expr.getArguments() instanceof ArgumentListExpression args && + !args.getExpressions().isEmpty()) { + // TODO completions in file() + ObjectMapper goh = GroovyASTUtils.getMapperOfNode(expr, astContext); if (goh != null && goh.getCompleter() != null) { int index = -1; for (int i = 0; i < args.getExpressions().size(); i++) { @@ -136,154 +141,88 @@ private boolean populateItemsFromConstantExpression(ConstantExpression node, AST return true; } - private void populateItemsFromStaticMethodCallExpression(StaticMethodCallExpression methodCallExpr, Position position, Completions items) { + private void populateItemsFromStaticMethodCallExpression(StaticMethodCallExpression methodCallExpr, Position position, + Completions items) { Set existingNames = new ObjectOpenHashSet<>(); populateItemsFromGlobalScope(methodCallExpr.getMethod(), existingNames, items); } - private static void populateItemsFromGameObjects(String memberNamePrefix, - Set existingNames, Completions items) { - ObjectMapperManager.getObjectMappers().stream() - .filter(handler -> { - if (handler.getName().startsWith(memberNamePrefix) && !existingNames.contains(handler.getName())) { - existingNames.add(handler.getName()); - return true; - } - return false; - }).forEach(handler -> { - for (Class[] paramTypes : handler.getParamTypes()) { - var completionItem = CompletionItemFactory.createCompletion(CompletionItemKind.Method, handler.getName()); - completionItem.setDetail("(global scope)"); - StringBuilder builder = new StringBuilder().append('('); - for (int i = 0; i < paramTypes.length; i++) { - var parameter = paramTypes[i]; - builder.append(parameter.getSimpleName()); - if (i < paramTypes.length - 1) { - builder.append(","); - } - } - builder.append(") -> "); - builder.append(handler.getReturnType().getSimpleName()); - CompletionItemLabelDetails details = new CompletionItemLabelDetails(); - details.setDetail(builder.toString()); - completionItem.setLabelDetails(details); - items.add(completionItem); - } - }); + private void populateItemsFromPropertyExpression(PropertyExpression propExpr, Position position, Completions items) { + Range propertyRange = GroovyLSUtils.astNodeToRange(propExpr.getProperty()); + if (propertyRange == null) return; + populateItemsFromExpression(propExpr.getObjectExpression(), items); } - private void populateItemsFromPropertyExpression(PropertyExpression propExpr, Position position, - Completions items) { - Range propertyRange = GroovyLanguageServerUtils.astNodeToRange(propExpr.getProperty()); - if (propertyRange == null) { - return; - } - String memberName = getMemberName(propExpr.getPropertyAsString(), propertyRange, position); - populateItemsFromExpression(propExpr.getObjectExpression(), memberName, items); - } - - private void populateItemsFromMethodCallExpression(MethodCallExpression methodCallExpr, Position position, - Completions items) { - Range methodRange = GroovyLanguageServerUtils.astNodeToRange(methodCallExpr.getMethod()); - if (methodRange == null) { - return; - } - String memberName = getMemberName(methodCallExpr.getMethodAsString(), methodRange, position); - populateItemsFromExpression(methodCallExpr.getObjectExpression(), memberName, items); + private void populateItemsFromMethodCallExpression(MethodCallExpression methodCallExpr, Position position, Completions items) { + Range methodRange = GroovyLSUtils.astNodeToRange(methodCallExpr.getMethod()); + if (methodRange == null) return; + populateItemsFromExpression(methodCallExpr.getObjectExpression(), items); } private void populateItemsFromImportNode(ImportNode importNode, Position position, Completions items) { - Range importRange = GroovyLanguageServerUtils.astNodeToRange(importNode); - if (importRange == null) { - return; - } + Range importRange = GroovyLSUtils.astNodeToRange(importNode); + if (importRange == null) return; // skip the "import " at the beginning importRange.setStart(new Position(importRange.getEnd().getLine(), importRange.getEnd().getCharacter() - importNode.getType().getName().length())); String importText = getMemberName(importNode.getType().getName(), importRange, position); - ModuleNode enclosingModule = (ModuleNode) GroovyASTUtils.getEnclosingNodeOfType(importNode, ModuleNode.class, - astContext); + ModuleNode enclosingModule = getModule(); + if (enclosingModule == null) return; String enclosingPackageName = enclosingModule.getPackageName(); - List importNames = enclosingModule.getImports().stream() - .map(ImportNode::getClassName).collect(Collectors.toList()); - - List localClassItems = astContext.getVisitor().getClassNodes().stream().filter(classNode -> { + items.addAll(astContext.getVisitor().getClassNodes(), classNode -> { String packageName = classNode.getPackageName(); - if (packageName == null || packageName.length() == 0 || packageName.equals(enclosingPackageName)) { - return false; + if (packageName == null || packageName.isEmpty() || packageName.equals(enclosingPackageName)) { + return null; } String className = classNode.getName(); String classNameWithoutPackage = classNode.getNameWithoutPackage(); - if (!className.startsWith(importText) && !classNameWithoutPackage.startsWith(importText)) { - return false; - } - if (importNames.contains(className)) { - return false; + if ((!className.startsWith(importText) && !classNameWithoutPackage.startsWith(importText)) || + GroovyLSUtils.hasImport(enclosingModule, className)) { + return null; } - return true; - }).map(classNode -> { CompletionItem item = CompletionItemFactory.createCompletion(classNode, classNode.getName(), astContext); item.setTextEdit(Either.forLeft(new TextEdit(importRange, classNode.getName()))); if (classNode.getNameWithoutPackage().startsWith(importText)) { item.setSortText(classNode.getNameWithoutPackage()); } return item; - }).collect(Collectors.toList()); - items.addAll(localClassItems); - - List classes = astContext.getLanguageServerContext().getScanResult().getAllClasses(); - List packages = astContext.getLanguageServerContext().getScanResult().getPackageInfo(); + }); - List packageItems = packages.stream().filter(packageInfo -> { - String packageName = packageInfo.getName(); - if (packageName.startsWith(importText)) { - return true; - } - return false; - }).map(packageInfo -> { - CompletionItem item = CompletionItemFactory.createCompletion(CompletionItemKind.Module, packageInfo.getName()); - item.setTextEdit(Either.forLeft(new TextEdit(importRange, packageInfo.getName()))); + // scan packages + items.addAll(astContext.getLanguageServerContext().getScanResult().getPackageInfo(), p -> { + String packageName = p.getName(); + if (!packageName.startsWith(importText)) return null; + CompletionItem item = CompletionItemFactory.createCompletion(CompletionItemKind.Module, p.getName()); + item.setTextEdit(Either.forLeft(new TextEdit(importRange, p.getName()))); return item; - }).collect(Collectors.toList()); - items.addAll(packageItems); + }); - List classItems = classes.stream().filter(classInfo -> { - String packageName = classInfo.getPackageName(); - if (packageName == null || packageName.length() == 0 || packageName.equals(enclosingPackageName)) { - return false; + // scan all classes + items.addAll(astContext.getLanguageServerContext().getScanResult().getAllClasses(), c -> { + String packageName = c.getPackageName(); + if (packageName == null || packageName.isEmpty() || packageName.equals(enclosingPackageName)) { + return null; } - String className = classInfo.getName(); - String classNameWithoutPackage = classInfo.getSimpleName(); + String className = c.getName(); + String classNameWithoutPackage = c.getSimpleName(); if (!className.startsWith(importText) && !classNameWithoutPackage.startsWith(importText)) { - return false; + return null; } - if (importNames.contains(className)) { - return false; - } - return true; - }).map(classInfo -> { - CompletionItem item = CompletionItemFactory.createCompletion(classInfoToCompletionItemKind(classInfo), classInfo.getName()); - - item.setTextEdit(Either.forLeft(new TextEdit(importRange, classInfo.getName()))); - if (classInfo.getSimpleName().startsWith(importText)) { - item.setSortText(classInfo.getSimpleName()); + CompletionItem item = CompletionItemFactory.createCompletion(classInfoToCompletionItemKind(c), c.getName()); + item.setTextEdit(Either.forLeft(new TextEdit(importRange, c.getName()))); + if (c.getSimpleName().startsWith(importText)) { + item.setSortText(c.getSimpleName()); } return item; - }).collect(Collectors.toList()); - items.addAll(classItems); + }); } private void populateItemsFromClassNode(ClassNode classNode, Position position, Completions items) { ASTNode parentNode = astContext.getVisitor().getParent(classNode); - if (!(parentNode instanceof ClassNode)) { - return; - } - ClassNode parentClassNode = (ClassNode) parentNode; - Range classRange = GroovyLanguageServerUtils.astNodeToRange(classNode); - if (classRange == null) { - return; - } + if (!(parentNode instanceof ClassNode parentClassNode)) return; + Range classRange = GroovyLSUtils.astNodeToRange(classNode); + if (classRange == null) return; String className = getMemberName(classNode.getUnresolvedName(), classRange, position); if (classNode.equals(parentClassNode.getUnresolvedSuperClass())) { populateTypes(classNode, className, new HashSet<>(), true, false, false, items); @@ -292,130 +231,185 @@ private void populateItemsFromClassNode(ClassNode classNode, Position position, } } - private void populateItemsFromConstructorCallExpression(ConstructorCallExpression constructorCallExpr, - Position position, Completions items) { - Range typeRange = GroovyLanguageServerUtils.astNodeToRange(constructorCallExpr.getType()); - if (typeRange == null) { - return; - } + private void populateItemsFromConstructorCallExpression(ConstructorCallExpression constructorCallExpr, Position position, + Completions items) { + Range typeRange = GroovyLSUtils.astNodeToRange(constructorCallExpr.getType()); + if (typeRange == null) return; String typeName = getMemberName(constructorCallExpr.getType().getNameWithoutPackage(), typeRange, position); populateTypes(constructorCallExpr, typeName, new HashSet<>(), true, false, false, items); } - private void populateItemsFromVariableExpression(VariableExpression varExpr, Position position, - Completions items) { - Range varRange = GroovyLanguageServerUtils.astNodeToRange(varExpr); - if (varRange == null) { - return; - } + private void populateItemsFromVariableExpression(VariableExpression varExpr, Position position, Completions items) { + Range varRange = GroovyLSUtils.astNodeToRange(varExpr); + if (varRange == null) return; String memberName = getMemberName(varExpr.getName(), varRange, position); populateItemsFromScope(varExpr, memberName, items); } private void populateItemsFromPropertiesAndFields(List properties, List fields, - String memberNamePrefix, Set existingNames, Completions items) { - List propItems = properties.stream().filter(property -> { - String name = property.getName(); - // sometimes, a property and a field will have the same name - if (name.startsWith(memberNamePrefix) && !existingNames.contains(name)) { - existingNames.add(name); - return true; - } - return false; - }).map(property -> { - CompletionItem item = CompletionItemFactory.createCompletion(property, property.getName(), astContext); - - if (!property.isDynamicTyped()) { - item.setDetail(property.getType().getNameWithoutPackage()); + Set existingNames, Completions items) { + items.addAll(properties, p -> { + String name = p.getName(); + if (!p.isPublic() || existingNames.contains(name)) return null; + existingNames.add(name); + CompletionItem item = CompletionItemFactory.createCompletion(p, p.getName(), astContext); + if (!p.isDynamicTyped()) { + var details = new CompletionItemLabelDetails(); + details.setDetail(" -> " + appendType(p.getType(), new StringBuilder(), true)); + item.setLabelDetails(details); } return item; - }).collect(Collectors.toList()); - items.addAll(propItems); - List fieldItems = fields.stream().filter(field -> { - String name = field.getName(); - // sometimes, a property and a field will have the same name - if (name.startsWith(memberNamePrefix) && !existingNames.contains(name)) { - existingNames.add(name); - return true; + }); + items.addAll(fields, f -> { + String name = f.getName(); + if (!f.isPublic() || existingNames.contains(name)) return null; + existingNames.add(name); + CompletionItem item = CompletionItemFactory.createCompletion(f, f.getName(), astContext); + if (!f.isDynamicTyped()) { + var details = new CompletionItemLabelDetails(); + details.setDetail(" -> " + appendType(f.getType(), new StringBuilder(), true)); + item.setLabelDetails(details); } - return false; - }).map(field -> { - CompletionItem item = CompletionItemFactory.createCompletion(field, field.getName(), astContext); + return item; + }); + } - if (!field.isDynamicTyped()) { - item.setDetail(field.getType().getNameWithoutPackage()); + private void populateItemsFromMethods(List methods, Set existingNames, Completions items) { + items.addAll(methods, method -> { + String name = getDescriptor(method, true, false, false); + if (!method.isPublic() || existingNames.contains(name)) return null; + existingNames.add(name); + if (method.getDeclaringClass().isResolved() && + (method.getModifiers() & GroovyASTUtils.EXPANSION_MARKER) == 0 && + GroovyReflectionUtils.resolveMethodFromMethodNode(method, astContext) == null) { + return null; } + + CompletionItem item = CompletionItemFactory.createCompletion(method, method.getName(), astContext); + item.setLabelDetails(getMethodNodeDetails(method)); return item; - }).collect(Collectors.toList()); - items.addAll(fieldItems); - } - - private void populateItemsFromMethods(List methods, String memberNamePrefix, Set existingNames, - Completions items) { - List methodItems = methods.stream() - .filter(method -> { - String methodName = method.getName(); - // overloads can cause duplicates - if (methodName.startsWith(memberNamePrefix) && !existingNames.contains(methodName)) { - existingNames.add(methodName); - return !method.getDeclaringClass().isResolved() || - (method.getModifiers() & GroovyASTUtils.EXPANSION_MARKER) != 0 || - GroovyReflectionUtils.resolveMethodFromMethodNode(method, astContext).isPresent(); - } - return false; - }).map(method -> { - CompletionItem item = CompletionItemFactory.createCompletion(method, method.getName(), astContext); + }); + } - var details = getMethodNodeDetails(method); - item.setLabelDetails(details); + public static String getDescriptor(MethodNode node, boolean includeName, boolean includeReturn, boolean display) { + StringBuilder builder = new StringBuilder(); + if (includeName) builder.append(node.getName()); + builder.append("("); + var parameters = node.getParameters(); + for (int i = 0; i < parameters.length; i++) { + boolean last = i == parameters.length - 1; + if (parameters[i].isDynamicTyped()) { + builder.append("?"); + } else { + appendType(parameters[i].getType(), builder, display, last); + } + if (!last) builder.append(", "); + } + builder.append(")"); + if (!includeReturn) return builder.toString(); + if (node.getReturnType() != ClassHelper.VOID_TYPE) { + if (display) builder.append(" -> "); + appendType(node.getReturnType(), builder, display); + } + return builder.toString(); + } - return item; - }).collect(Collectors.toList()); - items.addAll(methodItems); + public static StringBuilder appendType(ClassNode type, StringBuilder builder, boolean display) { + return appendType(type, builder, display, false); } - @NotNull - private static CompletionItemLabelDetails getMethodNodeDetails(MethodNode method) { - var detailBuilder = new StringBuilder(); - detailBuilder.append("("); - var parameters = method.getParameters(); - for (int i = 0; i < parameters.length; i++) { - var parameter = parameters[i]; - detailBuilder.append(parameter.isDynamicTyped() ? "?" : parameter.getType().getNameWithoutPackage()); - if (i < parameters.length - 1) { - detailBuilder.append(","); + public static StringBuilder appendType(ClassNode type, StringBuilder builder, boolean display, boolean maybeVarargs) { + boolean isArray = type.getComponentType() != null; + if (isArray) type = type.getComponentType(); + if (type.isGenericsPlaceHolder()) { + // this type is a generic + GenericsType gt = type.asGenericsType(); + ClassNode bound = null; + if (gt.getUpperBounds() != null && gt.getUpperBounds().length > 0) { + bound = gt.getUpperBounds()[0]; + } else if (gt.getLowerBound() != null) { + bound = gt.getLowerBound(); } + if (bound == null || bound.equals(type)) { + // type has no bound (just T f.e.) + builder.append(gt.getName()); + if (isArray) { + builder.append(maybeVarargs ? "..." : "[]"); + } + return builder; + } + type = bound; + } + builder.append(display ? type.getNameWithoutPackage() : type.getName()); + appendGenerics(type, builder, display); + if (isArray) { + builder.append(maybeVarargs ? "..." : "[]"); + } + return builder; + } + + private static void appendGenerics(ClassNode type, StringBuilder builder, boolean display) { + GenericsType[] gt = type.getGenericsTypes(); + if (gt == null || gt.length == 0) return; + builder.append("<"); + for (int i = 0; i < gt.length; i++) { + GenericsType g = gt[i]; + if (g.isWildcard()) builder.append("?"); + else appendType(g.getType(), builder, display); + if (i < gt.length - 1) { + builder.append(", "); + } + } + builder.append(">"); + } + + public static String getDescriptor(MethodInfo node, boolean includeName, boolean includeReturn, boolean display) { + StringBuilder builder = new StringBuilder(); + if (includeName) builder.append(node.getName()); + builder.append("("); + var parameters = node.getParameterInfo(); + for (int i = 0; i < parameters.length; i++) { + boolean last = i == parameters.length - 1; + appendParameter(parameters[i], builder, display, last); + if (!last) builder.append(", "); } - detailBuilder.append(") -> "); - detailBuilder.append(method.getReturnType().getNameWithoutPackage()); + builder.append(")"); + if (!includeReturn) return builder.toString(); + var ret = display ? + node.getTypeSignatureOrTypeDescriptor().getResultType().toStringWithSimpleNames() : + node.getTypeDescriptor().getResultType().toString(); + if (!ret.equals("void")) { + if (display) builder.append(" -> "); + builder.append(ret); + } + return builder.toString(); + } + + public static StringBuilder appendParameter(MethodParameterInfo param, StringBuilder builder, boolean display, boolean maybeVarargs) { + builder.append(display ? + param.getTypeSignatureOrTypeDescriptor().toStringWithSimpleNames() : + param.getTypeDescriptor().toString()); // don't use generic types + if (maybeVarargs && builder.charAt(builder.length() - 1) == ']' && builder.charAt(builder.length() - 2) == '[') { + builder.delete(builder.length() - 2, builder.length()).append("..."); + } + return builder; + } + @NotNull + private static CompletionItemLabelDetails getMethodNodeDetails(MethodNode method) { var details = new CompletionItemLabelDetails(); - details.setDetail(detailBuilder.toString()); + details.setDetail(getDescriptor(method, false, true, true)); return details; } @NotNull private static CompletionItemLabelDetails getMethodInfoDetails(MethodInfo methodInfo) { - var detailBuilder = new StringBuilder(); - detailBuilder.append("("); - MethodParameterInfo[] info = methodInfo.getParameterInfo(); - for (int i = 0; i < info.length; i++) { - var parameterInfo = info[i]; - detailBuilder.append(parameterInfo.getTypeSignatureOrTypeDescriptor().toStringWithSimpleNames()); - if (i < info.length - 1) { - detailBuilder.append(","); - } - } - detailBuilder.append(") -> "); - detailBuilder.append(methodInfo.getTypeSignatureOrTypeDescriptor().getResultType().toStringWithSimpleNames()); - var details = new CompletionItemLabelDetails(); - details.setDetail(detailBuilder.toString()); + details.setDetail(getDescriptor(methodInfo, false, true, true)); return details; } - - private void populateItemsFromExpression(Expression leftSide, String memberNamePrefix, Completions items) { + private void populateItemsFromExpression(Expression leftSide, Completions items) { Set existingNames = new ObjectOpenHashSet<>(); ClassNode classNode = GroovyASTUtils.getTypeOfNode(leftSide, astContext); @@ -423,95 +417,88 @@ private void populateItemsFromExpression(Expression leftSide, String memberNameP GroovyASTUtils.fillClassNode(classNode); List properties = GroovyASTUtils.getPropertiesForLeftSideOfPropertyExpression(classNode, leftSide, astContext); List fields = GroovyASTUtils.getFieldsForLeftSideOfPropertyExpression(classNode, leftSide, astContext); - populateItemsFromPropertiesAndFields(properties, fields, memberNamePrefix, existingNames, items); + populateItemsFromPropertiesAndFields(properties, fields, existingNames, items); List methods = GroovyASTUtils.getMethodsForLeftSideOfPropertyExpression(classNode, leftSide, astContext); - populateItemsFromMethods(methods, memberNamePrefix, existingNames, items); + populateItemsFromMethods(methods, existingNames, items); } - private void populateItemsFromGlobalScope(String memberNamePrefix, - Set existingNames, List items) { - astContext.getLanguageServerContext().getSandbox().getBindings().forEach((variableName, value) -> { - if (!variableName.startsWith(memberNamePrefix) || existingNames.contains(variableName)) { - return; - } - existingNames.add(variableName); - if (value instanceof ObjectMapper goh) { + private void populateItemsFromGlobalScope(String memberNamePrefix, Set existingNames, Completions items) { + items.addAll(astContext.getLanguageServerContext().getSandbox().getBindings().entrySet(), entry -> { + String name = entry.getKey(); + if (!name.toLowerCase(Locale.ENGLISH).contains(memberNamePrefix) || existingNames.contains(name)) return null; + existingNames.add(name); + if (entry.getValue() instanceof ObjectMapper goh) { for (MethodNode method : goh.getMethodNodes()) { var item = CompletionItemFactory.createCompletion(method, goh.getName(), astContext); item.setLabelDetails(getMethodNodeDetails(method)); + // TODO items.add(item); } - } else if (value instanceof Closure closure) { - MethodNode method = GroovyASTUtils.methodNodeOfClosure(variableName, closure); - var item = CompletionItemFactory.createCompletion(method, variableName, astContext); + return null; + } else if (entry.getValue() instanceof Closure closure) { + MethodNode method = GroovyASTUtils.methodNodeOfClosure(name, closure); + var item = CompletionItemFactory.createCompletion(method, name, astContext); item.setLabelDetails(getMethodNodeDetails(method)); - items.add(item); + return item; } else { - var item = CompletionItemFactory.createCompletion(CompletionItemKind.Variable, variableName); + var item = CompletionItemFactory.createCompletion(CompletionItemKind.Variable, name); item.setDetail("(global scope)"); + return item; + } + }); + + ModuleNode enclosingModule = getModule(); + if (enclosingModule == null) return; + items.addAll(enclosingModule.getStaticStarImports().values(), in -> { + String name = in.getClassName(); + if (name == null) return null; + // TODO use meta class? + ClassInfo info = astContext.getLanguageServerContext().getScanResult().getClassInfo(name); + if (info == null) return null; + for (MethodInfo m : info.getMethodInfo()) { + String desc = getDescriptor(m, true, false, false); + if (!m.isStatic() || !m.isPublic() || !desc.toLowerCase(Locale.ENGLISH).contains(memberNamePrefix) || existingNames.contains(desc)) continue; + existingNames.add(desc); + if (GroovyReflectionUtils.resolveMethodFromMethodInfo(m, astContext) == null) continue; + var item = CompletionItemFactory.createCompletion(CompletionItemKind.Method, m.getName()); + item.setLabelDetails(getMethodInfoDetails(m)); + items.add(item); + } + for (FieldInfo m : info.getFieldInfo()) { + if (!m.isStatic() || !m.getName().toLowerCase(Locale.ENGLISH).contains(memberNamePrefix) || existingNames.contains(m.getName())) continue; + existingNames.add(m.getName()); + var item = CompletionItemFactory.createCompletion(CompletionItemKind.Field, m.getName()); items.add(item); } + return null; }); + } - List staticMethodItems = astContext.getLanguageServerContext().getSandbox().getStaticImports().stream() - .map(staticImport -> astContext.getLanguageServerContext().getScanResult().getClassInfo(staticImport.getName())) - .filter(Objects::nonNull) - .flatMap(classInfo -> classInfo.getMethodInfo().stream().filter(ClassMemberInfo::isStatic)) - .filter(methodInfo -> { - String methodName = methodInfo.getName(); - if (methodName.startsWith(memberNamePrefix) && !existingNames.contains(methodName)) { - existingNames.add(methodName); - return GroovyReflectionUtils.resolveMethodFromMethodInfo(methodInfo, astContext).isPresent(); - } - return false; - }) - .map(methodInfo -> { - var item = CompletionItemFactory.createCompletion(CompletionItemKind.Method, methodInfo.getName()); - - var details = getMethodInfoDetails(methodInfo); - item.setLabelDetails(details); - return item; - }) - .collect(Collectors.toList()); - items.addAll(staticMethodItems); - } - - private void populateItemsFromVariableScope(VariableScope variableScope, String memberNamePrefix, - Set existingNames, Completions items) { - //populateItemsFromGameObjects(memberNamePrefix, existingNames, items); + private void populateItemsFromVariableScope(VariableScope variableScope, String memberNamePrefix, Set existingNames, + Completions items) { populateItemsFromGlobalScope(memberNamePrefix, existingNames, items); - - List variableItems = variableScope.getDeclaredVariables().values().stream().filter(variable -> { - + items.addAll(variableScope.getDeclaredVariables().values(), variable -> { String variableName = variable.getName(); - // overloads can cause duplicates - if (variableName.startsWith(memberNamePrefix) && !existingNames.contains(variableName)) { - existingNames.add(variableName); - return true; - } - return false; - }).map(variable -> { + if (!variableName.toLowerCase(Locale.ENGLISH).contains(memberNamePrefix) || existingNames.contains(variableName)) return null; var item = CompletionItemFactory.createCompletion((ASTNode) variable, variable.getName(), astContext); - if (!variable.isDynamicTyped()) { item.setDetail(variable.getType().getName()); } return item; - }).collect(Collectors.toList()); - items.addAll(variableItems); + }); } private void populateItemsFromScope(ASTNode node, String namePrefix, Completions items) { - Set existingNames = new HashSet<>(); + Set existingNames = new ObjectOpenHashSet<>(); ASTNode current = node; ASTNode child = null; boolean isInClosure = false; int argIndex = -1; while (current != null) { if (current instanceof ClassNode classNode) { - populateItemsFromPropertiesAndFields(classNode.getProperties(), classNode.getFields(), namePrefix, existingNames, items); - populateItemsFromMethods(classNode.getMethods(), namePrefix, existingNames, items); + populateItemsFromPropertiesAndFields(classNode.getProperties(), classNode.getFields(), existingNames, items); + populateItemsFromMethods(classNode.getMethods(), existingNames, items); } else if (current instanceof MethodNode methodNode) { populateItemsFromVariableScope(methodNode.getVariableScope(), namePrefix, existingNames, items); } else if (current instanceof BlockStatement block) { @@ -544,15 +531,15 @@ private void populateItemsFromScope(ASTNode node, String namePrefix, Completions } } if (classNode != null) { - populateItemsFromPropertiesAndFields(classNode.getProperties(), classNode.getFields(), namePrefix, existingNames, items); - populateItemsFromMethods(classNode.getMethods(), namePrefix, existingNames, items); + populateItemsFromPropertiesAndFields(classNode.getProperties(), classNode.getFields(), + existingNames, items); + populateItemsFromMethods(classNode.getMethods(), existingNames, items); } } } } } if (current instanceof VariableExpression || current instanceof StaticMethodCallExpression) { - //populateItemsFromGameObjects(namePrefix, existingNames, items); populateItemsFromGlobalScope(namePrefix, existingNames, items); } child = current; @@ -561,75 +548,63 @@ private void populateItemsFromScope(ASTNode node, String namePrefix, Completions populateTypes(node, namePrefix, existingNames, items); } - private void populateTypes(ASTNode offsetNode, String namePrefix, Set existingNames, - Completions items) { + private void populateTypes(ASTNode offsetNode, String namePrefix, Set existingNames, Completions items) { populateTypes(offsetNode, namePrefix, existingNames, true, true, true, items); } private void populateTypes(ASTNode offsetNode, String namePrefix, Set existingNames, boolean includeClasses, boolean includeInterfaces, boolean includeEnums, Completions items) { - Range addImportRange = GroovyASTUtils.findAddImportRange(offsetNode, astContext); + Range addImportRange = GroovyASTUtils.findAddImportRange(doc, offsetNode, astContext); - ModuleNode enclosingModule = (ModuleNode) GroovyASTUtils.getEnclosingNodeOfType(offsetNode, ModuleNode.class, - astContext); + ModuleNode enclosingModule = getModule(); String enclosingPackageName = enclosingModule.getPackageName(); - List importNames = enclosingModule.getImports().stream().map(importNode -> importNode.getClassName()) - .collect(Collectors.toList()); - - List localClassItems = astContext.getVisitor().getClassNodes().stream().filter(classNode -> { - if (items.reachedLimit()) return false; + items.addAll(astContext.getVisitor().getClassNodes(), classNode -> { + if (!includeEnums && classNode.isEnum()) return null; + if (!includeInterfaces && classNode.isInterface()) return null; + if (!includeClasses && (!classNode.isInterface() && !classNode.isEnum())) return null; String classNameWithoutPackage = classNode.getNameWithoutPackage(); String className = classNode.getName(); - if (classNameWithoutPackage.startsWith(namePrefix) && !existingNames.contains(className)) { - existingNames.add(className); - return true; - } - return false; - }).map(classNode -> { - String className = classNode.getName(); + if (!classNameWithoutPackage.startsWith(namePrefix) || existingNames.contains(className)) return null; + existingNames.add(className); String packageName = classNode.getPackageName(); CompletionItem item = CompletionItemFactory.createCompletion(classNode, classNode.getNameWithoutPackage(), astContext); item.setDetail(packageName); - if (packageName != null && !packageName.equals(enclosingPackageName) && !importNames.contains(className)) { + if (packageName != null && !packageName.equals(enclosingPackageName) && !GroovyLSUtils.hasImport(enclosingModule, className)) { List additionalTextEdits = new ArrayList<>(); TextEdit addImportEdit = createAddImportTextEdit(className, addImportRange); additionalTextEdits.add(addImportEdit); item.setAdditionalTextEdits(additionalTextEdits); } return item; - }).collect(Collectors.toList()); - items.addAll(localClassItems); - - List classes = astContext.getLanguageServerContext().getScanResult().getAllClasses(); + }); - List classItems = classes.stream().filter(classInfo -> { - if (items.reachedLimit()) return false; + items.addAll(astContext.getLanguageServerContext().getScanResult().getAllClasses(), classInfo -> { + if (!includeEnums && classInfo.isEnum()) return null; + if (!includeInterfaces && classInfo.isInterface()) return null; + if (!includeClasses && (!classInfo.isInterface() && !classInfo.isEnum())) return null; String className = classInfo.getName(); String classNameWithoutPackage = classInfo.getSimpleName(); - if (classNameWithoutPackage.startsWith(namePrefix) && !existingNames.contains(className)) { - existingNames.add(className); - return true; - } - return false; - }).map(classInfo -> { - String className = classInfo.getName(); + if (!classNameWithoutPackage.startsWith(namePrefix) || existingNames.contains(className)) return null; + existingNames.add(className); String packageName = classInfo.getPackageName(); - CompletionItem item = CompletionItemFactory.createCompletion(classInfoToCompletionItemKind(classInfo), classInfo.getSimpleName()); + CompletionItem item = CompletionItemFactory.createCompletion(classInfoToCompletionItemKind(classInfo), + classInfo.getSimpleName()); item.setDetail(packageName); - if (packageName != null && !packageName.equals(enclosingPackageName) && !importNames.contains(className)) { + boolean hasImport = GroovyLSUtils.hasImport(enclosingModule, className); + // sort imported classes higher + if (hasImport) item.setSortText("aa" + classInfo.getSimpleName()); + if (packageName != null && !packageName.equals(enclosingPackageName) && !GroovyLSUtils.hasImport(enclosingModule, className)) { List additionalTextEdits = new ArrayList<>(); TextEdit addImportEdit = createAddImportTextEdit(className, addImportRange); additionalTextEdits.add(addImportEdit); item.setAdditionalTextEdits(additionalTextEdits); } return item; - }).collect(Collectors.toList()); - items.addAll(classItems); + }); } private String getMemberName(String memberName, Range range, Position position) { - if (position.getLine() == range.getStart().getLine() - && position.getCharacter() > range.getStart().getCharacter()) { + if (position.getLine() == range.getStart().getLine() && position.getCharacter() > range.getStart().getCharacter()) { int length = position.getCharacter() - range.getStart().getCharacter(); if (length > 0 && length <= memberName.length()) { return memberName.substring(0, length).trim(); @@ -649,13 +624,6 @@ private CompletionItemKind classInfoToCompletionItemKind(ClassInfo classInfo) { } private TextEdit createAddImportTextEdit(String className, Range range) { - TextEdit edit = new TextEdit(); - StringBuilder builder = new StringBuilder(); - builder.append("import "); - builder.append(className); - builder.append("\n"); - edit.setNewText(builder.toString()); - edit.setRange(range); - return edit; - } -} \ No newline at end of file + return new TextEdit(range, "import " + className + "\n"); + } +} diff --git a/src/main/java/net/prominic/groovyls/providers/DefinitionProvider.java b/src/main/java/net/prominic/groovyls/providers/DefinitionProvider.java index fc4d4e701..aa8e70eba 100644 --- a/src/main/java/net/prominic/groovyls/providers/DefinitionProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/DefinitionProvider.java @@ -19,13 +19,9 @@ //////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls.providers; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; - import net.prominic.groovyls.compiler.ast.ASTContext; -import net.prominic.groovyls.util.URIUtils; +import net.prominic.groovyls.compiler.util.GroovyASTUtils; +import net.prominic.groovyls.util.GroovyLSUtils; import org.codehaus.groovy.ast.ASTNode; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; @@ -33,22 +29,20 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; -import net.prominic.groovyls.compiler.util.GroovyASTUtils; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; - -public class DefinitionProvider { +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; - private final ASTContext astContext; +public class DefinitionProvider extends DocProvider { - public DefinitionProvider(ASTContext astContext) { - this.astContext = astContext; + public DefinitionProvider(URI doc, ASTContext astContext) { + super(doc, astContext); } public CompletableFuture, List>> provideDefinition( TextDocumentIdentifier textDocument, Position position) { - URI uri = URIUtils.toUri(textDocument.getUri()); - ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter()); + ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(doc, position.getLine(), position.getCharacter()); if (offsetNode == null) { return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); } @@ -60,14 +54,14 @@ public CompletableFuture, List getNodes() { + return astContext.getVisitor().getNodes(doc); + } + + public CompletableFuture future(T t) { + return CompletableFuture.completedFuture(t); + } +} diff --git a/src/main/java/net/prominic/groovyls/providers/DocumentSymbolProvider.java b/src/main/java/net/prominic/groovyls/providers/DocumentSymbolProvider.java index 1b0f2a22f..2571b4d58 100644 --- a/src/main/java/net/prominic/groovyls/providers/DocumentSymbolProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/DocumentSymbolProvider.java @@ -21,8 +21,7 @@ import net.prominic.groovyls.compiler.ast.ASTContext; import net.prominic.groovyls.compiler.util.GroovyASTUtils; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; -import net.prominic.groovyls.util.URIUtils; +import net.prominic.groovyls.util.GroovyLSUtils; import org.codehaus.groovy.ast.*; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.SymbolInformation; @@ -30,48 +29,40 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import java.net.URI; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -public class DocumentSymbolProvider { +public class DocumentSymbolProvider extends DocProvider { - private final ASTContext astContext; - - public DocumentSymbolProvider(ASTContext astContext) { - this.astContext = astContext; + public DocumentSymbolProvider(URI doc, ASTContext astContext) { + super(doc, astContext); } - public CompletableFuture>> provideDocumentSymbols( + public CompletableFuture>> provideDocumentSymbolsFuture( TextDocumentIdentifier textDocument) { - URI uri = URIUtils.toUri(textDocument.getUri()); - List nodes = astContext.getVisitor().getNodes(uri); - List> symbols = nodes.stream().filter(node -> { - return node instanceof ClassNode || node instanceof MethodNode || node instanceof FieldNode - || node instanceof PropertyNode; - }).map(node -> { - if (node instanceof ClassNode) { - ClassNode classNode = (ClassNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(classNode, uri, null); - } - ClassNode classNode = (ClassNode) GroovyASTUtils.getEnclosingNodeOfType(node, ClassNode.class, astContext); - if (node instanceof MethodNode) { - MethodNode methodNode = (MethodNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(methodNode, uri, classNode.getName()); - } - if (node instanceof PropertyNode) { - PropertyNode propNode = (PropertyNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(propNode, uri, classNode.getName()); - } - if (node instanceof FieldNode) { - FieldNode fieldNode = (FieldNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(fieldNode, uri, classNode.getName()); + return future(provideDocumentSymbols(textDocument)); + } + + public List> provideDocumentSymbols(TextDocumentIdentifier textDocument) { + List> symbols = new ArrayList<>(); + for (ASTNode node : astContext.getVisitor().getNodes(doc)) { + DocumentSymbol symbol = null; + if (node instanceof ClassNode classNode) { + symbol = GroovyLSUtils.astNodeToSymbolInformation(classNode, doc, null); + } else { + ClassNode classNode = GroovyASTUtils.getEnclosingNodeOfType(node, ClassNode.class, astContext); + if (classNode == null) continue; + if (node instanceof MethodNode methodNode) { + symbol = GroovyLSUtils.astNodeToSymbolInformation(methodNode, doc, classNode.getName()); + } else if (node instanceof PropertyNode propNode) { + symbol = GroovyLSUtils.astNodeToSymbolInformation(propNode, doc, classNode.getName()); + } else if (node instanceof FieldNode fieldNode) { + symbol = GroovyLSUtils.astNodeToSymbolInformation(fieldNode, doc, classNode.getName()); + } } - // this should never happen - return null; - }).filter(symbolInformation -> symbolInformation != null).map(node -> { - return Either.forLeft(node); - }).collect(Collectors.toList()); - return CompletableFuture.completedFuture(symbols); + if (symbol != null) symbols.add(Either.forRight(symbol)); + } + return symbols; } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/providers/HoverProvider.java b/src/main/java/net/prominic/groovyls/providers/HoverProvider.java index 1f145ea97..24fcea3e1 100644 --- a/src/main/java/net/prominic/groovyls/providers/HoverProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/HoverProvider.java @@ -23,24 +23,20 @@ import net.prominic.groovyls.compiler.ast.ASTContext; import net.prominic.groovyls.compiler.util.GroovyASTUtils; import net.prominic.groovyls.util.GroovyNodeToStringUtils; -import net.prominic.groovyls.util.URIUtils; import org.codehaus.groovy.ast.*; import org.eclipse.lsp4j.*; import java.net.URI; import java.util.concurrent.CompletableFuture; -public class HoverProvider { +public class HoverProvider extends DocProvider { - private final ASTContext astContext; - - public HoverProvider(ASTContext astContext) { - this.astContext = astContext; + public HoverProvider(URI doc, ASTContext astContext) { + super(doc, astContext); } public CompletableFuture provideHover(TextDocumentIdentifier textDocument, Position position) { - URI uri = URIUtils.toUri(textDocument.getUri()); - ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter()); + ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(doc, position.getLine(), position.getCharacter()); if (offsetNode == null) { return CompletableFuture.completedFuture(null); } @@ -78,18 +74,15 @@ public CompletableFuture provideHover(TextDocumentIdentifier textDocument } private String getContent(ASTNode hoverNode) { - if (hoverNode instanceof ClassNode) { - ClassNode classNode = (ClassNode) hoverNode; + if (hoverNode instanceof ClassNode classNode) { return GroovyNodeToStringUtils.classToString(classNode, astContext); - } else if (hoverNode instanceof MethodNode) { - MethodNode methodNode = (MethodNode) hoverNode; + } else if (hoverNode instanceof MethodNode methodNode) { return GroovyNodeToStringUtils.methodToString(methodNode, astContext); - } else if (hoverNode instanceof Variable) { - Variable varNode = (Variable) hoverNode; + } else if (hoverNode instanceof Variable varNode) { return GroovyNodeToStringUtils.variableToString(varNode, astContext); } else { GroovyScript.LOGGER.warn("*** hover not available for node: {}", hoverNode); } return null; } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java b/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java index dd79f132e..a40ef7da5 100644 --- a/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java @@ -19,45 +19,41 @@ //////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls.providers; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - import net.prominic.groovyls.compiler.ast.ASTContext; -import net.prominic.groovyls.util.URIUtils; +import net.prominic.groovyls.compiler.util.GroovyASTUtils; +import net.prominic.groovyls.util.GroovyLSUtils; import org.codehaus.groovy.ast.ASTNode; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.TextDocumentIdentifier; -import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; -import net.prominic.groovyls.compiler.util.GroovyASTUtils; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; - -public class ReferenceProvider { +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; - private final ASTContext astContext; +public class ReferenceProvider extends DocProvider { - public ReferenceProvider(ASTContext astContext) { - this.astContext = astContext; + public ReferenceProvider(URI doc, ASTContext astContext) { + super(doc, astContext); } - public CompletableFuture> provideReferences(TextDocumentIdentifier textDocument, - Position position) { - URI documentURI = URIUtils.toUri(textDocument.getUri()); - ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(documentURI, position.getLine(), position.getCharacter()); + public CompletableFuture> provideReferences(TextDocumentIdentifier textDocument, Position position) { + ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(doc, position.getLine(), position.getCharacter()); if (offsetNode == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } List references = GroovyASTUtils.getReferences(offsetNode, astContext); - List locations = references.stream().map(node -> { - URI uri = astContext.getVisitor().getURI(node); - return GroovyLanguageServerUtils.astNodeToLocation(node, uri); - }).filter(location -> location != null).collect(Collectors.toList()); + List locations = new ArrayList<>(); + for (ASTNode node : references) { + Location loc = GroovyLSUtils.astNodeToLocation(node, astContext.getVisitor().getURI(node)); + if (loc != null) { + locations.add(loc); + } + } return CompletableFuture.completedFuture(locations); } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/providers/RenameProvider.java b/src/main/java/net/prominic/groovyls/providers/RenameProvider.java index b97c644a6..802b96a8f 100644 --- a/src/main/java/net/prominic/groovyls/providers/RenameProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/RenameProvider.java @@ -19,53 +19,39 @@ //////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls.providers; -import java.net.URI; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import net.prominic.groovyls.compiler.ast.ASTContext; -import net.prominic.groovyls.util.URIUtils; +import net.prominic.groovyls.compiler.util.GroovyASTUtils; +import net.prominic.groovyls.util.FileContentsTracker; +import net.prominic.groovyls.util.GroovyLSUtils; +import net.prominic.groovyls.util.Ranges; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.VariableExpression; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.RenameFile; -import org.eclipse.lsp4j.RenameParams; -import org.eclipse.lsp4j.ResourceOperation; -import org.eclipse.lsp4j.TextDocumentEdit; -import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; -import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; -import net.prominic.groovyls.compiler.util.GroovyASTUtils; -import net.prominic.groovyls.util.FileContentsTracker; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; -import net.prominic.lsp.utils.Ranges; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -public class RenameProvider { +public class RenameProvider extends DocProvider { - private final ASTContext astContext; - private FileContentsTracker files; + private final FileContentsTracker files; - public RenameProvider(ASTContext astContext, FileContentsTracker files) { - this.astContext = astContext; + public RenameProvider(URI doc, ASTContext astContext, FileContentsTracker files) { + super(doc, astContext); this.files = files; } public CompletableFuture provideRename(RenameParams renameParams) { - TextDocumentIdentifier textDocument = renameParams.getTextDocument(); Position position = renameParams.getPosition(); String newName = renameParams.getNewName(); @@ -73,7 +59,7 @@ public CompletableFuture provideRename(RenameParams renameParams) List> documentChanges = new ArrayList<>(); WorkspaceEdit workspaceEdit = new WorkspaceEdit(documentChanges); - URI documentURI = URIUtils.toUri(textDocument.getUri()); + URI documentURI = doc; ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(documentURI, position.getLine(), position.getCharacter()); if (offsetNode == null) { return CompletableFuture.completedFuture(workspaceEdit); @@ -91,7 +77,7 @@ public CompletableFuture provideRename(RenameParams renameParams) // can't find the text? skip it return; } - Range range = GroovyLanguageServerUtils.astNodeToRange(node); + Range range = GroovyLSUtils.astNodeToRange(node); if (range == null) { // can't find the range? skip it return; @@ -102,8 +88,7 @@ public CompletableFuture provideRename(RenameParams renameParams) end.setCharacter(start.getCharacter() + contents.length()); TextEdit textEdit = null; - if (node instanceof ClassNode) { - ClassNode classNode = (ClassNode) node; + if (node instanceof ClassNode classNode) { textEdit = createTextEditToRenameClassNode(classNode, newName, contents, range); if (textEdit != null && astContext.getVisitor().getParent(classNode) == null) { String newURI = uri.toString(); @@ -116,11 +101,9 @@ public CompletableFuture provideRename(RenameParams renameParams) renameFile.setNewUri(newURI); documentChanges.add(Either.forRight(renameFile)); } - } else if (node instanceof MethodNode) { - MethodNode methodNode = (MethodNode) node; + } else if (node instanceof MethodNode methodNode) { textEdit = createTextEditToRenameMethodNode(methodNode, newName, contents, range); - } else if (node instanceof PropertyNode) { - PropertyNode propNode = (PropertyNode) node; + } else if (node instanceof PropertyNode propNode) { textEdit = createTextEditToRenamePropertyNode(propNode, newName, contents, range); } else if (node instanceof ConstantExpression || node instanceof VariableExpression) { textEdit = new TextEdit(); @@ -150,7 +133,7 @@ public CompletableFuture provideRename(RenameParams renameParams) } private String getPartialNodeText(URI uri, ASTNode node) { - Range range = GroovyLanguageServerUtils.astNodeToRange(node); + Range range = GroovyLSUtils.astNodeToRange(node); if (range == null) { return null; } @@ -166,7 +149,7 @@ private TextEdit createTextEditToRenameClassNode(ClassNode classNode, String new // need to find it manually String className = classNode.getNameWithoutPackage(); int dollarIndex = className.indexOf('$'); - if (dollarIndex != 01) { + if (dollarIndex != 1) { // it's an inner class, so remove the outer name prefix className = className.substring(dollarIndex + 1); } @@ -211,8 +194,7 @@ private TextEdit createTextEditToRenameMethodNode(MethodNode methodNode, String return textEdit; } - private TextEdit createTextEditToRenamePropertyNode(PropertyNode propNode, String newName, String text, - Range range) { + private TextEdit createTextEditToRenamePropertyNode(PropertyNode propNode, String newName, String text, Range range) { // the AST doesn't give us access to the name location, so we // need to find it manually Pattern propPattern = Pattern.compile("\\b" + propNode.getName() + "\\b"); @@ -232,4 +214,4 @@ private TextEdit createTextEditToRenamePropertyNode(PropertyNode propNode, Strin textEdit.setNewText(newName); return textEdit; } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/providers/SignatureHelpProvider.java b/src/main/java/net/prominic/groovyls/providers/SignatureHelpProvider.java index 50df7b35e..fe43d9b30 100644 --- a/src/main/java/net/prominic/groovyls/providers/SignatureHelpProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/SignatureHelpProvider.java @@ -21,9 +21,8 @@ import net.prominic.groovyls.compiler.ast.ASTContext; import net.prominic.groovyls.compiler.util.GroovyASTUtils; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; +import net.prominic.groovyls.util.GroovyLSUtils; import net.prominic.groovyls.util.GroovyNodeToStringUtils; -import net.prominic.groovyls.util.URIUtils; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Parameter; @@ -38,18 +37,14 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -public class SignatureHelpProvider { +public class SignatureHelpProvider extends DocProvider { - private final ASTContext astContext; - - public SignatureHelpProvider(ASTContext astContext) { - this.astContext = astContext; + public SignatureHelpProvider(URI doc, ASTContext astContext) { + super(doc, astContext); } - public CompletableFuture provideSignatureHelp(TextDocumentIdentifier textDocument, - Position position) { - URI uri = URIUtils.toUri(textDocument.getUri()); - ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter()); + public CompletableFuture provideSignatureHelp(TextDocumentIdentifier textDocument, Position position) { + ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(doc, position.getLine(), position.getCharacter()); if (offsetNode == null) { return CompletableFuture.completedFuture(new SignatureHelp(Collections.emptyList(), -1, -1)); } @@ -57,10 +52,9 @@ public CompletableFuture provideSignatureHelp(TextDocumentIdentif MethodCall methodCall = null; ASTNode parentNode = astContext.getVisitor().getParent(offsetNode); - if (offsetNode instanceof ArgumentListExpression) { + if (offsetNode instanceof ArgumentListExpression argsList) { methodCall = (MethodCall) parentNode; - ArgumentListExpression argsList = (ArgumentListExpression) offsetNode; List expressions = argsList.getExpressions(); activeParamIndex = getActiveParameter(position, expressions); } @@ -78,8 +72,7 @@ public CompletableFuture provideSignatureHelp(TextDocumentIdentif for (MethodNode method : methods) { List parameters = new ArrayList<>(); Parameter[] methodParams = method.getParameters(); - for (int i = 0; i < methodParams.length; i++) { - Parameter methodParam = methodParams[i]; + for (Parameter methodParam : methodParams) { ParameterInformation paramInfo = new ParameterInformation(); paramInfo.setLabel(GroovyNodeToStringUtils.variableToString(methodParam, astContext)); parameters.add(paramInfo); @@ -104,18 +97,17 @@ public CompletableFuture provideSignatureHelp(TextDocumentIdentif private int getActiveParameter(Position position, List expressions) { for (int i = 0; i < expressions.size(); i++) { Expression expr = expressions.get(i); - Range exprRange = GroovyLanguageServerUtils.astNodeToRange(expr); + Range exprRange = GroovyLSUtils.astNodeToRange(expr); if (exprRange == null) { continue; } if (position.getLine() < exprRange.getEnd().getLine()) { return i; } - if (position.getLine() == exprRange.getEnd().getLine() - && position.getCharacter() <= exprRange.getEnd().getCharacter()) { + if (position.getLine() == exprRange.getEnd().getLine() && position.getCharacter() <= exprRange.getEnd().getCharacter()) { return i; } } return expressions.size(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/providers/TypeDefinitionProvider.java b/src/main/java/net/prominic/groovyls/providers/TypeDefinitionProvider.java index 032172721..9c6702a95 100644 --- a/src/main/java/net/prominic/groovyls/providers/TypeDefinitionProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/TypeDefinitionProvider.java @@ -19,13 +19,9 @@ //////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls.providers; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; - import net.prominic.groovyls.compiler.ast.ASTContext; -import net.prominic.groovyls.util.URIUtils; +import net.prominic.groovyls.compiler.util.GroovyASTUtils; +import net.prominic.groovyls.util.GroovyLSUtils; import org.codehaus.groovy.ast.ASTNode; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; @@ -33,22 +29,20 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; -import net.prominic.groovyls.compiler.util.GroovyASTUtils; -import net.prominic.groovyls.util.GroovyLanguageServerUtils; - -public class TypeDefinitionProvider { +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; - private final ASTContext astContext; +public class TypeDefinitionProvider extends DocProvider { - public TypeDefinitionProvider(ASTContext astContext) { - this.astContext = astContext; + public TypeDefinitionProvider(URI doc, ASTContext astContext) { + super(doc, astContext); } public CompletableFuture, List>> provideTypeDefinition( TextDocumentIdentifier textDocument, Position position) { - URI uri = URIUtils.toUri(textDocument.getUri()); - ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter()); + ASTNode offsetNode = astContext.getVisitor().getNodeAtLineAndColumn(doc, position.getLine(), position.getCharacter()); if (offsetNode == null) { return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); } @@ -60,14 +54,14 @@ public CompletableFuture, List> provideWorkspaceSymbols(String query) { + public CompletableFuture> provideWorkspaceSymbols(String query) { String lowerCaseQuery = query.toLowerCase(); - List nodes = astContext.getVisitor().getNodes(); - List symbols = nodes.stream().filter(node -> { + List symbols = new ArrayList<>(); + for (ASTNode node : astContext.getVisitor().getNodes()) { String name = null; - if (node instanceof ClassNode) { - ClassNode classNode = (ClassNode) node; + String parent = null; + if (node instanceof ClassNode classNode) { name = classNode.getName(); - } else if (node instanceof MethodNode) { - MethodNode methodNode = (MethodNode) node; - name = methodNode.getName(); - } else if (node instanceof FieldNode) { - FieldNode fieldNode = (FieldNode) node; - name = fieldNode.getName(); - } else if (node instanceof PropertyNode) { - PropertyNode propNode = (PropertyNode) node; - name = propNode.getName(); - } - if (name == null) { - return false; + } else { + ClassNode classNode = GroovyASTUtils.getEnclosingNodeOfType(node, ClassNode.class, astContext); + if (classNode != null) parent = classNode.getName(); + if (node instanceof MethodNode methodNode) { + name = methodNode.getName(); + } else if (node instanceof FieldNode fieldNode) { + name = fieldNode.getName(); + } else if (node instanceof PropertyNode propNode) { + name = propNode.getName(); + } } - return name.toLowerCase().contains(lowerCaseQuery); - }).map(node -> { + if (name == null || !name.toLowerCase().contains(lowerCaseQuery)) continue; + Range range = GroovyLSUtils.astNodeToRange(node); + if (range == null) continue; + SymbolKind kind = GroovyLSUtils.astNodeToSymbolKind(node); URI uri = astContext.getVisitor().getURI(node); - if (node instanceof ClassNode) { - ClassNode classNode = (ClassNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(classNode, uri, null); - } - ClassNode classNode = (ClassNode) GroovyASTUtils.getEnclosingNodeOfType(node, ClassNode.class, astContext); - if (node instanceof MethodNode) { - MethodNode methodNode = (MethodNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(methodNode, uri, classNode.getName()); - } - if (node instanceof PropertyNode) { - PropertyNode propNode = (PropertyNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(propNode, uri, classNode.getName()); - } - if (node instanceof FieldNode) { - FieldNode fieldNode = (FieldNode) node; - return GroovyLanguageServerUtils.astNodeToSymbolInformation(fieldNode, uri, classNode.getName()); - } - // this should never happen - return null; - }).filter(symbolInformation -> symbolInformation != null).collect(Collectors.toList()); + symbols.add(new WorkspaceSymbol(name, kind, Either.forLeft(new Location(uri.toString(), range)), parent)); + } return CompletableFuture.completedFuture(symbols); } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/util/ClassGraphUtils.java b/src/main/java/net/prominic/groovyls/util/ClassGraphUtils.java index 2bf453960..7bf63da30 100644 --- a/src/main/java/net/prominic/groovyls/util/ClassGraphUtils.java +++ b/src/main/java/net/prominic/groovyls/util/ClassGraphUtils.java @@ -10,8 +10,7 @@ public class ClassGraphUtils { public static @Nullable ClassInfo resolveAllowedClassInfo(ClassNode node, ASTContext context) { ClassInfo result = null; while (result == null) { - if (node.equals(new ClassNode(Object.class))) - return null; + if (node.equals(new ClassNode(Object.class))) return null; result = context.getLanguageServerContext().getScanResult().getClassInfo(node.getName()); for (ClassNode anInterface : node.getInterfaces()) { result = context.getLanguageServerContext().getScanResult().getClassInfo(anInterface.getName()); diff --git a/src/main/java/net/prominic/groovyls/util/CompletionItemFactory.java b/src/main/java/net/prominic/groovyls/util/CompletionItemFactory.java index faa827d90..055d24a85 100644 --- a/src/main/java/net/prominic/groovyls/util/CompletionItemFactory.java +++ b/src/main/java/net/prominic/groovyls/util/CompletionItemFactory.java @@ -1,36 +1,79 @@ package net.prominic.groovyls.util; import net.prominic.groovyls.compiler.ast.ASTContext; -import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.AnnotatedNode; +import net.prominic.groovyls.compiler.documentation.DocumentationFactory; +import org.codehaus.groovy.ast.*; import org.eclipse.lsp4j.*; public class CompletionItemFactory { public static CompletionItem createCompletion(ASTNode node, String label, ASTContext astContext) { - var completionItem = createCompletion(GroovyLanguageServerUtils.astNodeToCompletionItemKind(node), label); - + var completionItem = createCompletion(node, label); if (node instanceof AnnotatedNode annotatedNode) { - var documentation = astContext.getLanguageServerContext().getDocumentationFactory().getDocumentation(annotatedNode, astContext); - + DocumentationFactory docs = astContext.getLanguageServerContext().getDocumentationFactory(); + var documentation = docs.getDocumentation(annotatedNode, astContext); if (documentation != null) { completionItem.setDocumentation(new MarkupContent(MarkupKind.MARKDOWN, documentation)); } + var sortText = docs.getSortText(annotatedNode, astContext); + if (sortText != null) { + completionItem.setSortText(sortText); + } } return completionItem; } + public static CompletionItem createCompletion(ASTNode node, String label) { + if (node instanceof MethodNode mn) { + var item = createCompletion(CompletionItemKind.Method, label); + int params = mn.getParameters() == null ? 0 : mn.getParameters().length; + item.setInsertTextFormat(InsertTextFormat.Snippet); + if (params > 0) { + Parameter p = mn.getParameters()[params - 1]; + ClassNode type = p.getType(); + if (!p.isDynamicTyped() && type.getComponentType() != null) { + params--; // last arg is array or varargs -> don't count it in + } + } + if (params == 0) { + item.setInsertText(label + "()"); + return item; + } + StringBuilder builder = new StringBuilder(label).append('('); + for (int i = 0; i < params; i++) { + builder.append('$').append(i + 1); + if (i < params - 1) builder.append(", "); + } + item.setInsertText(builder.append(")$0").toString()); + return item; + } + return createCompletion(GroovyLSUtils.astNodeToCompletionItemKind(node), label); + } + public static CompletionItem createCompletion(CompletionItemKind kind, String label) { var item = new CompletionItem(); item.setKind(kind); item.setLabel(label); - if (kind == CompletionItemKind.Method) { - item.setInsertTextFormat(InsertTextFormat.Snippet); - item.setInsertText(label + "($0)"); - } else if (kind == CompletionItemKind.Property || kind == CompletionItemKind.Field) { - item.setInsertTextFormat(InsertTextFormat.Snippet); - item.setInsertText(label + ".$0"); + return item; + } + + public static CompletionItem createKeywordCompletion(String keyword, boolean popular) { + return createKeywordCompletion(keyword, popular, null); + } + + public static CompletionItem createKeywordCompletion(String keyword, boolean popular, String insert) { + boolean insertSpace = keyword.endsWith(" "); + String realKeyword = insertSpace ? keyword.substring(0, keyword.length() - 1) : keyword; + var item = new CompletionItem(realKeyword); + item.setKind(CompletionItemKind.Keyword); + String sort = popular ? "zz" : "zzz"; + item.setSortText(sort + realKeyword); + item.setInsertTextFormat(InsertTextFormat.Snippet); + if (insert != null) { + item.setInsertText(realKeyword + insert); + } else if (insertSpace) { + item.setInsertText(realKeyword + " "); } return item; } diff --git a/src/main/java/net/prominic/groovyls/util/FileContentsTracker.java b/src/main/java/net/prominic/groovyls/util/FileContentsTracker.java index 2c9fad625..a75b39140 100644 --- a/src/main/java/net/prominic/groovyls/util/FileContentsTracker.java +++ b/src/main/java/net/prominic/groovyls/util/FileContentsTracker.java @@ -19,7 +19,7 @@ //////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls.util; -import net.prominic.lsp.utils.Positions; +import com.cleanroommc.groovyscript.sandbox.FileUtil; import org.eclipse.lsp4j.*; import java.io.BufferedReader; @@ -34,7 +34,7 @@ public class FileContentsTracker { - private Map openFiles = new HashMap<>(); + private final Map openFiles = new HashMap<>(); private Set changedFiles = new HashSet<>(); public Set getOpenURIs() { @@ -58,13 +58,13 @@ public boolean isOpen(URI uri) { } public void didOpen(DidOpenTextDocumentParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); openFiles.put(uri, params.getTextDocument().getText()); changedFiles.add(uri); } public void didChange(DidChangeTextDocumentParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); String oldText = openFiles.get(uri); TextDocumentContentChangeEvent change = params.getContentChanges().get(0); Range range = change.getRange(); @@ -73,41 +73,28 @@ public void didChange(DidChangeTextDocumentParams params) { } else { int offsetStart = Positions.getOffset(oldText, change.getRange().getStart()); int offsetEnd = Positions.getOffset(oldText, change.getRange().getEnd()); - StringBuilder builder = new StringBuilder(); - builder.append(oldText.substring(0, offsetStart)); - builder.append(change.getText()); - builder.append(oldText.substring(offsetEnd)); - openFiles.put(uri, builder.toString()); + openFiles.put(uri, oldText.substring(0, offsetStart) + change.getText() + oldText.substring(offsetEnd)); } changedFiles.add(uri); } public void didClose(DidCloseTextDocumentParams params) { - URI uri = URIUtils.toUri(params.getTextDocument().getUri()); + URI uri = FileUtil.fixUri(params.getTextDocument().getUri()); openFiles.remove(uri); changedFiles.add(uri); } public String getContents(URI uri) { if (!openFiles.containsKey(uri)) { - BufferedReader reader = null; - try { - reader = Files.newBufferedReader(Paths.get(uri)); + try (BufferedReader reader = Files.newBufferedReader(Paths.get(uri))) { StringBuilder builder = new StringBuilder(); - int next = -1; + int next; while ((next = reader.read()) != -1) { builder.append((char) next); } return builder.toString(); } catch (IOException e) { return ""; - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - } - } } } return openFiles.getOrDefault(uri, ""); @@ -116,4 +103,4 @@ public String getContents(URI uri) { public void setContents(URI uri, String contents) { openFiles.put(uri, contents); } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/util/GroovyLSUtils.java b/src/main/java/net/prominic/groovyls/util/GroovyLSUtils.java new file mode 100644 index 000000000..834c5a416 --- /dev/null +++ b/src/main/java/net/prominic/groovyls/util/GroovyLSUtils.java @@ -0,0 +1,149 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2022 Prominic.NET, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License +// +// Author: Prominic.NET, Inc. +// No warranty of merchantability or fitness of any kind. +// Use this software at your own risk. +//////////////////////////////////////////////////////////////////////////////// +package net.prominic.groovyls.util; + +import com.cleanroommc.groovyscript.core.mixin.groovy.ModuleNodeAccessor; +import org.codehaus.groovy.ast.*; +import org.codehaus.groovy.syntax.SyntaxException; +import org.eclipse.lsp4j.*; + +import java.net.URI; + +public class GroovyLSUtils { + + /** + * Converts a Groovy position to a LSP position. May return null if the Groovy line is -1 + */ + public static Position createGroovyPosition(int line, int column) { + if (line < 0) return null; + if (column < 0) { + column = 0; + } else if (column > 0) column--; + if (line > 0) line--; + return new Position(line, column); + } + + public static Range syntaxExceptionToRange(SyntaxException exception) { + return rangeOf(exception.getStartLine(), exception.getStartColumn(), exception.getEndLine(), exception.getEndColumn()); + } + + public static Range rangeOf(int startLine, int startCol, int endLine, int endCol) { + Position start = createGroovyPosition(startLine, startCol); + if (start == null) return null; + Position end = createGroovyPosition(endLine, endCol); + if (end == null) end = start; + return new Range(start, end); + } + + /** + * Converts a Groovy AST node to an LSP range. May return null if the node's start line is -1 + */ + public static Range astNodeToRange(ASTNode node) { + return rangeOf(node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()); + } + + public static Range astNodeToRange(ASTNode start, ASTNode end) { + return rangeOf(start.getLineNumber(), start.getColumnNumber(), end.getLastLineNumber(), end.getLastColumnNumber()); + } + + public static CompletionItemKind astNodeToCompletionItemKind(ASTNode node) { + if (node instanceof ClassNode classNode) { + if (classNode.isInterface()) { + return CompletionItemKind.Interface; + } else if (classNode.isEnum()) { + return CompletionItemKind.Enum; + } + return CompletionItemKind.Class; + } else if (node instanceof MethodNode) { + return CompletionItemKind.Method; + } else if (node instanceof Variable) { + if (node instanceof FieldNode || node instanceof PropertyNode) { + return CompletionItemKind.Field; + } + return CompletionItemKind.Variable; + } + return CompletionItemKind.Property; + } + + public static SymbolKind astNodeToSymbolKind(ASTNode node) { + if (node instanceof ClassNode classNode) { + if (classNode.isInterface()) { + return SymbolKind.Interface; + } else if (classNode.isEnum()) { + return SymbolKind.Enum; + } + return SymbolKind.Class; + } else if (node instanceof MethodNode) { + return SymbolKind.Method; + } else if (node instanceof Variable) { + if (node instanceof FieldNode || node instanceof PropertyNode) { + return SymbolKind.Field; + } + return SymbolKind.Variable; + } + return SymbolKind.Property; + } + + /** + * Converts a Groovy AST node to an LSP location. May return null if the node's start line is -1 + */ + public static Location astNodeToLocation(ASTNode node, URI uri) { + Range range = astNodeToRange(node); + if (range == null) return null; + return new Location(uri.toString(), range); + } + + public static DocumentSymbol astNodeToSymbolInformation(ClassNode node, URI uri, String parentName) { + Range location = astNodeToRange(node); + if (location == null) return null; + SymbolKind symbolKind = astNodeToSymbolKind(node); + // methods and fields could be added as children here, + // but we already iterate over every ast node in the script + return new DocumentSymbol(node.getName(), symbolKind, location, location, parentName); + } + + public static DocumentSymbol astNodeToSymbolInformation(MethodNode node, URI uri, String parentName) { + Range location = astNodeToRange(node); + if (location == null) return null; + SymbolKind symbolKind = astNodeToSymbolKind(node); + return new DocumentSymbol(node.getName(), symbolKind, location, location, parentName); + } + + public static DocumentSymbol astNodeToSymbolInformation(Variable node, URI uri, String parentName) { + if (!(node instanceof ASTNode astVar)) { + // DynamicVariable isn't an ASTNode + return null; + } + Range location = astNodeToRange(astVar); + if (location == null) return null; + SymbolKind symbolKind = astNodeToSymbolKind(astVar); + return new DocumentSymbol(node.getName(), symbolKind, location, location, parentName); + } + + public static boolean hasImport(ModuleNode module, String className) { + if (className == null) return false; + for (ImportNode in : ((ModuleNodeAccessor) module).getModifiableImports()) { + if (in.getType() != null && in.getType().getName().equals(className)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/prominic/groovyls/util/GroovyLanguageServerUtils.java b/src/main/java/net/prominic/groovyls/util/GroovyLanguageServerUtils.java deleted file mode 100644 index a0e2aed85..000000000 --- a/src/main/java/net/prominic/groovyls/util/GroovyLanguageServerUtils.java +++ /dev/null @@ -1,171 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright 2022 Prominic.NET, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License -// -// Author: Prominic.NET, Inc. -// No warranty of merchantability or fitness of any kind. -// Use this software at your own risk. -//////////////////////////////////////////////////////////////////////////////// -package net.prominic.groovyls.util; - -import java.net.URI; - -import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.FieldNode; -import org.codehaus.groovy.ast.MethodNode; -import org.codehaus.groovy.ast.PropertyNode; -import org.codehaus.groovy.ast.Variable; -import org.codehaus.groovy.syntax.SyntaxException; -import org.eclipse.lsp4j.CompletionItemKind; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.SymbolInformation; -import org.eclipse.lsp4j.SymbolKind; - -public class GroovyLanguageServerUtils { - /** - * Converts a Groovy position to a LSP position. - * - * May return null if the Groovy line is -1 - */ - public static Position createGroovyPosition(int groovyLine, int groovyColumn) { - if (groovyLine == -1) { - return null; - } - if (groovyColumn == -1) { - groovyColumn = 0; - } - int lspLine = groovyLine; - if (lspLine > 0) { - lspLine--; - } - int lspColumn = groovyColumn; - if (lspColumn > 0) { - lspColumn--; - } - return new Position(lspLine, lspColumn); - } - - public static Range syntaxExceptionToRange(SyntaxException exception) { - return new Range(createGroovyPosition(exception.getStartLine(), exception.getStartColumn()), - createGroovyPosition(exception.getEndLine(), exception.getEndColumn())); - } - - /** - * Converts a Groovy AST node to an LSP range. - * - * May return null if the node's start line is -1 - */ - public static Range astNodeToRange(ASTNode node) { - Position start = createGroovyPosition(node.getLineNumber(), node.getColumnNumber()); - if (start == null) { - return null; - } - Position end = createGroovyPosition(node.getLastLineNumber(), node.getLastColumnNumber()); - if (end == null) { - end = start; - } - return new Range(start, end); - } - - public static CompletionItemKind astNodeToCompletionItemKind(ASTNode node) { - if (node instanceof ClassNode) { - ClassNode classNode = (ClassNode) node; - if (classNode.isInterface()) { - return CompletionItemKind.Interface; - } else if (classNode.isEnum()) { - return CompletionItemKind.Enum; - } - return CompletionItemKind.Class; - } else if (node instanceof MethodNode) { - return CompletionItemKind.Method; - } else if (node instanceof Variable) { - if (node instanceof FieldNode || node instanceof PropertyNode) { - return CompletionItemKind.Field; - } - return CompletionItemKind.Variable; - } - return CompletionItemKind.Property; - } - - public static SymbolKind astNodeToSymbolKind(ASTNode node) { - if (node instanceof ClassNode) { - ClassNode classNode = (ClassNode) node; - if (classNode.isInterface()) { - return SymbolKind.Interface; - } else if (classNode.isEnum()) { - return SymbolKind.Enum; - } - return SymbolKind.Class; - } else if (node instanceof MethodNode) { - return SymbolKind.Method; - } else if (node instanceof Variable) { - if (node instanceof FieldNode || node instanceof PropertyNode) { - return SymbolKind.Field; - } - return SymbolKind.Variable; - } - return SymbolKind.Property; - } - - /** - * Converts a Groovy AST node to an LSP location. - * - * May return null if the node's start line is -1 - */ - public static Location astNodeToLocation(ASTNode node, URI uri) { - Range range = astNodeToRange(node); - if (range == null) { - return null; - } - return new Location(uri.toString(), range); - } - - public static SymbolInformation astNodeToSymbolInformation(ClassNode node, URI uri, String parentName) { - Location location = astNodeToLocation(node, uri); - if (location == null) { - return null; - } - SymbolKind symbolKind = astNodeToSymbolKind(node); - return new SymbolInformation(node.getName(), symbolKind, location, - parentName); - } - - public static SymbolInformation astNodeToSymbolInformation(MethodNode node, URI uri, String parentName) { - Location location = astNodeToLocation(node, uri); - if (location == null) { - return null; - } - SymbolKind symbolKind = astNodeToSymbolKind(node); - return new SymbolInformation(node.getName(), symbolKind, location, - parentName); - } - - public static SymbolInformation astNodeToSymbolInformation(Variable node, URI uri, String parentName) { - if (!(node instanceof ASTNode)) { - // DynamicVariable isn't an ASTNode - return null; - } - ASTNode astVar = (ASTNode) node; - Location location = astNodeToLocation(astVar, uri); - if (location == null) { - return null; - } - SymbolKind symbolKind = astNodeToSymbolKind(astVar); - return new SymbolInformation(node.getName(), symbolKind, location, - parentName); - } -} \ No newline at end of file diff --git a/src/main/java/net/prominic/groovyls/util/GroovyNodeToStringUtils.java b/src/main/java/net/prominic/groovyls/util/GroovyNodeToStringUtils.java index 498a5d82d..67fb62e13 100644 --- a/src/main/java/net/prominic/groovyls/util/GroovyNodeToStringUtils.java +++ b/src/main/java/net/prominic/groovyls/util/GroovyNodeToStringUtils.java @@ -20,16 +20,8 @@ package net.prominic.groovyls.util; import net.prominic.groovyls.compiler.ast.ASTContext; -import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.ConstructorNode; -import org.codehaus.groovy.ast.FieldNode; -import org.codehaus.groovy.ast.MethodNode; -import org.codehaus.groovy.ast.Parameter; -import org.codehaus.groovy.ast.Variable; - -import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; import net.prominic.groovyls.compiler.util.GroovyASTUtils; +import org.codehaus.groovy.ast.*; public class GroovyNodeToStringUtils { @@ -72,12 +64,11 @@ public static String classToString(ClassNode classNode, ASTContext astContext) { } public static String constructorToString(ConstructorNode constructorNode, ASTContext astContext) { - StringBuilder builder = new StringBuilder(); - builder.append(constructorNode.getDeclaringClass().getName()); - builder.append("("); - builder.append(parametersToString(constructorNode.getParameters(), astContext)); - builder.append(")"); - return builder.toString(); + String builder = constructorNode.getDeclaringClass().getName() + + "(" + + parametersToString(constructorNode.getParameters(), astContext) + + ")"; + return builder; } public static String methodToString(MethodNode methodNode, ASTContext astContext) { @@ -126,8 +117,7 @@ public static String parametersToString(Parameter[] params, ASTContext astContex public static String variableToString(Variable variable, ASTContext astContext) { StringBuilder builder = new StringBuilder(); - if (variable instanceof FieldNode) { - FieldNode fieldNode = (FieldNode) variable; + if (variable instanceof FieldNode fieldNode) { if (fieldNode.isPublic()) { builder.append("public "); } @@ -157,4 +147,4 @@ public static String variableToString(Variable variable, ASTContext astContext) builder.append(variable.getName()); return builder.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/prominic/groovyls/util/Positions.java b/src/main/java/net/prominic/groovyls/util/Positions.java new file mode 100644 index 000000000..bb904e9c7 --- /dev/null +++ b/src/main/java/net/prominic/groovyls/util/Positions.java @@ -0,0 +1,73 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2022 Prominic.NET, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License +// +// Author: Prominic.NET, Inc. +// No warranty of merchantability or fitness of any kind. +// Use this software at your own risk. +//////////////////////////////////////////////////////////////////////////////// +package net.prominic.groovyls.util; + +import org.eclipse.lsp4j.Position; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.Comparator; + +public class Positions { + + public static final Comparator COMPARATOR = (Position p1, Position p2) -> { + if (p1.getLine() != p2.getLine()) { + return p1.getLine() - p2.getLine(); + } + return p1.getCharacter() - p2.getCharacter(); + }; + + public static boolean valid(Position p) { + return p.getLine() >= 0 || p.getCharacter() >= 0; + } + + public static int getOffset(String string, Position position) { + int line = position.getLine(); + int character = position.getCharacter(); + int currentIndex = 0; + if (line > 0) { + BufferedReader reader = new BufferedReader(new StringReader(string)); + try { + int readLines = 0; + while (true) { + char currentChar = (char) reader.read(); + if (currentChar == -1) { + return -1; + } + currentIndex++; + if (currentChar == '\n') { + readLines++; + if (readLines == line) { + break; + } + } + } + } catch (IOException e) { + return -1; + } + try { + reader.close(); + } catch (IOException e) { + } + } + return currentIndex + character; + } +} diff --git a/src/main/java/net/prominic/groovyls/util/Ranges.java b/src/main/java/net/prominic/groovyls/util/Ranges.java new file mode 100644 index 000000000..ff93687fe --- /dev/null +++ b/src/main/java/net/prominic/groovyls/util/Ranges.java @@ -0,0 +1,92 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2022 Prominic.NET, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License +// +// Author: Prominic.NET, Inc. +// No warranty of merchantability or fitness of any kind. +// Use this software at your own risk. +//////////////////////////////////////////////////////////////////////////////// +package net.prominic.groovyls.util; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +public class Ranges { + + public static boolean contains(Range range, Position position) { + return Positions.COMPARATOR.compare(position, range.getStart()) >= 0 && Positions.COMPARATOR.compare(position, range.getEnd()) <= 0; + } + + public static boolean intersect(Range r1, Range r2) { + return contains(r1, r2.getStart()) || contains(r1, r2.getEnd()); + } + + public static String getSubstring(String string, Range range) { + return getSubstring(string, range, 0); + } + + public static String getSubstring(String string, Range range, int maxLines) { + BufferedReader reader = new BufferedReader(new StringReader(string)); + StringBuilder builder = new StringBuilder(); + Position start = range.getStart(); + Position end = range.getEnd(); + int startLine = start.getLine(); + int startChar = start.getCharacter(); + int endLine = end.getLine(); + int endChar = end.getCharacter(); + int lineCount = 1 + (endLine - startLine); + if (maxLines > 0 && lineCount > maxLines) { + endLine = startLine + maxLines - 1; + endChar = 0; + } + try { + for (int i = 0; i < startLine; i++) { + // ignore these lines + reader.readLine(); + } + for (int i = 0; i < startChar; i++) { + // ignore these characters + reader.read(); + } + int endCharStart = startChar; + int maxLineBreaks = endLine - startLine; + if (maxLineBreaks > 0) { + endCharStart = 0; + int readLines = 0; + while (readLines < maxLineBreaks) { + char character = (char) reader.read(); + if (character == '\n') { + readLines++; + } + builder.append(character); + } + } + // the remaining characters on the final line + for (int i = endCharStart; i < endChar; i++) { + builder.append((char) reader.read()); + } + } catch (IOException e) { + return null; + } + try { + reader.close(); + } catch (IOException ignored) { + } + return builder.toString(); + } +} diff --git a/src/main/java/net/prominic/groovyls/util/URIUtils.java b/src/main/java/net/prominic/groovyls/util/URIUtils.java deleted file mode 100644 index acf1a6c7c..000000000 --- a/src/main/java/net/prominic/groovyls/util/URIUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package net.prominic.groovyls.util; - -import java.net.URI; - -public final class URIUtils { - - public static URI toUri(String uriString) { - // for some reason vscode like to output garbage like file:///c%3A/Users/.. - // we cant decode here since paths with spaces will cause an error in URI.create() - var decodedUriString = uriString;//URLDecoder.decode(uriString, "UTF-8"); - - if (decodedUriString.matches("^file:///[a-z]:/.*$")) { - decodedUriString = "file:///" + decodedUriString.substring(8, 9).toUpperCase() + decodedUriString.substring(9); - } - return URI.create(decodedUriString); - } -} diff --git a/src/main/java/net/prominic/lsp/utils/Positions.java b/src/main/java/net/prominic/lsp/utils/Positions.java deleted file mode 100644 index b9716e6fc..000000000 --- a/src/main/java/net/prominic/lsp/utils/Positions.java +++ /dev/null @@ -1,72 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright 2022 Prominic.NET, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License -// -// Author: Prominic.NET, Inc. -// No warranty of merchantability or fitness of any kind. -// Use this software at your own risk. -//////////////////////////////////////////////////////////////////////////////// -package net.prominic.lsp.utils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.util.Comparator; - -import org.eclipse.lsp4j.Position; - -public class Positions { - public static final Comparator COMPARATOR = (Position p1, Position p2) -> { - if (p1.getLine() != p2.getLine()) { - return p1.getLine() - p2.getLine(); - } - return p1.getCharacter() - p2.getCharacter(); - }; - - public static boolean valid(Position p) { - return p.getLine() >= 0 || p.getCharacter() >= 0; - } - - public static int getOffset(String string, Position position) { - int line = position.getLine(); - int character = position.getCharacter(); - int currentIndex = 0; - if (line > 0) { - BufferedReader reader = new BufferedReader(new StringReader(string)); - try { - int readLines = 0; - while (true) { - char currentChar = (char) reader.read(); - if (currentChar == -1) { - return -1; - } - currentIndex++; - if (currentChar == '\n') { - readLines++; - if (readLines == line) { - break; - } - } - } - } catch (IOException e) { - return -1; - } - try { - reader.close(); - } catch (IOException e) { - } - } - return currentIndex + character; - } -} \ No newline at end of file diff --git a/src/main/java/net/prominic/lsp/utils/Ranges.java b/src/main/java/net/prominic/lsp/utils/Ranges.java deleted file mode 100644 index 69854748d..000000000 --- a/src/main/java/net/prominic/lsp/utils/Ranges.java +++ /dev/null @@ -1,92 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright 2022 Prominic.NET, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License -// -// Author: Prominic.NET, Inc. -// No warranty of merchantability or fitness of any kind. -// Use this software at your own risk. -//////////////////////////////////////////////////////////////////////////////// -package net.prominic.lsp.utils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; - -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; - -public class Ranges { - public static boolean contains(Range range, Position position) { - return Positions.COMPARATOR.compare(position, range.getStart()) >= 0 - && Positions.COMPARATOR.compare(position, range.getEnd()) <= 0; - } - - public static boolean intersect(Range r1, Range r2) { - return contains(r1, r2.getStart()) || contains(r1, r2.getEnd()); - } - - public static String getSubstring(String string, Range range) { - return getSubstring(string, range, 0); - } - - public static String getSubstring(String string, Range range, int maxLines) { - BufferedReader reader = new BufferedReader(new StringReader(string)); - StringBuilder builder = new StringBuilder(); - Position start = range.getStart(); - Position end = range.getEnd(); - int startLine = start.getLine(); - int startChar = start.getCharacter(); - int endLine = end.getLine(); - int endChar = end.getCharacter(); - int lineCount = 1 + (endLine - startLine); - if (maxLines > 0 && lineCount > maxLines) { - endLine = startLine + maxLines - 1; - endChar = 0; - } - try { - for (int i = 0; i < startLine; i++) { - // ignore these lines - reader.readLine(); - } - for (int i = 0; i < startChar; i++) { - // ignore these characters - reader.read(); - } - int endCharStart = startChar; - int maxLineBreaks = endLine - startLine; - if (maxLineBreaks > 0) { - endCharStart = 0; - int readLines = 0; - while (readLines < maxLineBreaks) { - char character = (char) reader.read(); - if (character == '\n') { - readLines++; - } - builder.append(character); - } - } - // the remaining characters on the final line - for (int i = endCharStart; i < endChar; i++) { - builder.append((char) reader.read()); - } - } catch (IOException e) { - return null; - } - try { - reader.close(); - } catch (IOException e) { - } - return builder.toString(); - } -} \ No newline at end of file