From 084ce50962f4a20073029fc98c88fed52cb345ec Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Tue, 5 Dec 2023 16:49:44 -0500 Subject: [PATCH 01/14] got a really basic worker functional so I can work on this down the road (if it stops working as I add things it needs to be addressed) --- esbuild.config.mjs | 2 + package-lock.json | 149 +++++++++++++++------------- package.json | 1 + src/main.ts | 9 ++ src/rules-runner/myworker.worker.ts | 10 ++ 5 files changed, 104 insertions(+), 67 deletions(-) create mode 100644 src/rules-runner/myworker.worker.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index bb77bbf5..4a80ac05 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -2,6 +2,7 @@ import esbuild from 'esbuild'; import process from 'process'; import builtins from 'builtin-modules'; import importGlobPlugin from 'esbuild-plugin-import-glob'; +import inlineWorkerPlugin from 'esbuild-plugin-inline-worker'; import {replace} from 'esbuild-plugin-replace'; const banner = @@ -73,6 +74,7 @@ const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) { entryPoints: [entryPoint], plugins: [ importGlobPlugin.default(), + inlineWorkerPlugin(), ...extraPlugins, ], bundle: true, diff --git a/package-lock.json b/package-lock.json index 5a60291f..bf9388de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@popperjs/core": "^2.11.6", "async-lock": "^1.4.1", "diff-match-patch": "^1.0.5", + "esbuild-plugin-inline-worker": "^0.1.1", "loglevel": "^1.9.1", "mdast-util-from-markdown": "^2.0.0", "mdast-util-frontmatter": "^2.0.1", @@ -1912,7 +1913,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -1928,7 +1928,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -1944,7 +1943,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -1960,7 +1958,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -1976,7 +1973,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1992,7 +1988,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -2008,7 +2003,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -2024,7 +2018,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -2040,7 +2033,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2056,7 +2048,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2072,7 +2063,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2088,7 +2078,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2104,7 +2093,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2120,7 +2108,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2136,7 +2123,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2152,7 +2138,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2168,7 +2153,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2184,7 +2168,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -2200,7 +2183,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -2216,7 +2198,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -2232,7 +2213,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -2248,7 +2228,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -2264,7 +2243,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -4897,6 +4875,11 @@ "node": ">= 12" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5442,7 +5425,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -5485,6 +5467,15 @@ "fast-glob": "^3.2.5" } }, + "node_modules/esbuild-plugin-inline-worker": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/esbuild-plugin-inline-worker/-/esbuild-plugin-inline-worker-0.1.1.tgz", + "integrity": "sha512-VmFqsQKxUlbM51C1y5bRiMeyc1x2yTdMXhKB6S//++g9aCBg8TfGsbKxl5ZDkCGquqLY+RmEk93TBNd0i35dPA==", + "dependencies": { + "esbuild": "latest", + "find-cache-dir": "^3.3.1" + } + }, "node_modules/esbuild-plugin-replace": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esbuild-plugin-replace/-/esbuild-plugin-replace-1.4.0.tgz", @@ -6318,11 +6309,40 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -8639,7 +8659,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -9694,7 +9713,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -9706,7 +9724,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -9721,7 +9738,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -9760,7 +9776,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -9838,7 +9853,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -10829,7 +10843,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -13107,161 +13120,138 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "dev": true, "optional": true }, "@esbuild/android-arm": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "dev": true, "optional": true }, "@esbuild/android-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "dev": true, "optional": true }, "@esbuild/android-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "dev": true, "optional": true }, "@esbuild/darwin-arm64": { "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==", - "dev": true, "optional": true }, "@esbuild/darwin-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "dev": true, "optional": true }, "@esbuild/freebsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "dev": true, "optional": true }, "@esbuild/linux-arm": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "dev": true, "optional": true }, "@esbuild/linux-arm64": { "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==", - "dev": true, "optional": true }, "@esbuild/linux-ia32": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "dev": true, "optional": true }, "@esbuild/linux-loong64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "dev": true, "optional": true }, "@esbuild/linux-mips64el": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "dev": true, "optional": true }, "@esbuild/linux-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "dev": true, "optional": true }, "@esbuild/linux-riscv64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "dev": true, "optional": true }, "@esbuild/linux-s390x": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "dev": true, "optional": true }, "@esbuild/linux-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "dev": true, "optional": true }, "@esbuild/netbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "dev": true, "optional": true }, "@esbuild/openbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "dev": true, "optional": true }, "@esbuild/sunos-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "dev": true, "optional": true }, "@esbuild/win32-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "dev": true, "optional": true }, "@esbuild/win32-ia32": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "dev": true, "optional": true }, "@esbuild/win32-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "dev": true, "optional": true }, "@eslint-community/eslint-utils": { @@ -15199,6 +15189,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -15584,7 +15579,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, "requires": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", @@ -15620,6 +15614,15 @@ "fast-glob": "^3.2.5" } }, + "esbuild-plugin-inline-worker": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/esbuild-plugin-inline-worker/-/esbuild-plugin-inline-worker-0.1.1.tgz", + "integrity": "sha512-VmFqsQKxUlbM51C1y5bRiMeyc1x2yTdMXhKB6S//++g9aCBg8TfGsbKxl5ZDkCGquqLY+RmEk93TBNd0i35dPA==", + "requires": { + "esbuild": "latest", + "find-cache-dir": "^3.3.1" + } + }, "esbuild-plugin-replace": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esbuild-plugin-replace/-/esbuild-plugin-replace-1.4.0.tgz", @@ -16199,11 +16202,30 @@ "to-regex-range": "^5.0.1" } }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + } + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -17899,7 +17921,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "requires": { "p-locate": "^4.1.0" } @@ -18604,7 +18625,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "requires": { "p-limit": "^2.2.0" }, @@ -18613,7 +18633,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -18623,8 +18642,7 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "parent-module": { "version": "1.0.1", @@ -18650,8 +18668,7 @@ "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" }, "path-is-absolute": { "version": "1.0.1", @@ -18705,7 +18722,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "requires": { "find-up": "^4.0.0" } @@ -19323,8 +19339,7 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "shebang-command": { "version": "2.0.0", diff --git a/package.json b/package.json index 359b21ff..04d15236 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@popperjs/core": "^2.11.6", "async-lock": "^1.4.1", "diff-match-patch": "^1.0.5", + "esbuild-plugin-inline-worker": "^0.1.1", "loglevel": "^1.9.1", "mdast-util-from-markdown": "^2.0.0", "mdast-util-frontmatter": "^2.0.1", diff --git a/src/main.ts b/src/main.ts index 14d69b8c..6e9778ce 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,6 +20,7 @@ import {warn} from 'loglevel'; import {CustomAutoCorrectContent} from './ui/linter-components/auto-correct-files-picker-option'; import {ChangeSpec} from '@codemirror/state'; import {downloadMisspellings, readInMisspellingsFile} from './utils/auto-correct-misspellings'; +import MyWorker from './rules-runner/myworker.worker'; // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43 const langToMomentLocale = { @@ -92,6 +93,14 @@ export default class LinterPlugin extends Plugin { addIcon(svg.id, svg.source); } + const worker = new MyWorker(); + + worker.postMessage(`Hello`); + worker.onmessage = (event: any) => { + console.log(`Main thread received message: ${event.data}`); + }; + + await this.loadSettings(); this.addCommands(); diff --git a/src/rules-runner/myworker.worker.ts b/src/rules-runner/myworker.worker.ts new file mode 100644 index 00000000..7cf7d81b --- /dev/null +++ b/src/rules-runner/myworker.worker.ts @@ -0,0 +1,10 @@ +// here is a worker to sent data back and forth + +// export default () => { + +// } + +onmessage = (event) => { + console.log(`Worker received message: ${event.data}`); + postMessage('Hello from worker!'); +}; From c2554e74163eddaf2be7337b1e047b36f4bc01e8 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Wed, 6 Dec 2023 07:47:11 -0500 Subject: [PATCH 02/14] got esbuild building, but it fails to run --- esbuild.config.mjs | 12 ++++++++---- src/rules-runner/myworker.worker.ts | 4 +++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 4a80ac05..a95b45da 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -66,6 +66,10 @@ const unusedCodeForProduction = [replace({ delimiters: ['', ''], })]; +const externalPackages = [ + 'obsidian', + ...builtins]; + const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) { return { banner: { @@ -74,13 +78,13 @@ const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) { entryPoints: [entryPoint], plugins: [ importGlobPlugin.default(), - inlineWorkerPlugin(), + inlineWorkerPlugin({ + external: externalPackages, + }), ...extraPlugins, ], bundle: true, - external: [ - 'obsidian', - ...builtins], + external: externalPackages, format: 'cjs', target: 'es2020', sourcemap: prod ? false : 'inline', diff --git a/src/rules-runner/myworker.worker.ts b/src/rules-runner/myworker.worker.ts index 7cf7d81b..eda51112 100644 --- a/src/rules-runner/myworker.worker.ts +++ b/src/rules-runner/myworker.worker.ts @@ -1,10 +1,12 @@ // here is a worker to sent data back and forth +import {moment} from 'obsidian'; + // export default () => { // } onmessage = (event) => { - console.log(`Worker received message: ${event.data}`); + console.log(`${moment().format('MM/DD/YYYY')}Worker received message: ${event.data}`); postMessage('Hello from worker!'); }; From 96428c384aa076eeb2eb498509831a48ddd61b38 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Wed, 6 Dec 2023 22:43:59 -0500 Subject: [PATCH 03/14] got things building, but need to work in other logic around managing the linting of file content --- esbuild.config.mjs | 1 + src/main.ts | 22 ++++++--- src/rules-runner/myworker.worker.ts | 12 ----- src/rules-runner/rules-runner.worker.ts | 8 ++++ src/typings/worker.ts | 59 +++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 18 deletions(-) delete mode 100644 src/rules-runner/myworker.worker.ts create mode 100644 src/rules-runner/rules-runner.worker.ts create mode 100644 src/typings/worker.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index a95b45da..dc45311f 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -80,6 +80,7 @@ const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) { importGlobPlugin.default(), inlineWorkerPlugin({ external: externalPackages, + format: 'cjs', }), ...extraPlugins, ], diff --git a/src/main.ts b/src/main.ts index 6e9778ce..81ede5a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,7 +20,9 @@ import {warn} from 'loglevel'; import {CustomAutoCorrectContent} from './ui/linter-components/auto-correct-files-picker-option'; import {ChangeSpec} from '@codemirror/state'; import {downloadMisspellings, readInMisspellingsFile} from './utils/auto-correct-misspellings'; -import MyWorker from './rules-runner/myworker.worker'; +// @ts-ignore because it does not have the expected default export, but it does not need one +import MyWorker from './rules-runner/rules-runner.worker'; +import {LinterWorker, WorkerArgs, WorkerResponseMessage} from './typings/worker'; // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43 const langToMomentLocale = { @@ -93,16 +95,24 @@ export default class LinterPlugin extends Plugin { addIcon(svg.id, svg.source); } - const worker = new MyWorker(); + await this.loadSettings(); + + const worker = new MyWorker() as LinterWorker; - worker.postMessage(`Hello`); - worker.onmessage = (event: any) => { + worker.postMessage({ + oldText: 'text', + fileInfo: { + name: 'file name', + createdAtFormatted: 'created', + modifiedAtFormatted: 'updated', + }, + settings: this.settings, + }); + worker.onmessage = (event: WorkerResponseMessage) => { console.log(`Main thread received message: ${event.data}`); }; - await this.loadSettings(); - this.addCommands(); this.registerEventsAndSaveCallback(); diff --git a/src/rules-runner/myworker.worker.ts b/src/rules-runner/myworker.worker.ts deleted file mode 100644 index eda51112..00000000 --- a/src/rules-runner/myworker.worker.ts +++ /dev/null @@ -1,12 +0,0 @@ -// here is a worker to sent data back and forth - -import {moment} from 'obsidian'; - -// export default () => { - -// } - -onmessage = (event) => { - console.log(`${moment().format('MM/DD/YYYY')}Worker received message: ${event.data}`); - postMessage('Hello from worker!'); -}; diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts new file mode 100644 index 00000000..bd02cd1f --- /dev/null +++ b/src/rules-runner/rules-runner.worker.ts @@ -0,0 +1,8 @@ +// here is a worker to sent data back and forth + +import {WorkerMessage as WorkerStartMessage} from '../typings/worker'; + +onmessage = (event: WorkerStartMessage) => { + console.log(`Worker received message: ${event.data.oldText}`); + postMessage('Hello from worker!'); +}; diff --git a/src/typings/worker.ts b/src/typings/worker.ts new file mode 100644 index 00000000..0f8cc222 --- /dev/null +++ b/src/typings/worker.ts @@ -0,0 +1,59 @@ +import {LinterSettings} from 'src/settings-data'; + +export interface TFile { + /** + * @public + */ + stat: { + /** + * Time of creation, represented as a unix timestamp, in milliseconds. + * @public + */ + ctime: number; + /** + * Time of last modification, represented as a unix timestamp, in milliseconds. + * @public + */ + mtime: number; + /** + * Size on disk, as bytes. + * @public + */ + size: number; + }; + /** + * @public + */ + basename: string; + /** + * @public + */ + extension: string; +} + +export type WorkerArgs = { + oldText: string, + fileInfo: { + name: string, + createdAtFormatted: string, + modifiedAtFormatted: string, + }, + settings: LinterSettings, +} + +export type WorkerMessage = { + data: WorkerArgs; +} + +export type WorkerResponse = { + newText: string, +} + +export type WorkerResponseMessage = { + data: WorkerResponse +} + +export interface LinterWorker { + postMessage: (data: WorkerArgs) => void; + onmessage: (data: WorkerResponseMessage) => void; +} From 2d445824817f91e93c6d35202ede7dede0985867 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Thu, 7 Dec 2023 08:02:03 -0500 Subject: [PATCH 04/14] got the worker to build and run in Obsidian --- esbuild.config.mjs | 12 + src/main.ts | 37 ++-- src/rule-runner copy/file-lint-manager.ts | 128 +++++++++++ src/rule-runner copy/rule-runner.worker.ts | 13 ++ src/rule-runner copy/rules-runner.ts | 185 ++++++++++++++++ src/rules-runner/file-lint-manager.ts | 129 +++++++++++ src/rules-runner/rules-runner.ts | 243 +++++++++++++++++++++ src/rules-runner/rules-runner.worker.ts | 23 +- src/typings/worker.ts | 16 ++ 9 files changed, 766 insertions(+), 20 deletions(-) create mode 100644 src/rule-runner copy/file-lint-manager.ts create mode 100644 src/rule-runner copy/rule-runner.worker.ts create mode 100644 src/rule-runner copy/rules-runner.ts create mode 100644 src/rules-runner/file-lint-manager.ts create mode 100644 src/rules-runner/rules-runner.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index dc45311f..1d1bfc1b 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -65,6 +65,17 @@ const unusedCodeForProduction = [replace({ }, delimiters: ['', ''], })]; +const webWorkerIgnores = [replace({ + values: { + // update usage of moment from obsidian to the node implementation of moment we have + 'import {moment} from \'obsidian\';': '', + // remove the use of obsidian in the options to allow for docs.js to run + 'import {Setting} from \'obsidian\';': '', + // remove the use of obsidian in settings helper to allow for docs.js to run + 'import {Component, MarkdownRenderer} from \'obsidian\';': '', + }, + delimiters: ['', ''], +})]; const externalPackages = [ 'obsidian', @@ -81,6 +92,7 @@ const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) { inlineWorkerPlugin({ external: externalPackages, format: 'cjs', + plugins: [...webWorkerIgnores], }), ...extraPlugins, ], diff --git a/src/main.ts b/src/main.ts index 81ede5a9..bd8392f6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,8 +21,9 @@ import {CustomAutoCorrectContent} from './ui/linter-components/auto-correct-file import {ChangeSpec} from '@codemirror/state'; import {downloadMisspellings, readInMisspellingsFile} from './utils/auto-correct-misspellings'; // @ts-ignore because it does not have the expected default export, but it does not need one -import MyWorker from './rules-runner/rules-runner.worker'; -import {LinterWorker, WorkerArgs, WorkerResponseMessage} from './typings/worker'; +// import MyWorker from './rules-runner/rules-runner.worker'; +// import {LinterWorker, WorkerArgs, WorkerResponseMessage} from './typings/worker'; +import {FileLintManager} from './rules-runner/file-lint-manager'; // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43 const langToMomentLocale = { @@ -81,6 +82,7 @@ export default class LinterPlugin extends Plugin { private activeFileChangeDebouncer: Map = new Map(); private defaultAutoCorrectMisspellings: Map = new Map(); private hasLoadedMisspellingFiles = false; + private lintFileManager: FileLintManager = undefined; async onload() { sortRules(); @@ -97,20 +99,22 @@ export default class LinterPlugin extends Plugin { await this.loadSettings(); - const worker = new MyWorker() as LinterWorker; + this.lintFileManager = new FileLintManager(1, this.momentLocale, this.settings, this.app.vault); - worker.postMessage({ - oldText: 'text', - fileInfo: { - name: 'file name', - createdAtFormatted: 'created', - modifiedAtFormatted: 'updated', - }, - settings: this.settings, - }); - worker.onmessage = (event: WorkerResponseMessage) => { - console.log(`Main thread received message: ${event.data}`); - }; + // const worker = new MyWorker() as LinterWorker; + + // worker.postMessage({ + // oldText: 'text', + // fileInfo: { + // name: 'file name', + // createdAtFormatted: 'created', + // modifiedAtFormatted: 'updated', + // }, + // settings: this.settings, + // }); + // worker.onmessage = (event: WorkerResponseMessage) => { + // console.log(`Main thread received message: ${event.data}`); + // }; this.addCommands(); @@ -605,7 +609,8 @@ export default class LinterPlugin extends Plugin { const oldText = editor.getValue(); let newText: string; try { - newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); + // newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); + this.lintFileManager.lintFile(file); } catch (error) { this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); return; diff --git a/src/rule-runner copy/file-lint-manager.ts b/src/rule-runner copy/file-lint-manager.ts new file mode 100644 index 00000000..46bb2c00 --- /dev/null +++ b/src/rule-runner copy/file-lint-manager.ts @@ -0,0 +1,128 @@ +/** Makes sure that files are linted and that they are handled appropriately in an asynchronous manner. */ + +// @ts-ignore because this is a web worker and it does not play well with Typescript checking +import Worker from './rule-runner.worker'; +import {TFile, Vault} from 'obsidian'; +import {createRunLinterRulesOptions} from '../rules-runner'; +import {LinterSettings} from 'src/rules'; + +/** Callback when a file is resolved. */ +// type FileCallback = (p: any) => void; + +/** Multi-threaded file linter which debounces rapid file requests automatically. */ +export class FileLintManager { + /* Background workers which do the actual file parsing. */ + workers: Worker[]; + /** Tracks which workers are actively parsing a file, to make sure we properly delegate results. */ + busy: boolean[]; + + /** List of files which have been queued for to be linted */ + lintQueue: TFile[]; + /** Fast-access set which holds the list of files queued to be linted; used for debouncing. */ + reloadSet: Set; + /** Paths -> promises for file reloads which have not yet been queued. */ + // callbacks: Map; + + public constructor(public numWorkers: number, public momentLocale: string, private settings: LinterSettings, private vault: Vault) { + this.workers = []; + this.busy = []; + + this.lintQueue = []; + this.reloadSet = new Set(); + // this.callbacks = new Map(); + + for (let index = 0; index < numWorkers; index++) { + // eslint-disable-next-line new-cap + const worker = Worker(); + worker.onmessage = (data: string) => { + console.log(data); + this.busy[index] = false; + }; + // worker.postMessage('first message'); + this.workers.push(worker); + // this.register(() => worker.terminate()); + this.busy.push(false); + } + } + + public lintFile(file: TFile): void { + // const promise: Promise = new Promise((resolve, reject) => { + // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); + // else this.callbacks.set(file.path, [[resolve, reject]]); + // }); + + // Immediately run this task if there are available workers; otherwise, add it to the queue. + const workerId = this.nextAvailableWorker(); + if (workerId !== undefined) { + this.send(file, workerId); + } else { + this.lintQueue.push(file); + } + + // return promise; + } + + /** + * Queue the given file for reloading. Multiple reload requests for the same file in a short time period will be de-bounced + * and all be resolved by a single actual file reload. + */ + // public reload(file: TFile): Promise { + // const promise: Promise = new Promise((resolve, reject) => { + // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); + // else this.callbacks.set(file.path, [[resolve, reject]]); + // }); + + // // De-bounce repeated requests for the same file. + // if (this.reloadSet.has(file.path)) return promise; + // this.reloadSet.add(file.path); + + // // Immediately run this task if there are available workers; otherwise, add it to the queue. + // const workerId = this.nextAvailableWorker(); + // if (workerId !== undefined) { + // this.send(file, workerId); + // } else { + // this.reloadQueue.push(file); + // } + + // return promise; + // } + + /** Finish the parsing of a file, potentially queueing a new file. */ + // private finish(path: string, data: any, index: number) { + // // Cache the callbacks before we do book-keeping. + // const calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); + + // // Book-keeping to clear metadata & allow the file to be re-loaded again. + // this.reloadSet.delete(path); + // this.callbacks.delete(path); + + // // Notify the queue this file is available for new work. + // this.busy[index] = false; + + // // Queue a new job onto this worker. + // const job = this.reloadQueue.shift(); + // if (job !== undefined) this.send(job, index); + + // // Resolve promises to let users know this file has finished. + // if ('$error' in data) { + // for (const [_, reject] of calls) reject(data['$error']); + // } else { + // for (const [callback, _] of calls) callback(data); + // } + // } + + // /** Send a new task to the given worker ID. */ + private send(file: TFile, workerId: number) { + this.busy[workerId] = true; + this.vault.cachedRead(file).then((oldText: string) => { + const lintRunnerSettings = createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings); + this.workers[workerId].postMessage(lintRunnerSettings); + }); + } + + // /** Find the next available, non-busy worker; return undefined if all workers are busy. */ + private nextAvailableWorker(): number | undefined { + const index = this.busy.indexOf(false); + return index == -1 ? undefined : index; + } +} diff --git a/src/rule-runner copy/rule-runner.worker.ts b/src/rule-runner copy/rule-runner.worker.ts new file mode 100644 index 00000000..eacf55b9 --- /dev/null +++ b/src/rule-runner copy/rule-runner.worker.ts @@ -0,0 +1,13 @@ +import {RulesRunner} from './rules-runner'; + +const ruleRunner = new RulesRunner(); + +onmessage = (e) => { + const originalText = e.data.oldText; + const newText = ruleRunner.lintText(e.data); + postMessage({ + originalText: originalText, + newText: newText, + ruleSettings: e.data, + }); +}; diff --git a/src/rule-runner copy/rules-runner.ts b/src/rule-runner copy/rules-runner.ts new file mode 100644 index 00000000..cfbcb3af --- /dev/null +++ b/src/rule-runner copy/rules-runner.ts @@ -0,0 +1,185 @@ +// import {TFile, moment} from 'obsidian'; +import {logDebug, logWarn} from '../logger'; +import {getDisabledRules, LinterSettings, rules, wrapLintError, LintCommand, RuleType} from '../rules'; +import BlockquotifyOnPaste from '../rules/blockquotify-on-paste'; +import EscapeYamlSpecialCharacters from '../rules/escape-yaml-special-characters'; +import ForceYamlEscape from '../rules/force-yaml-escape'; +import FormatTagsInYaml from '../rules/format-tags-in-yaml'; +import PreventDoubleChecklistIndicatorOnPaste from '../rules/prevent-double-checklist-indicator-on-paste'; +import PreventDoubleListItemIndicatorOnPaste from '../rules/prevent-double-list-item-indicator-on-paste'; +import ProperEllipsisOnPaste from '../rules/proper-ellipsis-on-paste'; +import RemoveHyphensOnPaste from '../rules/remove-hyphens-on-paste'; +import RemoveLeadingOrTrailingWhitespaceOnPaste from '../rules/remove-leading-or-trailing-whitespace-on-paste'; +import RemoveLeftoverFootnotesFromQuoteOnPaste from '../rules/remove-leftover-footnotes-from-quote-on-paste'; +import RemoveMultipleBlankLinesOnPaste from '../rules/remove-multiple-blank-lines-on-paste'; +import {RuleBuilderBase} from '../rules/rule-builder'; +// import YamlKeySort from '../rules/yaml-key-sort'; +// import YamlTimestamp from '../rules/yaml-timestamp'; +import {ObsidianCommandInterface} from '../typings/obsidian-ex'; + +export type RunLinterRulesOptions = { + oldText: string, + fileInfo: FileInfo, + settings: LinterSettings, + momentLocale: string, + // getCurrentTime: () => moment.Moment +} + +type FileInfo = { + name: string, + createdAtFormatted: string, + modifiedAtFormatted: string, +} + +export class RulesRunner { + private disabledRules: string[] = []; + + lintText(runOptions: RunLinterRulesOptions): string { + const originalText = runOptions.oldText; + this.disabledRules = getDisabledRules(originalText); + + let newText = this.runBeforeRegularRules(runOptions); + + for (const rule of rules) { + // if you are run prior to or after the regular rules or are a disabled rule, skip running the rule + if (this.disabledRules.includes(rule.alias())) { + logDebug(rule.alias() + ' is disabled'); + continue; + } else if (rule.hasSpecialExecutionOrder || rule.type === RuleType.PASTE) { + continue; + } + + [newText] = RuleBuilderBase.applyIfEnabledBase(rule, newText, runOptions.settings, { + fileCreatedTime: runOptions.fileInfo.createdAtFormatted, + fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, + fileName: runOptions.fileInfo.name, + locale: runOptions.momentLocale, + minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, + aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle, + tagArrayStyle: runOptions.settings.commonStyles.tagArrayStyle, + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); + } + + runOptions.oldText = newText; + + return this.runAfterRegularRules(originalText, runOptions); + } + + private runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + // remove hashtags from tags before parsing yaml + [newText] = FormatTagsInYaml.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + // escape YAML where possible before parsing yaml + [newText] = EscapeYamlSpecialCharacters.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); + + return newText; + } + + private runAfterRegularRules(originalText: string, runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + + [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); + + return newText; + } + + // runRulesThatCannotRunInWebWorker(originalText: string, runOptions: RunLinterRulesOptions, getCurrentTime: () => moment.Moment): string { + // let newText = runOptions.oldText; + // let currentTime = getCurrentTime(); + // // run yaml timestamp at the end to help determine if something has changed + // let isYamlTimestampEnabled; + // [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + // fileCreatedTime: runOptions.fileInfo.createdAtFormatted, + // fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, + // currentTime: currentTime, + // alreadyModified: originalText != newText, + // locale: runOptions.momentLocale, + // }); + + // const yamlTimestampOptions = YamlTimestamp.getRuleOptions(runOptions.settings); + + // currentTime = getCurrentTime(); + // [newText] = YamlKeySort.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + // currentTimeFormatted: currentTime.format(yamlTimestampOptions.format), + // yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, + // dateModifiedKey: yamlTimestampOptions.dateModifiedKey, + // }); + + // return newText; + // } + + runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface) { + // execute custom commands after regular rules, but before the timestamp rules + logDebug(`Running Custom Lint Commands`); + const commandsRun = new Set(); + for (const commandInfo of lintCommands) { + if (!commandInfo.id) { + continue; + } else if (commandsRun.has(commandInfo.id)) { + logWarn(`You cannot run the same command ("${commandInfo.name}") as a custom lint rule twice.`); + continue; + } + + try { + commandsRun.add(commandInfo.id); + commands.executeCommandById(commandInfo.id); + } catch (error) { + wrapLintError(error, `Custom Lint Command ${commandInfo.id}`); + } + } + } + + runPasteLint(currentLine: string, runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + + [newText] = RemoveHyphensOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = RemoveMultipleBlankLinesOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = RemoveLeftoverFootnotesFromQuoteOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = ProperEllipsisOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = RemoveLeadingOrTrailingWhitespaceOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = PreventDoubleChecklistIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); + + [newText] = PreventDoubleListItemIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); + + [newText] = BlockquotifyOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); + + return newText; + } +} + +// export function createRunLinterRulesOptions(text: string, file: TFile = null, momentLocale: string, settings: LinterSettings): RunLinterRulesOptions { +// const createdAt = file ? moment(file.stat.ctime): moment(); +// createdAt.locale(momentLocale); +// const modifiedAt = file ? moment(file.stat.mtime): moment(); +// modifiedAt.locale(momentLocale); +// const modifiedAtTime = modifiedAt.format(); +// const createdAtTime = createdAt.format(); + +// // const currentTime = moment(); +// // currentTime.locale(momentLocale); + +// return { +// oldText: text, +// fileInfo: { +// name: file ? file.basename: '', +// createdAtFormatted: createdAtTime, +// modifiedAtFormatted: modifiedAtTime, +// }, +// settings: settings, +// momentLocale: momentLocale, +// // getCurrentTime: () => { +// // return undefined; +// // }, +// }; +// } diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts new file mode 100644 index 00000000..81d4062e --- /dev/null +++ b/src/rules-runner/file-lint-manager.ts @@ -0,0 +1,129 @@ +/** Makes sure that files are linted and that they are handled appropriately in an asynchronous manner. */ + +// @ts-ignore because this is a web worker and it does not play well with Typescript checking +import Worker from './rules-runner.worker'; +import {TFile, Vault} from 'obsidian'; +import {createRunLinterRulesOptions} from './rules-runner'; +import {LinterSettings} from '../settings-data'; +import {LinterWorker} from 'src/typings/worker'; + +/** Callback when a file is resolved. */ +// type FileCallback = (p: any) => void; + +/** Multi-threaded file linter which debounces rapid file requests automatically. */ +export class FileLintManager { + /* Background workers which do the actual file parsing. */ + workers: LinterWorker[]; + /** Tracks which workers are actively parsing a file, to make sure we properly delegate results. */ + busy: boolean[]; + + /** List of files which have been queued for to be linted */ + lintQueue: TFile[]; + /** Fast-access set which holds the list of files queued to be linted; used for debouncing. */ + reloadSet: Set; + /** Paths -> promises for file reloads which have not yet been queued. */ + // callbacks: Map; + + public constructor(public numWorkers: number, public momentLocale: string, private settings: LinterSettings, private vault: Vault) { + this.workers = []; + this.busy = []; + + this.lintQueue = []; + this.reloadSet = new Set(); + // this.callbacks = new Map(); + + for (let index = 0; index < numWorkers; index++) { + // eslint-disable-next-line new-cap + const worker = Worker(); + worker.onmessage = (data: string) => { + console.log(data); + this.busy[index] = false; + }; + // worker.postMessage('first message'); + this.workers.push(worker); + // this.register(() => worker.terminate()); + this.busy.push(false); + } + } + + public lintFile(file: TFile): void { + // const promise: Promise = new Promise((resolve, reject) => { + // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); + // else this.callbacks.set(file.path, [[resolve, reject]]); + // }); + + // Immediately run this task if there are available workers; otherwise, add it to the queue. + const workerId = this.nextAvailableWorker(); + if (workerId !== undefined) { + this.send(file, workerId); + } else { + this.lintQueue.push(file); + } + + // return promise; + } + + /** + * Queue the given file for reloading. Multiple reload requests for the same file in a short time period will be de-bounced + * and all be resolved by a single actual file reload. + */ + // public reload(file: TFile): Promise { + // const promise: Promise = new Promise((resolve, reject) => { + // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); + // else this.callbacks.set(file.path, [[resolve, reject]]); + // }); + + // // De-bounce repeated requests for the same file. + // if (this.reloadSet.has(file.path)) return promise; + // this.reloadSet.add(file.path); + + // // Immediately run this task if there are available workers; otherwise, add it to the queue. + // const workerId = this.nextAvailableWorker(); + // if (workerId !== undefined) { + // this.send(file, workerId); + // } else { + // this.reloadQueue.push(file); + // } + + // return promise; + // } + + /** Finish the parsing of a file, potentially queueing a new file. */ + // private finish(path: string, data: any, index: number) { + // // Cache the callbacks before we do book-keeping. + // const calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); + + // // Book-keeping to clear metadata & allow the file to be re-loaded again. + // this.reloadSet.delete(path); + // this.callbacks.delete(path); + + // // Notify the queue this file is available for new work. + // this.busy[index] = false; + + // // Queue a new job onto this worker. + // const job = this.reloadQueue.shift(); + // if (job !== undefined) this.send(job, index); + + // // Resolve promises to let users know this file has finished. + // if ('$error' in data) { + // for (const [_, reject] of calls) reject(data['$error']); + // } else { + // for (const [callback, _] of calls) callback(data); + // } + // } + + // /** Send a new task to the given worker ID. */ + private send(file: TFile, workerId: number) { + this.busy[workerId] = true; + this.vault.cachedRead(file).then((oldText: string) => { + const lintRunnerSettings = createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings); + this.workers[workerId].postMessage(lintRunnerSettings); + }); + } + + // /** Find the next available, non-busy worker; return undefined if all workers are busy. */ + private nextAvailableWorker(): number | undefined { + const index = this.busy.indexOf(false); + return index == -1 ? undefined : index; + } +} diff --git a/src/rules-runner/rules-runner.ts b/src/rules-runner/rules-runner.ts new file mode 100644 index 00000000..4edfd0fc --- /dev/null +++ b/src/rules-runner/rules-runner.ts @@ -0,0 +1,243 @@ +// import {TFile, moment} from 'obsidian'; +import {logDebug, logWarn, timingBegin, timingEnd} from '../utils/logger'; +import {getDisabledRules, rules, wrapLintError, RuleType} from '../rules'; +import BlockquotifyOnPaste from '../rules/blockquotify-on-paste'; +import EscapeYamlSpecialCharacters from '../rules/escape-yaml-special-characters'; +// import ForceYamlEscape from '../rules/force-yaml-escape'; +import FormatTagsInYaml from '../rules/format-tags-in-yaml'; +import PreventDoubleChecklistIndicatorOnPaste from '../rules/prevent-double-checklist-indicator-on-paste'; +import PreventDoubleListItemIndicatorOnPaste from '../rules/prevent-double-list-item-indicator-on-paste'; +import ProperEllipsisOnPaste from '../rules/proper-ellipsis-on-paste'; +import RemoveHyphensOnPaste from '../rules/remove-hyphens-on-paste'; +import RemoveLeadingOrTrailingWhitespaceOnPaste from '../rules/remove-leading-or-trailing-whitespace-on-paste'; +import RemoveLeftoverFootnotesFromQuoteOnPaste from '../rules/remove-leftover-footnotes-from-quote-on-paste'; +import RemoveMultipleBlankLinesOnPaste from '../rules/remove-multiple-blank-lines-on-paste'; +import {RuleBuilderBase} from '../rules/rule-builder'; +// import YamlKeySort from '../rules/yaml-key-sort'; +// import YamlTimestamp from '../rules/yaml-timestamp'; +import {ObsidianCommandInterface} from '../typings/obsidian-ex'; +import {CustomReplace} from '../ui/linter-components/custom-replace-option'; +import {LintCommand} from '../ui/linter-components/custom-command-option'; +import {convertStringVersionOfEscapeCharactersToEscapeCharacters} from '../utils/strings'; +import {getTextInLanguage} from '../lang/helpers'; +// import CapitalizeHeadings from '../rules/capitalize-headings'; +// import BlockquoteStyle from '../rules/blockquote-style'; +import {IgnoreTypes, ignoreListOfTypes} from '../utils/ignore-types'; +import MoveMathBlockIndicatorsToOwnLine from '../rules/move-math-block-indicators-to-own-line'; +import {LinterSettings} from '../settings-data'; +import {TFile} from '../typings/worker'; +// import TrailingSpaces from '../rules/trailing-spaces'; + +export type RunLinterRulesOptions = { + oldText: string, + fileInfo: FileInfo, + settings: LinterSettings, +} + +type FileInfo = { + name: string, + createdAtFormatted: string, + modifiedAtFormatted: string, +} + +export class RulesRunner { + private disabledRules: string[] = []; + skipFile: boolean; + + lintText(runOptions: RunLinterRulesOptions): string { + this.skipFile = false; + const originalText = runOptions.oldText; + [this.disabledRules, this.skipFile] = getDisabledRules(originalText); + if (this.skipFile) { + return originalText; + } + + timingBegin(getTextInLanguage('logs.rule-running')); + + const preRuleText = getTextInLanguage('logs.pre-rules'); + timingBegin(preRuleText); + let newText = this.runBeforeRegularRules(runOptions); + timingEnd(preRuleText); + + const disabledRuleText = getTextInLanguage('logs.disabled-text'); + for (const rule of rules) { + // if you are run prior to or after the regular rules or are a disabled rule, skip running the rule + if (this.disabledRules.includes(rule.alias)) { + logDebug(rule.alias + ' ' + disabledRuleText); + continue; + } else if (rule.hasSpecialExecutionOrder || rule.type === RuleType.PASTE) { + continue; + } + + [newText] = RuleBuilderBase.applyIfEnabledBase(rule, newText, runOptions.settings, { + fileCreatedTime: runOptions.fileInfo.createdAtFormatted, + fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, + fileName: runOptions.fileInfo.name, + // locale: runOptions.momentLocale, + minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, + aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle, + tagArrayStyle: runOptions.settings.commonStyles.tagArrayStyle, + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + removeUnnecessaryEscapeCharsForMultiLineArrays: runOptions.settings.commonStyles.removeUnnecessaryEscapeCharsForMultiLineArrays, + }); + } + + const customRegexLogText = getTextInLanguage('logs.custom-regex'); + timingBegin(customRegexLogText); + newText = this.runCustomRegexReplacement(runOptions.settings.customRegexes, newText); + timingEnd(customRegexLogText); + + runOptions.oldText = newText; + + return newText; + // return this.runAfterRegularRules(originalText, runOptions); + } + + private runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + // remove hashtags from tags before parsing yaml + [newText] = FormatTagsInYaml.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + // escape YAML where possible before parsing yaml + [newText] = EscapeYamlSpecialCharacters.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); + + [newText] = MoveMathBlockIndicatorsToOwnLine.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, + }); + + return newText; + } + + // private runAfterRegularRules(originalText: string, runOptions: RunLinterRulesOptions): string { + // let newText = runOptions.oldText; + // const postRuleLogText = getTextInLanguage('logs.post-rules'); + // timingBegin(postRuleLogText); + // [newText] = CapitalizeHeadings.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + // [newText] = BlockquoteStyle.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + // [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + // defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + // }); + + // [newText] = TrailingSpaces.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + // let currentTime = runOptions.getCurrentTime(); + // // run YAML timestamp at the end to help determine if something has changed + // let isYamlTimestampEnabled; + // [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + // fileCreatedTime: runOptions.fileInfo.createdAtFormatted, + // fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, + // currentTime: currentTime, + // alreadyModified: originalText != newText, + // locale: runOptions.momentLocale, + // }); + + // const yamlTimestampOptions = YamlTimestamp.getRuleOptions(runOptions.settings); + + // currentTime = runOptions.getCurrentTime(); + // [newText] = YamlKeySort.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + // currentTimeFormatted: currentTime.format(yamlTimestampOptions.format.trimEnd()), + // yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, + // dateModifiedKey: yamlTimestampOptions.dateModifiedKey, + // }); + // timingEnd(postRuleLogText); + // timingEnd(getTextInLanguage('logs.rule-running')); + // return newText; + // } + + runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface) { + if (this.skipFile) { + return; + } + + logDebug(getTextInLanguage('logs.running-custom-lint-command')); + const commandsRun = new Set(); + for (const commandInfo of lintCommands) { + if (!commandInfo.id) { + continue; + } else if (commandsRun.has(commandInfo.id)) { + logWarn(getTextInLanguage('logs.custom-lint-duplicate-warning').replace('{COMMAND_NAME}', commandInfo.name)); + continue; + } + + try { + commandsRun.add(commandInfo.id); + commands.executeCommandById(commandInfo.id); + } catch (error) { + wrapLintError(error, `${getTextInLanguage('logs.custom-lint-error-message')} ${commandInfo.id}`); + } + } + } + + runCustomRegexReplacement(customRegexes: CustomReplace[], oldText: string): string { + return ignoreListOfTypes([IgnoreTypes.customIgnore], oldText, (text: string) => { + logDebug(getTextInLanguage('logs.running-custom-regex')); + + let newText = text; + for (const eachRegex of customRegexes) { + const findIsEmpty = eachRegex.find === undefined || eachRegex.find == '' || eachRegex.find === null; + const replaceIsEmpty = eachRegex.replace === undefined || eachRegex.replace === null; + if (findIsEmpty || replaceIsEmpty) { + continue; + } + + const regex = new RegExp(`${eachRegex.find}`, eachRegex.flags); + // make sure that characters are not string escaped unescape in the replace value to make sure things like \n and \t are correctly inserted + newText = newText.replace(regex, convertStringVersionOfEscapeCharactersToEscapeCharacters(eachRegex.replace)); + } + + return newText; + }); + } + + runPasteLint(currentLine: string, selectedText: string, runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + + [newText] = RemoveHyphensOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = RemoveMultipleBlankLinesOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = RemoveLeftoverFootnotesFromQuoteOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = ProperEllipsisOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = RemoveLeadingOrTrailingWhitespaceOnPaste.applyIfEnabled(newText, runOptions.settings, []); + + [newText] = PreventDoubleChecklistIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine, selectedText: selectedText}); + + [newText] = PreventDoubleListItemIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine, selectedText: selectedText}); + + [newText] = BlockquotifyOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); + + return newText; + } +} + +export function createRunLinterRulesOptions(text: string, file: TFile = null, momentLocale: string, settings: LinterSettings): RunLinterRulesOptions { + // const createdAt = file ? moment(file.stat.ctime): moment(); + // createdAt.locale(momentLocale); + // const modifiedAt = file ? moment(file.stat.mtime): moment(); + // modifiedAt.locale(momentLocale); + // const modifiedAtTime = modifiedAt.format(); + // const createdAtTime = createdAt.format(); + + // const currentTime = moment(); + // currentTime.locale(momentLocale); + + return { + oldText: text, + fileInfo: { + name: file ? file.basename: '', + createdAtFormatted: 'created', + modifiedAtFormatted: 'modified', + }, + settings: settings, + // momentLocale: momentLocale, + // getCurrentTime: () => { + // return undefined; + // }, + }; +} diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts index bd02cd1f..ee1b8cfe 100644 --- a/src/rules-runner/rules-runner.worker.ts +++ b/src/rules-runner/rules-runner.worker.ts @@ -1,8 +1,23 @@ // here is a worker to sent data back and forth -import {WorkerMessage as WorkerStartMessage} from '../typings/worker'; +import {RulesRunner, WorkerMessage} from '../typings/worker'; -onmessage = (event: WorkerStartMessage) => { - console.log(`Worker received message: ${event.data.oldText}`); - postMessage('Hello from worker!'); + +let rulesRunner: RulesRunner = null; +import('./rules-runner').then((mod: any) => { + rulesRunner = new mod.RulesRunner(); +}); + +// import {RulesRunner} from './rules-runner'; + + +self.document = { + createElement: () => {}, +}; + +onmessage = (event: WorkerMessage) => { + console.log(event.data.oldText); + const newText = rulesRunner.lintText(event.data); + console.log(newText); + postMessage(newText); }; diff --git a/src/typings/worker.ts b/src/typings/worker.ts index 0f8cc222..2cdfbf8b 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -57,3 +57,19 @@ export interface LinterWorker { postMessage: (data: WorkerArgs) => void; onmessage: (data: WorkerResponseMessage) => void; } + +export type RunLinterRulesOptions = { + oldText: string, + fileInfo: FileInfo, + settings: LinterSettings, +} + +type FileInfo = { + name: string, + createdAtFormatted: string, + modifiedAtFormatted: string, +} + +export interface RulesRunner { + lintText(runOptions: RunLinterRulesOptions): string +} From 60df40aa2534d6b02c59c539d6fe4e91f9de975c Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Fri, 15 Mar 2024 18:05:55 -0400 Subject: [PATCH 05/14] made some changes and got a preliminary version of linting a single file working. Note that the logging is not working properly. --- src/main.ts | 39 +++---- src/rules-runner/file-lint-manager.ts | 108 +++++++++++++----- src/rules-runner/rules-runner.ts | 139 ++++++++++++------------ src/rules-runner/rules-runner.worker.ts | 6 +- src/typings/worker.ts | 26 +++-- 5 files changed, 191 insertions(+), 127 deletions(-) diff --git a/src/main.ts b/src/main.ts index bd8392f6..50cca5e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -606,31 +606,32 @@ export default class LinterPlugin extends Plugin { logInfo(getTextInLanguage('logs.linter-run')); const file = this.app.workspace.getActiveFile(); - const oldText = editor.getValue(); - let newText: string; + // const oldText = editor.getValue(); + // let newText: string; try { // newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); - this.lintFileManager.lintFile(file); - } catch (error) { - this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); - return; - } + this.lintFileManager.lintFile(file, (oldText: string, newText: string) => { + const changes = this.updateEditor(oldText, newText, editor); + const charsAdded = changes.map((change) => change[0] == DiffMatchPatch.DIFF_INSERT ? change[1].length : 0).reduce((a, b) => a + b, 0); + const charsRemoved = changes.map((change) => change[0] == DiffMatchPatch.DIFF_DELETE ? change[1].length : 0).reduce((a, b) => a + b, 0); - const changes = this.updateEditor(oldText, newText, editor); - const charsAdded = changes.map((change) => change[0] == DiffMatchPatch.DIFF_INSERT ? change[1].length : 0).reduce((a, b) => a + b, 0); - const charsRemoved = changes.map((change) => change[0] == DiffMatchPatch.DIFF_DELETE ? change[1].length : 0).reduce((a, b) => a + b, 0); + this.displayChangedMessage(charsAdded, charsRemoved); - this.displayChangedMessage(charsAdded, charsRemoved); + // run custom commands now since no change was made + if (!charsAdded && !charsRemoved) { + void this.runCustomCommands(file); + } else { + this.updateFileDebouncerText(file, newText); + this.editorLintFiles.push(file); + } - // run custom commands now since no change was made - if (!charsAdded && !charsRemoved) { - void this.runCustomCommands(file); - } else { - this.updateFileDebouncerText(file, newText); - this.editorLintFiles.push(file); + setCollectLogs(false); + }); + // newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings)); + } catch (error) { + this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); + return; } - - setCollectLogs(false); } // based on https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L85-L109 diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts index 81d4062e..626f5b7c 100644 --- a/src/rules-runner/file-lint-manager.ts +++ b/src/rules-runner/file-lint-manager.ts @@ -1,14 +1,17 @@ +// based on https://github.com/blacksmithgu/obsidian-dataview/blob/75b564bcfd23876f12fa3faf7f86184cdfcd91f1/src/data-import/web-worker/import-manager.ts /** Makes sure that files are linted and that they are handled appropriately in an asynchronous manner. */ // @ts-ignore because this is a web worker and it does not play well with Typescript checking import Worker from './rules-runner.worker'; -import {TFile, Vault} from 'obsidian'; +import {TFile, Vault, moment} from 'obsidian'; import {createRunLinterRulesOptions} from './rules-runner'; import {LinterSettings} from '../settings-data'; -import {LinterWorker} from 'src/typings/worker'; +import {LinterWorker, RunLinterRulesOptions} from '../typings/worker'; +import YamlTimestamp from '../rules/yaml-timestamp'; +import YamlKeySort from '../rules/yaml-key-sort'; /** Callback when a file is resolved. */ -// type FileCallback = (p: any) => void; +type FileCallback = (oldText: string, newText: string) => void; /** Multi-threaded file linter which debounces rapid file requests automatically. */ export class FileLintManager { @@ -21,6 +24,11 @@ export class FileLintManager { lintQueue: TFile[]; /** Fast-access set which holds the list of files queued to be linted; used for debouncing. */ reloadSet: Set; + /** Paths -> callback function to run once file linting has finished running rules. + * Note: this does not mean that the logic for running custom commands has run. + */ + callbacks: Map; + /** Paths -> promises for file reloads which have not yet been queued. */ // callbacks: Map; @@ -30,14 +38,16 @@ export class FileLintManager { this.lintQueue = []; this.reloadSet = new Set(); - // this.callbacks = new Map(); + this.callbacks = new Map(); for (let index = 0; index < numWorkers; index++) { // eslint-disable-next-line new-cap const worker = Worker(); - worker.onmessage = (data: string) => { - console.log(data); - this.busy[index] = false; + // worker.onmessage = evt => this.finish(evt.data.path, Transferable.value(evt.data.result), index); + worker.onmessage = (resp: any) => { + // console.log(data); + // this.busy[index] = false; + this.finish(resp.data as RunLinterRulesOptions, index); }; // worker.postMessage('first message'); this.workers.push(worker); @@ -46,10 +56,16 @@ export class FileLintManager { } } - public lintFile(file: TFile): void { + public lintFile(file: TFile, callback: FileCallback): void { + if (this.callbacks.has(file.path)) { + this.callbacks.get(file.path)?.push(callback); + } else { + this.callbacks.set(file.path, [callback]); + } + // const promise: Promise = new Promise((resolve, reject) => { - // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); - // else this.callbacks.set(file.path, [[resolve, reject]]); + // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); + // else this.callbacks.set(file.path, [[resolve, reject]]); // }); // Immediately run this task if there are available workers; otherwise, add it to the queue. @@ -89,28 +105,64 @@ export class FileLintManager { // } /** Finish the parsing of a file, potentially queueing a new file. */ - // private finish(path: string, data: any, index: number) { - // // Cache the callbacks before we do book-keeping. - // const calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); + private finish(data: RunLinterRulesOptions, index: number) { + // Cache the callbacks before we do book-keeping. + // const calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); - // // Book-keeping to clear metadata & allow the file to be re-loaded again. - // this.reloadSet.delete(path); - // this.callbacks.delete(path); + // Book-keeping to clear metadata & allow the file to be re-loaded again. + // this.reloadSet.delete(path); + // this.callbacks.delete(path); - // // Notify the queue this file is available for new work. - // this.busy[index] = false; + // Notify the queue this file is available for new work. + this.busy[index] = false; - // // Queue a new job onto this worker. - // const job = this.reloadQueue.shift(); - // if (job !== undefined) this.send(job, index); + // Queue a new job onto this worker. + const job = this.lintQueue.shift(); + if (job !== undefined) { + this.send(job, index); + } - // // Resolve promises to let users know this file has finished. - // if ('$error' in data) { - // for (const [_, reject] of calls) reject(data['$error']); - // } else { - // for (const [callback, _] of calls) callback(data); - // } - // } + // run lint actions related to moment and other areas that cannot be run in the worker + let currentTime = moment(); + currentTime.locale(data.momentLocale); + // let currentTime = runOptions.getCurrentTime(); + // run YAML timestamp at the end to help determine if something has changed + let isYamlTimestampEnabled: boolean; + let newText: string; + [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(data.newText, data.settings, data.disabledRules, { + fileCreatedTime: data.fileInfo.createdAtFormatted, + fileModifiedTime: data.fileInfo.modifiedAtFormatted, + currentTime: currentTime, + alreadyModified: data.oldText != newText, + locale: data.momentLocale, + }); + + const yamlTimestampOptions = YamlTimestamp.getRuleOptions(data.settings); + + currentTime = moment(); + currentTime.locale(data.momentLocale); + [newText] = YamlKeySort.applyIfEnabled(newText, data.settings, data.disabledRules, { + currentTimeFormatted: currentTime.format(yamlTimestampOptions.format.trimEnd()), + yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, + dateModifiedKey: yamlTimestampOptions.dateModifiedKey, + }); + + // TODO: add the callback resolution here... + if (this.callbacks.has(data.fileInfo.path)) { + const callbacks = this.callbacks.get(data.fileInfo.path); + this.callbacks.delete(data.fileInfo.path); + for (const callback of callbacks) { + callback(data.oldText, newText); + } + } + + // Resolve promises to let users know this file has finished. + // if ('$error' in data) { + // for (const [_, reject] of calls) reject(data['$error']); + // } else { + // for (const [callback, _] of calls) callback(data); + // } + } // /** Send a new task to the given worker ID. */ private send(file: TFile, workerId: number) { diff --git a/src/rules-runner/rules-runner.ts b/src/rules-runner/rules-runner.ts index 4edfd0fc..21e89278 100644 --- a/src/rules-runner/rules-runner.ts +++ b/src/rules-runner/rules-runner.ts @@ -1,9 +1,9 @@ -// import {TFile, moment} from 'obsidian'; +import {moment} from 'obsidian'; import {logDebug, logWarn, timingBegin, timingEnd} from '../utils/logger'; import {getDisabledRules, rules, wrapLintError, RuleType} from '../rules'; import BlockquotifyOnPaste from '../rules/blockquotify-on-paste'; import EscapeYamlSpecialCharacters from '../rules/escape-yaml-special-characters'; -// import ForceYamlEscape from '../rules/force-yaml-escape'; +import ForceYamlEscape from '../rules/force-yaml-escape'; import FormatTagsInYaml from '../rules/format-tags-in-yaml'; import PreventDoubleChecklistIndicatorOnPaste from '../rules/prevent-double-checklist-indicator-on-paste'; import PreventDoubleListItemIndicatorOnPaste from '../rules/prevent-double-list-item-indicator-on-paste'; @@ -20,25 +20,25 @@ import {CustomReplace} from '../ui/linter-components/custom-replace-option'; import {LintCommand} from '../ui/linter-components/custom-command-option'; import {convertStringVersionOfEscapeCharactersToEscapeCharacters} from '../utils/strings'; import {getTextInLanguage} from '../lang/helpers'; -// import CapitalizeHeadings from '../rules/capitalize-headings'; -// import BlockquoteStyle from '../rules/blockquote-style'; +import CapitalizeHeadings from '../rules/capitalize-headings'; +import BlockquoteStyle from '../rules/blockquote-style'; import {IgnoreTypes, ignoreListOfTypes} from '../utils/ignore-types'; import MoveMathBlockIndicatorsToOwnLine from '../rules/move-math-block-indicators-to-own-line'; import {LinterSettings} from '../settings-data'; -import {TFile} from '../typings/worker'; -// import TrailingSpaces from '../rules/trailing-spaces'; +import {RunLinterRulesOptions, TFile} from '../typings/worker'; +import TrailingSpaces from '../rules/trailing-spaces'; -export type RunLinterRulesOptions = { - oldText: string, - fileInfo: FileInfo, - settings: LinterSettings, -} +// export type RunLinterRulesOptions = { +// oldText: string, +// fileInfo: FileInfo, +// settings: LinterSettings, +// } -type FileInfo = { - name: string, - createdAtFormatted: string, - modifiedAtFormatted: string, -} +// type FileInfo = { +// name: string, +// createdAtFormatted: string, +// modifiedAtFormatted: string, +// } export class RulesRunner { private disabledRules: string[] = []; @@ -48,6 +48,8 @@ export class RulesRunner { this.skipFile = false; const originalText = runOptions.oldText; [this.disabledRules, this.skipFile] = getDisabledRules(originalText); + runOptions.skipFile = this.skipFile; + runOptions.disabledRules = this.disabledRules; if (this.skipFile) { return originalText; } @@ -89,8 +91,7 @@ export class RulesRunner { runOptions.oldText = newText; - return newText; - // return this.runAfterRegularRules(originalText, runOptions); + return this.runAfterRegularRules(originalText, runOptions); } private runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { @@ -110,43 +111,43 @@ export class RulesRunner { return newText; } - // private runAfterRegularRules(originalText: string, runOptions: RunLinterRulesOptions): string { - // let newText = runOptions.oldText; - // const postRuleLogText = getTextInLanguage('logs.post-rules'); - // timingBegin(postRuleLogText); - // [newText] = CapitalizeHeadings.applyIfEnabled(newText, runOptions.settings, this.disabledRules); - - // [newText] = BlockquoteStyle.applyIfEnabled(newText, runOptions.settings, this.disabledRules); - - // [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - // defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, - // }); - - // [newText] = TrailingSpaces.applyIfEnabled(newText, runOptions.settings, this.disabledRules); - - // let currentTime = runOptions.getCurrentTime(); - // // run YAML timestamp at the end to help determine if something has changed - // let isYamlTimestampEnabled; - // [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - // fileCreatedTime: runOptions.fileInfo.createdAtFormatted, - // fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, - // currentTime: currentTime, - // alreadyModified: originalText != newText, - // locale: runOptions.momentLocale, - // }); - - // const yamlTimestampOptions = YamlTimestamp.getRuleOptions(runOptions.settings); - - // currentTime = runOptions.getCurrentTime(); - // [newText] = YamlKeySort.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - // currentTimeFormatted: currentTime.format(yamlTimestampOptions.format.trimEnd()), - // yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, - // dateModifiedKey: yamlTimestampOptions.dateModifiedKey, - // }); - // timingEnd(postRuleLogText); - // timingEnd(getTextInLanguage('logs.rule-running')); - // return newText; - // } + private runAfterRegularRules(originalText: string, runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + const postRuleLogText = getTextInLanguage('logs.post-rules'); + timingBegin(postRuleLogText); + [newText] = CapitalizeHeadings.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + [newText] = BlockquoteStyle.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); + + [newText] = TrailingSpaces.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + + // let currentTime = runOptions.getCurrentTime(); + // // run YAML timestamp at the end to help determine if something has changed + // let isYamlTimestampEnabled; + // [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + // fileCreatedTime: runOptions.fileInfo.createdAtFormatted, + // fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, + // currentTime: currentTime, + // alreadyModified: originalText != newText, + // locale: runOptions.momentLocale, + // }); + + // const yamlTimestampOptions = YamlTimestamp.getRuleOptions(runOptions.settings); + + // currentTime = runOptions.getCurrentTime(); + // [newText] = YamlKeySort.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { + // currentTimeFormatted: currentTime.format(yamlTimestampOptions.format.trimEnd()), + // yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, + // dateModifiedKey: yamlTimestampOptions.dateModifiedKey, + // }); + timingEnd(postRuleLogText); + timingEnd(getTextInLanguage('logs.rule-running')); + return newText; + } runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface) { if (this.skipFile) { @@ -217,27 +218,25 @@ export class RulesRunner { } export function createRunLinterRulesOptions(text: string, file: TFile = null, momentLocale: string, settings: LinterSettings): RunLinterRulesOptions { - // const createdAt = file ? moment(file.stat.ctime): moment(); - // createdAt.locale(momentLocale); - // const modifiedAt = file ? moment(file.stat.mtime): moment(); - // modifiedAt.locale(momentLocale); - // const modifiedAtTime = modifiedAt.format(); - // const createdAtTime = createdAt.format(); - - // const currentTime = moment(); - // currentTime.locale(momentLocale); + const createdAt = file ? moment(file.stat.ctime): moment(); + createdAt.locale(momentLocale); + const modifiedAt = file ? moment(file.stat.mtime): moment(); + modifiedAt.locale(momentLocale); + const modifiedAtTime = modifiedAt.format(); + const createdAtTime = createdAt.format(); return { oldText: text, + newText: '', + momentLocale: momentLocale, fileInfo: { + path: file ? file.path: '', name: file ? file.basename: '', - createdAtFormatted: 'created', - modifiedAtFormatted: 'modified', + createdAtFormatted: createdAtTime, + modifiedAtFormatted: modifiedAtTime, }, settings: settings, - // momentLocale: momentLocale, - // getCurrentTime: () => { - // return undefined; - // }, + skipFile: false, + disabledRules: [], }; } diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts index ee1b8cfe..54e7b91f 100644 --- a/src/rules-runner/rules-runner.worker.ts +++ b/src/rules-runner/rules-runner.worker.ts @@ -12,12 +12,12 @@ import('./rules-runner').then((mod: any) => { self.document = { + // @ts-ignore this is meant for preventing an error on run, but it is not really needed beyond that createElement: () => {}, }; onmessage = (event: WorkerMessage) => { console.log(event.data.oldText); - const newText = rulesRunner.lintText(event.data); - console.log(newText); - postMessage(newText); + event.data.newText = rulesRunner.lintText(event.data); + postMessage(event.data); }; diff --git a/src/typings/worker.ts b/src/typings/worker.ts index 2cdfbf8b..d9341964 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -29,28 +29,35 @@ export interface TFile { * @public */ extension: string; + /** + * @public + */ + path: string; } export type WorkerArgs = { oldText: string, - fileInfo: { - name: string, - createdAtFormatted: string, - modifiedAtFormatted: string, - }, + fileInfo: FileInfo, settings: LinterSettings, + skipFile: boolean, + disabledRules: string[], } export type WorkerMessage = { - data: WorkerArgs; + data: RunLinterRulesOptions, } export type WorkerResponse = { + oldText: string, newText: string, + fileInfo: FileInfo, + settings: LinterSettings, + skipFile: boolean, + disabledRules: string[], } export type WorkerResponseMessage = { - data: WorkerResponse + data: RunLinterRulesOptions, } export interface LinterWorker { @@ -60,11 +67,16 @@ export interface LinterWorker { export type RunLinterRulesOptions = { oldText: string, + newText: string, + momentLocale: string, fileInfo: FileInfo, settings: LinterSettings, + skipFile: boolean, + disabledRules: string[], } type FileInfo = { + path: string, name: string, createdAtFormatted: string, modifiedAtFormatted: string, From 30b430d93fe4b82c745e288c3f1f538c08f95a94 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Fri, 15 Mar 2024 18:53:20 -0400 Subject: [PATCH 06/14] cleaned up the logic so that the logs get recorded and properly passed from the worker to the main thread --- src/rules-runner/file-lint-manager.ts | 7 ++++- src/rules-runner/rules-runner.ts | 39 ++----------------------- src/rules-runner/rules-runner.worker.ts | 14 ++++++++- src/typings/worker.ts | 1 + src/utils/logger.ts | 4 +++ 5 files changed, 27 insertions(+), 38 deletions(-) diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts index 626f5b7c..46bc2dba 100644 --- a/src/rules-runner/file-lint-manager.ts +++ b/src/rules-runner/file-lint-manager.ts @@ -9,6 +9,7 @@ import {LinterSettings} from '../settings-data'; import {LinterWorker, RunLinterRulesOptions} from '../typings/worker'; import YamlTimestamp from '../rules/yaml-timestamp'; import YamlKeySort from '../rules/yaml-key-sort'; +import {setLogs} from '../utils/logger'; /** Callback when a file is resolved. */ type FileCallback = (oldText: string, newText: string) => void; @@ -104,7 +105,7 @@ export class FileLintManager { // return promise; // } - /** Finish the parsing of a file, potentially queueing a new file. */ + // Finish the parsing of a file, potentially queueing a new file. private finish(data: RunLinterRulesOptions, index: number) { // Cache the callbacks before we do book-keeping. // const calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); @@ -122,6 +123,10 @@ export class FileLintManager { this.send(job, index); } + if (data.settings.recordLintOnSaveLogs) { + setLogs(data.logsFromRun); + } + // run lint actions related to moment and other areas that cannot be run in the worker let currentTime = moment(); currentTime.locale(data.momentLocale); diff --git a/src/rules-runner/rules-runner.ts b/src/rules-runner/rules-runner.ts index 21e89278..5acdb4c3 100644 --- a/src/rules-runner/rules-runner.ts +++ b/src/rules-runner/rules-runner.ts @@ -13,8 +13,6 @@ import RemoveLeadingOrTrailingWhitespaceOnPaste from '../rules/remove-leading-or import RemoveLeftoverFootnotesFromQuoteOnPaste from '../rules/remove-leftover-footnotes-from-quote-on-paste'; import RemoveMultipleBlankLinesOnPaste from '../rules/remove-multiple-blank-lines-on-paste'; import {RuleBuilderBase} from '../rules/rule-builder'; -// import YamlKeySort from '../rules/yaml-key-sort'; -// import YamlTimestamp from '../rules/yaml-timestamp'; import {ObsidianCommandInterface} from '../typings/obsidian-ex'; import {CustomReplace} from '../ui/linter-components/custom-replace-option'; import {LintCommand} from '../ui/linter-components/custom-command-option'; @@ -28,18 +26,6 @@ import {LinterSettings} from '../settings-data'; import {RunLinterRulesOptions, TFile} from '../typings/worker'; import TrailingSpaces from '../rules/trailing-spaces'; -// export type RunLinterRulesOptions = { -// oldText: string, -// fileInfo: FileInfo, -// settings: LinterSettings, -// } - -// type FileInfo = { -// name: string, -// createdAtFormatted: string, -// modifiedAtFormatted: string, -// } - export class RulesRunner { private disabledRules: string[] = []; skipFile: boolean; @@ -75,7 +61,6 @@ export class RulesRunner { fileCreatedTime: runOptions.fileInfo.createdAtFormatted, fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, fileName: runOptions.fileInfo.name, - // locale: runOptions.momentLocale, minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle, tagArrayStyle: runOptions.settings.commonStyles.tagArrayStyle, @@ -91,7 +76,7 @@ export class RulesRunner { runOptions.oldText = newText; - return this.runAfterRegularRules(originalText, runOptions); + return this.runAfterRegularRules(runOptions); } private runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { @@ -111,7 +96,7 @@ export class RulesRunner { return newText; } - private runAfterRegularRules(originalText: string, runOptions: RunLinterRulesOptions): string { + private runAfterRegularRules(runOptions: RunLinterRulesOptions): string { let newText = runOptions.oldText; const postRuleLogText = getTextInLanguage('logs.post-rules'); timingBegin(postRuleLogText); @@ -125,25 +110,6 @@ export class RulesRunner { [newText] = TrailingSpaces.applyIfEnabled(newText, runOptions.settings, this.disabledRules); - // let currentTime = runOptions.getCurrentTime(); - // // run YAML timestamp at the end to help determine if something has changed - // let isYamlTimestampEnabled; - // [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - // fileCreatedTime: runOptions.fileInfo.createdAtFormatted, - // fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, - // currentTime: currentTime, - // alreadyModified: originalText != newText, - // locale: runOptions.momentLocale, - // }); - - // const yamlTimestampOptions = YamlTimestamp.getRuleOptions(runOptions.settings); - - // currentTime = runOptions.getCurrentTime(); - // [newText] = YamlKeySort.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - // currentTimeFormatted: currentTime.format(yamlTimestampOptions.format.trimEnd()), - // yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, - // dateModifiedKey: yamlTimestampOptions.dateModifiedKey, - // }); timingEnd(postRuleLogText); timingEnd(getTextInLanguage('logs.rule-running')); return newText; @@ -238,5 +204,6 @@ export function createRunLinterRulesOptions(text: string, file: TFile = null, mo settings: settings, skipFile: false, disabledRules: [], + logsFromRun: [], }; } diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts index 54e7b91f..2a731613 100644 --- a/src/rules-runner/rules-runner.worker.ts +++ b/src/rules-runner/rules-runner.worker.ts @@ -1,5 +1,6 @@ // here is a worker to sent data back and forth +import {clearLogs, logsFromLastRun, setCollectLogs, setLogLevel} from '../utils/logger'; import {RulesRunner, WorkerMessage} from '../typings/worker'; @@ -17,7 +18,18 @@ self.document = { }; onmessage = (event: WorkerMessage) => { - console.log(event.data.oldText); + const oldText = event.data.oldText; + + setLogLevel(event.data.settings.logLevel); + setCollectLogs(event.data.settings.recordLintOnSaveLogs); + clearLogs(); + event.data.newText = rulesRunner.lintText(event.data); + event.data.oldText = oldText; + + if (event.data.settings.recordLintOnSaveLogs) { + event.data.logsFromRun = logsFromLastRun; + } + postMessage(event.data); }; diff --git a/src/typings/worker.ts b/src/typings/worker.ts index d9341964..fb50023a 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -73,6 +73,7 @@ export type RunLinterRulesOptions = { settings: LinterSettings, skipFile: boolean, disabledRules: string[], + logsFromRun: string[], } type FileInfo = { diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 90423e56..1d3499a5 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -16,6 +16,10 @@ export enum LogLevels { export let logsFromLastRun: string[] = []; +export function setLogs(logs: string[]) { + logsFromLastRun = logs; +} + /** * Allows for the logging of errors * @param {string} labelForError The label for the error From 9f715a3d482522180c541aab20c8b64a519e2d4b Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Sat, 16 Mar 2024 12:32:49 -0400 Subject: [PATCH 07/14] added the dummy mocks to the esbuild for the worker since they are required for rules to run and added some more changes to get closer to getting the worker setup in place for all linting types --- esbuild.config.mjs | 8 +- src/main.ts | 35 ++-- src/rules-runner/file-lint-manager.ts | 91 +++------ src/rules-runner/rules-runner.ts | 233 +++++++++++------------- src/rules-runner/rules-runner.worker.ts | 32 ++-- src/typings/worker.ts | 5 +- 6 files changed, 165 insertions(+), 239 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 1d1bfc1b..9e3f0fd3 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -12,7 +12,7 @@ if you want to view the source, please visit the github repository of this plugi */ `; -const dummyMocksForDocs = ` +const dummyMocksForDocsAndWorker = ` document = { createElement: function() {}, }; @@ -20,7 +20,7 @@ document = { const prod = (process.argv[2] === 'production'); -const mockedBanner = banner + dummyMocksForDocs; +const mockedBanner = banner + dummyMocksForDocsAndWorker; const mockedPlugins = [replace({ values: { // update usage of moment from obsidian to the node implementation of moment we have @@ -90,6 +90,10 @@ const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) { plugins: [ importGlobPlugin.default(), inlineWorkerPlugin({ + banner: + { + js: dummyMocksForDocsAndWorker, + }, external: externalPackages, format: 'cjs', plugins: [...webWorkerIgnores], diff --git a/src/main.ts b/src/main.ts index 50cca5e4..4341deda 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,8 @@ import {downloadMisspellings, readInMisspellingsFile} from './utils/auto-correct // import MyWorker from './rules-runner/rules-runner.worker'; // import {LinterWorker, WorkerArgs, WorkerResponseMessage} from './typings/worker'; import {FileLintManager} from './rules-runner/file-lint-manager'; +import {RunLinterRulesOptions} from './typings/worker'; +import {runCustomCommands} from './rules-runner/rules-runner'; // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43 const langToMomentLocale = { @@ -101,22 +103,6 @@ export default class LinterPlugin extends Plugin { this.lintFileManager = new FileLintManager(1, this.momentLocale, this.settings, this.app.vault); - // const worker = new MyWorker() as LinterWorker; - - // worker.postMessage({ - // oldText: 'text', - // fileInfo: { - // name: 'file name', - // createdAtFormatted: 'created', - // modifiedAtFormatted: 'updated', - // }, - // settings: this.settings, - // }); - // worker.onmessage = (event: WorkerResponseMessage) => { - // console.log(`Main thread received message: ${event.data}`); - // }; - - this.addCommands(); this.registerEventsAndSaveCallback(); @@ -141,6 +127,8 @@ export default class LinterPlugin extends Plugin { if (saveCommandDefinition && saveCommandDefinition.checkCallback && this.originalSaveCallback) { saveCommandDefinition.checkCallback = this.originalSaveCallback; } + + this.lintFileManager.terminateWorkers(); } async loadSettings() { @@ -380,7 +368,8 @@ export default class LinterPlugin extends Plugin { if (this.editorLintFiles.includes(file)) { this.editorLintFiles.remove(file); - void this.runCustomCommands(file); + // TODO: come back and fix this since I need a way to pass over the options for this pjk + this.runCustomCommands(file, null); } else if (this.fileLintFiles.has(file)) { this.fileLintFiles.delete(file); @@ -606,12 +595,10 @@ export default class LinterPlugin extends Plugin { logInfo(getTextInLanguage('logs.linter-run')); const file = this.app.workspace.getActiveFile(); - // const oldText = editor.getValue(); - // let newText: string; try { // newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); - this.lintFileManager.lintFile(file, (oldText: string, newText: string) => { - const changes = this.updateEditor(oldText, newText, editor); + this.lintFileManager.lintFile(file, (runOptions: RunLinterRulesOptions) => { + const changes = this.updateEditor(runOptions.oldText, runOptions.newText, editor); const charsAdded = changes.map((change) => change[0] == DiffMatchPatch.DIFF_INSERT ? change[1].length : 0).reduce((a, b) => a + b, 0); const charsRemoved = changes.map((change) => change[0] == DiffMatchPatch.DIFF_DELETE ? change[1].length : 0).reduce((a, b) => a + b, 0); @@ -621,7 +608,7 @@ export default class LinterPlugin extends Plugin { if (!charsAdded && !charsRemoved) { void this.runCustomCommands(file); } else { - this.updateFileDebouncerText(file, newText); + this.updateFileDebouncerText(file, runOptions.newText); this.editorLintFiles.push(file); } @@ -1092,14 +1079,14 @@ export default class LinterPlugin extends Plugin { this.currentlyOpeningSidebar = false; } - private async runCustomCommands(file: TFile) { + private async runCustomCommands(file: TFile, runOptions: RunLinterRulesOptions) { if (!this.settings.lintCommands || this.settings.lintCommands.length == 0 || !this.hasCustomCommands) { return; } await this.customCommandsLock.acquire('command', async () => { try { - this.rulesRunner.runCustomCommands(this.settings.lintCommands, this.app.commands); + runCustomCommands(this.settings.lintCommands, this.app.commands, runOptions); } catch (error) { this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); } diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts index 46bc2dba..30a63e7d 100644 --- a/src/rules-runner/file-lint-manager.ts +++ b/src/rules-runner/file-lint-manager.ts @@ -1,5 +1,6 @@ -// based on https://github.com/blacksmithgu/obsidian-dataview/blob/75b564bcfd23876f12fa3faf7f86184cdfcd91f1/src/data-import/web-worker/import-manager.ts -/** Makes sure that files are linted and that they are handled appropriately in an asynchronous manner. */ +/** Makes sure that files are linted and that they are handled appropriately in an asynchronous manner. + * based on https://github.com/blacksmithgu/obsidian-dataview/blob/75b564bcfd23876f12fa3faf7f86184cdfcd91f1/src/data-import/web-worker/import-manager.ts +*/ // @ts-ignore because this is a web worker and it does not play well with Typescript checking import Worker from './rules-runner.worker'; @@ -12,7 +13,7 @@ import YamlKeySort from '../rules/yaml-key-sort'; import {setLogs} from '../utils/logger'; /** Callback when a file is resolved. */ -type FileCallback = (oldText: string, newText: string) => void; +type FileCallback = (runOptions: RunLinterRulesOptions) => void; /** Multi-threaded file linter which debounces rapid file requests automatically. */ export class FileLintManager { @@ -23,52 +24,38 @@ export class FileLintManager { /** List of files which have been queued for to be linted */ lintQueue: TFile[]; - /** Fast-access set which holds the list of files queued to be linted; used for debouncing. */ - reloadSet: Set; /** Paths -> callback function to run once file linting has finished running rules. * Note: this does not mean that the logic for running custom commands has run. */ - callbacks: Map; - - /** Paths -> promises for file reloads which have not yet been queued. */ - // callbacks: Map; + callbacks: Map; public constructor(public numWorkers: number, public momentLocale: string, private settings: LinterSettings, private vault: Vault) { this.workers = []; this.busy = []; this.lintQueue = []; - this.reloadSet = new Set(); this.callbacks = new Map(); for (let index = 0; index < numWorkers; index++) { // eslint-disable-next-line new-cap const worker = Worker(); - // worker.onmessage = evt => this.finish(evt.data.path, Transferable.value(evt.data.result), index); worker.onmessage = (resp: any) => { - // console.log(data); - // this.busy[index] = false; this.finish(resp.data as RunLinterRulesOptions, index); }; - // worker.postMessage('first message'); + this.workers.push(worker); - // this.register(() => worker.terminate()); this.busy.push(false); } } public lintFile(file: TFile, callback: FileCallback): void { + // if the file is already in the list of files to process, we should skip it if (this.callbacks.has(file.path)) { - this.callbacks.get(file.path)?.push(callback); + return; } else { - this.callbacks.set(file.path, [callback]); + this.callbacks.set(file.path, callback); } - // const promise: Promise = new Promise((resolve, reject) => { - // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); - // else this.callbacks.set(file.path, [[resolve, reject]]); - // }); - // Immediately run this task if there are available workers; otherwise, add it to the queue. const workerId = this.nextAvailableWorker(); if (workerId !== undefined) { @@ -76,44 +63,19 @@ export class FileLintManager { } else { this.lintQueue.push(file); } + } + + public terminateWorkers(): void { + for (const worker of this.workers) { + worker.terminate(); + } - // return promise; + this.workers = []; } - /** - * Queue the given file for reloading. Multiple reload requests for the same file in a short time period will be de-bounced - * and all be resolved by a single actual file reload. - */ - // public reload(file: TFile): Promise { - // const promise: Promise = new Promise((resolve, reject) => { - // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); - // else this.callbacks.set(file.path, [[resolve, reject]]); - // }); - - // // De-bounce repeated requests for the same file. - // if (this.reloadSet.has(file.path)) return promise; - // this.reloadSet.add(file.path); - - // // Immediately run this task if there are available workers; otherwise, add it to the queue. - // const workerId = this.nextAvailableWorker(); - // if (workerId !== undefined) { - // this.send(file, workerId); - // } else { - // this.reloadQueue.push(file); - // } - - // return promise; - // } // Finish the parsing of a file, potentially queueing a new file. private finish(data: RunLinterRulesOptions, index: number) { - // Cache the callbacks before we do book-keeping. - // const calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); - - // Book-keeping to clear metadata & allow the file to be re-loaded again. - // this.reloadSet.delete(path); - // this.callbacks.delete(path); - // Notify the queue this file is available for new work. this.busy[index] = false; @@ -130,7 +92,7 @@ export class FileLintManager { // run lint actions related to moment and other areas that cannot be run in the worker let currentTime = moment(); currentTime.locale(data.momentLocale); - // let currentTime = runOptions.getCurrentTime(); + // run YAML timestamp at the end to help determine if something has changed let isYamlTimestampEnabled: boolean; let newText: string; @@ -152,21 +114,16 @@ export class FileLintManager { dateModifiedKey: yamlTimestampOptions.dateModifiedKey, }); - // TODO: add the callback resolution here... if (this.callbacks.has(data.fileInfo.path)) { - const callbacks = this.callbacks.get(data.fileInfo.path); + const callback = this.callbacks.get(data.fileInfo.path); this.callbacks.delete(data.fileInfo.path); - for (const callback of callbacks) { - callback(data.oldText, newText); - } - } + data.newText = newText; + callback(data); - // Resolve promises to let users know this file has finished. - // if ('$error' in data) { - // for (const [_, reject] of calls) reject(data['$error']); - // } else { - // for (const [callback, _] of calls) callback(data); - // } + // TODO: see about hashing the file contents here if possible, but custom commands may make this not viable + // likely needs to be called at the end of run custom commands since that waits until the cache is ready after + // the file is updated by the regular lint + } } // /** Send a new task to the given worker ID. */ diff --git a/src/rules-runner/rules-runner.ts b/src/rules-runner/rules-runner.ts index 5acdb4c3..623aab20 100644 --- a/src/rules-runner/rules-runner.ts +++ b/src/rules-runner/rules-runner.ts @@ -1,6 +1,6 @@ import {moment} from 'obsidian'; import {logDebug, logWarn, timingBegin, timingEnd} from '../utils/logger'; -import {getDisabledRules, rules, wrapLintError, RuleType} from '../rules'; +import {rules, wrapLintError, RuleType} from '../rules'; import BlockquotifyOnPaste from '../rules/blockquotify-on-paste'; import EscapeYamlSpecialCharacters from '../rules/escape-yaml-special-characters'; import ForceYamlEscape from '../rules/force-yaml-escape'; @@ -26,161 +26,150 @@ import {LinterSettings} from '../settings-data'; import {RunLinterRulesOptions, TFile} from '../typings/worker'; import TrailingSpaces from '../rules/trailing-spaces'; -export class RulesRunner { - private disabledRules: string[] = []; - skipFile: boolean; - - lintText(runOptions: RunLinterRulesOptions): string { - this.skipFile = false; - const originalText = runOptions.oldText; - [this.disabledRules, this.skipFile] = getDisabledRules(originalText); - runOptions.skipFile = this.skipFile; - runOptions.disabledRules = this.disabledRules; - if (this.skipFile) { - return originalText; +/** + * Lints the text provided in @runOptions. + * @param {RunLinterRulesOptions} runOptions the different options provided when linting text + * @return {string} the text after all of the updates have been made. + */ +export function lintText(runOptions: RunLinterRulesOptions): string { + timingBegin(getTextInLanguage('logs.rule-running')); + + const preRuleText = getTextInLanguage('logs.pre-rules'); + timingBegin(preRuleText); + let newText = runBeforeRegularRules(runOptions); + timingEnd(preRuleText); + + const disabledRuleText = getTextInLanguage('logs.disabled-text'); + for (const rule of rules) { + // if you are run prior to or after the regular rules or are a disabled rule, skip running the rule + if (runOptions.disabledRules.includes(rule.alias)) { + logDebug(rule.alias + ' ' + disabledRuleText); + continue; + } else if (rule.hasSpecialExecutionOrder || rule.type === RuleType.PASTE) { + continue; } - timingBegin(getTextInLanguage('logs.rule-running')); + [newText] = RuleBuilderBase.applyIfEnabledBase(rule, newText, runOptions.settings, { + fileCreatedTime: runOptions.fileInfo.createdAtFormatted, + fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, + fileName: runOptions.fileInfo.name, + minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, + aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle, + tagArrayStyle: runOptions.settings.commonStyles.tagArrayStyle, + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + removeUnnecessaryEscapeCharsForMultiLineArrays: runOptions.settings.commonStyles.removeUnnecessaryEscapeCharsForMultiLineArrays, + }); + } - const preRuleText = getTextInLanguage('logs.pre-rules'); - timingBegin(preRuleText); - let newText = this.runBeforeRegularRules(runOptions); - timingEnd(preRuleText); + const customRegexLogText = getTextInLanguage('logs.custom-regex'); + timingBegin(customRegexLogText); + newText = runCustomRegexReplacement(runOptions.settings.customRegexes, newText); + timingEnd(customRegexLogText); - const disabledRuleText = getTextInLanguage('logs.disabled-text'); - for (const rule of rules) { - // if you are run prior to or after the regular rules or are a disabled rule, skip running the rule - if (this.disabledRules.includes(rule.alias)) { - logDebug(rule.alias + ' ' + disabledRuleText); - continue; - } else if (rule.hasSpecialExecutionOrder || rule.type === RuleType.PASTE) { - continue; - } + return runAfterRegularRules(newText, runOptions); +} - [newText] = RuleBuilderBase.applyIfEnabledBase(rule, newText, runOptions.settings, { - fileCreatedTime: runOptions.fileInfo.createdAtFormatted, - fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, - fileName: runOptions.fileInfo.name, - minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, - aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle, - tagArrayStyle: runOptions.settings.commonStyles.tagArrayStyle, - defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, - removeUnnecessaryEscapeCharsForMultiLineArrays: runOptions.settings.commonStyles.removeUnnecessaryEscapeCharsForMultiLineArrays, - }); - } +function runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + // remove hashtags from tags before parsing yaml + [newText] = FormatTagsInYaml.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); - const customRegexLogText = getTextInLanguage('logs.custom-regex'); - timingBegin(customRegexLogText); - newText = this.runCustomRegexReplacement(runOptions.settings.customRegexes, newText); - timingEnd(customRegexLogText); + // escape YAML where possible before parsing yaml + [newText] = EscapeYamlSpecialCharacters.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules, { + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); - runOptions.oldText = newText; + [newText] = MoveMathBlockIndicatorsToOwnLine.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules, { + minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, + }); - return this.runAfterRegularRules(runOptions); - } + return newText; +} - private runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { - let newText = runOptions.oldText; - // remove hashtags from tags before parsing yaml - [newText] = FormatTagsInYaml.applyIfEnabled(newText, runOptions.settings, this.disabledRules); +function runAfterRegularRules(currentText: string, runOptions: RunLinterRulesOptions): string { + let newText = currentText; + const postRuleLogText = getTextInLanguage('logs.post-rules'); + timingBegin(postRuleLogText); + [newText] = CapitalizeHeadings.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); - // escape YAML where possible before parsing yaml - [newText] = EscapeYamlSpecialCharacters.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, - }); + [newText] = BlockquoteStyle.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); - [newText] = MoveMathBlockIndicatorsToOwnLine.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, - }); + [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules, { + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); - return newText; - } + [newText] = TrailingSpaces.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); - private runAfterRegularRules(runOptions: RunLinterRulesOptions): string { - let newText = runOptions.oldText; - const postRuleLogText = getTextInLanguage('logs.post-rules'); - timingBegin(postRuleLogText); - [newText] = CapitalizeHeadings.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + timingEnd(postRuleLogText); + timingEnd(getTextInLanguage('logs.rule-running')); + return newText; +} - [newText] = BlockquoteStyle.applyIfEnabled(newText, runOptions.settings, this.disabledRules); +function runCustomRegexReplacement(customRegexes: CustomReplace[], oldText: string): string { + return ignoreListOfTypes([IgnoreTypes.customIgnore], oldText, (text: string) => { + logDebug(getTextInLanguage('logs.running-custom-regex')); - [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, - }); + let newText = text; + for (const eachRegex of customRegexes) { + const findIsEmpty = eachRegex.find === undefined || eachRegex.find == '' || eachRegex.find === null; + const replaceIsEmpty = eachRegex.replace === undefined || eachRegex.replace === null; + if (findIsEmpty || replaceIsEmpty) { + continue; + } - [newText] = TrailingSpaces.applyIfEnabled(newText, runOptions.settings, this.disabledRules); + const regex = new RegExp(`${eachRegex.find}`, eachRegex.flags); + // make sure that characters are not string escaped unescape in the replace value to make sure things like \n and \t are correctly inserted + newText = newText.replace(regex, convertStringVersionOfEscapeCharactersToEscapeCharacters(eachRegex.replace)); + } - timingEnd(postRuleLogText); - timingEnd(getTextInLanguage('logs.rule-running')); return newText; + }); +} + +export function runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface, runOptions: RunLinterRulesOptions) { + if (runOptions.skipFile) { + return; } - runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface) { - if (this.skipFile) { - return; + logDebug(getTextInLanguage('logs.running-custom-lint-command')); + const commandsRun = new Set(); + for (const commandInfo of lintCommands) { + if (!commandInfo.id) { + continue; + } else if (commandsRun.has(commandInfo.id)) { + logWarn(getTextInLanguage('logs.custom-lint-duplicate-warning').replace('{COMMAND_NAME}', commandInfo.name)); + continue; } - logDebug(getTextInLanguage('logs.running-custom-lint-command')); - const commandsRun = new Set(); - for (const commandInfo of lintCommands) { - if (!commandInfo.id) { - continue; - } else if (commandsRun.has(commandInfo.id)) { - logWarn(getTextInLanguage('logs.custom-lint-duplicate-warning').replace('{COMMAND_NAME}', commandInfo.name)); - continue; - } - - try { - commandsRun.add(commandInfo.id); - commands.executeCommandById(commandInfo.id); - } catch (error) { - wrapLintError(error, `${getTextInLanguage('logs.custom-lint-error-message')} ${commandInfo.id}`); - } + try { + commandsRun.add(commandInfo.id); + commands.executeCommandById(commandInfo.id); + } catch (error) { + wrapLintError(error, `${getTextInLanguage('logs.custom-lint-error-message')} ${commandInfo.id}`); } } +} - runCustomRegexReplacement(customRegexes: CustomReplace[], oldText: string): string { - return ignoreListOfTypes([IgnoreTypes.customIgnore], oldText, (text: string) => { - logDebug(getTextInLanguage('logs.running-custom-regex')); - - let newText = text; - for (const eachRegex of customRegexes) { - const findIsEmpty = eachRegex.find === undefined || eachRegex.find == '' || eachRegex.find === null; - const replaceIsEmpty = eachRegex.replace === undefined || eachRegex.replace === null; - if (findIsEmpty || replaceIsEmpty) { - continue; - } - - const regex = new RegExp(`${eachRegex.find}`, eachRegex.flags); - // make sure that characters are not string escaped unescape in the replace value to make sure things like \n and \t are correctly inserted - newText = newText.replace(regex, convertStringVersionOfEscapeCharactersToEscapeCharacters(eachRegex.replace)); - } - - return newText; - }); - } - - runPasteLint(currentLine: string, selectedText: string, runOptions: RunLinterRulesOptions): string { - let newText = runOptions.oldText; +export function runPasteLint(currentLine: string, selectedText: string, runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; - [newText] = RemoveHyphensOnPaste.applyIfEnabled(newText, runOptions.settings, []); + [newText] = RemoveHyphensOnPaste.applyIfEnabled(newText, runOptions.settings, []); - [newText] = RemoveMultipleBlankLinesOnPaste.applyIfEnabled(newText, runOptions.settings, []); + [newText] = RemoveMultipleBlankLinesOnPaste.applyIfEnabled(newText, runOptions.settings, []); - [newText] = RemoveLeftoverFootnotesFromQuoteOnPaste.applyIfEnabled(newText, runOptions.settings, []); + [newText] = RemoveLeftoverFootnotesFromQuoteOnPaste.applyIfEnabled(newText, runOptions.settings, []); - [newText] = ProperEllipsisOnPaste.applyIfEnabled(newText, runOptions.settings, []); + [newText] = ProperEllipsisOnPaste.applyIfEnabled(newText, runOptions.settings, []); - [newText] = RemoveLeadingOrTrailingWhitespaceOnPaste.applyIfEnabled(newText, runOptions.settings, []); + [newText] = RemoveLeadingOrTrailingWhitespaceOnPaste.applyIfEnabled(newText, runOptions.settings, []); - [newText] = PreventDoubleChecklistIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine, selectedText: selectedText}); + [newText] = PreventDoubleChecklistIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine, selectedText: selectedText}); - [newText] = PreventDoubleListItemIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine, selectedText: selectedText}); + [newText] = PreventDoubleListItemIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine, selectedText: selectedText}); - [newText] = BlockquotifyOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); + [newText] = BlockquotifyOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); - return newText; - } + return newText; } export function createRunLinterRulesOptions(text: string, file: TFile = null, momentLocale: string, settings: LinterSettings): RunLinterRulesOptions { diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts index 2a731613..6ef6931a 100644 --- a/src/rules-runner/rules-runner.worker.ts +++ b/src/rules-runner/rules-runner.worker.ts @@ -1,31 +1,23 @@ -// here is a worker to sent data back and forth +// This worker here is designed expressly for the purpose of running lint rules as possible off of the main thread. import {clearLogs, logsFromLastRun, setCollectLogs, setLogLevel} from '../utils/logger'; -import {RulesRunner, WorkerMessage} from '../typings/worker'; - - -let rulesRunner: RulesRunner = null; -import('./rules-runner').then((mod: any) => { - rulesRunner = new mod.RulesRunner(); -}); - -// import {RulesRunner} from './rules-runner'; - - -self.document = { - // @ts-ignore this is meant for preventing an error on run, but it is not really needed beyond that - createElement: () => {}, -}; +import {WorkerMessage} from '../typings/worker'; +import {getDisabledRules} from '../rules'; +import {lintText} from './rules-runner'; onmessage = (event: WorkerMessage) => { - const oldText = event.data.oldText; - setLogLevel(event.data.settings.logLevel); setCollectLogs(event.data.settings.recordLintOnSaveLogs); clearLogs(); - event.data.newText = rulesRunner.lintText(event.data); - event.data.oldText = oldText; + const originalText = event.data.oldText; + const [disabledRules, skipFile] = getDisabledRules(originalText); + event.data.skipFile = skipFile; + event.data.disabledRules = disabledRules; + + if (!skipFile) { + event.data.newText = lintText(event.data); + } if (event.data.settings.recordLintOnSaveLogs) { event.data.logsFromRun = logsFromLastRun; diff --git a/src/typings/worker.ts b/src/typings/worker.ts index fb50023a..dacff4ef 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -63,6 +63,7 @@ export type WorkerResponseMessage = { export interface LinterWorker { postMessage: (data: WorkerArgs) => void; onmessage: (data: WorkerResponseMessage) => void; + terminate: () => void; } export type RunLinterRulesOptions = { @@ -82,7 +83,3 @@ type FileInfo = { createdAtFormatted: string, modifiedAtFormatted: string, } - -export interface RulesRunner { - lintText(runOptions: RunLinterRulesOptions): string -} From a8bb0747eb55f5f7631e7456723c342735292f40 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Sat, 16 Mar 2024 13:15:20 -0400 Subject: [PATCH 08/14] cleaned up some more logic including the removal of duplicate code and the old rules runner which should be replaced by the worker now --- src/main.ts | 69 ++++---- src/option.ts | 1 - src/rule-runner copy/file-lint-manager.ts | 128 -------------- src/rule-runner copy/rule-runner.worker.ts | 13 -- src/rule-runner copy/rules-runner.ts | 185 --------------------- src/rules-runner/file-lint-manager.ts | 19 ++- src/rules-runner/rules-runner.ts | 8 +- src/typings/worker.ts | 19 +-- 8 files changed, 49 insertions(+), 393 deletions(-) delete mode 100644 src/rule-runner copy/file-lint-manager.ts delete mode 100644 src/rule-runner copy/rule-runner.worker.ts delete mode 100644 src/rule-runner copy/rules-runner.ts diff --git a/src/main.ts b/src/main.ts index 4341deda..baec6ccf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,6 @@ import {logInfo, logError, logDebug, setLogLevel, logWarn, setCollectLogs, clear import {moment} from 'obsidian'; import './rules-registry'; import {iconInfo} from './ui/icons'; -import {createRunLinterRulesOptions, RulesRunner} from './rules-runner'; import {LinterError} from './linter-error'; import {LintConfirmationModal} from './ui/modals/lint-confirmation-modal'; import {SettingTab} from './ui/settings'; @@ -25,7 +24,7 @@ import {downloadMisspellings, readInMisspellingsFile} from './utils/auto-correct // import {LinterWorker, WorkerArgs, WorkerResponseMessage} from './typings/worker'; import {FileLintManager} from './rules-runner/file-lint-manager'; import {RunLinterRulesOptions} from './typings/worker'; -import {runCustomCommands} from './rules-runner/rules-runner'; +import {runCustomCommands, runPasteLint, createRunLinterRulesOptions} from './rules-runner/rules-runner'; // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43 const langToMomentLocale = { @@ -68,7 +67,6 @@ export default class LinterPlugin extends Plugin { private eventRefs: EventRef[] = []; private momentLocale: string; private isEnabled: boolean = true; - private rulesRunner = new RulesRunner(); private lastActiveFile: TFile; private overridePaste: boolean = false; private hasCustomCommands: boolean = false; @@ -76,7 +74,7 @@ export default class LinterPlugin extends Plugin { private originalSaveCallback?: (checking: boolean) => boolean | void = null; // The amount of files you can use editor lint on at once is pretty small, so we will use an array private editorLintFiles: TFile[] = []; - // the amount of files that can be linted as a file can be quite large, so we will want to use a set to make + // The amount of files that can be linted as a file can be quite large, so we will want to use a set to make // search and other operations faster private fileLintFiles: Set = new Set(); private customCommandsCallback: (file: TFile) => Promise = null; @@ -368,8 +366,7 @@ export default class LinterPlugin extends Plugin { if (this.editorLintFiles.includes(file)) { this.editorLintFiles.remove(file); - // TODO: come back and fix this since I need a way to pass over the options for this pjk - this.runCustomCommands(file, null); + this.runCustomCommands(file); } else if (this.fileLintFiles.has(file)) { this.fileLintFiles.delete(file); @@ -504,29 +501,33 @@ export default class LinterPlugin extends Plugin { } async runLinterFile(file: TFile, lintingLastActiveFile: boolean = false) { - const oldText = stripCr(await this.app.vault.read(file)); - const newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); + // const newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); + this.lintFileManager.lintFile(file, async (runOptions: RunLinterRulesOptions) => { + if (runOptions.oldText != runOptions.newText) { + await this.app.vault.modify(file, runOptions.newText); + + if (lintingLastActiveFile) { + const message = getTextInLanguage('logs.file-change-lint-message-start') + ' ' + this.lastActiveFile.path; + if (this.settings.displayLintOnFileChangeNotice) { + new Notice(message); + } - if (oldText != newText) { - await this.app.vault.modify(file, newText); + logInfo(message); + } - if (lintingLastActiveFile) { - const message = getTextInLanguage('logs.file-change-lint-message-start') + ' ' + this.lastActiveFile.path; - if (this.settings.displayLintOnFileChangeNotice) { - new Notice(message); + if (!runOptions.skipFile) { + // when a change is made to the file we know that the cache will update down the road + // so we can defer running the custom commands to the cache callback + this.fileLintFiles.add(file); } - logInfo(message); + return; } - // when a change is made to the file we know that the cache will update down the road - // so we can defer running the custom commands to the cache callback - this.fileLintFiles.add(file); - - return; - } - - await this.runCustomCommandsInSidebar(file); + if (!runOptions.skipFile) { + await this.runCustomCommandsInSidebar(file); + } + }); } async runLinterAllFiles(app: App) { @@ -604,17 +605,18 @@ export default class LinterPlugin extends Plugin { this.displayChangedMessage(charsAdded, charsRemoved); - // run custom commands now since no change was made - if (!charsAdded && !charsRemoved) { - void this.runCustomCommands(file); - } else { - this.updateFileDebouncerText(file, runOptions.newText); - this.editorLintFiles.push(file); + if (!runOptions.skipFile) { + // run custom commands now since no change was made + if (!charsAdded && !charsRemoved) { + void this.runCustomCommands(file); + } else { + this.updateFileDebouncerText(file, runOptions.newText); + this.editorLintFiles.push(file); + } } setCollectLogs(false); }); - // newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings)); } catch (error) { this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); return; @@ -972,7 +974,7 @@ export default class LinterPlugin extends Plugin { const cursorSelections = editor.listSelections(); if (cursorSelections.length === 1) { const cursorSelection = cursorSelections[0]; - clipboardText = this.rulesRunner.runPasteLint(this.getLineContent(editor, cursorSelection), + clipboardText = runPasteLint(this.getLineContent(editor, cursorSelection), editor.getSelection() ?? '', createRunLinterRulesOptions(clipboardText, null, this.momentLocale, this.settings, null), ); @@ -994,7 +996,8 @@ export default class LinterPlugin extends Plugin { const editorChange: EditorChange[] = []; cursorSelections.forEach((cursorSelection: EditorSelection, index: number) => { - clipboardText = this.rulesRunner.runPasteLint(this.getLineContent(editor, cursorSelection), editor.getRange(cursorSelection.anchor, cursorSelection.head) ?? '', createRunLinterRulesOptions(pasteContentPerCursor[index], null, this.momentLocale, this.settings, null)); + // clipboardText = this.rulesRunner.runPasteLint(this.getLineContent(editor, cursorSelection), editor.getRange(cursorSelection.anchor, cursorSelection.head) ?? '', createRunLinterRulesOptions(pasteContentPerCursor[index], null, this.momentLocale, this.settings, null)); + clipboardText = runPasteLint(this.getLineContent(editor, cursorSelection), editor.getRange(cursorSelection.anchor, cursorSelection.head) ?? '', createRunLinterRulesOptions(pasteContentPerCursor[index], null, this.momentLocale, this.settings)); editorChange.push({ text: clipboardText, from: cursorSelection.anchor, @@ -1086,7 +1089,7 @@ export default class LinterPlugin extends Plugin { await this.customCommandsLock.acquire('command', async () => { try { - runCustomCommands(this.settings.lintCommands, this.app.commands, runOptions); + runCustomCommands(this.settings.lintCommands, this.app.commands); } catch (error) { this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); } diff --git a/src/option.ts b/src/option.ts index 2d49c0d3..a50d31b4 100644 --- a/src/option.ts +++ b/src/option.ts @@ -8,7 +8,6 @@ import {AutoCorrectFilesPickerOption} from './ui/linter-components/auto-correct- export type SearchOptionInfo = {name: string, description: string, options?: DropdownRecord[]} /** Class representing an option of a rule */ - export abstract class Option { public ruleAlias: string; protected setting: Setting; diff --git a/src/rule-runner copy/file-lint-manager.ts b/src/rule-runner copy/file-lint-manager.ts deleted file mode 100644 index 46bb2c00..00000000 --- a/src/rule-runner copy/file-lint-manager.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** Makes sure that files are linted and that they are handled appropriately in an asynchronous manner. */ - -// @ts-ignore because this is a web worker and it does not play well with Typescript checking -import Worker from './rule-runner.worker'; -import {TFile, Vault} from 'obsidian'; -import {createRunLinterRulesOptions} from '../rules-runner'; -import {LinterSettings} from 'src/rules'; - -/** Callback when a file is resolved. */ -// type FileCallback = (p: any) => void; - -/** Multi-threaded file linter which debounces rapid file requests automatically. */ -export class FileLintManager { - /* Background workers which do the actual file parsing. */ - workers: Worker[]; - /** Tracks which workers are actively parsing a file, to make sure we properly delegate results. */ - busy: boolean[]; - - /** List of files which have been queued for to be linted */ - lintQueue: TFile[]; - /** Fast-access set which holds the list of files queued to be linted; used for debouncing. */ - reloadSet: Set; - /** Paths -> promises for file reloads which have not yet been queued. */ - // callbacks: Map; - - public constructor(public numWorkers: number, public momentLocale: string, private settings: LinterSettings, private vault: Vault) { - this.workers = []; - this.busy = []; - - this.lintQueue = []; - this.reloadSet = new Set(); - // this.callbacks = new Map(); - - for (let index = 0; index < numWorkers; index++) { - // eslint-disable-next-line new-cap - const worker = Worker(); - worker.onmessage = (data: string) => { - console.log(data); - this.busy[index] = false; - }; - // worker.postMessage('first message'); - this.workers.push(worker); - // this.register(() => worker.terminate()); - this.busy.push(false); - } - } - - public lintFile(file: TFile): void { - // const promise: Promise = new Promise((resolve, reject) => { - // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); - // else this.callbacks.set(file.path, [[resolve, reject]]); - // }); - - // Immediately run this task if there are available workers; otherwise, add it to the queue. - const workerId = this.nextAvailableWorker(); - if (workerId !== undefined) { - this.send(file, workerId); - } else { - this.lintQueue.push(file); - } - - // return promise; - } - - /** - * Queue the given file for reloading. Multiple reload requests for the same file in a short time period will be de-bounced - * and all be resolved by a single actual file reload. - */ - // public reload(file: TFile): Promise { - // const promise: Promise = new Promise((resolve, reject) => { - // if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); - // else this.callbacks.set(file.path, [[resolve, reject]]); - // }); - - // // De-bounce repeated requests for the same file. - // if (this.reloadSet.has(file.path)) return promise; - // this.reloadSet.add(file.path); - - // // Immediately run this task if there are available workers; otherwise, add it to the queue. - // const workerId = this.nextAvailableWorker(); - // if (workerId !== undefined) { - // this.send(file, workerId); - // } else { - // this.reloadQueue.push(file); - // } - - // return promise; - // } - - /** Finish the parsing of a file, potentially queueing a new file. */ - // private finish(path: string, data: any, index: number) { - // // Cache the callbacks before we do book-keeping. - // const calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); - - // // Book-keeping to clear metadata & allow the file to be re-loaded again. - // this.reloadSet.delete(path); - // this.callbacks.delete(path); - - // // Notify the queue this file is available for new work. - // this.busy[index] = false; - - // // Queue a new job onto this worker. - // const job = this.reloadQueue.shift(); - // if (job !== undefined) this.send(job, index); - - // // Resolve promises to let users know this file has finished. - // if ('$error' in data) { - // for (const [_, reject] of calls) reject(data['$error']); - // } else { - // for (const [callback, _] of calls) callback(data); - // } - // } - - // /** Send a new task to the given worker ID. */ - private send(file: TFile, workerId: number) { - this.busy[workerId] = true; - this.vault.cachedRead(file).then((oldText: string) => { - const lintRunnerSettings = createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings); - this.workers[workerId].postMessage(lintRunnerSettings); - }); - } - - // /** Find the next available, non-busy worker; return undefined if all workers are busy. */ - private nextAvailableWorker(): number | undefined { - const index = this.busy.indexOf(false); - return index == -1 ? undefined : index; - } -} diff --git a/src/rule-runner copy/rule-runner.worker.ts b/src/rule-runner copy/rule-runner.worker.ts deleted file mode 100644 index eacf55b9..00000000 --- a/src/rule-runner copy/rule-runner.worker.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {RulesRunner} from './rules-runner'; - -const ruleRunner = new RulesRunner(); - -onmessage = (e) => { - const originalText = e.data.oldText; - const newText = ruleRunner.lintText(e.data); - postMessage({ - originalText: originalText, - newText: newText, - ruleSettings: e.data, - }); -}; diff --git a/src/rule-runner copy/rules-runner.ts b/src/rule-runner copy/rules-runner.ts deleted file mode 100644 index cfbcb3af..00000000 --- a/src/rule-runner copy/rules-runner.ts +++ /dev/null @@ -1,185 +0,0 @@ -// import {TFile, moment} from 'obsidian'; -import {logDebug, logWarn} from '../logger'; -import {getDisabledRules, LinterSettings, rules, wrapLintError, LintCommand, RuleType} from '../rules'; -import BlockquotifyOnPaste from '../rules/blockquotify-on-paste'; -import EscapeYamlSpecialCharacters from '../rules/escape-yaml-special-characters'; -import ForceYamlEscape from '../rules/force-yaml-escape'; -import FormatTagsInYaml from '../rules/format-tags-in-yaml'; -import PreventDoubleChecklistIndicatorOnPaste from '../rules/prevent-double-checklist-indicator-on-paste'; -import PreventDoubleListItemIndicatorOnPaste from '../rules/prevent-double-list-item-indicator-on-paste'; -import ProperEllipsisOnPaste from '../rules/proper-ellipsis-on-paste'; -import RemoveHyphensOnPaste from '../rules/remove-hyphens-on-paste'; -import RemoveLeadingOrTrailingWhitespaceOnPaste from '../rules/remove-leading-or-trailing-whitespace-on-paste'; -import RemoveLeftoverFootnotesFromQuoteOnPaste from '../rules/remove-leftover-footnotes-from-quote-on-paste'; -import RemoveMultipleBlankLinesOnPaste from '../rules/remove-multiple-blank-lines-on-paste'; -import {RuleBuilderBase} from '../rules/rule-builder'; -// import YamlKeySort from '../rules/yaml-key-sort'; -// import YamlTimestamp from '../rules/yaml-timestamp'; -import {ObsidianCommandInterface} from '../typings/obsidian-ex'; - -export type RunLinterRulesOptions = { - oldText: string, - fileInfo: FileInfo, - settings: LinterSettings, - momentLocale: string, - // getCurrentTime: () => moment.Moment -} - -type FileInfo = { - name: string, - createdAtFormatted: string, - modifiedAtFormatted: string, -} - -export class RulesRunner { - private disabledRules: string[] = []; - - lintText(runOptions: RunLinterRulesOptions): string { - const originalText = runOptions.oldText; - this.disabledRules = getDisabledRules(originalText); - - let newText = this.runBeforeRegularRules(runOptions); - - for (const rule of rules) { - // if you are run prior to or after the regular rules or are a disabled rule, skip running the rule - if (this.disabledRules.includes(rule.alias())) { - logDebug(rule.alias() + ' is disabled'); - continue; - } else if (rule.hasSpecialExecutionOrder || rule.type === RuleType.PASTE) { - continue; - } - - [newText] = RuleBuilderBase.applyIfEnabledBase(rule, newText, runOptions.settings, { - fileCreatedTime: runOptions.fileInfo.createdAtFormatted, - fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, - fileName: runOptions.fileInfo.name, - locale: runOptions.momentLocale, - minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, - aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle, - tagArrayStyle: runOptions.settings.commonStyles.tagArrayStyle, - defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, - }); - } - - runOptions.oldText = newText; - - return this.runAfterRegularRules(originalText, runOptions); - } - - private runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { - let newText = runOptions.oldText; - // remove hashtags from tags before parsing yaml - [newText] = FormatTagsInYaml.applyIfEnabled(newText, runOptions.settings, this.disabledRules); - - // escape YAML where possible before parsing yaml - [newText] = EscapeYamlSpecialCharacters.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, - }); - - return newText; - } - - private runAfterRegularRules(originalText: string, runOptions: RunLinterRulesOptions): string { - let newText = runOptions.oldText; - - [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, - }); - - return newText; - } - - // runRulesThatCannotRunInWebWorker(originalText: string, runOptions: RunLinterRulesOptions, getCurrentTime: () => moment.Moment): string { - // let newText = runOptions.oldText; - // let currentTime = getCurrentTime(); - // // run yaml timestamp at the end to help determine if something has changed - // let isYamlTimestampEnabled; - // [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - // fileCreatedTime: runOptions.fileInfo.createdAtFormatted, - // fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, - // currentTime: currentTime, - // alreadyModified: originalText != newText, - // locale: runOptions.momentLocale, - // }); - - // const yamlTimestampOptions = YamlTimestamp.getRuleOptions(runOptions.settings); - - // currentTime = getCurrentTime(); - // [newText] = YamlKeySort.applyIfEnabled(newText, runOptions.settings, this.disabledRules, { - // currentTimeFormatted: currentTime.format(yamlTimestampOptions.format), - // yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, - // dateModifiedKey: yamlTimestampOptions.dateModifiedKey, - // }); - - // return newText; - // } - - runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface) { - // execute custom commands after regular rules, but before the timestamp rules - logDebug(`Running Custom Lint Commands`); - const commandsRun = new Set(); - for (const commandInfo of lintCommands) { - if (!commandInfo.id) { - continue; - } else if (commandsRun.has(commandInfo.id)) { - logWarn(`You cannot run the same command ("${commandInfo.name}") as a custom lint rule twice.`); - continue; - } - - try { - commandsRun.add(commandInfo.id); - commands.executeCommandById(commandInfo.id); - } catch (error) { - wrapLintError(error, `Custom Lint Command ${commandInfo.id}`); - } - } - } - - runPasteLint(currentLine: string, runOptions: RunLinterRulesOptions): string { - let newText = runOptions.oldText; - - [newText] = RemoveHyphensOnPaste.applyIfEnabled(newText, runOptions.settings, []); - - [newText] = RemoveMultipleBlankLinesOnPaste.applyIfEnabled(newText, runOptions.settings, []); - - [newText] = RemoveLeftoverFootnotesFromQuoteOnPaste.applyIfEnabled(newText, runOptions.settings, []); - - [newText] = ProperEllipsisOnPaste.applyIfEnabled(newText, runOptions.settings, []); - - [newText] = RemoveLeadingOrTrailingWhitespaceOnPaste.applyIfEnabled(newText, runOptions.settings, []); - - [newText] = PreventDoubleChecklistIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); - - [newText] = PreventDoubleListItemIndicatorOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); - - [newText] = BlockquotifyOnPaste.applyIfEnabled(newText, runOptions.settings, [], {lineContent: currentLine}); - - return newText; - } -} - -// export function createRunLinterRulesOptions(text: string, file: TFile = null, momentLocale: string, settings: LinterSettings): RunLinterRulesOptions { -// const createdAt = file ? moment(file.stat.ctime): moment(); -// createdAt.locale(momentLocale); -// const modifiedAt = file ? moment(file.stat.mtime): moment(); -// modifiedAt.locale(momentLocale); -// const modifiedAtTime = modifiedAt.format(); -// const createdAtTime = createdAt.format(); - -// // const currentTime = moment(); -// // currentTime.locale(momentLocale); - -// return { -// oldText: text, -// fileInfo: { -// name: file ? file.basename: '', -// createdAtFormatted: createdAtTime, -// modifiedAtFormatted: modifiedAtTime, -// }, -// settings: settings, -// momentLocale: momentLocale, -// // getCurrentTime: () => { -// // return undefined; -// // }, -// }; -// } diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts index 30a63e7d..8a42812b 100644 --- a/src/rules-runner/file-lint-manager.ts +++ b/src/rules-runner/file-lint-manager.ts @@ -11,18 +11,19 @@ import {LinterWorker, RunLinterRulesOptions} from '../typings/worker'; import YamlTimestamp from '../rules/yaml-timestamp'; import YamlKeySort from '../rules/yaml-key-sort'; import {setLogs} from '../utils/logger'; +import {stripCr} from '../utils/strings'; /** Callback when a file is resolved. */ type FileCallback = (runOptions: RunLinterRulesOptions) => void; /** Multi-threaded file linter which debounces rapid file requests automatically. */ export class FileLintManager { - /* Background workers which do the actual file parsing. */ + /* Background workers which do the actual file linting. */ workers: LinterWorker[]; - /** Tracks which workers are actively parsing a file, to make sure we properly delegate results. */ + /** Tracks which workers are actively linting a file, to make sure we properly delegate results. */ busy: boolean[]; - /** List of files which have been queued for to be linted */ + /** List of files which have been queued to be linted */ lintQueue: TFile[]; /** Paths -> callback function to run once file linting has finished running rules. * Note: this does not mean that the logic for running custom commands has run. @@ -39,8 +40,8 @@ export class FileLintManager { for (let index = 0; index < numWorkers; index++) { // eslint-disable-next-line new-cap const worker = Worker(); - worker.onmessage = (resp: any) => { - this.finish(resp.data as RunLinterRulesOptions, index); + worker.onmessage = async (resp: any) => { + await this.finish(resp.data as RunLinterRulesOptions, index); }; this.workers.push(worker); @@ -75,7 +76,7 @@ export class FileLintManager { // Finish the parsing of a file, potentially queueing a new file. - private finish(data: RunLinterRulesOptions, index: number) { + private async finish(data: RunLinterRulesOptions, index: number) { // Notify the queue this file is available for new work. this.busy[index] = false; @@ -118,7 +119,7 @@ export class FileLintManager { const callback = this.callbacks.get(data.fileInfo.path); this.callbacks.delete(data.fileInfo.path); data.newText = newText; - callback(data); + await callback(data); // TODO: see about hashing the file contents here if possible, but custom commands may make this not viable // likely needs to be called at the end of run custom commands since that waits until the cache is ready after @@ -129,8 +130,8 @@ export class FileLintManager { // /** Send a new task to the given worker ID. */ private send(file: TFile, workerId: number) { this.busy[workerId] = true; - this.vault.cachedRead(file).then((oldText: string) => { - const lintRunnerSettings = createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings); + this.vault.read(file).then((oldText: string) => { + const lintRunnerSettings = createRunLinterRulesOptions(stripCr(oldText), file, this.momentLocale, this.settings); this.workers[workerId].postMessage(lintRunnerSettings); }); } diff --git a/src/rules-runner/rules-runner.ts b/src/rules-runner/rules-runner.ts index 623aab20..ae8f20c9 100644 --- a/src/rules-runner/rules-runner.ts +++ b/src/rules-runner/rules-runner.ts @@ -27,7 +27,7 @@ import {RunLinterRulesOptions, TFile} from '../typings/worker'; import TrailingSpaces from '../rules/trailing-spaces'; /** - * Lints the text provided in @runOptions. + * Lints the text provided in runOptions. * @param {RunLinterRulesOptions} runOptions the different options provided when linting text * @return {string} the text after all of the updates have been made. */ @@ -126,11 +126,7 @@ function runCustomRegexReplacement(customRegexes: CustomReplace[], oldText: stri }); } -export function runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface, runOptions: RunLinterRulesOptions) { - if (runOptions.skipFile) { - return; - } - +export function runCustomCommands(lintCommands: LintCommand[], commands: ObsidianCommandInterface) { logDebug(getTextInLanguage('logs.running-custom-lint-command')); const commandsRun = new Set(); for (const commandInfo of lintCommands) { diff --git a/src/typings/worker.ts b/src/typings/worker.ts index dacff4ef..3045bad2 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -35,33 +35,16 @@ export interface TFile { path: string; } -export type WorkerArgs = { - oldText: string, - fileInfo: FileInfo, - settings: LinterSettings, - skipFile: boolean, - disabledRules: string[], -} - export type WorkerMessage = { data: RunLinterRulesOptions, } -export type WorkerResponse = { - oldText: string, - newText: string, - fileInfo: FileInfo, - settings: LinterSettings, - skipFile: boolean, - disabledRules: string[], -} - export type WorkerResponseMessage = { data: RunLinterRulesOptions, } export interface LinterWorker { - postMessage: (data: WorkerArgs) => void; + postMessage: (data: RunLinterRulesOptions) => void; onmessage: (data: WorkerResponseMessage) => void; terminate: () => void; } From 2230588b282115ea64df6e87e854eebbe7b5604c Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Wed, 21 Aug 2024 19:23:34 -0400 Subject: [PATCH 09/14] added a fix for yaml timestamp always running --- src/rules-runner/file-lint-manager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts index 8a42812b..d0639ed6 100644 --- a/src/rules-runner/file-lint-manager.ts +++ b/src/rules-runner/file-lint-manager.ts @@ -101,12 +101,11 @@ export class FileLintManager { fileCreatedTime: data.fileInfo.createdAtFormatted, fileModifiedTime: data.fileInfo.modifiedAtFormatted, currentTime: currentTime, - alreadyModified: data.oldText != newText, + alreadyModified: data.oldText != data.newText, locale: data.momentLocale, }); const yamlTimestampOptions = YamlTimestamp.getRuleOptions(data.settings); - currentTime = moment(); currentTime.locale(data.momentLocale); [newText] = YamlKeySort.applyIfEnabled(newText, data.settings, data.disabledRules, { From 675a4957d4394e34084e154ec9727d49c9efae13 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Tue, 13 May 2025 19:31:17 -0400 Subject: [PATCH 10/14] updated esbuild to get the web worker running again --- esbuild.config.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 9e3f0fd3..8c101696 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -70,9 +70,21 @@ const webWorkerIgnores = [replace({ // update usage of moment from obsidian to the node implementation of moment we have 'import {moment} from \'obsidian\';': '', // remove the use of obsidian in the options to allow for docs.js to run - 'import {Setting} from \'obsidian\';': '', + 'import {App, Setting, ToggleComponent} from \'obsidian\';': '', // remove the use of obsidian in settings helper to allow for docs.js to run - 'import {Component, MarkdownRenderer} from \'obsidian\';': '', + 'import {App, MarkdownRenderer} from \'obsidian\';': '', + // remove the use of obsidian in the auto-correct files picker to allow for docs.js to run + 'import {Setting, App, TFile, normalizePath, ExtraButtonComponent} from \'obsidian\';': '', + // remove the use of obsidian in add custom row to allow for docs.js to run + 'import {App, Setting} from \'obsidian\';': '', + // remove the use of obsidian in suggest to allow for docs.js to run + 'import {App, ISuggestOwner, Scope} from \'obsidian\';': '', + // remove the use of obsidian in md file suggester to allow for docs.js to run + 'import {App, TFile} from \'obsidian\';': '', + // remove the use of obsidian in parse results modal to allow for docs.js to run + 'import {Modal, App} from \'obsidian\';': 'class Modal {}', + // remove the use of app from a couple of settings for docs.js to run + 'import {App} from \'obsidian\';': '', }, delimiters: ['', ''], })]; From 4e782e0477facff74312d247d31bbba3df85acab Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Tue, 13 May 2025 21:49:08 -0400 Subject: [PATCH 11/14] cleaned up some logic, filled in some blanks, and got more logic added --- esbuild.config.mjs | 1 + src/main.ts | 18 ++++++++------ src/rules-runner/file-lint-manager.ts | 9 +++++-- src/rules-runner/rules-runner.ts | 8 +++++- src/rules-runner/rules-runner.worker.ts | 1 + src/rules-runner/yaml-timestamp-by-itself.ts | 26 ++++++++++++++++++++ src/typings/worker.ts | 1 + 7 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 src/rules-runner/yaml-timestamp-by-itself.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 8c101696..27782652 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -85,6 +85,7 @@ const webWorkerIgnores = [replace({ 'import {Modal, App} from \'obsidian\';': 'class Modal {}', // remove the use of app from a couple of settings for docs.js to run 'import {App} from \'obsidian\';': '', + 'new Worker(url)': 'new Worker(url, \'linter-worker\')' }, delimiters: ['', ''], })]; diff --git a/src/main.ts b/src/main.ts index baec6ccf..6e5dc040 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,7 @@ import {downloadMisspellings, readInMisspellingsFile} from './utils/auto-correct import {FileLintManager} from './rules-runner/file-lint-manager'; import {RunLinterRulesOptions} from './typings/worker'; import {runCustomCommands, runPasteLint, createRunLinterRulesOptions} from './rules-runner/rules-runner'; +import {runYAMLTimestampByItself} from './rules-runner/yaml-timestamp-by-itself'; // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43 const langToMomentLocale = { @@ -366,7 +367,7 @@ export default class LinterPlugin extends Plugin { if (this.editorLintFiles.includes(file)) { this.editorLintFiles.remove(file); - this.runCustomCommands(file); + void this.runCustomCommands(file); } else if (this.fileLintFiles.has(file)) { this.fileLintFiles.delete(file); @@ -397,6 +398,8 @@ export default class LinterPlugin extends Plugin { this.defaultAutoCorrectMisspellings = parseCustomReplacements(stripCr(await readInMisspellingsFile(this))); + this.lintFileManager.setDefaultMisspellings(this.defaultAutoCorrectMisspellings); + // load custom-auto-correct replacements if they exist for (const replacementFileInfo of this.settings.ruleConfigs['auto-correct-common-misspellings']['extra-auto-correct-files'] ?? [] as CustomAutoCorrectContent[]) { if (replacementFileInfo.filePath != '') { @@ -598,7 +601,7 @@ export default class LinterPlugin extends Plugin { const file = this.app.workspace.getActiveFile(); try { // newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); - this.lintFileManager.lintFile(file, (runOptions: RunLinterRulesOptions) => { + this.lintFileManager.lintFile(file, async (runOptions: RunLinterRulesOptions) => { const changes = this.updateEditor(runOptions.oldText, runOptions.newText, editor); const charsAdded = changes.map((change) => change[0] == DiffMatchPatch.DIFF_INSERT ? change[1].length : 0).reduce((a, b) => a + b, 0); const charsRemoved = changes.map((change) => change[0] == DiffMatchPatch.DIFF_DELETE ? change[1].length : 0).reduce((a, b) => a + b, 0); @@ -808,10 +811,10 @@ export default class LinterPlugin extends Plugin { oldText = editorValue; let newText = oldText; - if (oldText != activeFileChangeInfo.originalText ) { + if (oldText != activeFileChangeInfo.originalText) { logInfo(getTextInLanguage('logs.file-change-yaml-lint-run')); try { - newText = this.rulesRunner.runYAMLTimestampByItself(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, null)); + newText = runYAMLTimestampByItself(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, null)); } catch (error) { this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); return; @@ -849,7 +852,7 @@ export default class LinterPlugin extends Plugin { await this.app.vault.process(file, (data: string) => { logInfo(getTextInLanguage('logs.file-change-yaml-lint-run')); try { - return this.rulesRunner.runYAMLTimestampByItself(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, null)); + return runYAMLTimestampByItself(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, null)); } catch (error) { this.handleLintError(file, error, getTextInLanguage('commands.lint-file.error-message') + ' \'{FILE_PATH}\'', false); return data; @@ -996,8 +999,7 @@ export default class LinterPlugin extends Plugin { const editorChange: EditorChange[] = []; cursorSelections.forEach((cursorSelection: EditorSelection, index: number) => { - // clipboardText = this.rulesRunner.runPasteLint(this.getLineContent(editor, cursorSelection), editor.getRange(cursorSelection.anchor, cursorSelection.head) ?? '', createRunLinterRulesOptions(pasteContentPerCursor[index], null, this.momentLocale, this.settings, null)); - clipboardText = runPasteLint(this.getLineContent(editor, cursorSelection), editor.getRange(cursorSelection.anchor, cursorSelection.head) ?? '', createRunLinterRulesOptions(pasteContentPerCursor[index], null, this.momentLocale, this.settings)); + clipboardText = runPasteLint(this.getLineContent(editor, cursorSelection), editor.getRange(cursorSelection.anchor, cursorSelection.head) ?? '', createRunLinterRulesOptions(pasteContentPerCursor[index], null, this.momentLocale, this.settings, null)); editorChange.push({ text: clipboardText, from: cursorSelection.anchor, @@ -1082,7 +1084,7 @@ export default class LinterPlugin extends Plugin { this.currentlyOpeningSidebar = false; } - private async runCustomCommands(file: TFile, runOptions: RunLinterRulesOptions) { + private async runCustomCommands(file: TFile) { if (!this.settings.lintCommands || this.settings.lintCommands.length == 0 || !this.hasCustomCommands) { return; } diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts index d0639ed6..ef725408 100644 --- a/src/rules-runner/file-lint-manager.ts +++ b/src/rules-runner/file-lint-manager.ts @@ -29,6 +29,7 @@ export class FileLintManager { * Note: this does not mean that the logic for running custom commands has run. */ callbacks: Map; + defaultMisspellings: Map; public constructor(public numWorkers: number, public momentLocale: string, private settings: LinterSettings, private vault: Vault) { this.workers = []; @@ -49,6 +50,10 @@ export class FileLintManager { } } + public setDefaultMisspellings(defaultMisspellings: Map): void { + this.defaultMisspellings = defaultMisspellings; + } + public lintFile(file: TFile, callback: FileCallback): void { // if the file is already in the list of files to process, we should skip it if (this.callbacks.has(file.path)) { @@ -129,8 +134,8 @@ export class FileLintManager { // /** Send a new task to the given worker ID. */ private send(file: TFile, workerId: number) { this.busy[workerId] = true; - this.vault.read(file).then((oldText: string) => { - const lintRunnerSettings = createRunLinterRulesOptions(stripCr(oldText), file, this.momentLocale, this.settings); + void this.vault.read(file).then((oldText: string) => { + const lintRunnerSettings = createRunLinterRulesOptions(stripCr(oldText), file, this.momentLocale, this.settings, this.defaultMisspellings); this.workers[workerId].postMessage(lintRunnerSettings); }); } diff --git a/src/rules-runner/rules-runner.ts b/src/rules-runner/rules-runner.ts index ae8f20c9..feb97988 100644 --- a/src/rules-runner/rules-runner.ts +++ b/src/rules-runner/rules-runner.ts @@ -25,6 +25,7 @@ import MoveMathBlockIndicatorsToOwnLine from '../rules/move-math-block-indicator import {LinterSettings} from '../settings-data'; import {RunLinterRulesOptions, TFile} from '../typings/worker'; import TrailingSpaces from '../rules/trailing-spaces'; +import AutoCorrectCommonMisspellings from '../rules/auto-correct-common-misspellings'; /** * Lints the text provided in runOptions. @@ -83,6 +84,10 @@ function runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { minimumNumberOfDollarSignsToBeAMathBlock: runOptions.settings.commonStyles.minimumNumberOfDollarSignsToBeAMathBlock, }); + [newText] = AutoCorrectCommonMisspellings.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules, { + misspellingToCorrection: runOptions.defaultMisspellings, + }); + return newText; } @@ -168,7 +173,7 @@ export function runPasteLint(currentLine: string, selectedText: string, runOptio return newText; } -export function createRunLinterRulesOptions(text: string, file: TFile = null, momentLocale: string, settings: LinterSettings): RunLinterRulesOptions { +export function createRunLinterRulesOptions(text: string, file: TFile = null, momentLocale: string, settings: LinterSettings, defaultMisspellings: Map): RunLinterRulesOptions { const createdAt = file ? moment(file.stat.ctime): moment(); createdAt.locale(momentLocale); const modifiedAt = file ? moment(file.stat.mtime): moment(); @@ -190,5 +195,6 @@ export function createRunLinterRulesOptions(text: string, file: TFile = null, mo skipFile: false, disabledRules: [], logsFromRun: [], + defaultMisspellings: defaultMisspellings, }; } diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts index 6ef6931a..8148dd4f 100644 --- a/src/rules-runner/rules-runner.worker.ts +++ b/src/rules-runner/rules-runner.worker.ts @@ -24,4 +24,5 @@ onmessage = (event: WorkerMessage) => { } postMessage(event.data); + clearLogs(); }; diff --git a/src/rules-runner/yaml-timestamp-by-itself.ts b/src/rules-runner/yaml-timestamp-by-itself.ts new file mode 100644 index 00000000..b15b688e --- /dev/null +++ b/src/rules-runner/yaml-timestamp-by-itself.ts @@ -0,0 +1,26 @@ +import {moment} from 'obsidian'; +import YamlTimestamp from '../rules/yaml-timestamp'; +import {RunLinterRulesOptions} from '../typings/worker'; +import {getDisabledRules} from '../rules'; + +export function runYAMLTimestampByItself(runOptions: RunLinterRulesOptions): string { + let newText = runOptions.oldText; + + const currentTime = moment(); + currentTime.locale(runOptions.momentLocale); + + const [disabledRules, skipFile] = getDisabledRules(runOptions.oldText); + if (skipFile) { + return newText; + } + + [newText] = YamlTimestamp.applyIfEnabled(newText, runOptions.settings, disabledRules, { + fileCreatedTime: runOptions.fileInfo.createdAtFormatted, + fileModifiedTime: runOptions.fileInfo.modifiedAtFormatted, + currentTime: currentTime, + alreadyModified: true, + locale: runOptions.momentLocale, + }); + + return newText; +} diff --git a/src/typings/worker.ts b/src/typings/worker.ts index 3045bad2..87e467e7 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -58,6 +58,7 @@ export type RunLinterRulesOptions = { skipFile: boolean, disabledRules: string[], logsFromRun: string[], + defaultMisspellings: Map, } type FileInfo = { From 42c1e7c343ec259b47445c6ec7784d0a8afef761 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Wed, 14 May 2025 00:16:01 -0400 Subject: [PATCH 12/14] update YAML error to handl to the new one used by the library, fix an issue with open file having changed to no longer default to making the opened file active, and other small changes --- src/main.ts | 8 ++--- src/rules-runner/file-lint-manager.ts | 48 +++++++++++++------------ src/rules-runner/rules-runner.worker.ts | 1 + 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/main.ts b/src/main.ts index 6e5dc040..45f784ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -504,13 +504,12 @@ export default class LinterPlugin extends Plugin { } async runLinterFile(file: TFile, lintingLastActiveFile: boolean = false) { - // const newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings, this.defaultAutoCorrectMisspellings)); this.lintFileManager.lintFile(file, async (runOptions: RunLinterRulesOptions) => { if (runOptions.oldText != runOptions.newText) { await this.app.vault.modify(file, runOptions.newText); if (lintingLastActiveFile) { - const message = getTextInLanguage('logs.file-change-lint-message-start') + ' ' + this.lastActiveFile.path; + const message = getTextInLanguage('logs.file-change-lint-message-start') + ' ' + file.path; if (this.settings.displayLintOnFileChangeNotice) { new Notice(message); } @@ -520,7 +519,7 @@ export default class LinterPlugin extends Plugin { if (!runOptions.skipFile) { // when a change is made to the file we know that the cache will update down the road - // so we can defer running the custom commands to the cache callback + // so we can defer running the custom commands to the cache callback this.fileLintFiles.add(file); } @@ -1071,7 +1070,8 @@ export default class LinterPlugin extends Plugin { this.currentlyOpeningSidebar = true; await sidebarTab.openFile(file, {active: true}); - this.rulesRunner.runCustomCommands(this.settings.lintCommands, this.app.commands); + + runCustomCommands(this.settings.lintCommands, this.app.commands); if (this.customCommandsCallback) { await this.customCommandsCallback(file); } diff --git a/src/rules-runner/file-lint-manager.ts b/src/rules-runner/file-lint-manager.ts index ef725408..a35462d5 100644 --- a/src/rules-runner/file-lint-manager.ts +++ b/src/rules-runner/file-lint-manager.ts @@ -95,29 +95,31 @@ export class FileLintManager { setLogs(data.logsFromRun); } - // run lint actions related to moment and other areas that cannot be run in the worker - let currentTime = moment(); - currentTime.locale(data.momentLocale); - - // run YAML timestamp at the end to help determine if something has changed - let isYamlTimestampEnabled: boolean; - let newText: string; - [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(data.newText, data.settings, data.disabledRules, { - fileCreatedTime: data.fileInfo.createdAtFormatted, - fileModifiedTime: data.fileInfo.modifiedAtFormatted, - currentTime: currentTime, - alreadyModified: data.oldText != data.newText, - locale: data.momentLocale, - }); - - const yamlTimestampOptions = YamlTimestamp.getRuleOptions(data.settings); - currentTime = moment(); - currentTime.locale(data.momentLocale); - [newText] = YamlKeySort.applyIfEnabled(newText, data.settings, data.disabledRules, { - currentTimeFormatted: currentTime.format(yamlTimestampOptions.format.trimEnd()), - yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, - dateModifiedKey: yamlTimestampOptions.dateModifiedKey, - }); + let newText = data.newText; + if (!data.skipFile) { + // run lint actions related to moment and other areas that cannot be run in the worker + let currentTime = moment(); + currentTime.locale(data.momentLocale); + + // run YAML timestamp at the end to help determine if something has changed + let isYamlTimestampEnabled: boolean; + [newText, isYamlTimestampEnabled] = YamlTimestamp.applyIfEnabled(data.newText, data.settings, data.disabledRules, { + fileCreatedTime: data.fileInfo.createdAtFormatted, + fileModifiedTime: data.fileInfo.modifiedAtFormatted, + currentTime: currentTime, + alreadyModified: data.oldText != data.newText, + locale: data.momentLocale, + }); + + const yamlTimestampOptions = YamlTimestamp.getRuleOptions(data.settings); + currentTime = moment(); + currentTime.locale(data.momentLocale); + [newText] = YamlKeySort.applyIfEnabled(newText, data.settings, data.disabledRules, { + currentTimeFormatted: currentTime.format(yamlTimestampOptions.format.trimEnd()), + yamlTimestampDateModifiedEnabled: isYamlTimestampEnabled && yamlTimestampOptions.dateModified, + dateModifiedKey: yamlTimestampOptions.dateModifiedKey, + }); + } if (this.callbacks.has(data.fileInfo.path)) { const callback = this.callbacks.get(data.fileInfo.path); diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts index 8148dd4f..c20edc9e 100644 --- a/src/rules-runner/rules-runner.worker.ts +++ b/src/rules-runner/rules-runner.worker.ts @@ -15,6 +15,7 @@ onmessage = (event: WorkerMessage) => { event.data.skipFile = skipFile; event.data.disabledRules = disabledRules; + event.data.newText = event.data.oldText; if (!skipFile) { event.data.newText = lintText(event.data); } From cf7ddd5cfb7df4b8b8e12581d19564482456e278 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Wed, 14 May 2025 00:45:59 -0400 Subject: [PATCH 13/14] got all rules importing and running inside of the web worker --- esbuild.config.mjs | 19 +++++++++++++++++-- src/rules-runner/rules-runner.worker.ts | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 27782652..ce4c26bd 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -85,7 +85,22 @@ const webWorkerIgnores = [replace({ 'import {Modal, App} from \'obsidian\';': 'class Modal {}', // remove the use of app from a couple of settings for docs.js to run 'import {App} from \'obsidian\';': '', - 'new Worker(url)': 'new Worker(url, \'linter-worker\')' + // remove values for examples as they are not necessary in the actual plugin when it goes out to users + 'abstract get exampleBuilders(): ExampleBuilder[];': '', + // removes eslint disabling that was just meant for examples + '/* eslint-disable no-tabs */': '', + '/* eslint-disable no-mixed-spaces-and-tabs, no-tabs */': '', + // remove eslint enabling that was just meant for examples + '/* eslint-enable no-tabs */': '', + '/* eslint-enable no-mixed-spaces-and-tabs, no-tabs */': '', + // add the multiline comment to remove the examples + 'get exampleBuilders():': '/*', + // add the ending of the multiline comment that will remove the examples + '}\n get optionBuilders()': '*/ get optionBuilders()', + // removes the logic that adds the examples to the rule + 'builder.exampleBuilders.map((b) => b.example),': '', + // removes the expectation that examples will exist on the rule class + 'public examples: Array,': '', }, delimiters: ['', ''], })]; @@ -109,7 +124,7 @@ const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) { }, external: externalPackages, format: 'cjs', - plugins: [...webWorkerIgnores], + plugins: [importGlobPlugin.default(), ...webWorkerIgnores], }), ...extraPlugins, ], diff --git a/src/rules-runner/rules-runner.worker.ts b/src/rules-runner/rules-runner.worker.ts index c20edc9e..2e60dcaf 100644 --- a/src/rules-runner/rules-runner.worker.ts +++ b/src/rules-runner/rules-runner.worker.ts @@ -4,6 +4,7 @@ import {clearLogs, logsFromLastRun, setCollectLogs, setLogLevel} from '../utils/ import {WorkerMessage} from '../typings/worker'; import {getDisabledRules} from '../rules'; import {lintText} from './rules-runner'; +import '../rules-registry'; onmessage = (event: WorkerMessage) => { setLogLevel(event.data.settings.logLevel); From d582d1033c2a0efeaa49a73ddf45e32c5a7827c6 Mon Sep 17 00:00:00 2001 From: Peter Kaufman Date: Fri, 7 Nov 2025 15:08:57 -0500 Subject: [PATCH 14/14] include the latest version of running rules information in the new rules runner --- package-lock.json | 4 ++-- src/rules-runner/rules-runner.ts | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf9388de..d637717c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-linter", - "version": "1.29.2", + "version": "1.30.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-linter", - "version": "1.29.2", + "version": "1.30.0", "license": "MIT", "dependencies": { "@popperjs/core": "^2.11.6", diff --git a/src/rules-runner/rules-runner.ts b/src/rules-runner/rules-runner.ts index feb97988..8ad9eacc 100644 --- a/src/rules-runner/rules-runner.ts +++ b/src/rules-runner/rules-runner.ts @@ -26,6 +26,11 @@ import {LinterSettings} from '../settings-data'; import {RunLinterRulesOptions, TFile} from '../typings/worker'; import TrailingSpaces from '../rules/trailing-spaces'; import AutoCorrectCommonMisspellings from '../rules/auto-correct-common-misspellings'; +import YamlTitle from 'src/rules/yaml-title'; +import YamlTitleAlias from 'src/rules/yaml-title-alias'; +import ConsecutiveBlankLines from 'src/rules/consecutive-blank-lines'; +import {yamlRegex} from 'src/utils/regex'; +import AddBlankLineAfterYAML from 'src/rules/add-blank-line-after-yaml'; /** * Lints the text provided in runOptions. @@ -92,11 +97,23 @@ function runBeforeRegularRules(runOptions: RunLinterRulesOptions): string { } function runAfterRegularRules(currentText: string, runOptions: RunLinterRulesOptions): string { - let newText = currentText; + let newText = runOptions.oldText; const postRuleLogText = getTextInLanguage('logs.post-rules'); timingBegin(postRuleLogText); [newText] = CapitalizeHeadings.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); + [newText] = YamlTitle.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules, { + fileName: runOptions.fileInfo.name, + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + }); + + [newText] = YamlTitleAlias.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules, { + fileName: runOptions.fileInfo.name, + aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle, + defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter, + removeUnnecessaryEscapeCharsForMultiLineArrays: runOptions.settings.commonStyles.removeUnnecessaryEscapeCharsForMultiLineArrays, + }); + [newText] = BlockquoteStyle.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); [newText] = ForceYamlEscape.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules, { @@ -105,6 +122,13 @@ function runAfterRegularRules(currentText: string, runOptions: RunLinterRulesOpt [newText] = TrailingSpaces.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); + [newText] = ConsecutiveBlankLines.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); + + const yaml = newText.match(yamlRegex); + if (yaml != null) { + [newText] = AddBlankLineAfterYAML.applyIfEnabled(newText, runOptions.settings, runOptions.disabledRules); + } + timingEnd(postRuleLogText); timingEnd(getTextInLanguage('logs.rule-running')); return newText;