diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 91a5e98..0000000 --- a/package-lock.json +++ /dev/null @@ -1,791 +0,0 @@ -{ - "name": "iconic", - "version": "1.1.8", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@codemirror/language": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", - "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", - "dev": true, - "requires": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "@codemirror/state": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", - "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", - "dev": true, - "requires": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "@codemirror/view": { - "version": "6.38.6", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", - "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", - "dev": true, - "requires": { - "@codemirror/state": "^6.5.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", - "dev": true, - "optional": true - }, - "@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", - "dev": true, - "optional": true - }, - "@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", - "dev": true - }, - "@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", - "dev": true, - "requires": { - "@lezer/common": "^1.0.0" - } - }, - "@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", - "dev": true, - "requires": { - "@lezer/common": "^1.0.0" - } - }, - "@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/codemirror": { - "version": "5.60.8", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", - "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", - "dev": true, - "requires": { - "@types/tern": "*" - } - }, - "@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "@types/node": { - "version": "16.18.98", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.98.tgz", - "integrity": "sha512-fpiC20NvLpTLAzo3oVBKIqBGR6Fx/8oAK/SSf7G+fydnXMY1x4x9RZ6sBXhqKlCU21g2QapUsbLlhv3+a7wS+Q==", - "dev": true - }, - "@types/tern": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", - "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", - "dev": true, - "requires": { - "@types/estree": "*" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz", - "integrity": "sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.29.0", - "@typescript-eslint/type-utils": "5.29.0", - "@typescript-eslint/utils": "5.29.0", - "debug": "^4.3.4", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.2.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz", - "integrity": "sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.29.0", - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/typescript-estree": "5.29.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz", - "integrity": "sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/visitor-keys": "5.29.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz", - "integrity": "sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "5.29.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.29.0.tgz", - "integrity": "sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz", - "integrity": "sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/visitor-keys": "5.29.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.29.0.tgz", - "integrity": "sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.29.0", - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/typescript-estree": "5.29.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz", - "integrity": "sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.29.0", - "eslint-visitor-keys": "^3.3.0" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "requires": { - "fill-range": "^7.1.1" - } - }, - "builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true - }, - "crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "dev": true - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "requires": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - } - }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "obsidian": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.11.4.tgz", - "integrity": "sha512-n0KD3S+VndgaByrEtEe8NELy0ya6/s+KZ7OcxA6xOm5NN4thxKpQjo6eqEudHEvfGCeT/TYToAKJzitQ1I3XTg==", - "dev": true, - "requires": { - "@types/codemirror": "5.60.8", - "moment": "2.29.4" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true - }, - "w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } -} diff --git a/src/ColorUtils.ts b/src/ColorUtils.ts index ccc8c3b..b454a25 100644 --- a/src/ColorUtils.ts +++ b/src/ColorUtils.ts @@ -1,4 +1,5 @@ import { RGB } from 'obsidian'; +import { LRUCache } from './utils/LRUCache'; /** * 9 basic colors and their Obsidian CSS variables. @@ -180,12 +181,19 @@ const REGEX_COLOR_MIX = /color-mix\(in srgb, rgba?\((\d+), (\d+), (\d+)(?:, ([\d */ export default class ColorUtils { private static readonly convertEl = document.createElement('div'); + private static readonly rgbCache = new LRUCache(100); /** * Convert color into rgb/rgba() string. * @param color a color name, or a specific CSS color */ static toRgb(color: string | null | undefined): string { + const cacheKey = color ?? 'null'; + const cached = this.rgbCache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + let cssVar = '--icon-color'; let cssColor = RGB_FALLBACK; if (!color) { @@ -198,6 +206,7 @@ export default class ColorUtils { } else if (CSS.supports('color', color)) { cssColor = color; } else { + this.rgbCache.set(cacheKey, RGB_FALLBACK); return RGB_FALLBACK; } @@ -206,13 +215,18 @@ export default class ColorUtils { const rgbValue = this.convertEl.style.color; // Value might still be wrapped in color-mix() + let result: string; if (rgbValue.startsWith('color-mix')) { - return this.mixToRgb(rgbValue); + result = this.mixToRgb(rgbValue); } else if (rgbValue.startsWith('rgb')) { - return rgbValue; + result = rgbValue; } else { - return RGB_FALLBACK; + result = RGB_FALLBACK; } + + // Cache the result (LRU automatically handles eviction) + this.rgbCache.set(cacheKey, result); + return result; } /** diff --git a/src/IconicPlugin.ts b/src/IconicPlugin.ts index 020f650..ce33461 100644 --- a/src/IconicPlugin.ts +++ b/src/IconicPlugin.ts @@ -1,4 +1,20 @@ -import { Command, Notice, Platform, Plugin, TAbstractFile, TFile, TFolder, View, WorkspaceFloating, WorkspaceLeaf, WorkspaceRoot, getIconIds, getLanguage, normalizePath } from 'obsidian'; +import { + Command, + Notice, + Platform, + Plugin, + TAbstractFile, + TFile, + TFolder, + View, + WorkspaceFloating, + WorkspaceLeaf, + WorkspaceRoot, + getIconIds, + getLanguage, + normalizePath, + MetadataCache +} from 'obsidian'; import IconicSettingTab from 'src/IconicSettingTab'; import EMOJIS from 'src/Emojis'; import STRINGS from 'src/Strings'; @@ -333,6 +349,7 @@ export default class IconicPlugin extends Plugin { this.refreshBody(); this.registerEvent(this.app.vault.on('create', tAbstractFile => { + this.invalidateFileItemsCache(); const page = tAbstractFile instanceof TFile ? 'file' : 'folder'; // If a created file/folder triggers a new ruling, refresh icons if (this.ruleManager.triggerRulings(page, 'rename', 'move', 'modify')) { @@ -341,6 +358,7 @@ export default class IconicPlugin extends Plugin { })); this.registerEvent(this.app.vault.on('rename', (tAbstractFile, oldPath) => { + this.invalidateFileItemsCache(); const { path } = tAbstractFile; const fileIcon = this.settings.fileIcons[oldPath]; if (fileIcon) { @@ -365,9 +383,12 @@ export default class IconicPlugin extends Plugin { })); this.registerEvent(this.app.metadataCache.on('changed', tAbstractFile => { this.onFileModify(tAbstractFile); + // Invalidate tag cache when metadata changes (tags might have changed) + this.invalidateTagItemsCache(); })); this.registerEvent(this.app.vault.on('delete', (tAbstractFile) => { + this.invalidateFileItemsCache(); const { path } = tAbstractFile; if (this.settings.rememberDeletedItems === false) { delete this.settings.fileIcons[path]; @@ -633,36 +654,36 @@ export default class IconicPlugin extends Plugin { * Refresh all icon managers, or a specific group of them. */ refreshManagers(...categories: Category[]): void { - if (categories) { + if (categories.length === 0) { categories = ['app', 'tab', 'file', 'folder', 'tag', 'property', 'ribbon']; } const managers = new Set(); - if (categories?.includes('app')) { + if (categories.includes('app')) { managers.add(this.appIconManager); } - if (categories?.includes('tab')) { + if (categories.includes('tab')) { managers.add(this.tabIconManager); } - if (categories?.includes('file')) { + if (categories.includes('file')) { managers.add(this.tabIconManager); managers.add(this.fileIconManager); managers.add(this.bookmarkIconManager); managers.add(this.editorIconManager); } - if (categories?.includes('folder')) { + if (categories.includes('folder')) { managers.add(this.fileIconManager); managers.add(this.bookmarkIconManager); } - if (categories?.includes('tag')) { + if (categories.includes('tag')) { managers.add(this.tagIconManager); managers.add(this.editorIconManager); } - if (categories?.includes('property')) { + if (categories.includes('property')) { managers.add(this.propertyIconManager); managers.add(this.editorIconManager); } - if (categories?.includes('ribbon')) { + if (categories.includes('ribbon')) { managers.add(this.ribbonIconManager); } @@ -868,15 +889,54 @@ export default class IconicPlugin extends Plugin { } /** - * Get array of file definitions. + * File items cache to avoid rebuilding on every call. */ + private fileItemsCache: FileItem[] | null = null; + private fileItemsCacheVersion = 0; + private fileItemsCacheBuiltVersion = -1; + /** + * Tag items cache to avoid rebuilding on every call. + */ + private tagItemsCache: TagItem[] | null = null; + private tagItemsCacheVersion = 0; + private tagItemsCacheBuiltVersion = -1; + private tagInvalidateTimer: number | null = null; + private cachedTagIds: string[] | null = null; + getFileItems(unloading?: boolean): FileItem[] { + if (unloading) { + return this.buildFileItems(unloading); + } + + // Check if cache is still valid + if (this.fileItemsCacheBuiltVersion === this.fileItemsCacheVersion && this.fileItemsCache) { + return this.fileItemsCache; + } + + // Build and cache + const items = this.buildFileItems(unloading); + this.fileItemsCache = items; + this.fileItemsCacheBuiltVersion = this.fileItemsCacheVersion; + return items; + } + + + /** + * Invalidate file items cache (call when vault changes). + */ + invalidateFileItemsCache(): void { + this.fileItemsCacheVersion++; + } + + + private buildFileItems(unloading?: boolean): FileItem[] { const tFiles = this.app.vault.getAllLoadedFiles(); const rootFolder = tFiles.find(tFile => tFile.path === '/'); if (rootFolder) tFiles.remove(rootFolder); return tFiles.map(tFile => this.defineFileItem(tFile, tFile.path, unloading)); } + /** * Get file definition. */ @@ -1071,27 +1131,95 @@ export default class IconicPlugin extends Plugin { /** * Get array of tag definitions. + * Uses cache with event-based invalidation and version tracking. */ getTagItems(unloading?: boolean): TagItem[] { - // @ts-expect-error (Private API) - const tagHashes: string[] = Object.keys(this.app.metadataCache.getTags()) ?? []; - const tagBases = tagHashes.map(tagHash => { - return { + if (unloading) { + return this.buildTagItems(true); + } + + // Check if cache is still valid + if (this.tagItemsCacheBuiltVersion === this.tagItemsCacheVersion && this.tagItemsCache) { + return this.tagItemsCache; + } + + const metadataCache = this.app.metadataCache as MetadataCache & { + getTags(): Record; + }; + + const allTags = metadataCache.getTags?.() ?? {}; + const tagIds = Object.keys(allTags); + + const items = this.buildTagItems(false, tagIds); + + this.tagItemsCache = items; + this.tagItemsCacheBuiltVersion = this.tagItemsCacheVersion; + + return items; + } + + /** + * Invalidate tag items cache (debounced to handle rapid metadata changes). + * @param immediate If true, invalidate immediately instead of debouncing. + */ + invalidateTagItemsCache(immediate?: boolean): void { + if (this.tagInvalidateTimer !== null) { + window.clearTimeout(this.tagInvalidateTimer); + this.tagInvalidateTimer = null; + } + + const invalidate = () => { + this.tagItemsCacheVersion++; + this.cachedTagIds = null; + }; + + if (immediate) { + invalidate(); + } else { + this.tagInvalidateTimer = window.setTimeout(invalidate, 100); + } + } + + + /** + * Build tag items array from metadata cache. + */ + private buildTagItems(unloading?: boolean, tagHashes?: string[]): TagItem[] { + if (!tagHashes) { + // Use cached tag IDs if available + if (this.cachedTagIds) { + tagHashes = this.cachedTagIds; + } else { + const metadataCache = this.app.metadataCache as MetadataCache & { + getTags(): Record; + }; + const allTags = metadataCache.getTags() ?? {}; + tagHashes = Object.keys(allTags); + this.cachedTagIds = tagHashes; + } + } + + // Single-pass: combine map operations + return tagHashes.map(tagHash => { + const tagBase = { id: tagHash.replace('#', ''), name: tagHash, - } + }; + return this.defineTagItem(tagBase, unloading); }); - return tagBases.map(tagBase => this.defineTagItem(tagBase, unloading)); } + /** * Get tag definition. */ getTagItem(tagId: string, unloading?: boolean): TagItem | null { const tagHash = '#' + tagId; - // @ts-expect-error (Private API) - const tagHashes: string[] = Object.keys(this.app.metadataCache.getTags()) ?? []; - return tagHashes.includes(tagHash) + const metadataCache = this.app.metadataCache as MetadataCache & { + getTags(): Record; + }; + const allTags = metadataCache.getTags() ?? {}; + return allTags[tagHash] !== undefined ? this.defineTagItem({ id: tagId, name: tagHash, @@ -1210,6 +1338,7 @@ export default class IconicPlugin extends Plugin { * Save file icon changes to settings. */ saveFileIcon(file: FileItem, icon: string | null, color: string | null): void { + this.invalidateFileItemsCache(); const triggers: Set = new Set(); const fileBase = this.settings.fileIcons[file.id]; if (icon !== fileBase?.icon) triggers.add('icon'); @@ -1225,6 +1354,7 @@ export default class IconicPlugin extends Plugin { * @param color If undefined, leave colors unchanged */ saveFileIcons(files: FileItem[], icon: string | null | undefined, color: string | null | undefined): void { + this.invalidateFileItemsCache(); const triggers: Set = new Set(); for (const file of files) { if (icon !== undefined) file.icon = icon; @@ -1242,6 +1372,7 @@ export default class IconicPlugin extends Plugin { * Save bookmark icon changes to settings. */ saveBookmarkIcon(bmark: BookmarkItem, icon: string | null, color: string | null): void { + this.invalidateFileItemsCache(); const triggers: Set = new Set(); switch (bmark.category) { case 'file': // Fallthrough @@ -1265,6 +1396,7 @@ export default class IconicPlugin extends Plugin { * @param color If undefined, leave colors unchanged */ saveBookmarkIcons(bmarks: BookmarkItem[], icon: string | null | undefined, color: string | null | undefined): void { + this.invalidateFileItemsCache(); const triggers: Set = new Set(); for (const bmark of bmarks) { if (icon !== undefined) bmark.icon = icon; @@ -1290,6 +1422,7 @@ export default class IconicPlugin extends Plugin { * Save tag icon changes to settings. */ saveTagIcon(tag: TagItem, icon: string | null, color: string | null): void { + this.invalidateTagItemsCache(true); this.updateIconSetting(this.settings.tagIcons, tag.id, icon, color); this.saveSettings(); } @@ -1558,6 +1691,12 @@ export default class IconicPlugin extends Plugin { * @override */ onunload(): void { + // Clear any pending timers + if (this.tagInvalidateTimer !== null) { + window.clearTimeout(this.tagInvalidateTimer); + this.tagInvalidateTimer = null; + } + this.menuManager.unload(); this.ruleManager.unload(); this.appIconManager?.unload(); diff --git a/src/managers/BookmarkIconManager.ts b/src/managers/BookmarkIconManager.ts index 6dcd706..4630085 100644 --- a/src/managers/BookmarkIconManager.ts +++ b/src/managers/BookmarkIconManager.ts @@ -4,6 +4,7 @@ import { RuleItem } from 'src/managers/RuleManager'; import IconManager from 'src/managers/IconManager'; import RuleEditor from 'src/dialogs/RuleEditor'; import IconPicker from 'src/dialogs/IconPicker'; +import { Debouncer } from 'src/utils/Debouncer'; /** * Handles icons in the Bookmarks pane. @@ -12,6 +13,7 @@ export default class BookmarkIconManager extends IconManager { private containerEl: HTMLElement; private isTouchActive = false; private readonly selectionLookup = new Map(); + private debouncer = new Debouncer(); constructor(plugin: IconicPlugin) { super(plugin); @@ -22,10 +24,10 @@ export default class BookmarkIconManager extends IconManager { this.app.workspace.iterateAllLeaves(leaf => this.manageLeaf(leaf)); } })); - // Compatibility with Iconize plugin + // Compatibility with Iconize plugin (debounced to not block) if (this.plugin.isPluginEnabled('obsidian-icon-folder')) { this.plugin.registerEvent(this.app.workspace.on('active-leaf-change', () => { - this.refreshIcons(); + this.debouncer.debounce('leaf-change', () => this.refreshIcons(), 100); })); } this.app.workspace.iterateAllLeaves(leaf => this.manageLeaf(leaf)); @@ -282,6 +284,7 @@ export default class BookmarkIconManager extends IconManager { * @override */ unload(): void { + this.debouncer.cancelAll(); this.refreshIcons(true); super.unload(); } diff --git a/src/managers/EditorIconManager.ts b/src/managers/EditorIconManager.ts index 7ee1a5c..21c01c8 100644 --- a/src/managers/EditorIconManager.ts +++ b/src/managers/EditorIconManager.ts @@ -6,19 +6,27 @@ import ColorUtils from 'src/ColorUtils'; import IconManager from 'src/managers/IconManager'; import RuleEditor from 'src/dialogs/RuleEditor'; import IconPicker from 'src/dialogs/IconPicker'; +import { Debouncer } from 'src/utils/Debouncer'; /** * Handles icons in the editor window of Markdown tabs. */ export default class EditorIconManager extends IconManager { + private debouncer = new Debouncer(); + constructor(plugin: IconicPlugin) { super(plugin); - // Style hashtags in reading mode + // Style hashtags in reading mode (deferred to not block rendering) this.plugin.registerMarkdownPostProcessor(sectionEl => { - const tags = this.plugin.getTagItems(); const tagEls = sectionEl.findAll('a.tag'); - this.refreshReadingModeHashtags(tags, tagEls); + if (tagEls.length === 0) return; + + // Defer icon updates to next frame to not block markdown rendering + requestAnimationFrame(() => { + const tags = this.plugin.getTagItems(); + this.refreshReadingModeHashtags(tags, tagEls); + }); }); const manager = this; @@ -51,11 +59,18 @@ export default class EditorIconManager extends IconManager { } })); - // Initialize MarkdownViews as they open + // Initialize MarkdownViews as they open (deferred to not block content loading) this.plugin.registerEvent(this.app.workspace.on('active-leaf-change', leaf => { if (leaf?.view instanceof MarkdownView) { - this.observeViewIcons(leaf.view); - this.refreshViewIcons(leaf.view); + const view = leaf.view; + // Use longer delay to not block file content loading + this.debouncer.debounce('leaf-change', () => { + // Defer to next idle period to not block rendering + requestIdleCallback(() => { + this.observeViewIcons(view); + this.refreshViewIcons(view); + }, { timeout: 500 }); + }, 100); // Increased from 50ms } })); @@ -67,9 +82,15 @@ export default class EditorIconManager extends IconManager { } } - // If we add a new property to a file, refresh property icons - this.plugin.registerEvent(this.app.vault.on('modify', () => { - this.refreshIcons(); + // If we add a new property to a file, refresh property icons (debounced) + this.plugin.registerEvent(this.app.vault.on('modify', (file) => { + this.debouncer.debounce('vault-modify', () => { + // Only refresh if the modified file is currently open + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + if (activeView?.file?.path === file.path) { + this.refreshViewIcons(activeView); + } + }, 150); })); } @@ -226,12 +247,30 @@ export default class EditorIconManager extends IconManager { } // Set up title wrapper - const wrapperEl = headerEl.find(':scope > .iconic-title-wrapper') - ?? createDiv({ cls: 'iconic-title-wrapper' }); - const iconEl = wrapperEl.find(':scope > .iconic-icon') - ?? createDiv({ cls: 'iconic-icon' }); - wrapperEl.append(iconEl, titleEl); - headerEl.prepend(wrapperEl); + let wrapperEl = headerEl.find(':scope > .iconic-title-wrapper'); + if (!wrapperEl) { + wrapperEl = createDiv({ cls: 'iconic-title-wrapper' }); + } + + // Ensure correct structure: iconEl, then titleEl, nothing else + let iconEl = wrapperEl.find(':scope > .iconic-icon'); + if (titleEl.parentElement !== wrapperEl || !iconEl) { + wrapperEl.empty(); + iconEl = createDiv({ cls: 'iconic-icon' }); + wrapperEl.append(iconEl, titleEl); + } + + // Move any extra children out of the wrapper (like metadata-container) + for (const child of Array.from(wrapperEl.children)) { + if (child !== titleEl && child !== iconEl && child instanceof HTMLElement) { + wrapperEl.after(child); + } + } + + // Ensure wrapper is first child of header + if (headerEl.firstChild !== wrapperEl) { + headerEl.prepend(wrapperEl); + } // Re-select title if necessary if (isSelected) { @@ -245,18 +284,21 @@ export default class EditorIconManager extends IconManager { // Get file and/or rule icon const file = this.plugin.getFileItem(view.file.path); const rule = this.plugin.ruleManager.checkRuling('file', file.id) ?? file; - if (!rule.icon && !rule.color) file.iconDefault = null; + + // Hide default icon if no custom icon/color is set + const itemToShow = { ...rule }; + if (!itemToShow.icon && !itemToShow.color) itemToShow.iconDefault = null; // Refresh icon if (this.plugin.isSettingEnabled('clickableIcons')) { - this.refreshIcon(rule, iconEl, () => { + this.refreshIcon(itemToShow, iconEl, () => { IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { this.plugin.saveFileIcon(file, newIcon, newColor); this.plugin.refreshManagers('file'); }); }); } else { - this.refreshIcon(rule, iconEl); + this.refreshIcon(itemToShow, iconEl); } iconEl.addClass('iconic-icon'); @@ -519,6 +561,7 @@ export default class EditorIconManager extends IconManager { * @override */ unload(): void { + this.debouncer.cancelAll(); this.refreshIcons(true); this.stopMutationObservers(); super.unload(); diff --git a/src/managers/RuleManager.ts b/src/managers/RuleManager.ts index 2f03a83..16ec144 100644 --- a/src/managers/RuleManager.ts +++ b/src/managers/RuleManager.ts @@ -1,5 +1,6 @@ import { TFile } from 'obsidian'; import IconicPlugin, { Category, Item, FileItem, ICONS, EMOJIS, STRINGS } from 'src/IconicPlugin'; +import { LRUCache } from 'src/utils/LRUCache'; const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; @@ -279,19 +280,38 @@ export default class RuleManager { /** * Check the ruling for a given item. */ + private rulingCache = new LRUCache(1000); + private cacheVersion = 0; + checkRuling(page: Category, itemId: string, unloading?: boolean): RuleItem | null { if (unloading) return null; + + const cacheKey = `${page}:${itemId}:${this.cacheVersion}`; + const cached = this.rulingCache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + + let result: RuleItem | null = null; switch (page) { - case 'file': return this.fileRulings.get(itemId) ?? null; - case 'folder': return this.folderRulings.get(itemId) ?? null; - default: return null; + case 'file': result = this.fileRulings.get(itemId) ?? null; break; + case 'folder': result = this.folderRulings.get(itemId) ?? null; break; + default: result = null; } + + this.rulingCache.set(cacheKey, result); + return result; } /** * Update rulings for a given page, and return true if this changes any rulings. */ updateRulings(page: Category): boolean { + // Invalidate cache when rulings are updated + this.cacheVersion++; + // LRU cache automatically handles size, just clear old version entries + this.rulingCache.clear(); + const now = new Date(); // Use this timestamp to check any chronological conditions const enabledRules = this.getRules(page).filter(rule => rule.enabled); @@ -318,15 +338,27 @@ export default class RuleManager { switch (page) { case 'file': { - const files = this.plugin.getFileItems().filter(file => !file.items); + // Get all items once and separate files/folders + const allItems = this.plugin.getFileItems(); + const files: FileItem[] = []; + const existingIds = new Set(); + + // Single pass to filter and build Set + for (const item of allItems) { + if (!item.items) { + files.push(item); + existingIds.add(item.id); + } + } + // Prune file rulings (remove files that no longer exist) - const existingIds = files.map(file => file.id); for (const [fileId] of this.fileRulings) { - if (!existingIds.contains(fileId)) { + if (!existingIds.has(fileId)) { this.fileRulings.delete(fileId); anyRulingsChanged = true; } } + // Update file rulings for (const file of files) { // Judge whether a rule matches this file @@ -357,11 +389,22 @@ export default class RuleManager { break; } case 'folder': { - const folders = this.plugin.getFileItems().filter(folder => folder.items); + // Get all items once and separate files/folders + const allItems = this.plugin.getFileItems(); + const folders: FileItem[] = []; + const folderIds = new Set(); + + // Single pass to filter and build Set + for (const item of allItems) { + if (item.items) { + folders.push(item); + folderIds.add(item.id); + } + } + // Prune folder rulings (remove folders that no longer exist) - const folderIds = folders.map(folder => folder.id); for (const [folderId] of this.folderRulings) { - if (!folderIds.contains(folderId)) { + if (!folderIds.has(folderId)) { this.folderRulings.delete(folderId); anyRulingsChanged = true; } @@ -523,44 +566,67 @@ export default class RuleManager { * @param ignoreEnabled Ignore whether the rule is enabled. */ judgeFiles(rule: RuleItem, now: Date, ignoreEnabled?: true): FileItem[] { - const files = this.plugin.getFileItems().filter(file => !file.items); - const matches: FileItem[] = []; - for (const file of files) { - if (this.judgeFile(file, rule, now, ignoreEnabled)) { - matches.push(file); + const allItems = this.plugin.getFileItems(); + const matches: FileItem[] = []; + for (const item of allItems) { + if (!item.items && this.judgeFile(item, rule, now, ignoreEnabled)) { + matches.push(item); + } } + return matches; } - return matches; - } + /** * Judge how many folders match a given rule. * @param ignoreEnabled Ignore whether the rule is enabled. */ judgeFolders(rule: RuleItem, now: Date, ignoreEnabled?: true): FileItem[] { - const folders = this.plugin.getFileItems().filter(file => file.items); - const matches: FileItem[] = []; - for (const folder of folders) { - if (this.judgeFile(folder, rule, now, ignoreEnabled)) { - matches.push(folder); + const allItems = this.plugin.getFileItems(); + const matches: FileItem[] = []; + for (const item of allItems) { + if (item.items && this.judgeFile(item, rule, now, ignoreEnabled)) { + matches.push(item); + } } + return matches; } - return matches; - } + /** * Judge whether a given file matches a given rule. * @param ignoreEnabled Ignore whether the rule is enabled. */ + private metadataCache = new LRUCache(200); + private pathCache = new LRUCache>(500); + judgeFile(file: FileItem, rule: RuleItem, now: Date, ignoreEnabled?: true): boolean { if (!file.id || rule.conditions.length === 0) return false; if (!rule.enabled && !ignoreEnabled) return false; - const { basename, filename, extension, path, tree } = this.plugin.splitFilePath(file.id); + + // Cache path splitting with LRU + let pathData = this.pathCache.get(file.id); + if (pathData === undefined) { + pathData = this.plugin.splitFilePath(file.id); + this.pathCache.set(file.id, pathData); + } + const { basename, filename, extension, path, tree } = pathData; + const tAbstractFile = this.plugin.app.vault.getAbstractFileByPath(path); if (!tAbstractFile) return false; - const metadata = tAbstractFile instanceof TFile - ? this.plugin.app.metadataCache.getFileCache(tAbstractFile) - : null; + + // Cache metadata lookups with LRU + let metadata = null; + if (tAbstractFile instanceof TFile) { + const cacheKey = `${path}:${tAbstractFile.stat.mtime}`; + const cached = this.metadataCache.get(cacheKey); + if (cached !== undefined) { + metadata = cached; + } else { + metadata = this.plugin.app.metadataCache.getFileCache(tAbstractFile); + this.metadataCache.set(cacheKey, metadata); + } + } for (const condition of rule.conditions) { let isConditionMatched = false; @@ -575,7 +641,7 @@ export default class RuleManager { if (metadata?.frontmatter) { const fmProps = Object.entries(metadata.frontmatter); const fmProp = fmProps.find(([fmPropId]) => fmPropId.toLowerCase() === propId.toLowerCase()); - if (Array.isArray(fmProp)) source = fmProp[1]; + if (Array.isArray(fmProp) && fmProp.length > 1) source = fmProp[1] as any; } } else switch (condition.source) { case 'icon': { @@ -594,13 +660,13 @@ export default class RuleManager { case 'extension': source = extension; break; case 'tree': source = tree; break; case 'path': source = path; break; - case 'headings': source = metadata?.headings?.map(heading => heading.heading) ?? []; break; - case 'links': source = metadata?.links?.map(link => link.link) ?? []; break; - case 'embeds': source = metadata?.embeds?.map(embed => embed.link) ?? []; break; + case 'headings': source = metadata?.headings?.map((heading: any) => heading.heading) ?? []; break; + case 'links': source = metadata?.links?.map((link: any) => link.link) ?? []; break; + case 'embeds': source = metadata?.embeds?.map((embed: any) => embed.link) ?? []; break; case 'tags': { source = []; const propTags = metadata?.frontmatter?.tags ?? []; - const inlineTags = metadata?.tags?.map(tag => tag.tag.replace('#', '')) ?? []; + const inlineTags = metadata?.tags?.map((tag: any) => tag.tag.replace('#', '')) ?? []; for (const tag of [...propTags, ...inlineTags]) { if (!source.includes(tag)) source.push(tag); } @@ -611,10 +677,32 @@ export default class RuleManager { case 'clock': source = now.getTime(); break; } - // Prepare case-insensitive strings - const sourceLower = String.isString(source) ? source.toLowerCase() : ''; - const sourceLowers = Array.isArray(source) ? source.map(item => String(item).toLowerCase()) : []; - const valueLower = String.isString(value) ? value.toLowerCase() : ''; + // Prepare case-insensitive strings (only when needed) + let sourceLower = ''; + let sourceLowers: string[] = []; + let valueLower = ''; + + // Only compute lowercase versions if operator requires it + const needsLowerCase = operator === 'is' || operator === 'contains' || operator === 'startsWith' || + operator === 'endsWith' || operator === 'includes' || operator === 'allAre' || + operator === 'allContain' || operator === 'allStartWith' || operator === 'allEndWith' || + operator === 'anyContain' || operator === 'anyStartWith' || operator === 'anyEndWith' || + operator === 'noneContain' || operator === 'noneStartWith' || operator === 'noneEndWith' || + operator === 'iconIs' || operator === 'nameIs' || operator === 'nameContains' || + operator === 'nameStartsWith' || operator === 'nameEndsWith' || operator === 'colorIs' || + operator === 'hexIs'; + + if (needsLowerCase) { + if (String.isString(source)) { + sourceLower = source.toLowerCase(); + } + if (Array.isArray(source)) { + sourceLowers = source.map(item => String(item).toLowerCase()); + } + if (String.isString(value)) { + valueLower = value.toLowerCase(); + } + } // Check if condition is true if (operator === 'hasValue') { @@ -755,11 +843,22 @@ export default class RuleManager { /** * Remove forwardslash delimiters from regex if present. + * Cached to avoid recompiling same patterns. */ + private static regexCache = new LRUCache(50); + private static unwrapRegex(value: string): RegExp { - return value.startsWith('/') && value.endsWith('/') + const cached = this.regexCache.get(value); + if (cached !== undefined) { + return cached; + } + + const regex = value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : new RegExp(value); + + this.regexCache.set(value, regex); + return regex; } /** diff --git a/src/managers/SuggestionIconManager.ts b/src/managers/SuggestionIconManager.ts index 4613016..eb17292 100644 --- a/src/managers/SuggestionIconManager.ts +++ b/src/managers/SuggestionIconManager.ts @@ -211,20 +211,33 @@ export default class SuggestionIconManager extends IconManager { */ private refreshTagIcon(value: any, el: HTMLElement): void { const tagId = value?.tag; - if (tagId) { - el.addClass('mod-complex', 'iconic-item'); - const tag = this.plugin.getTagItem(tagId); - const iconContainerEl = el.find(':scope > .suggestion-icon') - ?? createDiv({ cls: 'suggestion-icon' }); - const iconEl = iconContainerEl.find(':scope > .suggestion-flair') - ?? iconContainerEl.createSpan({ cls: 'suggestion-flair' }); - el.prepend(iconContainerEl); - if (tag) { - tag.iconDefault = 'lucide-tag'; - if (!tag.icon && !tag.color) iconEl.addClass('iconic-invisible'); - this.refreshIcon(tag, iconEl); - } + if (!tagId) return; + + el.addClass('mod-complex', 'iconic-item'); + + const iconContainerEl = el.find(':scope > .suggestion-icon') + ?? createDiv({ cls: 'suggestion-icon' }); + const iconEl = iconContainerEl.find(':scope > .suggestion-flair') + ?? iconContainerEl.createSpan({ cls: 'suggestion-flair' }); + el.prepend(iconContainerEl); + + // Get icon directly from settings without calling getTagItem + const tagIcon = this.plugin.settings.tagIcons[tagId]; + const icon = tagIcon?.icon ?? null; + const color = tagIcon?.color ?? null; + + if (!icon && !color) { + iconEl.addClass('iconic-invisible'); } + + this.refreshIcon({ + id: tagId, + name: '#' + tagId, + category: 'tag', + iconDefault: 'lucide-tag', + icon, + color + }, iconEl); } /** diff --git a/src/managers/TabIconManager.ts b/src/managers/TabIconManager.ts index ed9f9bb..9711091 100644 --- a/src/managers/TabIconManager.ts +++ b/src/managers/TabIconManager.ts @@ -3,15 +3,38 @@ import IconicPlugin, { Category, FileItem, TabItem, STRINGS } from 'src/IconicPl import IconManager from 'src/managers/IconManager'; import RuleEditor from 'src/dialogs/RuleEditor'; import IconPicker from 'src/dialogs/IconPicker'; +import { Debouncer } from 'src/utils/Debouncer'; /** * Handles icons in workspace tab headers. */ export default class TabIconManager extends IconManager { + private debouncer = new Debouncer(); + private lastRefreshTime = 0; + private readonly MIN_REFRESH_INTERVAL = 100; // Minimum time between refreshes + constructor(plugin: IconicPlugin) { super(plugin); - this.plugin.registerEvent(this.app.workspace.on('layout-change', () => this.refreshIcons())); - this.plugin.registerEvent(this.app.workspace.on('active-leaf-change', () => this.refreshIcons())); + + // Debounce layout and leaf changes to avoid excessive refreshes + // Use longer delays and check if refresh is actually needed + this.plugin.registerEvent(this.app.workspace.on('layout-change', () => { + this.debouncer.debounce('layout-change', () => { + // Only refresh if enough time has passed + const now = Date.now(); + if (now - this.lastRefreshTime >= this.MIN_REFRESH_INTERVAL) { + this.lastRefreshTime = now; + this.refreshIcons(); + } + }, 150); // Increased from 100ms + })); + + this.plugin.registerEvent(this.app.workspace.on('active-leaf-change', () => { + // For leaf changes, only update the active tab icon, not all tabs + this.debouncer.debounce('leaf-change', () => { + this.refreshActiveTabOnly(); + }, 50); + })); // Refresh icons in tab selector dropdown ▼ const tabListEl = activeDocument.body.find('.mod-root .workspace-tab-header-tab-list > .clickable-icon'); @@ -44,101 +67,126 @@ export default class TabIconManager extends IconManager { const tabs = this.plugin.getTabItems(unloading); for (const tab of tabs) { - const tabEl = tab.tabEl; - const iconEl = tab.iconEl; - if (!tabEl || !iconEl || tab.id === 'webviewer') continue; + this.refreshSingleTab(tab, unloading); + } + } + + /** + * Refresh only the active tab icon (faster than refreshing all). + */ + private refreshActiveTabOnly(): void { + const activeLeaf = this.app.workspace.activeLeaf; + if (!activeLeaf) return; - // Check for an icon ruling - const rule = tab.category === 'file' - ? this.plugin.ruleManager.checkRuling('file', tab.id, unloading) ?? tab - : tab; + const tabs = this.plugin.getTabItems(); + const activeTab = tabs.find(tab => tab.isActive); + if (activeTab) { + this.refreshSingleTab(activeTab, false); + } + } - if (tab.isRoot && this.plugin.isSettingEnabled('clickableIcons')) { - if (tab.category === 'file') { - const file = this.plugin.getFileItem(tab.id); - this.refreshIcon(rule, iconEl, event => { - IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { - this.plugin.saveFileIcon(file, newIcon, newColor); - this.plugin.refreshManagers('file'); - }); - event.stopPropagation(); - }); - } else { - this.refreshIcon(rule, iconEl, event => { - IconPicker.openSingle(this.plugin, tab, (newIcon, newColor) => { - this.plugin.saveTabIcon(tab, newIcon, newColor); - this.plugin.refreshManagers('tab'); - }); - event.stopPropagation(); + /** + * Refresh a single tab icon. + */ + private refreshSingleTab(tab: TabItem, unloading?: boolean): void { + const tabEl = tab.tabEl; + const iconEl = tab.iconEl; + if (!tabEl || !iconEl || tab.id === 'webviewer') return; + + // Check for an icon ruling + const rule = tab.category === 'file' + ? this.plugin.ruleManager.checkRuling('file', tab.id, unloading) ?? tab + : tab; + + if (tab.isRoot && this.plugin.isSettingEnabled('clickableIcons')) { + if (tab.category === 'file') { + const file = this.plugin.getFileItem(tab.id); + this.refreshIcon(rule, iconEl, event => { + IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { + this.plugin.saveFileIcon(file, newIcon, newColor); + this.plugin.refreshManagers('file'); }); - } + event.stopPropagation(); + }); } else { - this.refreshIcon(rule, iconEl); + this.refreshIcon(rule, iconEl, event => { + IconPicker.openSingle(this.plugin, tab, (newIcon, newColor) => { + this.plugin.saveTabIcon(tab, newIcon, newColor); + this.plugin.refreshManagers('tab'); + }); + event.stopPropagation(); + }); } + } else { + this.refreshIcon(rule, iconEl); + } - // Update ghost icon when dragging - this.setEventListener(tabEl, 'dragstart', () => { - if (rule.icon || rule.iconDefault) { - const ghostEl = tabEl.doc.body.find(':scope > .drag-ghost > .drag-ghost-icon'); - if (ghostEl) { - this.refreshIcon({ icon: rule.icon ?? rule.iconDefault, color: rule.color }, ghostEl); - } + // Update ghost icon when dragging + this.setEventListener(tabEl, 'dragstart', () => { + if (rule.icon || rule.iconDefault) { + const ghostEl = tabEl.doc.body.find(':scope > .drag-ghost > .drag-ghost-icon'); + if (ghostEl) { + this.refreshIcon({ icon: rule.icon ?? rule.iconDefault, color: rule.color }, ghostEl); } - }); - - // Skip menu listener if tab is handled by workspace.on('file-menu') - if (!this.plugin.settings.showMenuActions || tab.category === 'file' && (tab.isActive || tab.isStacked)) { - this.stopEventListener(tabEl, 'contextmenu'); - } else { - this.setEventListener(tabEl, 'contextmenu', () => this.onContextMenu(tab.id, tab.category)); } + }); - // Refresh when tab is pinned/unpinned - const statusEl = tabEl.find(':scope > .workspace-tab-header-inner > .workspace-tab-header-status-container'); - this.setMutationObserver(statusEl, { childList: true }, mutation => { - for (const addedNode of mutation.addedNodes) { - if (addedNode instanceof HTMLElement && addedNode.hasClass('mod-pinned')) { - this.refreshIcons(); - return; - } + // Skip menu listener if tab is handled by workspace.on('file-menu') + if (!this.plugin.settings.showMenuActions || tab.category === 'file' && (tab.isActive || tab.isStacked)) { + this.stopEventListener(tabEl, 'contextmenu'); + } else { + this.setEventListener(tabEl, 'contextmenu', () => this.onContextMenu(tab.id, tab.category)); + } + + // Refresh when tab is pinned/unpinned (throttled) + const statusEl = tabEl.find(':scope > .workspace-tab-header-inner > .workspace-tab-header-status-container'); + this.setMutationObserver(statusEl, { childList: true }, mutation => { + for (const addedNode of mutation.addedNodes) { + if (addedNode instanceof HTMLElement && addedNode.hasClass('mod-pinned')) { + this.debouncer.debounce('pin-change', () => this.refreshIcons(), 100); + return; } - for (const removedNode of mutation.removedNodes) { - if (removedNode instanceof HTMLElement && removedNode.hasClass('mod-pinned')) { - this.refreshIcons(); - return; - } + } + for (const removedNode of mutation.removedNodes) { + if (removedNode instanceof HTMLElement && removedNode.hasClass('mod-pinned')) { + this.debouncer.debounce('pin-change', () => this.refreshIcons(), 100); + return; } + } + }); + + // Update mobile sidebars (throttled) + if (Platform.isMobile) { + // @ts-expect-error (Private API) + this.setEventListener(this.app.workspace.leftSplit.activeTabSelectEl, 'change', () => { + this.debouncer.debounce('mobile-left', () => this.refreshIcons(), 100); + }); + // @ts-expect-error (Private API) + this.setEventListener(this.app.workspace.rightSplit.activeTabSelectEl, 'change', () => { + this.debouncer.debounce('mobile-right', () => this.refreshIcons(), 100); }); - // Update mobile sidebars - if (Platform.isMobile) { + // @ts-expect-error (Private API) + if (this.app.workspace.leftSplit.activeTabIconEl === iconEl) { // @ts-expect-error (Private API) - this.setEventListener(this.app.workspace.leftSplit.activeTabSelectEl, 'change', () => this.refreshIcons()); + const leftActiveTabEl = this.app.workspace.leftSplit.activeTabHeaderEl; + if (this.plugin.settings.showMenuActions) { + this.setEventListener(leftActiveTabEl, 'contextmenu', () => { + this.onContextMenu(tab.id, tab.category); + }); + } else { + this.stopEventListener(leftActiveTabEl, 'contextmenu'); + } // @ts-expect-error (Private API) - this.setEventListener(this.app.workspace.rightSplit.activeTabSelectEl, 'change', () => this.refreshIcons()); - + } else if (this.app.workspace.rightSplit.activeTabIconEl === iconEl) { // @ts-expect-error (Private API) - if (this.app.workspace.leftSplit.activeTabIconEl === iconEl) { - // @ts-expect-error (Private API) - const leftActiveTabEl = this.app.workspace.leftSplit.activeTabHeaderEl; - if (this.plugin.settings.showMenuActions) { - this.setEventListener(leftActiveTabEl, 'contextmenu', () => { - this.onContextMenu(tab.id, tab.category); - }); - } else { - this.stopEventListener(leftActiveTabEl, 'contextmenu'); - } - // @ts-expect-error (Private API) - } else if (this.app.workspace.rightSplit.activeTabIconEl === iconEl) { - // @ts-expect-error (Private API) - const rightActiveTabEl = this.app.workspace.rightSplit.activeTabHeaderEl; - if (this.plugin.settings.showMenuActions) { - this.setEventListener(rightActiveTabEl, 'contextmenu', () => { - this.onContextMenu(tab.id, tab.category); - }); - } else { - this.stopEventListener(rightActiveTabEl, 'contextmenu'); - } + const rightActiveTabEl = this.app.workspace.rightSplit.activeTabHeaderEl; + if (this.plugin.settings.showMenuActions) { + this.setEventListener(rightActiveTabEl, 'contextmenu', () => { + this.onContextMenu(tab.id, tab.category); + }); + } else { + this.stopEventListener(rightActiveTabEl, 'contextmenu'); } } } @@ -242,6 +290,7 @@ export default class TabIconManager extends IconManager { * @override */ unload(): void { + this.debouncer.cancelAll(); this.refreshIcons(true); super.unload(); } diff --git a/src/utils/Debouncer.ts b/src/utils/Debouncer.ts new file mode 100644 index 0000000..1230ab1 --- /dev/null +++ b/src/utils/Debouncer.ts @@ -0,0 +1,41 @@ +/** + * Debouncer utility for delaying function execution. + * Properly handles cleanup and cancellation. + */ +export class Debouncer { + private timeouts: Map = new Map(); + + /** + * Debounce a function call. + * @param key Unique identifier for this debounced operation + * @param fn Function to execute + * @param delay Delay in milliseconds + */ + debounce(key: string, fn: () => void, delay: number): void { + this.cancel(key); + const timeout = window.setTimeout(() => { + this.timeouts.delete(key); + fn(); + }, delay); + this.timeouts.set(key, timeout); + } + + /** + * Cancel a pending debounced operation. + */ + cancel(key: string): void { + const timeout = this.timeouts.get(key); + if (timeout !== undefined) { + window.clearTimeout(timeout); + this.timeouts.delete(key); + } + } + + /** + * Cancel all pending operations. + */ + cancelAll(): void { + this.timeouts.forEach(timeout => window.clearTimeout(timeout)); + this.timeouts.clear(); + } +} diff --git a/src/utils/LRUCache.ts b/src/utils/LRUCache.ts new file mode 100644 index 0000000..227f2e9 --- /dev/null +++ b/src/utils/LRUCache.ts @@ -0,0 +1,51 @@ +/** + * Least Recently Used (LRU) Cache implementation. + * Automatically evicts least recently used items when capacity is reached. + */ +export class LRUCache { + private cache: Map; + private readonly capacity: number; + + constructor(capacity: number) { + this.capacity = capacity; + this.cache = new Map(); + } + + get(key: K): V | undefined { + if (!this.cache.has(key)) { + return undefined; + } + // Move to end (most recently used) + const value = this.cache.get(key)!; + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + + set(key: K, value: V): void { + // Remove if exists to update position + if (this.cache.has(key)) { + this.cache.delete(key); + } + // Add to end (most recently used) + this.cache.set(key, value); + + // Evict least recently used if over capacity + if (this.cache.size > this.capacity) { + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } + } + + has(key: K): boolean { + return this.cache.has(key); + } + + clear(): void { + this.cache.clear(); + } + + get size(): number { + return this.cache.size; + } +}