From 9aedd86e00c4a4cbb354594b55be2b98c03c2ff1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Feb 2025 01:57:40 +0000 Subject: [PATCH 01/65] Bump serialize-javascript and mocha Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) to 6.0.2 and updates ancestor dependency [mocha](https://github.com/mochajs/mocha). These dependencies need to be updated together. Updates `serialize-javascript` from 6.0.0 to 6.0.2 - [Release notes](https://github.com/yahoo/serialize-javascript/releases) - [Commits](https://github.com/yahoo/serialize-javascript/compare/v6.0.0...v6.0.2) Updates `mocha` from 9.2.2 to 11.1.0 - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v9.2.2...v11.1.0) --- updated-dependencies: - dependency-name: serialize-javascript dependency-type: indirect - dependency-name: mocha dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 569 +++++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 387 insertions(+), 184 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c142795d..9ce83d760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "jsdom": "^21.1.0", "link-module-alias": "^1.2.0", "mini-css-extract-plugin": "^2.7.2", - "mocha": "^9.1.3", + "mocha": "^11.1.0", "mochapack": "^2.1.4", "node-loader": "^2.0.0", "ora": "^8.1.1", @@ -339,6 +339,109 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -801,6 +904,17 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1204,12 +1318,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -1588,10 +1696,11 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2136,27 +2245,33 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2579,10 +2694,11 @@ "optional": true }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2634,6 +2750,13 @@ "tslib": "^2.0.3" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron": { "version": "30.5.1", "resolved": "https://registry.npmjs.org/electron/-/electron-30.5.1.tgz", @@ -3340,6 +3463,23 @@ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", @@ -3692,15 +3832,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -4145,6 +4276,22 @@ "integrity": "sha512-ex9JyqY+tCjBlxN1pXlqxEgtGGUGp1TG83ll1xpu8SfPgOhfAhEGCuepNHlB+d7Le+hLoBcfCu/G0ZQaFbi9hA==", "dev": true }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -4837,151 +4984,99 @@ } }, "node_modules/mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "4.2.1", - "ms": "2.1.3", - "nanoid": "3.3.1", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", - "mocha": "bin/mocha" + "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { - "ms": "2.1.2" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=6.0" + "bin": { + "glob": "dist/esm/bin.mjs" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "node_modules/mocha/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" } }, "node_modules/mocha/node_modules/supports-color": { @@ -5343,18 +5438,6 @@ "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5867,6 +5950,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -5960,6 +6050,40 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6864,10 +6988,11 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -7073,6 +7198,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -7112,6 +7260,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -7347,15 +7509,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -8145,16 +8298,18 @@ "dev": true }, "node_modules/workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8167,17 +8322,60 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8234,6 +8432,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -8245,30 +8444,32 @@ "dev": true }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { @@ -8290,13 +8491,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", diff --git a/package.json b/package.json index 5045a536d..4e885eacc 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "jsdom": "^21.1.0", "link-module-alias": "^1.2.0", "mini-css-extract-plugin": "^2.7.2", - "mocha": "^9.1.3", + "mocha": "^11.1.0", "mochapack": "^2.1.4", "node-loader": "^2.0.0", "ora": "^8.1.1", From e4168c09500055b7190f2a43e8a358b1edfe03aa Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 15 Feb 2025 21:03:35 -0500 Subject: [PATCH 02/65] [Fix] potential memory leak --- src/base/common/event.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/base/common/event.ts b/src/base/common/event.ts index 7ae984280..0d0a66759 100644 --- a/src/base/common/event.ts +++ b/src/base/common/event.ts @@ -1,6 +1,6 @@ import type { IO } from "src/base/common/utilities/functional"; import { LinkedList } from "src/base/common/structures/linkedList"; -import { Disposable, DisposableBucket, IDisposable, isDisposable, LooseDisposableBucket, toDisposable, untrackDisposable } from "src/base/common/dispose"; +import { Disposable, DisposableBucket, IDisposable, isDisposable, LooseDisposableBucket, safeDisposable, toDisposable, untrackDisposable } from "src/base/common/dispose"; import { ErrorHandler } from "src/base/common/error"; import { panic } from "src/base/common/utilities/panic"; import { PriorityQueue } from "src/base/common/structures/priorityQueue"; @@ -184,7 +184,7 @@ export interface IEmitterOptions { // region - AbstractEmitter -interface IListenerContainer extends Iterable { +interface IListenerContainer extends Iterable, IDisposable { size(): number; add(listener: TListener): any; remove(token: any): void; @@ -355,6 +355,7 @@ export class Emitter extends AbstractEmitter, __Listener> i empty: () => list.empty(), size: () => list.size(), [Symbol.iterator]: list[Symbol.iterator].bind(list), + dispose: () => list.clear(), }; } } @@ -763,9 +764,9 @@ export class PriorityEmitter extends AbstractEmitter, __Priori } protected override __constructContainer(): IListenerContainer<__PriorityListener> { - const pq = new PriorityQueue<__PriorityListener>( + const pq = safeDisposable(new PriorityQueue<__PriorityListener>( (a, b) => b.priority - a.priority // higher number, higher priority - ); + )); return { add: (listener) => { pq.enqueue(listener); return listener; }, remove: (token) => pq.remove(token), @@ -773,6 +774,7 @@ export class PriorityEmitter extends AbstractEmitter, __Priori empty: () => pq.empty(), size: () => pq.size(), [Symbol.iterator]: pq[Symbol.iterator].bind(pq), + dispose: () => pq.dispose(), }; } } From a69f3c66c7781c768e3a044cf545ebd077fa917f Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 15 Feb 2025 21:09:04 -0500 Subject: [PATCH 03/65] [Env] update version to 0.7.1 --- assets/locale/en.json | 2 +- assets/locale/zh-cn.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- product.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/locale/en.json b/assets/locale/en.json index 5b6693ac9..00b43d23a 100644 --- a/assets/locale/en.json +++ b/assets/locale/en.json @@ -5,7 +5,7 @@ "--------------------------------------------------------------------------------------------", "Do not edit this file. It is machine generated." ], - "version": "0.7.0", + "version": "0.7.1", "contents": { "editor/common/markdown": { "code": "Code", diff --git a/assets/locale/zh-cn.json b/assets/locale/zh-cn.json index 0ddf6f066..bfcd370f8 100644 --- a/assets/locale/zh-cn.json +++ b/assets/locale/zh-cn.json @@ -5,7 +5,7 @@ "--------------------------------------------------------------------------------------------", "Do not edit this file. It is machine generated." ], - "version": "0.7.0", + "version": "0.7.1", "contents": { "platform/i18n/browser/i18nService": { "relaunchDisplayLanguageMessage": "重新启动 {name} 以切换到语言:{languageName}?" diff --git a/package-lock.json b/package-lock.json index 1c142795d..c52ffc0d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nota", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nota", - "version": "0.7.0", + "version": "0.7.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5045a536d..617d0e1d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nota", - "version": "0.7.0", + "version": "0.7.1", "description": "A cross-platform markdown note-taking app.", "main": "./dist/main-bundle.js", "type": "commonjs", diff --git a/product.json b/product.json index fa318055d..083606386 100644 --- a/product.json +++ b/product.json @@ -8,6 +8,6 @@ "projectName": "nota", "applicationName": "nota", "description": "A cross-platform markdown note-taking app.", - "version": "0.7.0", + "version": "0.7.1", "license": "MIT" } \ No newline at end of file From 79fc50e6e9b666a30185ed6705ff8a20aa8f4e50 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Mon, 17 Feb 2025 16:36:34 -0500 Subject: [PATCH 04/65] [Refactor] extract `LOCALIZE_REGEX` to a separate module --- scripts/i18n/i18n.js | 8 ++------ scripts/i18n/localizeRegExp.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 scripts/i18n/localizeRegExp.js diff --git a/scripts/i18n/i18n.js b/scripts/i18n/i18n.js index 8a1e1be2a..5d4ff201e 100644 --- a/scripts/i18n/i18n.js +++ b/scripts/i18n/i18n.js @@ -1,6 +1,7 @@ const fs = require('fs'); const path = require('path'); -const { log, SmartRegExp } = require('../utility'); +const { log } = require('../utility'); +const { LOCALIZE_REGEX } = require('./localizeRegExp'); /** * {@link localizationGenerator} @@ -350,11 +351,6 @@ class localizationGenerator { } #parseFile(filePath) { - const LOCALIZE_REGEX = - new SmartRegExp(/localize\(quote(str)quote,\s*quote(str)quote[\),]/g) - .replace('str', /.*?/) - .replace('quote', /["'`]/) - .get(); let fileContent = fs.readFileSync(filePath, 'utf-8'); fileContent = removeComments(fileContent); diff --git a/scripts/i18n/localizeRegExp.js b/scripts/i18n/localizeRegExp.js new file mode 100644 index 000000000..f2e360ee4 --- /dev/null +++ b/scripts/i18n/localizeRegExp.js @@ -0,0 +1,10 @@ +const { SmartRegExp } = require('../utility'); + + +const LOCALIZE_REGEX = + new SmartRegExp(/localize\(quote(str)quote,\s*quote(str)quote[\),]/g) + .replace('str', /.*?/) + .replace('quote', /["'`]/) + .get(); + +module.exports = { LOCALIZE_REGEX }; \ No newline at end of file From 2100bff4cb9b7bd397ca7e7f09620989c2b4d240 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Mon, 17 Feb 2025 17:35:06 -0500 Subject: [PATCH 05/65] [Refactor] encapsulate into `BlockInsertPalette` for quick document block insertion --- .../slashCommandExtension.ts | 235 ++------------- .../blockInsertPalette/blockInsertPlette.ts | 268 ++++++++++++++++++ 2 files changed, 293 insertions(+), 210 deletions(-) create mode 100644 src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts diff --git a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts b/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts index a71b3d16d..05cec340f 100644 --- a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts +++ b/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts @@ -1,21 +1,16 @@ import "src/editor/contrib/slashCommandExtension/slashCommand.scss"; -import { AnchorPrimaryAxisAlignment, AnchorVerticalPosition } from "src/base/browser/basic/contextMenu/contextMenu"; -import { MenuAction, MenuItemType, SimpleMenuAction, SubmenuAction } from "src/base/browser/basic/menu/menuItem"; -import { IPosition } from "src/base/common/utilities/size"; +import { MenuItemType } from "src/base/browser/basic/menu/menuItem"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { IEditorWidget } from "src/editor/editorWidget"; import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; -import { IContextMenuService } from "src/workbench/services/contextMenu/contextMenuService"; -import { Disposable, DisposableBucket, IDisposable, safeDisposable } from "src/base/common/dispose"; +import { DisposableBucket, IDisposable, safeDisposable } from "src/base/common/dispose"; import { KeyCode } from "src/base/common/keyboard"; -import { ProseAttrs, ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; -import { Emitter, Priority } from "src/base/common/event"; -import { getTokenReadableName, Markdown, TokenEnum } from "src/editor/common/markdown"; -import { II18nService } from "src/platform/i18n/browser/i18nService"; -import { Arrays } from "src/base/common/utilities/array"; -import { ErrorHandler } from "src/base/common/error"; +import { ProseEditorView } from "src/editor/common/proseMirror"; +import { Priority } from "src/base/common/event"; +import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPlette"; +import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; interface IEditorSlashCommandExtension extends IEditorExtension { @@ -29,35 +24,25 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi // [fields] public override readonly id = EditorExtensionIDs.SlashCommand; - private readonly _menuRenderer: SlashMenuRenderer; - private readonly _menuController: SlashMenuController; + + private readonly _palette: BlockInsertPalette; private readonly _keyboardController: SlashKeyboardController; constructor( editorWidget: IEditorWidget, - @IContextMenuService contextMenuService: IContextMenuService, - @II18nService i18nService: II18nService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(editorWidget); - this._keyboardController = this.__register(new SlashKeyboardController(this, contextMenuService)); - this._menuController = new SlashMenuController(editorWidget); - this._menuRenderer = this.__register(new SlashMenuRenderer(editorWidget, contextMenuService, i18nService)); + this._palette = this.__register(instantiationService.createInstance(BlockInsertPalette, editorWidget)); + this._keyboardController = this.__register(new SlashKeyboardController(this, this._palette)); // slash-command rendering this.__register(this.onTextInput(e => { this.__tryShowSlashCommand(e); })); - // always back to normal - this.__register(this._menuRenderer.onMenuDestroy(() => { + this.__register(this._palette.onMenuDestroy(() => { this._keyboardController.unlisten(); - editorWidget.view.editor.focus(); - })); - - // menu click logic - this.__register(this._menuRenderer.onClick(e => { - contextMenuService.contextMenu.destroy(); - this._menuController.onClick(e); })); } @@ -80,7 +65,7 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi // show slash command const position = view.coordsAtPos(selection.$from.pos); - this._menuRenderer.show(position); + this._palette.render(position); // re-focus back to editor, not the slash command. view.focus(); @@ -104,7 +89,7 @@ class SlashKeyboardController implements IDisposable { constructor( private readonly extension: EditorSlashCommandExtension, - private readonly contextMenuService: IContextMenuService, + private readonly palette: BlockInsertPalette, ) { this._triggeredNode = undefined; } @@ -151,24 +136,24 @@ class SlashKeyboardController implements IDisposable { // escape: destroy the slash command if (pressed === KeyCode.Escape) { - this.contextMenuService.contextMenu.destroy(); + this.palette.destroy(); } // handle up/down arrows else if (pressed === KeyCode.UpArrow) { - this.contextMenuService.contextMenu.focusPrev(); + this.palette.focusPrev(); } else if (pressed === KeyCode.DownArrow) { - this.contextMenuService.contextMenu.focusNext(); + this.palette.focusNext(); } // handle right/left arrows else if (pressed === KeyCode.RightArrow || pressed === KeyCode.LeftArrow) { - const index = this.contextMenuService.contextMenu.getFocus(); - const currAction = this.contextMenuService.contextMenu.getAction(index); + const index = this.palette.getFocus(); + const currAction = this.palette.getAction(index); if (!currAction || currAction.type !== MenuItemType.Submenu) { return false; } if (pressed === KeyCode.RightArrow) { - const opened = this.contextMenuService.contextMenu.tryOpenSubmenu(); + const opened = this.palette.tryOpenSubmenu(); return opened; } else { return false; @@ -176,12 +161,12 @@ class SlashKeyboardController implements IDisposable { } // enter else if (pressed === KeyCode.Enter) { - const hasFocus = this.contextMenuService.contextMenu.hasFocus(); + const hasFocus = this.palette.hasFocus(); if (!hasFocus) { - this.contextMenuService.contextMenu.destroy(); + this.palette.destroy(); return false; } - this.contextMenuService.contextMenu.runFocus(); + this.palette.runFocus(); } // make sure to re-focus back to editor @@ -201,7 +186,7 @@ class SlashKeyboardController implements IDisposable { const { $from } = view.state.selection; const isEmptyBlock = ProseTools.Node.isEmptyTextBlock($from.parent); if (isEmptyBlock) { - this.contextMenuService.contextMenu.destroy(); + this.palette.destroy(); } })); @@ -209,7 +194,7 @@ class SlashKeyboardController implements IDisposable { * Destroy slash command whenever the selection changes to other blocks. */ bucket.register(this.extension.onDidSelectionChange(e => { - const menu = this.contextMenuService.contextMenu; + const menu = this.palette; const triggeredNode = this._triggeredNode; if (!triggeredNode) { return; @@ -241,174 +226,4 @@ class SlashKeyboardController implements IDisposable { nodeType: $pos.parent.type.name, }; } -} - -// region - SlashMenuRenderer - -type SlashOnClickEvent = { - readonly type: string; - readonly name: string; - readonly attr: ProseAttrs; -}; - -class SlashMenuRenderer extends Disposable { - - private readonly _onMenuDestroy = this.__register(new Emitter()); - public readonly onMenuDestroy = this._onMenuDestroy.registerListener; - - private readonly _onClick = this.__register(new Emitter()); - public readonly onClick = this._onClick.registerListener; - - constructor( - private readonly editorWidget: IEditorWidget, - private readonly contextMenuService: IContextMenuService, - private readonly i18nService: II18nService, - ) { - super(); - } - - public show(position?: IPosition): void { - if (!position) { - return; - } - const { overlayContainer: parentElement } = this.editorWidget.view.editor; - - this.contextMenuService.showContextMenuCustom({ - getActions: () => this.__obtainSlashCommandContent(), - getContext: () => undefined, - getAnchor: () => ({ x: position.left, y: position.top, height: 24 }), - getExtraContextMenuClassName: () => 'editor-slash-command', - primaryAlignment: AnchorPrimaryAxisAlignment.Vertical, - verticalPosition: AnchorVerticalPosition.Below, - - /** - * We need to capture the blur event to prevent auto destroy. We - * will handle destruction by ourselves. - */ - onBeforeDestroy: (cause) => { - const shouldPrevent = cause === 'blur'; - return shouldPrevent; - }, - // clean up - onDestroy: () => { - this._onMenuDestroy.fire(); - }, - }, parentElement); - } - - private __obtainSlashCommandContent(): MenuAction[] { - const nodes = this.__obtainValidContent(); - - // convert each node into menu action - return nodes.map(nodeName => { - // heading: submenu - if (nodeName === TokenEnum.Heading) { - return this.__getHeadingActions(); - } - // general case - const resolvedName = getTokenReadableName(this.i18nService, nodeName); - return new SimpleMenuAction({ - enabled: true, - id: resolvedName, - callback: () => this._onClick.fire({ - type: nodeName, - name: resolvedName, - attr: {}, - }), - }); - }); - } - - private __obtainValidContent(): string[] { - const blocks = this.editorWidget.model.getRegisteredDocumentNodes(); - const ordered = this.__filterContent(blocks, CONTENT_FILTER); - return ordered; - } - - private __filterContent(unordered: string[], expectOrder: string[]): string[] { - const ordered: string[] = []; - const unorderedSet = new Set(unordered); - for (const name of expectOrder) { - if (unorderedSet.has(name)) { - ordered.push(name); - unorderedSet.delete(name); - } else { - console.warn(`[SlashCommandExtension] missing node: ${name}`); - } - } - return ordered; - } - - private __getHeadingActions(): SubmenuAction { - const heading = getTokenReadableName(this.i18nService, TokenEnum.Heading); - return new SubmenuAction( - Arrays.range(1, 7).map(level => this.__getHeadingAction(heading, level)), - { enabled: true, id: heading } - ); - } - - private __getHeadingAction(name: string, level: number): MenuAction { - return new SimpleMenuAction({ - enabled: true, - id: `${name} ${level}`, - callback: () => this._onClick.fire({ - type: TokenEnum.Heading, - name: name, - attr: { level: level }, - }), - }); - } -} - -const CONTENT_FILTER = [ - TokenEnum.Paragraph, - TokenEnum.Blockquote, - TokenEnum.Heading, - TokenEnum.Image, - TokenEnum.List, - TokenEnum.Table, - TokenEnum.CodeBlock, - TokenEnum.MathBlock, - TokenEnum.HTML, - TokenEnum.HorizontalRule, -]; - -// region - SlashMenuController - -class SlashMenuController { - - constructor( - private readonly editorWidget: IEditorWidget, - ) {} - - public onClick(event: SlashOnClickEvent): void { - const { type, name, attr } = event; - const view = this.editorWidget.view.editor.internalView; - const state = view.state; - let tr = state.tr; - - const prevStart = tr.selection.$from.start(); - const $pos = state.doc.resolve(prevStart); - - // create an empy node with given type - const node = Markdown.Create.empty(state, type, attr); - if (!node) { - ErrorHandler.onUnexpectedError(new Error(`Cannot create node (${name})`)); - return; - } - - // replace the current node with the new node - tr = ProseTools.Position.replaceWithNode(state, $pos, node); - - // find next selectable text - const newStart = tr.mapping.map(prevStart); - const $newStart = tr.doc.resolve(newStart + 1); - const selection = ProseTextSelection.findFrom($newStart, 1, true); - if (selection) { - tr = tr.setSelection(selection); - } - - // update - view.dispatch(tr); - } } \ No newline at end of file diff --git a/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts b/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts new file mode 100644 index 000000000..2f86db1b7 --- /dev/null +++ b/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts @@ -0,0 +1,268 @@ +import { AnchorPrimaryAxisAlignment, AnchorVerticalPosition } from "src/base/browser/basic/contextMenu/contextMenu"; +import { MenuAction, SimpleMenuAction, SubmenuAction } from "src/base/browser/basic/menu/menuItem"; +import { Disposable } from "src/base/common/dispose"; +import { ErrorHandler } from "src/base/common/error"; +import { Emitter } from "src/base/common/event"; +import { Arrays } from "src/base/common/utilities/array"; +import { IPosition } from "src/base/common/utilities/size"; +import { getTokenReadableName, Markdown, TokenEnum } from "src/editor/common/markdown"; +import { ProseAttrs, ProseTextSelection } from "src/editor/common/proseMirror"; +import { ProseTools } from "src/editor/common/proseUtility"; +import { IEditorWidget } from "src/editor/editorWidget"; +import { II18nService } from "src/platform/i18n/browser/i18nService"; +import { IContextMenuService } from "src/workbench/services/contextMenu/contextMenuService"; + +// region - BlockInsertPalette + +/** + * {@link BlockInsertPalette} A contextual command palette component that + * provides quick insertion of various document block types at the current + * cursor position. The palette renders as a vertical menu containing: + * + * - Common block types (paragraphs, headings, code blocks etc.) + * - Nested submenus for complex block variations + * - Localized display names based on document schema + */ +export class BlockInsertPalette extends Disposable { + + // [event] + + get onMenuDestroy() { return this._menuRenderer.onMenuDestroy; } + + // [field] + + private readonly _menuRenderer: SlashMenuRenderer; + private readonly _menuController: SlashMenuController; + + // [constructor] + + constructor( + editorWidget: IEditorWidget, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @II18nService i18nService: II18nService, + ) { + super(); + this._menuController = new SlashMenuController(editorWidget); + this._menuRenderer = this.__register(new SlashMenuRenderer(editorWidget, contextMenuService, i18nService)); + + // always back to normal + this.__register(this._menuRenderer.onMenuDestroy(() => { + editorWidget.view.editor.focus(); + })); + + // menu click logic + this.__register(this._menuRenderer.onClick(e => { + contextMenuService.contextMenu.destroy(); + this._menuController.onClick(e); + })); + } + + // [public methods] + + public render(position: IPosition): void { + this._menuRenderer.show(position); + } + + public focusPrev(): void { + this.contextMenuService.contextMenu.focusPrev(); + } + + public focusNext(): void { + this.contextMenuService.contextMenu.focusNext(); + } + + public getFocus(): number { + return this.contextMenuService.contextMenu.getFocus(); + } + + public getAction(indexOrID: number | string): MenuAction | undefined { + return this.contextMenuService.contextMenu.getAction(indexOrID); + } + + public tryOpenSubmenu(): boolean { + return this.contextMenuService.contextMenu.tryOpenSubmenu(); + } + + public hasFocus(): boolean { + return this.contextMenuService.contextMenu.hasFocus(); + } + + public destroy(): void { + this.contextMenuService.contextMenu.destroy(); + } + + public runFocus(): void { + this.contextMenuService.contextMenu.runFocus(); + } + +} + +// region - SlashMenuRenderer + +type SlashOnClickEvent = { + readonly type: string; + readonly name: string; + readonly attr: ProseAttrs; +}; + +class SlashMenuRenderer extends Disposable { + + private readonly _onMenuDestroy = this.__register(new Emitter()); + public readonly onMenuDestroy = this._onMenuDestroy.registerListener; + + private readonly _onClick = this.__register(new Emitter()); + public readonly onClick = this._onClick.registerListener; + + constructor( + private readonly editorWidget: IEditorWidget, + private readonly contextMenuService: IContextMenuService, + private readonly i18nService: II18nService, + ) { + super(); + } + + public show(position?: IPosition): void { + if (!position) { + return; + } + const { overlayContainer: parentElement } = this.editorWidget.view.editor; + + this.contextMenuService.showContextMenuCustom({ + getActions: () => this.__obtainSlashCommandContent(), + getContext: () => undefined, + getAnchor: () => ({ x: position.left, y: position.top, height: 24 }), + getExtraContextMenuClassName: () => 'editor-slash-command', + primaryAlignment: AnchorPrimaryAxisAlignment.Vertical, + verticalPosition: AnchorVerticalPosition.Below, + + /** + * We need to capture the blur event to prevent auto destroy. We + * will handle destruction by ourselves. + */ + onBeforeDestroy: (cause) => { + const shouldPrevent = cause === 'blur'; + return shouldPrevent; + }, + // clean up + onDestroy: () => { + this._onMenuDestroy.fire(); + }, + }, parentElement); + } + + private __obtainSlashCommandContent(): MenuAction[] { + const nodes = this.__obtainValidContent(); + + // convert each node into menu action + return nodes.map(nodeName => { + // heading: submenu + if (nodeName === TokenEnum.Heading) { + return this.__getHeadingActions(); + } + // general case + const resolvedName = getTokenReadableName(this.i18nService, nodeName); + return new SimpleMenuAction({ + enabled: true, + id: resolvedName, + callback: () => this._onClick.fire({ + type: nodeName, + name: resolvedName, + attr: {}, + }), + }); + }); + } + + private __obtainValidContent(): string[] { + const blocks = this.editorWidget.model.getRegisteredDocumentNodes(); + const ordered = this.__filterContent(blocks, CONTENT_FILTER); + return ordered; + } + + private __filterContent(unordered: string[], expectOrder: string[]): string[] { + const ordered: string[] = []; + const unorderedSet = new Set(unordered); + for (const name of expectOrder) { + if (unorderedSet.has(name)) { + ordered.push(name); + unorderedSet.delete(name); + } else { + console.warn(`[SlashCommandExtension] missing node: ${name}`); + } + } + return ordered; + } + + private __getHeadingActions(): SubmenuAction { + const heading = getTokenReadableName(this.i18nService, TokenEnum.Heading); + return new SubmenuAction( + Arrays.range(1, 7).map(level => this.__getHeadingAction(heading, level)), + { enabled: true, id: heading } + ); + } + + private __getHeadingAction(name: string, level: number): MenuAction { + return new SimpleMenuAction({ + enabled: true, + id: `${name} ${level}`, + callback: () => this._onClick.fire({ + type: TokenEnum.Heading, + name: name, + attr: { level: level }, + }), + }); + } +} + +const CONTENT_FILTER = [ + TokenEnum.Paragraph, + TokenEnum.Blockquote, + TokenEnum.Heading, + TokenEnum.Image, + TokenEnum.List, + TokenEnum.Table, + TokenEnum.CodeBlock, + TokenEnum.MathBlock, + TokenEnum.HTML, + TokenEnum.HorizontalRule, +]; + +// region - SlashMenuController + +class SlashMenuController { + + constructor( + private readonly editorWidget: IEditorWidget, + ) {} + + public onClick(event: SlashOnClickEvent): void { + const { type, name, attr } = event; + const view = this.editorWidget.view.editor.internalView; + const state = view.state; + let tr = state.tr; + + const prevStart = tr.selection.$from.start(); + const $pos = state.doc.resolve(prevStart); + + // create an empy node with given type + const node = Markdown.Create.empty(state, type, attr); + if (!node) { + ErrorHandler.onUnexpectedError(new Error(`Cannot create node (${name})`)); + return; + } + + // replace the current node with the new node + tr = ProseTools.Position.replaceWithNode(state, $pos, node); + + // find next selectable text + const newStart = tr.mapping.map(prevStart); + const $newStart = tr.doc.resolve(newStart + 1); + const selection = ProseTextSelection.findFrom($newStart, 1, true); + if (selection) { + tr = tr.setSelection(selection); + } + + // update + view.dispatch(tr); + } +} \ No newline at end of file From c70096a3eff040e532ce846e98aed8be8d4784f1 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Mon, 17 Feb 2025 22:55:59 -0500 Subject: [PATCH 06/65] [Refactor] remove `BlockHandleButton` and replace with `AbstractBlockHandleButton` for improved button hierarchy --- .../browser/secondary/widgetBar/widgetBar.ts | 1 + .../blockHandleExtension/blockHandleButton.ts | 11 -- .../blockHandleExtension.ts | 164 +++++++++++------- 3 files changed, 99 insertions(+), 77 deletions(-) delete mode 100644 src/editor/contrib/blockHandleExtension/blockHandleButton.ts diff --git a/src/base/browser/secondary/widgetBar/widgetBar.ts b/src/base/browser/secondary/widgetBar/widgetBar.ts index 9f3654864..10f2cc73a 100644 --- a/src/base/browser/secondary/widgetBar/widgetBar.ts +++ b/src/base/browser/secondary/widgetBar/widgetBar.ts @@ -42,6 +42,7 @@ export interface IWidgetBar extends IDisposable { * as default. * * @note Method will invoke `item.item.render()` automatically. + * @note The lifecycle of the provided item will be bond with the widget bar. */ addItem(item: IWidgetBarItem, index?: number): void; diff --git a/src/editor/contrib/blockHandleExtension/blockHandleButton.ts b/src/editor/contrib/blockHandleExtension/blockHandleButton.ts deleted file mode 100644 index 9d38d4c22..000000000 --- a/src/editor/contrib/blockHandleExtension/blockHandleButton.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Button, IButtonOptions } from "src/base/browser/basic/button/button"; - -export class BlockHandleButton extends Button { - - constructor(opts: IButtonOptions) { - super({ - ...opts, - classes: ['block-handle-button', ...(opts.classes ?? [])], - }); - } -} \ No newline at end of file diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts index 137990c3a..fad7faa6f 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts @@ -5,13 +5,13 @@ import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { IEditorWidget } from "src/editor/editorWidget"; import { IEditorMouseEvent } from "src/editor/view/proseEventBroadcaster"; import { IWidgetBar, WidgetBar } from "src/base/browser/secondary/widgetBar/widgetBar"; -import { BlockHandleButton } from "src/editor/contrib/blockHandleExtension/blockHandleButton"; import { addDisposableListener, EventType, Orientation } from "src/base/browser/basic/dom"; import { RequestAnimateController, requestAtNextAnimationFrame } from "src/base/browser/basic/animation"; import { Event } from "src/base/common/event"; import { ProseEditorView } from "src/editor/common/proseMirror"; import { EditorDragState, getDropExactPosition } from "src/editor/common/cursorDrop"; import { DisposableBucket } from "src/base/common/dispose"; +import { Button, IButtonOptions } from "src/base/browser/basic/button/button"; /** * An interface only for {@link EditorBlockHandleExtension}. @@ -27,8 +27,14 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit public readonly id = EditorExtensionIDs.BlockHandle; - private _currPosition?: number; - private _widget?: IWidgetBar; + /** + * When dragging, this indicates the current exact dropping position in + * ProseMirror document. + */ + public get currDropPosition() { return this._currDropPosition; } + private _currDropPosition?: number; + + private _widget?: IWidgetBar; private readonly _renderController: RequestAnimateController<{ event: IEditorMouseEvent }>; // [constructor] @@ -43,7 +49,7 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit // unrender cases this.__register(Event.any([this.onMouseLeave, this.onDidRender, this.onDidBlur])(() => { - this.__unrenderWidget(); + this.unrenderWidget(); })); // RENDER LOGIC @@ -58,18 +64,18 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit } // same position, do nothing. - if (this._currPosition === e.target.resolvedPosition) { + if (this.currDropPosition === e.target.resolvedPosition) { return; } // not the top-level node, do nothing. const pos = e.view.state.doc.resolve(e.target.resolvedPosition); - if (pos.depth !== 0) { + if (pos.depth !== 0) { // review: shouldn't it compare to 1? return; } - this.__unrenderWidget(); - this.__renderWidget(editorWidget.view.editor.overlayContainer, e.target.resolvedPosition, e.target.nodeElement); + this.unrenderWidget(); + this.renderWidget(editorWidget.view.editor.overlayContainer, e.target.resolvedPosition, e.target.nodeElement); })); } @@ -80,33 +86,23 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit protected override onViewDestroy(view: ProseEditorView): void { this.release(this._widget); this._widget = undefined; - this._currPosition = undefined; + this._currDropPosition = undefined; this._renderController.cancel(); } - // [private methods] - - private __renderWidgetWithoutTarget(e: IEditorMouseEvent): void { - const position = getDropExactPosition(e.view, e.event, true); - if (this._currPosition === position) { - return; - } - - const node = e.view.nodeDOM(position) as HTMLElement | undefined; - if (!node) { - return; - } + // [public methods] - this.__unrenderWidget(); - this.__renderWidget(this._editorWidget.view.editor.overlayContainer, position, node); + public unrenderWidget(): void { + this._widget?.unrender(); + this._currDropPosition = undefined; } - private __renderWidget(container: HTMLElement, nodePosition: number, nodeElement: HTMLElement): void { + public renderWidget(container: HTMLElement, nodePosition: number, nodeElement: HTMLElement): void { if (!this._widget) { return; } - this._currPosition = nodePosition; + this._currDropPosition = nodePosition; // render under the editor overlay this._widget.container.setLeft(nodeElement.offsetLeft - 55); @@ -120,90 +116,126 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit }); } - private __unrenderWidget(): void { - this._widget?.unrender(); - this._currPosition = undefined; + // [private methods] + + private __renderWidgetWithoutTarget(e: IEditorMouseEvent): void { + const position = getDropExactPosition(e.view, e.event, true); + if (this.currDropPosition === position) { + return; + } + + const node = e.view.nodeDOM(position) as HTMLElement | undefined; + if (!node) { + return; + } + + this.unrenderWidget(); + this.renderWidget(this._editorWidget.view.editor.overlayContainer, position, node); } - private __initWidget(view: ProseEditorView): IWidgetBar { - const widget = new WidgetBar('block-handle-widget', { + private __initWidget(view: ProseEditorView): IWidgetBar { + const widget = new WidgetBar('block-handle-widget', { orientation: Orientation.Horizontal, }); - const buttonsOptions = [ - { id: 'add-new-block', icon: Icons.AddNew, classes: ['add-new-block'] }, - ]; - - for (const { id, icon, classes } of buttonsOptions) { - const button = new BlockHandleButton({ id: id, icon: icon, classes: [...classes] }); - - widget.addItem({ - id: id, - data: button, - disposable: button, - }); - } + // add-new-block + const addButtonBucket = new DisposableBucket(); + const addButton = addButtonBucket.register(new AddBlockButton()); + widget.addItem({ + id: addButton.id, + data: addButton, + disposable: addButtonBucket, + }); + // drag-handle const dragButtonLifecycle = new DisposableBucket(); - const dragButton = dragButtonLifecycle.register(new DragHandleButton()); + const dragButton = dragButtonLifecycle.register(new DragHandleButton(view, this._editorWidget, this)); widget.addItem({ id: dragButton.id, data: dragButton, disposable: dragButtonLifecycle, }); - this.__initDragButton(view, dragButton, dragButtonLifecycle); return widget; } +} + +class AbstractBlockHandleButton extends Button { + + constructor(opts: IButtonOptions) { + super({ + ...opts, + classes: ['block-handle-button', ...(opts.classes ?? [])], + }); + } +} + +class DragHandleButton extends AbstractBlockHandleButton { + + constructor( + private readonly view: ProseEditorView, + private readonly editorWidget: IEditorWidget, + private readonly extension: EditorBlockHandleExtension, + ) { + super({ + id: 'drag-handle', + icon: Icons.DragHandle, + classes: ['add-new-block'], + }); + } - private __initDragButton(view: ProseEditorView, button: DragHandleButton, lifecycle: DisposableBucket): void { + protected override __render(element: HTMLElement): void { + super.__render(element); // tell the browser the button is draggable - button.element.draggable = true; + this.element.draggable = true; // on drag start - lifecycle.register(addDisposableListener(button.element, EventType.dragstart, e => { - if (e.dataTransfer === null || this._currPosition === undefined) { + this.__register(addDisposableListener(this.element, EventType.dragstart, e => { + const currDropPosition = this.extension.currDropPosition; + if (e.dataTransfer === null || currDropPosition === undefined) { return; } - this._editorWidget.updateContext('editorDragState', EditorDragState.Block); - button.element.classList.add('dragging'); + this.editorWidget.updateContext('editorDragState', EditorDragState.Block); + this.element.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; - const element = view.nodeDOM(this._currPosition) as HTMLElement; + const element = this.view.nodeDOM(currDropPosition) as HTMLElement; e.dataTransfer.setDragImage(element, 0, 0); - const dragPosition = this._currPosition.toString(); + const dragPosition = currDropPosition.toString(); e.dataTransfer.setData('$nota-editor-block-handle', dragPosition); })); - lifecycle.register(addDisposableListener(button.element, EventType.dragend, e => { - this.__dropEndAfterWork(button); + this.__register(addDisposableListener(this.element, EventType.dragend, e => { + this.__dropEndAfterWork(); })); - lifecycle.register(this.onDrop(e => { - this.__dropEndAfterWork(button); + this.__register(this.extension.onDrop(e => { + this.__dropEndAfterWork(); })); - lifecycle.register(this.onDropOverlay(e => { - this.__dropEndAfterWork(button); + this.__register(this.extension.onDropOverlay(e => { + this.__dropEndAfterWork(); })); } - private __dropEndAfterWork(button: DragHandleButton): void { - button.element.classList.remove('dragging'); - this.__unrenderWidget(); + private __dropEndAfterWork(): void { + this.element.classList.remove('dragging'); + this.extension.unrenderWidget(); } } -class DragHandleButton extends BlockHandleButton { +class AddBlockButton extends AbstractBlockHandleButton { constructor() { - super({ - id: 'drag-handle', - icon: Icons.DragHandle, + super({ + id: 'add-new-block', + icon: Icons.AddNew, classes: ['add-new-block'], }); } + + // TODO } \ No newline at end of file From 91f5b31d4d736d9eba2d8db2cc6e83883d7766ef Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Mon, 17 Feb 2025 23:02:13 -0500 Subject: [PATCH 07/65] [Chore] streamline button registration --- .../blockHandleExtension.ts | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts index fad7faa6f..844faac18 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts @@ -138,22 +138,17 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit orientation: Orientation.Horizontal, }); - // add-new-block - const addButtonBucket = new DisposableBucket(); - const addButton = addButtonBucket.register(new AddBlockButton()); - widget.addItem({ - id: addButton.id, - data: addButton, - disposable: addButtonBucket, - }); - - // drag-handle - const dragButtonLifecycle = new DisposableBucket(); - const dragButton = dragButtonLifecycle.register(new DragHandleButton(view, this._editorWidget, this)); - widget.addItem({ - id: dragButton.id, - data: dragButton, - disposable: dragButtonLifecycle, + [ + AddBlockButton, + DragHandleButton, + ] + .forEach(buttonCtor => { + const button = new buttonCtor(view, this._editorWidget, this); + widget.addItem({ + id: button.id, + data: button, + disposable: button, + }); }); return widget; @@ -229,7 +224,11 @@ class DragHandleButton extends AbstractBlockHandleButton { class AddBlockButton extends AbstractBlockHandleButton { - constructor() { + constructor( + private readonly view: ProseEditorView, + private readonly editorWidget: IEditorWidget, + private readonly extension: EditorBlockHandleExtension, + ) { super({ id: 'add-new-block', icon: Icons.AddNew, @@ -237,5 +236,12 @@ class AddBlockButton extends AbstractBlockHandleButton { }); } - // TODO + protected override __render(element: HTMLElement): void { + super.__render(element); + + this.__register(this.onDidClick(() => { + + // create + })); + } } \ No newline at end of file From 8bd3d27677eeea1683586479d12492d3741fc01a Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Mon, 17 Feb 2025 23:28:00 -0500 Subject: [PATCH 08/65] [Feat] able to insert empty paragraph in `AddBlockButton` --- .../blockHandleExtension.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts index 844faac18..509e81abc 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts @@ -8,10 +8,12 @@ import { IWidgetBar, WidgetBar } from "src/base/browser/secondary/widgetBar/widg import { addDisposableListener, EventType, Orientation } from "src/base/browser/basic/dom"; import { RequestAnimateController, requestAtNextAnimationFrame } from "src/base/browser/basic/animation"; import { Event } from "src/base/common/event"; -import { ProseEditorView } from "src/editor/common/proseMirror"; +import { ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; import { EditorDragState, getDropExactPosition } from "src/editor/common/cursorDrop"; import { DisposableBucket } from "src/base/common/dispose"; import { Button, IButtonOptions } from "src/base/browser/basic/button/button"; +import { Markdown, TokenEnum } from "src/editor/common/markdown"; +import { assert } from "src/base/common/utilities/panic"; /** * An interface only for {@link EditorBlockHandleExtension}. @@ -240,8 +242,39 @@ class AddBlockButton extends AbstractBlockHandleButton { super.__render(element); this.__register(this.onDidClick(() => { + + // pre-condition checks + const currentDropPosition = this.extension.currDropPosition; + if (currentDropPosition === undefined) { + return; + } + + const { state } = this.view; + const $pos = state.doc.resolve(currentDropPosition); + if ($pos.depth !== 0) { + return; + } + + const currentNode = $pos.nodeAfter; + if (!currentNode) { + return; + } + + // insert an empty paragraph right below the current block + const insertPosition = currentDropPosition + currentNode.nodeSize; + const paragraph = assert(Markdown.Create.empty(state, TokenEnum.Paragraph, {})); + let newTr = state.tr.insert(insertPosition, paragraph); + + // set selection to it + const newPos = insertPosition + 1; + if (newPos <= newTr.doc.content.size) { + newTr = newTr.setSelection(ProseTextSelection.create(newTr.doc, newPos)); + } + + // update to view + this.view.dispatch(newTr); + this.view.focus(); - // create })); } } \ No newline at end of file From e28f30b1922f2e2daf7c640d849befc81fd17153 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Mon, 17 Feb 2025 23:42:16 -0500 Subject: [PATCH 09/65] [Feat] introduce `PaletteRenderer` for block insertion rendering --- .../blockHandleExtension.ts | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts index 509e81abc..25c826a06 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts @@ -10,10 +10,15 @@ import { RequestAnimateController, requestAtNextAnimationFrame } from "src/base/ import { Event } from "src/base/common/event"; import { ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; import { EditorDragState, getDropExactPosition } from "src/editor/common/cursorDrop"; -import { DisposableBucket } from "src/base/common/dispose"; import { Button, IButtonOptions } from "src/base/browser/basic/button/button"; import { Markdown, TokenEnum } from "src/editor/common/markdown"; import { assert } from "src/base/common/utilities/panic"; +import { Disposable } from "src/base/common/dispose"; +import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPlette"; +import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; +import { IPosition } from "src/base/common/utilities/size"; + +// region - EditorBlockHandleExtension /** * An interface only for {@link EditorBlockHandleExtension}. @@ -38,11 +43,16 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit private _widget?: IWidgetBar; private readonly _renderController: RequestAnimateController<{ event: IEditorMouseEvent }>; + private readonly _paletteRenderer: PaletteRenderer; // [constructor] - constructor(editorWidget: IEditorWidget) { + constructor( + editorWidget: IEditorWidget, + @IInstantiationService instantiationService: IInstantiationService, + ) { super(editorWidget); + this._paletteRenderer = this.__register(new PaletteRenderer(editorWidget, instantiationService)); // render widget when possible this.__register(this.onMouseMove(e => { @@ -118,6 +128,10 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit }); } + public renderPalette(position: IPosition): void { + this._paletteRenderer.render(position); + } + // [private methods] private __renderWidgetWithoutTarget(e: IEditorMouseEvent): void { @@ -157,6 +171,8 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit } } +// region - Buttons + class AbstractBlockHandleButton extends Button { constructor(opts: IButtonOptions) { @@ -271,10 +287,43 @@ class AddBlockButton extends AbstractBlockHandleButton { newTr = newTr.setSelection(ProseTextSelection.create(newTr.doc, newPos)); } + // render palette + const domPosition = this.view.coordsAtPos(insertPosition); + this.extension.renderPalette(domPosition); + // update to view this.view.dispatch(newTr); this.view.focus(); - })); } +} + +// region - PaletteRenderer + +class PaletteRenderer extends Disposable { + + // [field] + + private _palette?: BlockInsertPalette; + + // [constructor] + + constructor( + private readonly editorWidget: IEditorWidget, + private readonly instantiationService: IInstantiationService, + ) { + super(); + } + + // [public methods] + + public render(position: IPosition): void { + this.destroy(); + this._palette = this.instantiationService.createInstance(BlockInsertPalette, this.editorWidget); + this._palette.render(position); + } + + public destroy(): void { + this._palette?.destroy(); + } } \ No newline at end of file From d43dd91c9f4d7419ab0dbfd3f428663bdfe7f4e1 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 12:45:47 -0500 Subject: [PATCH 10/65] [Chore] --- .../blockHandleExtension/blockHandleExtension.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts index 25c826a06..1ea819437 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts @@ -287,12 +287,14 @@ class AddBlockButton extends AbstractBlockHandleButton { newTr = newTr.setSelection(ProseTextSelection.create(newTr.doc, newPos)); } + // update to view + this.view.dispatch(newTr); + // render palette const domPosition = this.view.coordsAtPos(insertPosition); this.extension.renderPalette(domPosition); - // update to view - this.view.dispatch(newTr); + // re-focus this.view.focus(); })); } @@ -304,7 +306,7 @@ class PaletteRenderer extends Disposable { // [field] - private _palette?: BlockInsertPalette; + private readonly _palette: BlockInsertPalette; // [constructor] @@ -313,13 +315,13 @@ class PaletteRenderer extends Disposable { private readonly instantiationService: IInstantiationService, ) { super(); + this._palette = this.__register(this.instantiationService.createInstance(BlockInsertPalette, this.editorWidget)); } // [public methods] public render(position: IPosition): void { this.destroy(); - this._palette = this.instantiationService.createInstance(BlockInsertPalette, this.editorWidget); this._palette.render(position); } From f26f66bd6491bf8b56c96652332024326fd7622f Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 12:46:42 -0500 Subject: [PATCH 11/65] [Refactor] move `SlashKeyboardController` into `BlockInsertPalette` --- .../slashCommandExtension.ts | 172 +----------------- .../blockInsertPalette/blockInsertPlette.ts | 169 ++++++++++++++++- 2 files changed, 165 insertions(+), 176 deletions(-) diff --git a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts b/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts index 05cec340f..f03dee02b 100644 --- a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts +++ b/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts @@ -1,14 +1,9 @@ import "src/editor/contrib/slashCommandExtension/slashCommand.scss"; -import { MenuItemType } from "src/base/browser/basic/menu/menuItem"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { IEditorWidget } from "src/editor/editorWidget"; import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; -import { DisposableBucket, IDisposable, safeDisposable } from "src/base/common/dispose"; -import { KeyCode } from "src/base/common/keyboard"; -import { ProseEditorView } from "src/editor/common/proseMirror"; -import { Priority } from "src/base/common/event"; import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPlette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; @@ -24,9 +19,7 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi // [fields] public override readonly id = EditorExtensionIDs.SlashCommand; - private readonly _palette: BlockInsertPalette; - private readonly _keyboardController: SlashKeyboardController; constructor( editorWidget: IEditorWidget, @@ -34,16 +27,9 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi ) { super(editorWidget); this._palette = this.__register(instantiationService.createInstance(BlockInsertPalette, editorWidget)); - this._keyboardController = this.__register(new SlashKeyboardController(this, this._palette)); // slash-command rendering - this.__register(this.onTextInput(e => { - this.__tryShowSlashCommand(e); - })); - - this.__register(this._palette.onMenuDestroy(() => { - this._keyboardController.unlisten(); - })); + this.__register(this.onTextInput(e => this.__tryShowSlashCommand(e))); } // [private methods] @@ -69,161 +55,5 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi // re-focus back to editor, not the slash command. view.focus(); - - // slash command shown, we capture certain key press. - this._keyboardController.listen(view); } } - -// region - SlashKeyboardController - -class SlashKeyboardController implements IDisposable { - - private _ongoing?: IDisposable; - - private _triggeredNode?: { - readonly pos: number; - readonly depth: number; - readonly nodeType: string; - }; - - constructor( - private readonly extension: EditorSlashCommandExtension, - private readonly palette: BlockInsertPalette, - ) { - this._triggeredNode = undefined; - } - - public dispose(): void { - this._ongoing?.dispose(); - this._ongoing = undefined; - this._triggeredNode = undefined; - } - - public unlisten(): void { - this.dispose(); - } - - /** - * Invoked whenever a menu is rendererd, we handle the keyboard logic here. - */ - public listen(view: ProseEditorView): void { - this.__trackCurrentNode(view); - - this._ongoing?.dispose(); - const bucket = (this._ongoing = safeDisposable(new DisposableBucket())); - - /** - * Capture certain key down we handle it by ourselves. - * @note Registered with {@link Priority.High} - */ - bucket.register(this.extension.onKeydown(e => { - const pressed = e.event.key; - const captureKey = [ - KeyCode.UpArrow, - KeyCode.DownArrow, - KeyCode.LeftArrow, - KeyCode.RightArrow, - - KeyCode.Escape, - KeyCode.Enter, - ]; - - // do nothing if non-capture key pressed. - if (!captureKey.includes(pressed)) { - return false; - } - - // escape: destroy the slash command - if (pressed === KeyCode.Escape) { - this.palette.destroy(); - } - // handle up/down arrows - else if (pressed === KeyCode.UpArrow) { - this.palette.focusPrev(); - } else if (pressed === KeyCode.DownArrow) { - this.palette.focusNext(); - } - // handle right/left arrows - else if (pressed === KeyCode.RightArrow || pressed === KeyCode.LeftArrow) { - const index = this.palette.getFocus(); - const currAction = this.palette.getAction(index); - if (!currAction || currAction.type !== MenuItemType.Submenu) { - return false; - } - - if (pressed === KeyCode.RightArrow) { - const opened = this.palette.tryOpenSubmenu(); - return opened; - } else { - return false; - } - } - // enter - else if (pressed === KeyCode.Enter) { - const hasFocus = this.palette.hasFocus(); - if (!hasFocus) { - this.palette.destroy(); - return false; - } - this.palette.runFocus(); - } - - // make sure to re-focus back to editor - view.focus(); - - // tell the editor we handled this event, stop propagation. - e.preventDefault(); - return true; - - }, undefined, Priority.High)); - - /** - * Whenever current textblock back to empty state, destroy the slash - * command. - */ - bucket.register(this.extension.onDidContentChange(() => { - const { $from } = view.state.selection; - const isEmptyBlock = ProseTools.Node.isEmptyTextBlock($from.parent); - if (isEmptyBlock) { - this.palette.destroy(); - } - })); - - /** - * Destroy slash command whenever the selection changes to other blocks. - */ - bucket.register(this.extension.onDidSelectionChange(e => { - const menu = this.palette; - const triggeredNode = this._triggeredNode; - if (!triggeredNode) { - return; - } - - // obtain current selection's node info - const currSelection = e.transaction.selection; - const $current = currSelection.$from; - const mappedPos = e.transaction.mapping.map(triggeredNode.pos); - - const isSameNode = ( - $current.parent.type.name === triggeredNode.nodeType && - $current.before() === mappedPos && - $current.depth === triggeredNode.depth - ); - - if (!isSameNode) { - menu.destroy(); - } - })); - } - - private __trackCurrentNode(view: ProseEditorView): void { - const { state } = view; - const $pos = state.selection.$from; - this._triggeredNode = { - pos: $pos.before(), - depth: $pos.depth, - nodeType: $pos.parent.type.name, - }; - } -} \ No newline at end of file diff --git a/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts b/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts index 2f86db1b7..558a42fb7 100644 --- a/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts +++ b/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts @@ -1,13 +1,15 @@ import { AnchorPrimaryAxisAlignment, AnchorVerticalPosition } from "src/base/browser/basic/contextMenu/contextMenu"; -import { MenuAction, SimpleMenuAction, SubmenuAction } from "src/base/browser/basic/menu/menuItem"; -import { Disposable } from "src/base/common/dispose"; +import { MenuAction, MenuItemType, SimpleMenuAction, SubmenuAction } from "src/base/browser/basic/menu/menuItem"; +import { Disposable, DisposableBucket, IDisposable, safeDisposable } from "src/base/common/dispose"; import { ErrorHandler } from "src/base/common/error"; -import { Emitter } from "src/base/common/event"; +import { Emitter, Priority } from "src/base/common/event"; +import { KeyCode } from "src/base/common/keyboard"; import { Arrays } from "src/base/common/utilities/array"; import { IPosition } from "src/base/common/utilities/size"; import { getTokenReadableName, Markdown, TokenEnum } from "src/editor/common/markdown"; -import { ProseAttrs, ProseTextSelection } from "src/editor/common/proseMirror"; +import { ProseAttrs, ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; import { ProseTools } from "src/editor/common/proseUtility"; +import { EditorSlashCommandExtension } from "src/editor/contrib/slashCommandExtension/slashCommandExtension"; import { IEditorWidget } from "src/editor/editorWidget"; import { II18nService } from "src/platform/i18n/browser/i18nService"; import { IContextMenuService } from "src/workbench/services/contextMenu/contextMenuService"; @@ -33,20 +35,23 @@ export class BlockInsertPalette extends Disposable { private readonly _menuRenderer: SlashMenuRenderer; private readonly _menuController: SlashMenuController; + private readonly _keyboardController: SlashKeyboardController; // [constructor] constructor( - editorWidget: IEditorWidget, + private readonly editorWidget: IEditorWidget, @IContextMenuService private readonly contextMenuService: IContextMenuService, @II18nService i18nService: II18nService, ) { super(); this._menuController = new SlashMenuController(editorWidget); this._menuRenderer = this.__register(new SlashMenuRenderer(editorWidget, contextMenuService, i18nService)); + this._keyboardController = this.__register(new SlashKeyboardController(editorWidget, this)); // always back to normal this.__register(this._menuRenderer.onMenuDestroy(() => { + this._keyboardController.unlisten(); editorWidget.view.editor.focus(); })); @@ -61,6 +66,7 @@ export class BlockInsertPalette extends Disposable { public render(position: IPosition): void { this._menuRenderer.show(position); + this._keyboardController.listen(this.editorWidget.view.editor.internalView); } public focusPrev(): void { @@ -265,4 +271,157 @@ class SlashMenuController { // update view.dispatch(tr); } +} + +// region - SlashKeyboardController + +class SlashKeyboardController implements IDisposable { + + private _ongoing?: IDisposable; + + private _triggeredNode?: { + readonly pos: number; + readonly depth: number; + readonly nodeType: string; + }; + + constructor( + private readonly editorWidget: IEditorWidget, + private readonly palette: BlockInsertPalette, + ) { + this._triggeredNode = undefined; + } + + public dispose(): void { + this._ongoing?.dispose(); + this._ongoing = undefined; + this._triggeredNode = undefined; + } + + public unlisten(): void { + this.dispose(); + } + + /** + * Invoked whenever a menu is rendererd, we handle the keyboard logic here. + */ + public listen(view: ProseEditorView): void { + this.__trackCurrentNode(view); + + this._ongoing?.dispose(); + const bucket = (this._ongoing = safeDisposable(new DisposableBucket())); + + /** + * Capture certain key down we handle it by ourselves. + * @note Registered with {@link Priority.High} + */ + bucket.register(this.editorWidget.onKeydown(e => { + const pressed = e.event.key; + const captureKey = [ + KeyCode.UpArrow, + KeyCode.DownArrow, + KeyCode.LeftArrow, + KeyCode.RightArrow, + + KeyCode.Escape, + KeyCode.Enter, + ]; + + // do nothing if non-capture key pressed. + if (!captureKey.includes(pressed)) { + return false; + } + + // escape: destroy the slash command + if (pressed === KeyCode.Escape) { + this.palette.destroy(); + } + // handle up/down arrows + else if (pressed === KeyCode.UpArrow) { + this.palette.focusPrev(); + } else if (pressed === KeyCode.DownArrow) { + this.palette.focusNext(); + } + // handle right/left arrows + else if (pressed === KeyCode.RightArrow || pressed === KeyCode.LeftArrow) { + const index = this.palette.getFocus(); + const currAction = this.palette.getAction(index); + if (!currAction || currAction.type !== MenuItemType.Submenu) { + return false; + } + + if (pressed === KeyCode.RightArrow) { + const opened = this.palette.tryOpenSubmenu(); + return opened; + } else { + return false; + } + } + // enter + else if (pressed === KeyCode.Enter) { + const hasFocus = this.palette.hasFocus(); + if (!hasFocus) { + this.palette.destroy(); + return false; + } + this.palette.runFocus(); + } + + // make sure to re-focus back to editor + view.focus(); + + // tell the editor we handled this event, stop propagation. + e.preventDefault(); + return true; + + }, undefined, Priority.High)); + + /** + * Whenever current textblock back to empty state, destroy the slash + * command. + */ + bucket.register(this.editorWidget.onDidContentChange(() => { + const { $from } = view.state.selection; + const isEmptyBlock = ProseTools.Node.isEmptyTextBlock($from.parent); + if (isEmptyBlock) { + this.palette.destroy(); + } + })); + + /** + * Destroy slash command whenever the selection changes to other blocks. + */ + bucket.register(this.editorWidget.onDidSelectionChange(e => { + const menu = this.palette; + const triggeredNode = this._triggeredNode; + if (!triggeredNode) { + return; + } + + // obtain current selection's node info + const currSelection = e.transaction.selection; + const $current = currSelection.$from; + const mappedPos = e.transaction.mapping.map(triggeredNode.pos); + + const isSameNode = ( + $current.parent.type.name === triggeredNode.nodeType && + $current.before() === mappedPos && + $current.depth === triggeredNode.depth + ); + + if (!isSameNode) { + menu.destroy(); + } + })); + } + + private __trackCurrentNode(view: ProseEditorView): void { + const { state } = view; + const $pos = state.selection.$from; + this._triggeredNode = { + pos: $pos.before(), + depth: $pos.depth, + nodeType: $pos.parent.type.name, + }; + } } \ No newline at end of file From 0ffd982f01693b4725b4da6051348f847f233911 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 12:47:13 -0500 Subject: [PATCH 12/65] [Chore] fix typo --- src/editor/contrib/blockHandleExtension/blockHandleExtension.ts | 2 +- .../contrib/slashCommandExtension/slashCommandExtension.ts | 2 +- .../{blockInsertPlette.ts => blockInsertPalette.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/editor/view/widget/blockInsertPalette/{blockInsertPlette.ts => blockInsertPalette.ts} (100%) diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts index 1ea819437..a06567cf0 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts @@ -14,7 +14,7 @@ import { Button, IButtonOptions } from "src/base/browser/basic/button/button"; import { Markdown, TokenEnum } from "src/editor/common/markdown"; import { assert } from "src/base/common/utilities/panic"; import { Disposable } from "src/base/common/dispose"; -import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPlette"; +import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPalette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; import { IPosition } from "src/base/common/utilities/size"; diff --git a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts b/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts index f03dee02b..e4430df1f 100644 --- a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts +++ b/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts @@ -4,7 +4,7 @@ import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { IEditorWidget } from "src/editor/editorWidget"; import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; -import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPlette"; +import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPalette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; interface IEditorSlashCommandExtension extends IEditorExtension { diff --git a/src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts b/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts similarity index 100% rename from src/editor/view/widget/blockInsertPalette/blockInsertPlette.ts rename to src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts From d2a8515fe57544d1a69851afecd9fcd6ca1cdff8 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 12:58:18 -0500 Subject: [PATCH 13/65] [Feat] insert empty paragraph conditionally based on current node state --- .../blockHandleExtension.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts index a06567cf0..a7608928f 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts @@ -8,7 +8,7 @@ import { IWidgetBar, WidgetBar } from "src/base/browser/secondary/widgetBar/widg import { addDisposableListener, EventType, Orientation } from "src/base/browser/basic/dom"; import { RequestAnimateController, requestAtNextAnimationFrame } from "src/base/browser/basic/animation"; import { Event } from "src/base/common/event"; -import { ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; +import { ProseEditorView, ProseTextSelection, ProseTransaction } from "src/editor/common/proseMirror"; import { EditorDragState, getDropExactPosition } from "src/editor/common/cursorDrop"; import { Button, IButtonOptions } from "src/base/browser/basic/button/button"; import { Markdown, TokenEnum } from "src/editor/common/markdown"; @@ -17,6 +17,7 @@ import { Disposable } from "src/base/common/dispose"; import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPalette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; import { IPosition } from "src/base/common/utilities/size"; +import { ProseTools } from "src/editor/common/proseUtility"; // region - EditorBlockHandleExtension @@ -82,7 +83,7 @@ export class EditorBlockHandleExtension extends EditorExtension implements IEdit // not the top-level node, do nothing. const pos = e.view.state.doc.resolve(e.target.resolvedPosition); - if (pos.depth !== 0) { // review: shouldn't it compare to 1? + if (pos.depth !== 0) { return; } @@ -276,15 +277,26 @@ class AddBlockButton extends AbstractBlockHandleButton { return; } - // insert an empty paragraph right below the current block - const insertPosition = currentDropPosition + currentNode.nodeSize; - const paragraph = assert(Markdown.Create.empty(state, TokenEnum.Paragraph, {})); - let newTr = state.tr.insert(insertPosition, paragraph); + let newTr: ProseTransaction; + let insertPosition: number; - // set selection to it - const newPos = insertPosition + 1; - if (newPos <= newTr.doc.content.size) { - newTr = newTr.setSelection(ProseTextSelection.create(newTr.doc, newPos)); + /** + * If the current node is non-empty, insert an empty paragraph right + * below the current block. + */ + if (!ProseTools.Node.isEmptyTextBlock(currentNode)) { + insertPosition = currentDropPosition + currentNode.nodeSize; + const paragraph = assert(Markdown.Create.empty(state, TokenEnum.Paragraph, {})); + newTr = state.tr.insert(insertPosition, paragraph); + const newPos = insertPosition + 1; + if (newPos <= newTr.doc.content.size) { + newTr = newTr.setSelection(ProseTextSelection.create(newTr.doc, newPos)); + } + } + // If the current node is empty text, we simply select it. + else { + insertPosition = currentDropPosition + 1; + newTr = state.tr.setSelection(ProseTextSelection.create(state.tr.doc, insertPosition)); } // update to view From f68731f9e72a788bf2be30a836314af1a148c8a3 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 13:13:55 -0500 Subject: [PATCH 14/65] [Chore] rename bunch of files by removing postfix `Extension` --- assets/locale/en.json | 2 +- assets/locale/zh-cn.json | 6 +++--- .../{autoSaveExtension.ts => autoSave.ts} | 0 .../blockHandle.scss} | 0 .../blockHandle.ts} | 2 +- .../blockPlaceHolder.scss | 0 .../blockPlaceHolder.ts} | 4 ++-- src/editor/contrib/builtInExtensionList.ts | 16 ++++++++-------- .../commandExtension.ts => command/command.ts} | 2 +- .../editorCommands.ts | 3 +-- .../dragAndDrop.scss} | 0 .../dragAndDrop.ts} | 8 ++++---- .../dropBlinkRenderer.scss | 0 .../dropBlinkRenderer.ts | 2 +- .../dropCursorRenderer.ts | 0 .../scrollOnEdgeController.ts | 0 .../historyExtension.ts => history/history.ts} | 0 .../editorInputRules.ts | 2 +- .../inputRule.ts} | 2 +- .../slashCommand.scss | 0 .../slashCommand.ts} | 2 +- .../blockInsertPalette/blockInsertPalette.ts | 2 +- test/editor/view/contrib/editorCommands.test.ts | 2 +- 23 files changed, 27 insertions(+), 28 deletions(-) rename src/editor/contrib/{autoSaveExtension.ts => autoSave.ts} (100%) rename src/editor/contrib/{blockHandleExtension/blockHandleExtension.scss => blockHandle/blockHandle.scss} (100%) rename src/editor/contrib/{blockHandleExtension/blockHandleExtension.ts => blockHandle/blockHandle.ts} (99%) rename src/editor/contrib/{blockPlaceHolderExtension => blockPlaceHolder}/blockPlaceHolder.scss (100%) rename src/editor/contrib/{blockPlaceHolderExtension/blockPlaceHolderExtension.ts => blockPlaceHolder/blockPlaceHolder.ts} (93%) rename src/editor/contrib/{commandExtension/commandExtension.ts => command/command.ts} (99%) rename src/editor/contrib/{commandExtension => command}/editorCommands.ts (99%) rename src/editor/contrib/{dragAndDropExtension/dragAndDropExtension.scss => dragAndDrop/dragAndDrop.scss} (100%) rename src/editor/contrib/{dragAndDropExtension/dragAndDropExtension.ts => dragAndDrop/dragAndDrop.ts} (97%) rename src/editor/contrib/{dragAndDropExtension => dragAndDrop}/dropBlinkRenderer.scss (100%) rename src/editor/contrib/{dragAndDropExtension => dragAndDrop}/dropBlinkRenderer.ts (95%) rename src/editor/contrib/{dragAndDropExtension => dragAndDrop}/dropCursorRenderer.ts (100%) rename src/editor/contrib/{dragAndDropExtension => dragAndDrop}/scrollOnEdgeController.ts (100%) rename src/editor/contrib/{historyExtension/historyExtension.ts => history/history.ts} (100%) rename src/editor/contrib/{inputRuleExtension => inputRule}/editorInputRules.ts (99%) rename src/editor/contrib/{inputRuleExtension/inputRuleExtension.ts => inputRule/inputRule.ts} (99%) rename src/editor/contrib/{slashCommandExtension => slashCommand}/slashCommand.scss (100%) rename src/editor/contrib/{slashCommandExtension/slashCommandExtension.ts => slashCommand/slashCommand.ts} (96%) diff --git a/assets/locale/en.json b/assets/locale/en.json index 00b43d23a..8b76a58a9 100644 --- a/assets/locale/en.json +++ b/assets/locale/en.json @@ -24,7 +24,7 @@ "rendering": "Rendering...", "error": "Error Equations" }, - "editor/contrib/blockPlaceHolderExtension/blockPlaceHolderExtension": { + "editor/contrib/blockPlaceHolder/blockPlaceHolder": { "emptyBlockPlaceHolder": "Start typing, or press '@' for AI, '/' for commands..." }, "editor/model/documentNode/node/html/html": { diff --git a/assets/locale/zh-cn.json b/assets/locale/zh-cn.json index bfcd370f8..7c651c703 100644 --- a/assets/locale/zh-cn.json +++ b/assets/locale/zh-cn.json @@ -19,9 +19,6 @@ "rendering": "渲染中...", "error": "错误公式" }, - "editor/contrib/blockPlaceHolderExtension/blockPlaceHolderExtension": { - "emptyBlockPlaceHolder": "开始输入,或者试试 '@' 触发 AI,'/' 呼出命令..." - }, "editor/common/markdown": { "code": "代码", "heading": "标题", @@ -36,6 +33,9 @@ }, "editor/model/documentNode/node/html/html": { "empty": "空HTML" + }, + "editor/contrib/blockPlaceHolder/blockPlaceHolder": { + "emptyBlockPlaceHolder": "开始输入,或者试试 '@' 触发 AI,'/' 呼出命令..." } } } \ No newline at end of file diff --git a/src/editor/contrib/autoSaveExtension.ts b/src/editor/contrib/autoSave.ts similarity index 100% rename from src/editor/contrib/autoSaveExtension.ts rename to src/editor/contrib/autoSave.ts diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.scss b/src/editor/contrib/blockHandle/blockHandle.scss similarity index 100% rename from src/editor/contrib/blockHandleExtension/blockHandleExtension.scss rename to src/editor/contrib/blockHandle/blockHandle.scss diff --git a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts b/src/editor/contrib/blockHandle/blockHandle.ts similarity index 99% rename from src/editor/contrib/blockHandleExtension/blockHandleExtension.ts rename to src/editor/contrib/blockHandle/blockHandle.ts index a7608928f..dfc675d9b 100644 --- a/src/editor/contrib/blockHandleExtension/blockHandleExtension.ts +++ b/src/editor/contrib/blockHandle/blockHandle.ts @@ -1,4 +1,4 @@ -import "src/editor/contrib/blockHandleExtension/blockHandleExtension.scss"; +import "src/editor/contrib/blockHandle/blockHandle.scss"; import { Icons } from "src/base/browser/icon/icons"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; diff --git a/src/editor/contrib/blockPlaceHolderExtension/blockPlaceHolder.scss b/src/editor/contrib/blockPlaceHolder/blockPlaceHolder.scss similarity index 100% rename from src/editor/contrib/blockPlaceHolderExtension/blockPlaceHolder.scss rename to src/editor/contrib/blockPlaceHolder/blockPlaceHolder.scss diff --git a/src/editor/contrib/blockPlaceHolderExtension/blockPlaceHolderExtension.ts b/src/editor/contrib/blockPlaceHolder/blockPlaceHolder.ts similarity index 93% rename from src/editor/contrib/blockPlaceHolderExtension/blockPlaceHolderExtension.ts rename to src/editor/contrib/blockPlaceHolder/blockPlaceHolder.ts index dc6b500a1..8f218dabf 100644 --- a/src/editor/contrib/blockPlaceHolderExtension/blockPlaceHolderExtension.ts +++ b/src/editor/contrib/blockPlaceHolder/blockPlaceHolder.ts @@ -1,9 +1,9 @@ -import "src/editor/contrib/blockPlaceHolderExtension/blockPlaceHolder.scss"; +import "src/editor/contrib/blockPlaceHolder/blockPlaceHolder.scss"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { IEditorWidget } from "src/editor/editorWidget"; -import { ProseDecoration, ProseDecorationSet, ProseDecorationSource, ProseEditorState, ProseEditorView } from "src/editor/common/proseMirror"; +import { ProseDecoration, ProseDecorationSet, ProseDecorationSource, ProseEditorState } from "src/editor/common/proseMirror"; import { I18nService, II18nService } from "src/platform/i18n/browser/i18nService"; interface IEditorBlockPlaceHolderExtension extends IEditorExtension { diff --git a/src/editor/contrib/builtInExtensionList.ts b/src/editor/contrib/builtInExtensionList.ts index 317b7b529..4ff2722d3 100644 --- a/src/editor/contrib/builtInExtensionList.ts +++ b/src/editor/contrib/builtInExtensionList.ts @@ -1,13 +1,13 @@ import { Constructor } from "src/base/common/utilities/type"; import { EditorExtension } from "src/editor/common/editorExtension"; -import { EditorCommandExtension } from "src/editor/contrib/commandExtension/commandExtension"; -import { EditorAutoSaveExtension } from "src/editor/contrib/autoSaveExtension"; -import { EditorInputRuleExtension } from "src/editor/contrib/inputRuleExtension/inputRuleExtension"; -import { EditorDragAndDropExtension } from "src/editor/contrib/dragAndDropExtension/dragAndDropExtension"; -import { EditorBlockHandleExtension } from "src/editor/contrib/blockHandleExtension/blockHandleExtension"; -import { EditorBlockPlaceHolderExtension } from "src/editor/contrib/blockPlaceHolderExtension/blockPlaceHolderExtension"; -import { EditorSlashCommandExtension } from "src/editor/contrib/slashCommandExtension/slashCommandExtension"; -// import { EditorHistoryExtension } from "src/editor/contrib/historyExtension/historyExtension"; +import { EditorCommandExtension } from "src/editor/contrib/command/command"; +import { EditorAutoSaveExtension } from "src/editor/contrib/autoSave"; +import { EditorInputRuleExtension } from "src/editor/contrib/inputRule/inputRule"; +import { EditorDragAndDropExtension } from "src/editor/contrib/dragAndDrop/dragAndDrop"; +import { EditorBlockHandleExtension } from "src/editor/contrib/blockHandle/blockHandle"; +import { EditorBlockPlaceHolderExtension } from "src/editor/contrib/blockPlaceHolder/blockPlaceHolder"; +import { EditorSlashCommandExtension } from "src/editor/contrib/slashCommand/slashCommand"; +// import { EditorHistoryExtension } from "src/editor/contrib/history/history"; export const enum EditorExtensionIDs { Command = 'editor-command-extension', diff --git a/src/editor/contrib/commandExtension/commandExtension.ts b/src/editor/contrib/command/command.ts similarity index 99% rename from src/editor/contrib/commandExtension/commandExtension.ts rename to src/editor/contrib/command/command.ts index 764bbfc06..00cfe49db 100644 --- a/src/editor/contrib/commandExtension/commandExtension.ts +++ b/src/editor/contrib/command/command.ts @@ -6,7 +6,7 @@ import { ILogService } from "src/base/common/logger"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { IEditorWidget } from "src/editor/editorWidget"; -import { registerBasicEditorCommands } from "src/editor/contrib/commandExtension/editorCommands"; +import { registerBasicEditorCommands } from "src/editor/contrib/command/editorCommands"; import { Command } from "src/platform/command/common/command"; import { ICommandService } from "src/platform/command/common/commandService"; import { RegistrantType } from "src/platform/registrant/common/registrant"; diff --git a/src/editor/contrib/commandExtension/editorCommands.ts b/src/editor/contrib/command/editorCommands.ts similarity index 99% rename from src/editor/contrib/commandExtension/editorCommands.ts rename to src/editor/contrib/command/editorCommands.ts index ed9e63143..e55a82843 100644 --- a/src/editor/contrib/commandExtension/editorCommands.ts +++ b/src/editor/contrib/command/editorCommands.ts @@ -1,4 +1,4 @@ -import type { IEditorCommandExtension } from "src/editor/contrib/commandExtension/commandExtension"; +import type { IEditorCommandExtension } from "src/editor/contrib/command/command"; import type { IEditorWidget } from "src/editor/editorWidget"; import { ReplaceAroundStep, canJoin, canSplit, liftTarget, replaceStep } from "prosemirror-transform"; import { ILogService } from "src/base/common/logger"; @@ -7,7 +7,6 @@ import { ProseEditorState, ProseTransaction, ProseAllSelection, ProseTextSelecti import { ProseTools } from "src/editor/common/proseUtility"; import { EditorSchema } from "src/editor/model/schema"; import { Command, ICommandSchema, buildChainCommand } from "src/platform/command/common/command"; -import { ICommandService } from "src/platform/command/common/commandService"; import { CreateContextKeyExpr } from "src/platform/context/common/contextKeyExpr"; import { IServiceProvider } from "src/platform/instantiation/common/instantiation"; import { EditorContextKeys } from "src/editor/common/editorContextKeys"; diff --git a/src/editor/contrib/dragAndDropExtension/dragAndDropExtension.scss b/src/editor/contrib/dragAndDrop/dragAndDrop.scss similarity index 100% rename from src/editor/contrib/dragAndDropExtension/dragAndDropExtension.scss rename to src/editor/contrib/dragAndDrop/dragAndDrop.scss diff --git a/src/editor/contrib/dragAndDropExtension/dragAndDropExtension.ts b/src/editor/contrib/dragAndDrop/dragAndDrop.ts similarity index 97% rename from src/editor/contrib/dragAndDropExtension/dragAndDropExtension.ts rename to src/editor/contrib/dragAndDrop/dragAndDrop.ts index c25ffd51b..e3a99ba97 100644 --- a/src/editor/contrib/dragAndDropExtension/dragAndDropExtension.ts +++ b/src/editor/contrib/dragAndDrop/dragAndDrop.ts @@ -1,4 +1,4 @@ -import "src/editor/contrib/dragAndDropExtension/dragAndDropExtension.scss"; +import "src/editor/contrib/dragAndDrop/dragAndDrop.scss"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { ProseDecorationSource, ProseEditorState, ProseEditorView } from "src/editor/common/proseMirror"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; @@ -6,11 +6,11 @@ import { IEditorWidget } from "src/editor/editorWidget"; import { EditorDragState, getDropExactPosition } from "src/editor/common/cursorDrop"; import { IContextService } from "src/platform/context/common/contextService"; import { EditorContextKeys } from "src/editor/common/editorContextKeys"; -import { DropCursorRenderer } from "src/editor/contrib/dragAndDropExtension/dropCursorRenderer"; +import { DropCursorRenderer } from "src/editor/contrib/dragAndDrop/dropCursorRenderer"; import { IEditorDragEvent } from "src/editor/view/proseEventBroadcaster"; import { Numbers } from "src/base/common/utilities/number"; -import { DropBlinkRenderer } from "src/editor/contrib/dragAndDropExtension/dropBlinkRenderer"; -import { ScrollOnEdgeController } from "src/editor/contrib/dragAndDropExtension/scrollOnEdgeController"; +import { DropBlinkRenderer } from "src/editor/contrib/dragAndDrop/dropBlinkRenderer"; +import { ScrollOnEdgeController } from "src/editor/contrib/dragAndDrop/scrollOnEdgeController"; import { nullable } from "src/base/common/utilities/type"; export interface IEditorDragAndDropExtension extends IEditorExtension { diff --git a/src/editor/contrib/dragAndDropExtension/dropBlinkRenderer.scss b/src/editor/contrib/dragAndDrop/dropBlinkRenderer.scss similarity index 100% rename from src/editor/contrib/dragAndDropExtension/dropBlinkRenderer.scss rename to src/editor/contrib/dragAndDrop/dropBlinkRenderer.scss diff --git a/src/editor/contrib/dragAndDropExtension/dropBlinkRenderer.ts b/src/editor/contrib/dragAndDrop/dropBlinkRenderer.ts similarity index 95% rename from src/editor/contrib/dragAndDropExtension/dropBlinkRenderer.ts rename to src/editor/contrib/dragAndDrop/dropBlinkRenderer.ts index c1d58ec5c..a4c648b42 100644 --- a/src/editor/contrib/dragAndDropExtension/dropBlinkRenderer.ts +++ b/src/editor/contrib/dragAndDrop/dropBlinkRenderer.ts @@ -1,4 +1,4 @@ -import "src/editor/contrib/dragAndDropExtension/dropBlinkRenderer.scss"; +import "src/editor/contrib/dragAndDrop/dropBlinkRenderer.scss"; import { Disposable, IDisposable } from "src/base/common/dispose"; import { ProseDecoration, ProseDecorationSet, ProseEditorView } from "src/editor/common/proseMirror"; import { IEditorWidget } from "src/editor/editorWidget"; diff --git a/src/editor/contrib/dragAndDropExtension/dropCursorRenderer.ts b/src/editor/contrib/dragAndDrop/dropCursorRenderer.ts similarity index 100% rename from src/editor/contrib/dragAndDropExtension/dropCursorRenderer.ts rename to src/editor/contrib/dragAndDrop/dropCursorRenderer.ts diff --git a/src/editor/contrib/dragAndDropExtension/scrollOnEdgeController.ts b/src/editor/contrib/dragAndDrop/scrollOnEdgeController.ts similarity index 100% rename from src/editor/contrib/dragAndDropExtension/scrollOnEdgeController.ts rename to src/editor/contrib/dragAndDrop/scrollOnEdgeController.ts diff --git a/src/editor/contrib/historyExtension/historyExtension.ts b/src/editor/contrib/history/history.ts similarity index 100% rename from src/editor/contrib/historyExtension/historyExtension.ts rename to src/editor/contrib/history/history.ts diff --git a/src/editor/contrib/inputRuleExtension/editorInputRules.ts b/src/editor/contrib/inputRule/editorInputRules.ts similarity index 99% rename from src/editor/contrib/inputRuleExtension/editorInputRules.ts rename to src/editor/contrib/inputRule/editorInputRules.ts index 68140d245..2b06d3280 100644 --- a/src/editor/contrib/inputRuleExtension/editorInputRules.ts +++ b/src/editor/contrib/inputRule/editorInputRules.ts @@ -1,7 +1,7 @@ import { EditorState, Transaction } from "prosemirror-state"; import { canJoin, findWrapping } from "prosemirror-transform"; import { TokenEnum } from "src/editor/common/markdown"; -import { IEditorInputRuleExtension, InputRuleReplacement } from "src/editor/contrib/inputRuleExtension/inputRuleExtension"; +import { IEditorInputRuleExtension, InputRuleReplacement } from "src/editor/contrib/inputRule/inputRule"; import { CodeBlockAttrs } from "src/editor/model/documentNode/node/codeBlock/codeBlock"; import { HeadingAttrs } from "src/editor/model/documentNode/node/heading"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; diff --git a/src/editor/contrib/inputRuleExtension/inputRuleExtension.ts b/src/editor/contrib/inputRule/inputRule.ts similarity index 99% rename from src/editor/contrib/inputRuleExtension/inputRuleExtension.ts rename to src/editor/contrib/inputRule/inputRule.ts index f94298ae1..966568f98 100644 --- a/src/editor/contrib/inputRuleExtension/inputRuleExtension.ts +++ b/src/editor/contrib/inputRule/inputRule.ts @@ -8,7 +8,7 @@ import { Dictionary, isString } from "src/base/common/utilities/type"; import { ProseEditorView, ProseNode, ProseResolvedPos, ProseTextSelection } from "src/editor/common/proseMirror"; import { KeyCode } from "src/base/common/keyboard"; import { TokenEnum } from "src/editor/common/markdown"; -import { IInputRule, InputRule, registerDefaultInputRules } from "src/editor/contrib/inputRuleExtension/editorInputRules"; +import { IInputRule, InputRule, registerDefaultInputRules } from "src/editor/contrib/inputRule/editorInputRules"; import { IInstantiationService, IServiceProvider } from "src/platform/instantiation/common/instantiation"; /** diff --git a/src/editor/contrib/slashCommandExtension/slashCommand.scss b/src/editor/contrib/slashCommand/slashCommand.scss similarity index 100% rename from src/editor/contrib/slashCommandExtension/slashCommand.scss rename to src/editor/contrib/slashCommand/slashCommand.scss diff --git a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts b/src/editor/contrib/slashCommand/slashCommand.ts similarity index 96% rename from src/editor/contrib/slashCommandExtension/slashCommandExtension.ts rename to src/editor/contrib/slashCommand/slashCommand.ts index e4430df1f..82605db6f 100644 --- a/src/editor/contrib/slashCommandExtension/slashCommandExtension.ts +++ b/src/editor/contrib/slashCommand/slashCommand.ts @@ -1,4 +1,4 @@ -import "src/editor/contrib/slashCommandExtension/slashCommand.scss"; +import "src/editor/contrib/slashCommand/slashCommand.scss"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; diff --git a/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts b/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts index 558a42fb7..679c054d3 100644 --- a/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts +++ b/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts @@ -9,7 +9,7 @@ import { IPosition } from "src/base/common/utilities/size"; import { getTokenReadableName, Markdown, TokenEnum } from "src/editor/common/markdown"; import { ProseAttrs, ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; import { ProseTools } from "src/editor/common/proseUtility"; -import { EditorSlashCommandExtension } from "src/editor/contrib/slashCommandExtension/slashCommandExtension"; +import { EditorSlashCommandExtension } from "src/editor/contrib/slashCommand/slashCommand"; import { IEditorWidget } from "src/editor/editorWidget"; import { II18nService } from "src/platform/i18n/browser/i18nService"; import { IContextMenuService } from "src/workbench/services/contextMenu/contextMenuService"; diff --git a/test/editor/view/contrib/editorCommands.test.ts b/test/editor/view/contrib/editorCommands.test.ts index 9211cbcf6..c13fd2059 100644 --- a/test/editor/view/contrib/editorCommands.test.ts +++ b/test/editor/view/contrib/editorCommands.test.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import { ProseUtilsTest } from 'test/editor/view/editorHelpers'; import ist from 'ist'; import { ProseEditorState, ProseNode, ProseNodeSelection, ProseSchema, ProseSelection, ProseTextSelection } from 'src/editor/common/proseMirror'; -import { EditorCommandBase, EditorCommands } from 'src/editor/contrib/commandExtension/editorCommands'; +import { EditorCommandBase, EditorCommands } from 'src/editor/contrib/command/editorCommands'; import { nullObject } from 'test/utils/helpers'; const { doc, p, blockquote, hr, ul, li } = ProseUtilsTest.defaultNodes; From e823834759047a4294990df5be59a10165f9468b Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 13:20:20 -0500 Subject: [PATCH 15/65] [Chore] --- src/editor/contrib/slashCommand/slashCommand.ts | 6 +++--- .../view/widget/blockInsertPalette/blockInsertPalette.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/editor/contrib/slashCommand/slashCommand.ts b/src/editor/contrib/slashCommand/slashCommand.ts index 82605db6f..b888f1d25 100644 --- a/src/editor/contrib/slashCommand/slashCommand.ts +++ b/src/editor/contrib/slashCommand/slashCommand.ts @@ -29,12 +29,12 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi this._palette = this.__register(instantiationService.createInstance(BlockInsertPalette, editorWidget)); // slash-command rendering - this.__register(this.onTextInput(e => this.__tryShowSlashCommand(e))); + this.__register(this.onTextInput(e => this.tryShowSlashCommand(e))); } - // [private methods] + // [public methods] - private __tryShowSlashCommand(e: IOnTextInputEvent): void { + public tryShowSlashCommand(e: IOnTextInputEvent): void { const { text, view } = e; const { selection } = view.state; diff --git a/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts b/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts index 679c054d3..8d63bc18e 100644 --- a/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts +++ b/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts @@ -9,7 +9,6 @@ import { IPosition } from "src/base/common/utilities/size"; import { getTokenReadableName, Markdown, TokenEnum } from "src/editor/common/markdown"; import { ProseAttrs, ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; import { ProseTools } from "src/editor/common/proseUtility"; -import { EditorSlashCommandExtension } from "src/editor/contrib/slashCommand/slashCommand"; import { IEditorWidget } from "src/editor/editorWidget"; import { II18nService } from "src/platform/i18n/browser/i18nService"; import { IContextMenuService } from "src/workbench/services/contextMenu/contextMenuService"; From ed86b1fb23b1882c97195f3a3897aeac749f95ea Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 13:22:25 -0500 Subject: [PATCH 16/65] [Feat] start up --- src/editor/contrib/askAI/askAI.ts | 55 ++++++++++++++++++++++ src/editor/contrib/builtInExtensionList.ts | 1 + 2 files changed, 56 insertions(+) create mode 100644 src/editor/contrib/askAI/askAI.ts diff --git a/src/editor/contrib/askAI/askAI.ts b/src/editor/contrib/askAI/askAI.ts new file mode 100644 index 000000000..9c78693de --- /dev/null +++ b/src/editor/contrib/askAI/askAI.ts @@ -0,0 +1,55 @@ +import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; +import { ProseTools } from "src/editor/common/proseUtility"; +import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; +import { IEditorWidget } from "src/editor/editorWidget"; +import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; + +interface IEditorAskAIExtension extends IEditorExtension { + + readonly id: EditorExtensionIDs.AskAI; +} + +// region - EditorAskAIExtension + +export class EditorAskAIExtension extends EditorExtension implements IEditorAskAIExtension { + + // [field] + + public override readonly id = EditorExtensionIDs.AskAI; + + // [constructor] + + constructor( + editorWidget: IEditorWidget, + ) { + super(editorWidget); + + this.__register(this.onTextInput(e => this.tryShowAskAI(e))); + } + + // [public methods] + + public tryShowAskAI(e: IOnTextInputEvent): void { + + const { text, view } = e; + const { selection } = view.state; + + const isCursor = ProseTools.Cursor.isCursor(selection); + if (!isCursor) { + return; + } + + const isEmptyBlock = ProseTools.Cursor.isOnEmpty(selection); + const isSlash = text === '@'; + if (!isEmptyBlock || !isSlash) { + return; + } + + // show ask-AI palette + const position = view.coordsAtPos(selection.$from.pos); + // TODO: render palette + + // re-focus back to editor, not the slash command. + view.focus(); + } +} \ No newline at end of file diff --git a/src/editor/contrib/builtInExtensionList.ts b/src/editor/contrib/builtInExtensionList.ts index 4ff2722d3..595f7c553 100644 --- a/src/editor/contrib/builtInExtensionList.ts +++ b/src/editor/contrib/builtInExtensionList.ts @@ -18,6 +18,7 @@ export const enum EditorExtensionIDs { BlockHandle = 'editor-block-handle-extension', BlockPlaceHolder = 'editor-block-place-holder-extension', SlashCommand = 'editor-slash-command-extension', + AskAI = 'editor-ask-AI', } /** From 42ac9a5b43eee1d3e8561c40e221bcdf84703a67 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 16:49:30 -0500 Subject: [PATCH 17/65] [Chore] --- src/editor/model/documentNode/documentNode.ts | 8 ++++---- src/editor/model/documentNode/documentNodeProvider.ts | 5 +++-- src/editor/model/documentNode/mark/mathInline.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/editor/model/documentNode/documentNode.ts b/src/editor/model/documentNode/documentNode.ts index f510e788c..0a4e9b4e8 100644 --- a/src/editor/model/documentNode/documentNode.ts +++ b/src/editor/model/documentNode/documentNode.ts @@ -77,7 +77,7 @@ export interface IDocumentNode): void; /** * An option that defines how the serialization behavior of {@link DocumentMark}. @@ -91,7 +91,7 @@ abstract class DocumentNodeBase): void; // serializer public abstract readonly serializer: TNode extends ProseMark ? IDocumentMarkSerializationOptions : Serializer; @@ -100,10 +100,10 @@ abstract class DocumentNodeBase extends DocumentNodeBase {} +export abstract class DocumentNode extends DocumentNodeBase {} /** * @class A document mark that represents a mark. Such as 'strong', 'emphasis', * 'link' and so on. */ -export abstract class DocumentMark extends DocumentNodeBase {} \ No newline at end of file +export abstract class DocumentMark extends DocumentNodeBase {} \ No newline at end of file diff --git a/src/editor/model/documentNode/documentNodeProvider.ts b/src/editor/model/documentNode/documentNodeProvider.ts index 45a24960d..683c4680e 100644 --- a/src/editor/model/documentNode/documentNodeProvider.ts +++ b/src/editor/model/documentNode/documentNodeProvider.ts @@ -2,6 +2,7 @@ import { IO } from "src/base/common/utilities/functional"; import { panic } from "src/base/common/utilities/panic"; import { Mutable } from "src/base/common/utilities/type"; import { TokenEnum, MarkEnum } from "src/editor/common/markdown"; +import { EditorToken } from "src/editor/common/model"; import { ProseNodeType, ProseMarkType } from "src/editor/common/proseMirror"; import { DocumentNode, DocumentMark } from "src/editor/model/documentNode/documentNode"; import { Codespan } from "src/editor/model/documentNode/mark/codespan"; @@ -105,11 +106,11 @@ export class DocumentNodeProvider { this._marks.set(mark.name, mark); } - public getNode(name: TokenEnum | string): DocumentNode | undefined { + public getNode(name: TokenEnum | string): DocumentNode | undefined { return this._nodes.get(name); } - public getMark(name: MarkEnum | string): DocumentMark | undefined { + public getMark(name: MarkEnum | string): DocumentMark | undefined { return this._marks.get(name); } diff --git a/src/editor/model/documentNode/mark/mathInline.ts b/src/editor/model/documentNode/mark/mathInline.ts index 4fbc8838f..95460509c 100644 --- a/src/editor/model/documentNode/mark/mathInline.ts +++ b/src/editor/model/documentNode/mark/mathInline.ts @@ -76,7 +76,7 @@ export class MathInline extends DocumentNode { }; } - public parseFromToken(state: IDocumentParseState, status: IParseTokenStatus): void { + public parseFromToken(state: IDocumentParseState, status: IParseTokenStatus): void { const token = status.token; state.activateNode(this.ctor, status, { attrs: { From 61cc7d7982ae78f7fa594fa48dccc0b67fba5284 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 17:11:49 -0500 Subject: [PATCH 18/65] [Refactor] remove `Space` node and map its token to `Paragraph` --- .../documentNode/documentNodeProvider.ts | 2 - src/editor/model/documentNode/node/space.ts | 44 ------------------- src/editor/model/editorModel.ts | 18 ++++++++ src/editor/model/parser.ts | 23 +++++++++- 4 files changed, 39 insertions(+), 48 deletions(-) delete mode 100644 src/editor/model/documentNode/node/space.ts diff --git a/src/editor/model/documentNode/documentNodeProvider.ts b/src/editor/model/documentNode/documentNodeProvider.ts index 683c4680e..8acc2273f 100644 --- a/src/editor/model/documentNode/documentNodeProvider.ts +++ b/src/editor/model/documentNode/documentNodeProvider.ts @@ -21,7 +21,6 @@ import { LineBreak } from "src/editor/model/documentNode/node/lineBreak"; import { List, ListItem } from "src/editor/model/documentNode/node/list"; import { MathBlock } from "src/editor/model/documentNode/node/mathBlock"; import { Paragraph } from "src/editor/model/documentNode/node/paragraph"; -import { Space } from "src/editor/model/documentNode/node/space"; import { Text } from "src/editor/model/documentNode/node/text"; import { EditorSchema, TOP_NODE_NAME } from "src/editor/model/schema"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; @@ -58,7 +57,6 @@ export class DocumentNodeProvider { */ register: () => { // nodes - provider.registerNode(instantiationService.createInstance(Space)); provider.registerNode(instantiationService.createInstance(Text)); provider.registerNode(instantiationService.createInstance(Escape)); provider.registerNode(instantiationService.createInstance(Paragraph)); diff --git a/src/editor/model/documentNode/node/space.ts b/src/editor/model/documentNode/node/space.ts deleted file mode 100644 index dd02e4f4d..000000000 --- a/src/editor/model/documentNode/node/space.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { TokenEnum } from "src/editor/common/markdown"; -import { EditorTokens } from "src/editor/common/model"; -import { ProseNode, ProseNodeSpec } from "src/editor/common/proseMirror"; -import { DocumentNode, IParseTokenStatus } from "src/editor/model/documentNode/documentNode"; -import { createDomOutputFromOptions } from "../../schema"; -import { IDocumentParseState } from "src/editor/model/parser"; -import { IMarkdownSerializerState } from "src/editor/model/serializer"; -import { memoize } from "src/base/common/memoization"; - -/** - * @class An empty space block. Represented in the DOM as an empty `

` - * element. - */ -export class Space extends DocumentNode { - - constructor() { - super(TokenEnum.Space); - } - - @memoize - public getSchema(): ProseNodeSpec { - return { - group: 'block', - content: 'inline*', - toDOM: () => { - return createDomOutputFromOptions({ - type: 'node', - tagName: 'p', - editable: true, - }); - } - }; - } - - public parseFromToken(state: IDocumentParseState, status: IParseTokenStatus): void { - state.activateNode(this.ctor, status, {}); - state.deactivateNode(); - } - - public serializer = (state: IMarkdownSerializerState, node: ProseNode, parent: ProseNode, index: number) => { - state.text(''); - state.closeBlock(node); - }; -} \ No newline at end of file diff --git a/src/editor/model/editorModel.ts b/src/editor/model/editorModel.ts index 260fbf8c1..71bf24eeb 100644 --- a/src/editor/model/editorModel.ts +++ b/src/editor/model/editorModel.ts @@ -19,6 +19,7 @@ import { IFileService } from "src/platform/files/common/fileService"; import { history } from "prosemirror-history"; import { IOnDidContentChangeEvent } from "src/editor/view/proseEventBroadcaster"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; +import { TokenEnum } from "src/editor/common/markdown"; export class EditorModel extends Disposable implements IEditorModel { @@ -77,6 +78,8 @@ export class EditorModel extends Disposable implements IEditorModel { this.__register(this._docParser.onLog(event => defaultLog(logService, event.level, 'EditorView', event.message, event.error, event.additional))); this._docSerializer = new MarkdownSerializer(this._nodeProvider, { strict: true, escapeExtraCharacters: undefined, }); + this.__initialization(); + logService.debug('EditorModel', 'Constructed'); } @@ -221,6 +224,21 @@ export class EditorModel extends Disposable implements IEditorModel { this._onDidStateChange.fire(); } + private __initialization(): void { + /** + * Mapping token: {@link TokenEnum.Space} to {@link TokenEnum.Paragraph} + * Because `space` are just special cases for `paragraph`. + */ + this._docParser.registerMapToken(TokenEnum.Space, (from) => { + return { + type: TokenEnum.Paragraph, + text: '', + raw: '', + tokens: [] + }; + }); + } + private __tokenizeAndParse(raw: string): ProseNode { const tokens = this._lexer.lex(raw); console.log(tokens); // TEST diff --git a/src/editor/model/parser.ts b/src/editor/model/parser.ts index f85f4df49..1df2482f5 100644 --- a/src/editor/model/parser.ts +++ b/src/editor/model/parser.ts @@ -1,4 +1,4 @@ -import { Disposable, IDisposable } from "src/base/common/dispose"; +import { Disposable } from "src/base/common/dispose"; import { Emitter, Register } from "src/base/common/event"; import { ILogEvent, LogLevel } from "src/base/common/logger"; import { Stack } from "src/base/common/structures/stack"; @@ -45,6 +45,15 @@ export interface IDocumentParser { * @param type The token type. */ isTokenIgnored(type: TokenEnum | MarkEnum | string): boolean; + + /** + * @description Register a converter that transfroms a token into another + * one. + * @param from The input token type name. + * @param converter The converter function. + */ + registerMapToken(from: string, converter: (from: EditorToken) => EditorToken): void; + mapToken(token: EditorToken): EditorToken; } /** @@ -57,6 +66,7 @@ export class DocumentParser extends Disposable implements IDocumentParser { private readonly _state: DocumentParseState; private readonly _ignored: Set; + private readonly _mapping: Map EditorToken>; // [event] @@ -70,6 +80,7 @@ export class DocumentParser extends Disposable implements IDocumentParser { ) { super(); this._ignored = new Set(); + this._mapping = new Map(); this._state = this.__register(new DocumentParseState(this, schema, nodeProvider)); this.onLog = this._state.onLog; } @@ -83,6 +94,14 @@ export class DocumentParser extends Disposable implements IDocumentParser { return documentRoot; } + public registerMapToken(from: string, converter: (from: EditorToken) => EditorToken): void { + this._mapping.set(from, converter); + } + + public mapToken(token: EditorToken): EditorToken { + return this._mapping.get(token.type)?.(token) ?? token; + } + public ignoreToken(type: TokenEnum | MarkEnum | string, ignore?: boolean): void { // toggling @@ -297,7 +316,7 @@ class DocumentParseState extends Disposable implements IDocumentParseState { public parseTokens(level: number, tokens: EditorToken[], parent: EditorToken | null): void { for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]!; + const token = this._parser.mapToken(tokens[i]!); const name = token.type; if (this._parser.isTokenIgnored(name)) { From 05da9d7e949b160b9c8578cb1bb870c506458735 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 17:14:13 -0500 Subject: [PATCH 19/65] [Refactor] standard serialization: `CodeBlock` --- .../documentNode/node/codeBlock/codeBlock.ts | 138 ++---------------- 1 file changed, 12 insertions(+), 126 deletions(-) diff --git a/src/editor/model/documentNode/node/codeBlock/codeBlock.ts b/src/editor/model/documentNode/node/codeBlock/codeBlock.ts index c6972b674..f1548d7e9 100644 --- a/src/editor/model/documentNode/node/codeBlock/codeBlock.ts +++ b/src/editor/model/documentNode/node/codeBlock/codeBlock.ts @@ -1,7 +1,5 @@ import 'src/editor/model/documentNode/node/codeBlock/codeBlock.scss'; import { memoize } from "src/base/common/memoization"; -import { Strings } from "src/base/common/utilities/string"; -import { isString } from "src/base/common/utilities/type"; import { CodeEditorView, minimalSetup } from "src/editor/common/codeMirror"; import { TokenEnum } from "src/editor/common/markdown"; import { EditorTokens } from "src/editor/common/model"; @@ -17,12 +15,6 @@ export type CodeBlockAttrs = { readonly view?: CodeEditorView; }; -const enum CodeBlockFenceType { - WaveLine = 'waveLine', - backTick = 'backTick', - Indent = 'indent', -} - // region - CodeBlock /** @@ -48,11 +40,6 @@ export class CodeBlock extends DocumentNode { attrs: >{ view: { default: CodeBlock.createView('') }, lang: { default: '' }, - fenceType: { default: CodeBlockFenceType.WaveLine }, - fenceLength: { default: 3 }, - hasEndFence: { default: true }, - hasSingleEndOfLine: { default: false }, - mismatchFence: { default: undefined }, }, toDOM: (node) => { const { view, lang } = node.attrs; @@ -72,22 +59,11 @@ export class CodeBlock extends DocumentNode { public parseFromToken(state: IDocumentParseState, status: IParseTokenStatus): void { const { token } = status; - const fenceResult = __resolveFenceStatus(token); - const attrs = { view: CodeBlock.createView(token.text), lang: token.lang, text: token.text, - fenceType: fenceResult.type, - fenceLength: fenceResult.length, }; - - if (fenceResult.type !== CodeBlockFenceType.Indent) { - attrs['hasEndFence'] = fenceResult.hasEndFence; - attrs['hasSingleEndOfLine'] = fenceResult.hasSingleEndOfLine; - attrs['mismatchFence'] = fenceResult.mismatchFence; - } - state.activateNode(this.ctor, status, { attrs: attrs }); state.deactivateNode(); } @@ -95,44 +71,20 @@ export class CodeBlock extends DocumentNode { public serializer = (state: IMarkdownSerializerState, node: ProseNode, parent: ProseNode, index: number) => { const lang = node.attrs['lang'] as string; const view = node.attrs['view'] as CodeEditorView; - const fenceType = node.attrs['fenceType'] as CodeBlockFenceType; - const fenceLength = node.attrs['fenceLength'] as number; - const hasEndFence = node.attrs['hasEndFence'] as boolean; - const hasSingleEndOfLine = node.attrs['hasSingleEndOfLine'] as boolean; - const mismatchFence = node.attrs['mismatchFence'] as string | undefined; - // when the ending fence mismatched, it should be serialized as plain text. - const textContent = - view.state.doc.toString() - + (isString(mismatchFence) ? mismatchFence : ''); - - // indent type has no fences - if (fenceType === CodeBlockFenceType.Indent) { - const indent = ' '; - state.setDefaultDelimiter(indent); + const textContent = view.state.doc.toString(); + + const fence = '`'.repeat(3); + state.write(fence + lang); + + if (textContent.length > 0) { + state.text('\n'); state.text(textContent, false); - state.setDefaultDelimiter(''); - } - // normal fence - else { - const fenceChar = fenceType === CodeBlockFenceType.WaveLine ? '~' : '`'; - const beginFence = fenceChar.repeat(fenceLength); - state.write(beginFence + lang); - - if (textContent.length > 0) { - state.text('\n'); - state.text(textContent, false); - } - - if (hasEndFence) { - state.write('\n'); - state.write(beginFence); - } - - if (hasSingleEndOfLine) { - state.text('\n', false); - } } + + state.write('\n'); + state.write(fence); + state.closeBlock(node); }; @@ -151,7 +103,7 @@ export class CodeBlock extends DocumentNode { const langLabel = document.createElement('span'); langLabel.classList.add('code-lang'); - langLabel.textContent = lang || 'Unknown'; + langLabel.textContent = lang; const copyButton = document.createElement('button'); copyButton.classList.add('code-copy'); @@ -168,69 +120,3 @@ export class CodeBlock extends DocumentNode { return header; } } - - -function __resolveFenceStatus(token: EditorTokens.CodeBlock): - { - type: CodeBlockFenceType.Indent, - length: undefined - } | { - type: CodeBlockFenceType.backTick | CodeBlockFenceType.WaveLine, - length: number, - hasEndFence: boolean, - hasSingleEndOfLine: boolean, - mismatchFence?: string - } -{ - if (token.codeBlockStyle === 'indented') { - return { type: CodeBlockFenceType.Indent, length: undefined }; - } - - const fenceRegex = /^(`{3,}|~{3,})[a-zA-Z0-9]*\s*$/gm; - const match = token.raw.match(fenceRegex); - - // default - if (!match) { - return { type: CodeBlockFenceType.backTick, length: 3, hasEndFence: true, hasSingleEndOfLine: false }; - } - - const hasSingleEndOfLine = token.raw.at(-1) === '\n'; - const beginFenceMatch = match[0]; - const endFenceMatch = match[1]; - - const beginFirstChar = beginFenceMatch.at(0)!; - const fenceType = (beginFirstChar === '~') ? CodeBlockFenceType.WaveLine : CodeBlockFenceType.backTick; - const beginFence = Strings.substringUntilNotChar(beginFenceMatch, beginFirstChar); - - if (!endFenceMatch) { - return { - type: fenceType, - length: beginFence.length, - hasEndFence: false, - hasSingleEndOfLine: hasSingleEndOfLine, - }; - } - - // has end fence, we check is they are the type and equal length - const endFirstChar = endFenceMatch.at(0)!; - const endFence = Strings.substringUntilNotChar(endFenceMatch, endFirstChar); - - // doesn't match the same fence type or length, indicates no end fence. - if (beginFirstChar !== endFirstChar || beginFence.length !== endFence.length) { - return { - type: fenceType, - length: beginFence.length, - hasEndFence: false, - hasSingleEndOfLine: hasSingleEndOfLine, - mismatchFence: endFence, - }; - } - - // fences match - return { - type: (beginFence.at(0) === '~') ? CodeBlockFenceType.WaveLine : CodeBlockFenceType.backTick, - length: beginFence.length, - hasEndFence: true, - hasSingleEndOfLine: hasSingleEndOfLine, - }; -} \ No newline at end of file From 8c4a18e6087e2f891f3e8ba357c05080a8b2b04a Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 17:21:20 -0500 Subject: [PATCH 20/65] [Refactor] standard serialization: `Blockquote` --- .../model/documentNode/node/blockquote.ts | 68 +------------------ src/editor/model/serializer.ts | 6 ++ 2 files changed, 9 insertions(+), 65 deletions(-) diff --git a/src/editor/model/documentNode/node/blockquote.ts b/src/editor/model/documentNode/node/blockquote.ts index 26938f2be..d1589eefc 100644 --- a/src/editor/model/documentNode/node/blockquote.ts +++ b/src/editor/model/documentNode/node/blockquote.ts @@ -2,12 +2,10 @@ import { TokenEnum } from "src/editor/common/markdown"; import { EditorTokens } from "src/editor/common/model"; import { GetProseAttrs, ProseNode, ProseNodeSpec } from "src/editor/common/proseMirror"; import { DocumentNode, IParseTokenStatus } from "src/editor/model/documentNode/documentNode"; -import { createDomOutputFromOptions } from "../../schema"; import { IDocumentParseState } from "src/editor/model/parser"; import { IMarkdownSerializerState } from "src/editor/model/serializer"; -import { Strings } from "src/base/common/utilities/string"; -import { assert } from "src/base/common/utilities/panic"; import { memoize } from "src/base/common/memoization"; +import { createDomOutputFromOptions } from "src/editor/model/schema"; export type BlockquoteAttrs = { // noop for now @@ -29,7 +27,6 @@ export class Blockquote extends DocumentNode { content: 'block+', defining: true, attrs: { - delimiters: { default: [] }, } satisfies GetProseAttrs, toDOM: () => { return createDomOutputFromOptions({ @@ -43,10 +40,9 @@ export class Blockquote extends DocumentNode { public parseFromToken(state: IDocumentParseState, status: IParseTokenStatus): void { const { token } = status; - const delimiters = this.__getDelimitersFromRaw(state, token.raw); state.activateNode(this.ctor, status, { - attrs: { delimiters: delimiters } + attrs: {} }); if (token.tokens) { @@ -57,64 +53,6 @@ export class Blockquote extends DocumentNode { } public serializer = (state: IMarkdownSerializerState, node: ProseNode) => { - const delimiters = node.attrs['delimiters'] as string[]; - state.serializeDelimitedBlock(delimiters, node); + state.wrapBlock(node, () => state.serializeBlock(node), "> "); }; - - // [private methods] - - private __getDelimitersFromRaw(state: IDocumentParseState, raw: string): string[] { - const delimiters: string[] = []; - const activeNode = assert(state.getActiveNode()); - const outMost = (activeNode.name !== TokenEnum.Blockquote); - - for (const { line, isLastLine } of Strings.iterateLines(raw)) { - if (line === '') { - continue; - } - - /** - * For cases like '> p1\n
', the second line '
' is - * considered part of the blockquote. This line could logically - * contain any text. - * - * If the last line includes the character `>`, it can interfere with - * the algorithm's parsing. To avoid this, special handling is required. - */ - if (isLastLine && /^ {0,3}>/.test(line) === false) { - delimiters.push(''); - continue; - } - - let delimiter = ''; - let startIndex = 0; - - // edge case: append the spaces before the first level '>' to the delimiter - if (outMost) { - const { index: firstLevelIdx, str: spaces } = Strings.substringUntilChar2(line, '>', 0); - delimiter += spaces; - delimiter += '>'; - startIndex = firstLevelIdx + 1; - } else { - delimiter = '>'; - startIndex = 1; - } - - // normal case: append the spaces after the '>' to the delimiter - const { index: nextCharIdx, char: nextChar } = Strings.firstNonSpaceChar(line, startIndex); - - // empty line, skip it. - if (nextChar === '') { - delimiters.push(delimiter); - continue; - } - - // we append the spaces before the nextChar - const space = line.slice(startIndex, nextCharIdx); - delimiter += space; - delimiters.push(delimiter); - } - - return delimiters; - } } \ No newline at end of file diff --git a/src/editor/model/serializer.ts b/src/editor/model/serializer.ts index 825716ad0..a37bd71bb 100644 --- a/src/editor/model/serializer.ts +++ b/src/editor/model/serializer.ts @@ -147,6 +147,7 @@ export interface IMarkdownSerializerState { serializeBlock(parent: ProseNode): void; serializeInline(parent: ProseNode, fromBlockStart?: boolean): void; serializeList(node: ProseNode, delimiter: string, firstDelimiter: (index: number) => string): void; + wrapBlock(node: ProseNode, f: () => void, delimiter: string, firstDelimiter?: string): void; write(content?: string): void; text(text: string, escape?: boolean): void; escaping(str: string, startOfLine?: boolean): string; @@ -155,6 +156,7 @@ export interface IMarkdownSerializerState { serializeDelimitedBlock(delimiters: string[], parent: ProseNode): void; setDelimiterIncrements(delimiters: string[]): void; setDefaultDelimiter(delimiter: string): void; + incrementDefaultDelimiter(increment: string): string; } /** @@ -341,6 +343,10 @@ class MarkdownSerializerState implements IMarkdownSerializerState { this._delimiter.setDefault(delimiter); } + public incrementDefaultDelimiter(increment: string): string { + return this._delimiter.incrementDefault(increment); + } + // [private methods] /** From 9c224c81c00d9a61735b150bf71abc1811cfdaba Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 17:50:47 -0500 Subject: [PATCH 21/65] [Fix] `CodeBlock` is not a text block --- src/editor/model/documentNode/node/codeBlock/codeBlock.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor/model/documentNode/node/codeBlock/codeBlock.ts b/src/editor/model/documentNode/node/codeBlock/codeBlock.ts index f1548d7e9..48b891d57 100644 --- a/src/editor/model/documentNode/node/codeBlock/codeBlock.ts +++ b/src/editor/model/documentNode/node/codeBlock/codeBlock.ts @@ -33,7 +33,6 @@ export class CodeBlock extends DocumentNode { public getSchema(): ProseNodeSpec { return { group: 'block', - content: 'text*', marks: '', // disallow any marks code: true, defining: true, From d4b0003f6ddb56fa87de50db6fadd26c0d2d670f Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 17:51:13 -0500 Subject: [PATCH 22/65] [Fix] correct condition for inserting empty paragraph --- src/editor/contrib/blockHandle/blockHandle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/contrib/blockHandle/blockHandle.ts b/src/editor/contrib/blockHandle/blockHandle.ts index dfc675d9b..42cb8646c 100644 --- a/src/editor/contrib/blockHandle/blockHandle.ts +++ b/src/editor/contrib/blockHandle/blockHandle.ts @@ -284,7 +284,7 @@ class AddBlockButton extends AbstractBlockHandleButton { * If the current node is non-empty, insert an empty paragraph right * below the current block. */ - if (!ProseTools.Node.isEmptyTextBlock(currentNode)) { + if (!currentNode.isTextblock || currentNode.textContent !== '') { insertPosition = currentDropPosition + currentNode.nodeSize; const paragraph = assert(Markdown.Create.empty(state, TokenEnum.Paragraph, {})); newTr = state.tr.insert(insertPosition, paragraph); From e5c6c0909dc1818d9414d378d68cb295fefbb56e Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 17:52:02 -0500 Subject: [PATCH 23/65] [Fix] handle serialization of empty paragraphs by simulating a space --- src/editor/model/documentNode/node/paragraph.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/editor/model/documentNode/node/paragraph.ts b/src/editor/model/documentNode/node/paragraph.ts index 235050761..aca92beee 100644 --- a/src/editor/model/documentNode/node/paragraph.ts +++ b/src/editor/model/documentNode/node/paragraph.ts @@ -47,7 +47,13 @@ export class Paragraph extends DocumentNode { } public serializer = (state: IMarkdownSerializerState, node: ProseNode, parent: ProseNode, index: number) => { - state.serializeInline(node); + if (node.childCount > 0) { + state.serializeInline(node); + } + // If empty paragraph, simulate as a `space`. + else { + state.write('\n'); + } state.closeBlock(node); }; } \ No newline at end of file From 013d7ad3ed984d7c52c15535de68580663ebbc16 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 18:03:29 -0500 Subject: [PATCH 24/65] [Refactor] standard serialization: `List` --- src/editor/model/documentNode/node/list.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/editor/model/documentNode/node/list.ts b/src/editor/model/documentNode/node/list.ts index 604fa9660..d86d34179 100644 --- a/src/editor/model/documentNode/node/list.ts +++ b/src/editor/model/documentNode/node/list.ts @@ -1,5 +1,5 @@ import { memoize } from "src/base/common/memoization"; -import { Strings } from "src/base/common/utilities/string"; +import { isString } from "src/base/common/utilities/type"; import { TokenEnum } from "src/editor/common/markdown"; import { EditorTokens } from "src/editor/common/model"; import { GetProseAttrs, ProseNode, ProseNodeSpec } from "src/editor/common/proseMirror"; @@ -17,6 +17,13 @@ export type ListAttrs = { * @default false */ readonly tight?: boolean; + + /** + * If {@link ordered=true}, this indicates the starting number of an ordered + * list. + * @default 1 + */ + readonly start?: number; }; /** @@ -38,7 +45,6 @@ export class List extends DocumentNode { ordered: { default: false }, tight: { default: false }, start: { default: 1, }, - bullet: { default: '*', } }, toDOM(node) { const { ordered, tight } = node.attrs; @@ -55,8 +61,7 @@ export class List extends DocumentNode { attrs: { ordered: token.ordered, tight: !token.loose, - start: token.start, - bullet: Strings.firstNonSpaceChar(token.raw, 0).char, + start: isString(token.start) ? 1 : token.start, } }); state.parseTokens(status.level + 1, token.items, token); @@ -65,12 +70,12 @@ export class List extends DocumentNode { public serializer = (state: IMarkdownSerializerState, node: ProseNode) => { const isOrdered = node.attrs['ordered'] as boolean; - const bullet = node.attrs['bullet'] as string; - const start = node.attrs['start'] as string; + const tight = node.attrs['tight'] as boolean; + const start = node.attrs['start'] as number; // un-ordered if (isOrdered === false) { - state.serializeList(node, ' ', () => (bullet + ' ')); + state.serializeList(node, ' ', () => ('* ')); } // ordered else { From 4d46d4e034c06a10bfb20540125cfbef8ead95f9 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 19:47:13 -0500 Subject: [PATCH 25/65] [Chore] rename `BlockInsertPalette` with `EditorPalette` --- src/editor/contrib/blockHandle/blockHandle.ts | 6 +++--- src/editor/contrib/slashCommand/slashCommand.ts | 6 +++--- .../blockInsertPalette.ts => palette/palette.ts} | 14 ++++---------- 3 files changed, 10 insertions(+), 16 deletions(-) rename src/editor/view/widget/{blockInsertPalette/blockInsertPalette.ts => palette/palette.ts} (96%) diff --git a/src/editor/contrib/blockHandle/blockHandle.ts b/src/editor/contrib/blockHandle/blockHandle.ts index 42cb8646c..d504c158b 100644 --- a/src/editor/contrib/blockHandle/blockHandle.ts +++ b/src/editor/contrib/blockHandle/blockHandle.ts @@ -14,7 +14,7 @@ import { Button, IButtonOptions } from "src/base/browser/basic/button/button"; import { Markdown, TokenEnum } from "src/editor/common/markdown"; import { assert } from "src/base/common/utilities/panic"; import { Disposable } from "src/base/common/dispose"; -import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPalette"; +import { EditorPalette } from "src/editor/view/widget/palette/palette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; import { IPosition } from "src/base/common/utilities/size"; import { ProseTools } from "src/editor/common/proseUtility"; @@ -318,7 +318,7 @@ class PaletteRenderer extends Disposable { // [field] - private readonly _palette: BlockInsertPalette; + private readonly _palette: EditorPalette; // [constructor] @@ -327,7 +327,7 @@ class PaletteRenderer extends Disposable { private readonly instantiationService: IInstantiationService, ) { super(); - this._palette = this.__register(this.instantiationService.createInstance(BlockInsertPalette, this.editorWidget)); + this._palette = this.__register(this.instantiationService.createInstance(EditorPalette, this.editorWidget)); } // [public methods] diff --git a/src/editor/contrib/slashCommand/slashCommand.ts b/src/editor/contrib/slashCommand/slashCommand.ts index b888f1d25..5d657826f 100644 --- a/src/editor/contrib/slashCommand/slashCommand.ts +++ b/src/editor/contrib/slashCommand/slashCommand.ts @@ -4,7 +4,7 @@ import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { IEditorWidget } from "src/editor/editorWidget"; import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; -import { BlockInsertPalette } from "src/editor/view/widget/blockInsertPalette/blockInsertPalette"; +import { EditorPalette } from "src/editor/view/widget/palette/palette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; interface IEditorSlashCommandExtension extends IEditorExtension { @@ -19,14 +19,14 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi // [fields] public override readonly id = EditorExtensionIDs.SlashCommand; - private readonly _palette: BlockInsertPalette; + private readonly _palette: EditorPalette; constructor( editorWidget: IEditorWidget, @IInstantiationService instantiationService: IInstantiationService, ) { super(editorWidget); - this._palette = this.__register(instantiationService.createInstance(BlockInsertPalette, editorWidget)); + this._palette = this.__register(instantiationService.createInstance(EditorPalette, editorWidget)); // slash-command rendering this.__register(this.onTextInput(e => this.tryShowSlashCommand(e))); diff --git a/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts b/src/editor/view/widget/palette/palette.ts similarity index 96% rename from src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts rename to src/editor/view/widget/palette/palette.ts index 8d63bc18e..4191c832d 100644 --- a/src/editor/view/widget/blockInsertPalette/blockInsertPalette.ts +++ b/src/editor/view/widget/palette/palette.ts @@ -13,18 +13,12 @@ import { IEditorWidget } from "src/editor/editorWidget"; import { II18nService } from "src/platform/i18n/browser/i18nService"; import { IContextMenuService } from "src/workbench/services/contextMenu/contextMenuService"; -// region - BlockInsertPalette +// region - EditorPalette /** - * {@link BlockInsertPalette} A contextual command palette component that - * provides quick insertion of various document block types at the current - * cursor position. The palette renders as a vertical menu containing: - * - * - Common block types (paragraphs, headings, code blocks etc.) - * - Nested submenus for complex block variations - * - Localized display names based on document schema + * {@link EditorPalette} // TODO */ -export class BlockInsertPalette extends Disposable { +export class EditorPalette extends Disposable { // [event] @@ -286,7 +280,7 @@ class SlashKeyboardController implements IDisposable { constructor( private readonly editorWidget: IEditorWidget, - private readonly palette: BlockInsertPalette, + private readonly palette: EditorPalette, ) { this._triggeredNode = undefined; } From 76e63d46c5ca681024d5d61a6496c2c460231c42 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 19:55:17 -0500 Subject: [PATCH 26/65] [Chore] remove unused `range.ts` --- src/editor/common/range.ts | 60 -------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 src/editor/common/range.ts diff --git a/src/editor/common/range.ts b/src/editor/common/range.ts deleted file mode 100644 index 47b7e4aa9..000000000 --- a/src/editor/common/range.ts +++ /dev/null @@ -1,60 +0,0 @@ - -/** - * A range representation in the editor. - */ -export interface IEditorRange { - /** - * Line number on which the range starts (zero-based). - */ - readonly startLineNumber: number; - /** - * Column on which the range starts in line `startLineNumber` (zero-based). - */ - readonly startColumn: number; - /** - * Line number on which the range ends. - */ - readonly endLineNumber: number; - /** - * Column on which the range ends in line `endLineNumber`. - */ - readonly endColumn: number; -} - -export class EditorRange implements IEditorRange { - - // [field] - - public readonly startLineNumber: number; - public readonly startColumn: number; - public readonly endLineNumber: number; - public readonly endColumn: number; - - // [constructor] - - constructor( - startLineNumber: number, startColumn: number, - endLineNumber: number, endColumn: number - ) { - if ((startLineNumber > endLineNumber) || - (startLineNumber === endLineNumber && startColumn > endColumn) - ) { - this.startLineNumber = endLineNumber; - this.startColumn = endColumn; - this.endLineNumber = startLineNumber; - this.endColumn = startColumn; - } else { - this.startLineNumber = startLineNumber; - this.startColumn = startColumn; - this.endLineNumber = endLineNumber; - this.endColumn = endColumn; - } - } - - // [static helper methods] - - public static isEmpty(range: IEditorRange): boolean { - return (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn); - } - -} \ No newline at end of file From 109be741696674a53fb38fc2b10e07c8c9f7941d Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 20:23:20 -0500 Subject: [PATCH 27/65] [Refactor] support dynamically provided content in `EditorPalette` --- src/editor/view/widget/palette/palette.ts | 170 +++--------------- .../contextMenu/contextMenuService.ts | 12 +- 2 files changed, 34 insertions(+), 148 deletions(-) diff --git a/src/editor/view/widget/palette/palette.ts b/src/editor/view/widget/palette/palette.ts index 4191c832d..d3f0059cd 100644 --- a/src/editor/view/widget/palette/palette.ts +++ b/src/editor/view/widget/palette/palette.ts @@ -1,13 +1,11 @@ import { AnchorPrimaryAxisAlignment, AnchorVerticalPosition } from "src/base/browser/basic/contextMenu/contextMenu"; -import { MenuAction, MenuItemType, SimpleMenuAction, SubmenuAction } from "src/base/browser/basic/menu/menuItem"; +import { MenuAction, MenuItemType } from "src/base/browser/basic/menu/menuItem"; import { Disposable, DisposableBucket, IDisposable, safeDisposable } from "src/base/common/dispose"; -import { ErrorHandler } from "src/base/common/error"; import { Emitter, Priority } from "src/base/common/event"; import { KeyCode } from "src/base/common/keyboard"; -import { Arrays } from "src/base/common/utilities/array"; +import { IO } from "src/base/common/utilities/functional"; import { IPosition } from "src/base/common/utilities/size"; -import { getTokenReadableName, Markdown, TokenEnum } from "src/editor/common/markdown"; -import { ProseAttrs, ProseEditorView, ProseTextSelection } from "src/editor/common/proseMirror"; +import { ProseEditorView } from "src/editor/common/proseMirror"; import { ProseTools } from "src/editor/common/proseUtility"; import { IEditorWidget } from "src/editor/editorWidget"; import { II18nService } from "src/platform/i18n/browser/i18nService"; @@ -15,6 +13,11 @@ import { IContextMenuService } from "src/workbench/services/contextMenu/contextM // region - EditorPalette +export interface IEditorPaletteOptions { + + readonly contentProvider: IO; +} + /** * {@link EditorPalette} // TODO */ @@ -22,43 +25,41 @@ export class EditorPalette extends Disposable { // [event] - get onMenuDestroy() { return this._menuRenderer.onMenuDestroy; } + get onDestroy() { return this._renderer.onMenuDestroy; } // [field] - private readonly _menuRenderer: SlashMenuRenderer; - private readonly _menuController: SlashMenuController; - private readonly _keyboardController: SlashKeyboardController; + private readonly _renderer: PaletteRenderer; + private readonly _keyboardController: PaletteKeyboardController; // [constructor] constructor( private readonly editorWidget: IEditorWidget, + options: IEditorPaletteOptions, @IContextMenuService private readonly contextMenuService: IContextMenuService, @II18nService i18nService: II18nService, ) { super(); - this._menuController = new SlashMenuController(editorWidget); - this._menuRenderer = this.__register(new SlashMenuRenderer(editorWidget, contextMenuService, i18nService)); - this._keyboardController = this.__register(new SlashKeyboardController(editorWidget, this)); + this._renderer = this.__register(new PaletteRenderer(editorWidget, options, contextMenuService, i18nService)); + this._keyboardController = this.__register(new PaletteKeyboardController(editorWidget, this)); // always back to normal - this.__register(this._menuRenderer.onMenuDestroy(() => { + this.__register(this._renderer.onMenuDestroy(() => { this._keyboardController.unlisten(); editorWidget.view.editor.focus(); })); // menu click logic - this.__register(this._menuRenderer.onClick(e => { + this.__register(this.contextMenuService.contextMenu.onActionRun(e => { contextMenuService.contextMenu.destroy(); - this._menuController.onClick(e); })); } // [public methods] public render(position: IPosition): void { - this._menuRenderer.show(position); + this._renderer.show(position); this._keyboardController.listen(this.editorWidget.view.editor.internalView); } @@ -93,27 +94,18 @@ export class EditorPalette extends Disposable { public runFocus(): void { this.contextMenuService.contextMenu.runFocus(); } - } -// region - SlashMenuRenderer - -type SlashOnClickEvent = { - readonly type: string; - readonly name: string; - readonly attr: ProseAttrs; -}; +// region - PaletteRenderer -class SlashMenuRenderer extends Disposable { +class PaletteRenderer extends Disposable { private readonly _onMenuDestroy = this.__register(new Emitter()); public readonly onMenuDestroy = this._onMenuDestroy.registerListener; - private readonly _onClick = this.__register(new Emitter()); - public readonly onClick = this._onClick.registerListener; - constructor( private readonly editorWidget: IEditorWidget, + private readonly options: IEditorPaletteOptions, private readonly contextMenuService: IContextMenuService, private readonly i18nService: II18nService, ) { @@ -127,7 +119,7 @@ class SlashMenuRenderer extends Disposable { const { overlayContainer: parentElement } = this.editorWidget.view.editor; this.contextMenuService.showContextMenuCustom({ - getActions: () => this.__obtainSlashCommandContent(), + getActions: () => this.options.contentProvider(), getContext: () => undefined, getAnchor: () => ({ x: position.left, y: position.top, height: 24 }), getExtraContextMenuClassName: () => 'editor-slash-command', @@ -148,127 +140,11 @@ class SlashMenuRenderer extends Disposable { }, }, parentElement); } - - private __obtainSlashCommandContent(): MenuAction[] { - const nodes = this.__obtainValidContent(); - - // convert each node into menu action - return nodes.map(nodeName => { - // heading: submenu - if (nodeName === TokenEnum.Heading) { - return this.__getHeadingActions(); - } - // general case - const resolvedName = getTokenReadableName(this.i18nService, nodeName); - return new SimpleMenuAction({ - enabled: true, - id: resolvedName, - callback: () => this._onClick.fire({ - type: nodeName, - name: resolvedName, - attr: {}, - }), - }); - }); - } - - private __obtainValidContent(): string[] { - const blocks = this.editorWidget.model.getRegisteredDocumentNodes(); - const ordered = this.__filterContent(blocks, CONTENT_FILTER); - return ordered; - } - - private __filterContent(unordered: string[], expectOrder: string[]): string[] { - const ordered: string[] = []; - const unorderedSet = new Set(unordered); - for (const name of expectOrder) { - if (unorderedSet.has(name)) { - ordered.push(name); - unorderedSet.delete(name); - } else { - console.warn(`[SlashCommandExtension] missing node: ${name}`); - } - } - return ordered; - } - - private __getHeadingActions(): SubmenuAction { - const heading = getTokenReadableName(this.i18nService, TokenEnum.Heading); - return new SubmenuAction( - Arrays.range(1, 7).map(level => this.__getHeadingAction(heading, level)), - { enabled: true, id: heading } - ); - } - - private __getHeadingAction(name: string, level: number): MenuAction { - return new SimpleMenuAction({ - enabled: true, - id: `${name} ${level}`, - callback: () => this._onClick.fire({ - type: TokenEnum.Heading, - name: name, - attr: { level: level }, - }), - }); - } -} - -const CONTENT_FILTER = [ - TokenEnum.Paragraph, - TokenEnum.Blockquote, - TokenEnum.Heading, - TokenEnum.Image, - TokenEnum.List, - TokenEnum.Table, - TokenEnum.CodeBlock, - TokenEnum.MathBlock, - TokenEnum.HTML, - TokenEnum.HorizontalRule, -]; - -// region - SlashMenuController - -class SlashMenuController { - - constructor( - private readonly editorWidget: IEditorWidget, - ) {} - - public onClick(event: SlashOnClickEvent): void { - const { type, name, attr } = event; - const view = this.editorWidget.view.editor.internalView; - const state = view.state; - let tr = state.tr; - - const prevStart = tr.selection.$from.start(); - const $pos = state.doc.resolve(prevStart); - - // create an empy node with given type - const node = Markdown.Create.empty(state, type, attr); - if (!node) { - ErrorHandler.onUnexpectedError(new Error(`Cannot create node (${name})`)); - return; - } - - // replace the current node with the new node - tr = ProseTools.Position.replaceWithNode(state, $pos, node); - - // find next selectable text - const newStart = tr.mapping.map(prevStart); - const $newStart = tr.doc.resolve(newStart + 1); - const selection = ProseTextSelection.findFrom($newStart, 1, true); - if (selection) { - tr = tr.setSelection(selection); - } - - // update - view.dispatch(tr); - } } -// region - SlashKeyboardController +// region - PaletteKeyboardController -class SlashKeyboardController implements IDisposable { +class PaletteKeyboardController implements IDisposable { private _ongoing?: IDisposable; diff --git a/src/workbench/services/contextMenu/contextMenuService.ts b/src/workbench/services/contextMenu/contextMenuService.ts index cd45e976e..e84d141e2 100644 --- a/src/workbench/services/contextMenu/contextMenuService.ts +++ b/src/workbench/services/contextMenu/contextMenuService.ts @@ -1,6 +1,6 @@ import { ContextMenuView, IAnchor, IContextMenu, IContextMenuDelegate, IContextMenuDelegateBase } from "src/base/browser/basic/contextMenu/contextMenu"; import { DomUtility, EventType } from "src/base/browser/basic/dom"; -import { DomEmitter } from "src/base/common/event"; +import { DomEmitter, Register } from "src/base/common/event"; import { IMenu, IMenuActionRunEvent, Menu, MenuWithSubmenu } from "src/base/browser/basic/menu/menu"; import { CheckMenuAction, MenuAction, MenuItemType, MenuSeparatorAction, SimpleMenuAction, SubmenuAction } from "src/base/browser/basic/menu/menuItem"; import { Disposable, DisposableBucket, IDisposable } from "src/base/common/dispose"; @@ -106,6 +106,9 @@ export interface IContextMenuService extends IService { * no context menu is presented. */ readonly contextMenu: { + readonly onActionRun: Register; + readonly onDidActionRun: Register; + /** * @description Destroy the current context menu. */ @@ -221,6 +224,8 @@ export class ContextMenuService extends Disposable implements IContextMenuServic getFocus: ensureExist(() => this._internalDelegate?.getFocus()), getAction: ensureExist((arg: number | string) => this._internalDelegate?.getAction(arg)), tryOpenSubmenu: ensureExist((index?: number) => this._internalDelegate?.tryOpenSubmenu(index)), + onActionRun: ensureExist(() => this._internalDelegate?.onBeforeActionRun), + onDidActionRun: ensureExist(() => this._internalDelegate?.onDidActionRun), }; } @@ -349,6 +354,11 @@ export class ContextMenuService extends Disposable implements IContextMenuServic class __ContextMenuDelegate implements IContextMenuDelegate { + // [event] + + get onBeforeActionRun() { return this._menu?.onBeforeRun; } + get onDidActionRun() { return this._menu?.onDidRun; } + // [fields] private _menu?: MenuWithSubmenu; From ff0485d327642d09cb72eecdaac6f78d631f082b Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 20:23:37 -0500 Subject: [PATCH 28/65] [Refactor] `BlockInsertProvider` for dynamic content insertion in `EditorPalette` --- src/editor/contrib/blockHandle/blockHandle.ts | 11 +- .../contrib/slashCommand/slashCommand.ts | 12 +- .../widget/palette/blockInsertProvider.ts | 133 ++++++++++++++++++ 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/editor/view/widget/palette/blockInsertProvider.ts diff --git a/src/editor/contrib/blockHandle/blockHandle.ts b/src/editor/contrib/blockHandle/blockHandle.ts index d504c158b..ab74291f5 100644 --- a/src/editor/contrib/blockHandle/blockHandle.ts +++ b/src/editor/contrib/blockHandle/blockHandle.ts @@ -18,6 +18,7 @@ import { EditorPalette } from "src/editor/view/widget/palette/palette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; import { IPosition } from "src/base/common/utilities/size"; import { ProseTools } from "src/editor/common/proseUtility"; +import { BlockInsertProvider } from "src/editor/view/widget/palette/blockInsertProvider"; // region - EditorBlockHandleExtension @@ -319,6 +320,7 @@ class PaletteRenderer extends Disposable { // [field] private readonly _palette: EditorPalette; + private readonly _contentProvider: BlockInsertProvider; // [constructor] @@ -327,7 +329,14 @@ class PaletteRenderer extends Disposable { private readonly instantiationService: IInstantiationService, ) { super(); - this._palette = this.__register(this.instantiationService.createInstance(EditorPalette, this.editorWidget)); + this._contentProvider = instantiationService.createInstance(BlockInsertProvider, editorWidget); + this._palette = this.__register(this.instantiationService.createInstance( + EditorPalette, + this.editorWidget, + { + contentProvider: () => this._contentProvider.getContent(), + } + )); } // [public methods] diff --git a/src/editor/contrib/slashCommand/slashCommand.ts b/src/editor/contrib/slashCommand/slashCommand.ts index 5d657826f..7f8effbdf 100644 --- a/src/editor/contrib/slashCommand/slashCommand.ts +++ b/src/editor/contrib/slashCommand/slashCommand.ts @@ -6,6 +6,7 @@ import { IEditorWidget } from "src/editor/editorWidget"; import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; import { EditorPalette } from "src/editor/view/widget/palette/palette"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; +import { BlockInsertProvider } from "src/editor/view/widget/palette/blockInsertProvider"; interface IEditorSlashCommandExtension extends IEditorExtension { @@ -20,13 +21,22 @@ export class EditorSlashCommandExtension extends EditorExtension implements IEdi public override readonly id = EditorExtensionIDs.SlashCommand; private readonly _palette: EditorPalette; + private readonly _contentProvider: BlockInsertProvider; constructor( editorWidget: IEditorWidget, @IInstantiationService instantiationService: IInstantiationService, ) { super(editorWidget); - this._palette = this.__register(instantiationService.createInstance(EditorPalette, editorWidget)); + this._contentProvider = instantiationService.createInstance(BlockInsertProvider, editorWidget); + + this._palette = this.__register(instantiationService.createInstance( + EditorPalette, + editorWidget, + { + contentProvider: () => this._contentProvider.getContent(), + } + )); // slash-command rendering this.__register(this.onTextInput(e => this.tryShowSlashCommand(e))); diff --git a/src/editor/view/widget/palette/blockInsertProvider.ts b/src/editor/view/widget/palette/blockInsertProvider.ts new file mode 100644 index 000000000..f96f56681 --- /dev/null +++ b/src/editor/view/widget/palette/blockInsertProvider.ts @@ -0,0 +1,133 @@ +import { MenuAction, SimpleMenuAction, SubmenuAction } from "src/base/browser/basic/menu/menuItem"; +import { ErrorHandler } from "src/base/common/error"; +import { Arrays } from "src/base/common/utilities/array"; +import { Markdown, TokenEnum, getTokenReadableName } from "src/editor/common/markdown"; +import { ProseAttrs, ProseTextSelection } from "src/editor/common/proseMirror"; +import { ProseTools } from "src/editor/common/proseUtility"; +import { IEditorWidget } from "src/editor/editorWidget"; +import { II18nService } from "src/platform/i18n/browser/i18nService"; + +export class BlockInsertProvider { + + constructor( + private readonly editorWidget: IEditorWidget, + @II18nService private readonly i18nService: II18nService, + ) {} + + // [public methods] + + public getContent(): MenuAction[] { + const nodes = this.__obtainValidContent(); + + // convert each node into menu action + return nodes.map(nodeName => { + // heading: submenu + if (nodeName === TokenEnum.Heading) { + return this.__getHeadingActions(); + } + // general case + const resolvedName = getTokenReadableName(this.i18nService, nodeName); + return new SimpleMenuAction({ + enabled: true, + id: resolvedName, + callback: () => this.insertEmptyBlock({ + type: nodeName, + name: resolvedName, + attr: {}, + }), + }); + }); + } + + public insertEmptyBlock(event: OnBlockInsertEvent): void { + const { type, name, attr } = event; + const view = this.editorWidget.view.editor.internalView; + const state = view.state; + let tr = state.tr; + + const prevStart = tr.selection.$from.start(); + const $pos = state.doc.resolve(prevStart); + + // create an empy node with given type + const node = Markdown.Create.empty(state, type, attr); + if (!node) { + ErrorHandler.onUnexpectedError(new Error(`Cannot create node (${name})`)); + return; + } + + // replace the current node with the new node + tr = ProseTools.Position.replaceWithNode(state, $pos, node); + + // find next selectable text + const newStart = tr.mapping.map(prevStart); + const $newStart = tr.doc.resolve(newStart + 1); + const selection = ProseTextSelection.findFrom($newStart, 1, true); + if (selection) { + tr = tr.setSelection(selection); + } + + // update + view.dispatch(tr); + } + + // [private methods] + + private __obtainValidContent(): string[] { + const blocks = this.editorWidget.model.getRegisteredDocumentNodes(); + const ordered = this.__filterContent(blocks, CONTENT_FILTER); + return ordered; + } + + private __filterContent(unordered: string[], expectOrder: string[]): string[] { + const ordered: string[] = []; + const unorderedSet = new Set(unordered); + for (const name of expectOrder) { + if (unorderedSet.has(name)) { + ordered.push(name); + unorderedSet.delete(name); + } else { + console.warn(`[SlashCommandExtension] missing node: ${name}`); + } + } + return ordered; + } + + private __getHeadingActions(): SubmenuAction { + const heading = getTokenReadableName(this.i18nService, TokenEnum.Heading); + return new SubmenuAction( + Arrays.range(1, 7).map(level => this.__getHeadingAction(heading, level)), + { enabled: true, id: heading } + ); + } + + private __getHeadingAction(name: string, level: number): MenuAction { + return new SimpleMenuAction({ + enabled: true, + id: `${name} ${level}`, + callback: () => this.insertEmptyBlock({ + type: TokenEnum.Heading, + name: name, + attr: { level: level }, + }), + }); + } +} + +type OnBlockInsertEvent = { + readonly type: string; + readonly name: string; + readonly attr: ProseAttrs; +}; + +const CONTENT_FILTER = [ + TokenEnum.Paragraph, + TokenEnum.Blockquote, + TokenEnum.Heading, + TokenEnum.Image, + TokenEnum.List, + TokenEnum.Table, + TokenEnum.CodeBlock, + TokenEnum.MathBlock, + TokenEnum.HTML, + TokenEnum.HorizontalRule, +]; \ No newline at end of file From 7ef43f24f74f8326330c1500623ea9efc2fd543b Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 18 Feb 2025 20:37:22 -0500 Subject: [PATCH 29/65] [Feat] enable `EditorAskAIExtension` --- src/editor/contrib/askAI/askAI.ts | 19 ++++++++++++++++++- src/editor/contrib/builtInExtensionList.ts | 2 ++ .../view/widget/palette/askAIProvider.ts | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/editor/view/widget/palette/askAIProvider.ts diff --git a/src/editor/contrib/askAI/askAI.ts b/src/editor/contrib/askAI/askAI.ts index 9c78693de..e89b7b38b 100644 --- a/src/editor/contrib/askAI/askAI.ts +++ b/src/editor/contrib/askAI/askAI.ts @@ -3,6 +3,9 @@ import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; import { IEditorWidget } from "src/editor/editorWidget"; import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; +import { AskAIProvider } from "src/editor/view/widget/palette/askAIProvider"; +import { EditorPalette } from "src/editor/view/widget/palette/palette"; +import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; interface IEditorAskAIExtension extends IEditorExtension { @@ -16,14 +19,25 @@ export class EditorAskAIExtension extends EditorExtension implements IEditorAskA // [field] public override readonly id = EditorExtensionIDs.AskAI; + private readonly _palette: EditorPalette; // [constructor] constructor( editorWidget: IEditorWidget, + @IInstantiationService instantiationService: IInstantiationService, ) { super(editorWidget); + const provider = instantiationService.createInstance(AskAIProvider, editorWidget); + this._palette = this.__register(instantiationService.createInstance( + EditorPalette, + editorWidget, + { + contentProvider: () => provider.getContent(), + } + )); + // show event this.__register(this.onTextInput(e => this.tryShowAskAI(e))); } @@ -45,9 +59,12 @@ export class EditorAskAIExtension extends EditorExtension implements IEditorAskA return; } + // prevent actual insert '@' character + e.preventDefault(); + // show ask-AI palette const position = view.coordsAtPos(selection.$from.pos); - // TODO: render palette + this._palette.render(position); // re-focus back to editor, not the slash command. view.focus(); diff --git a/src/editor/contrib/builtInExtensionList.ts b/src/editor/contrib/builtInExtensionList.ts index 595f7c553..72fdbb085 100644 --- a/src/editor/contrib/builtInExtensionList.ts +++ b/src/editor/contrib/builtInExtensionList.ts @@ -7,6 +7,7 @@ import { EditorDragAndDropExtension } from "src/editor/contrib/dragAndDrop/dragA import { EditorBlockHandleExtension } from "src/editor/contrib/blockHandle/blockHandle"; import { EditorBlockPlaceHolderExtension } from "src/editor/contrib/blockPlaceHolder/blockPlaceHolder"; import { EditorSlashCommandExtension } from "src/editor/contrib/slashCommand/slashCommand"; +import { EditorAskAIExtension } from "src/editor/contrib/askAI/askAI"; // import { EditorHistoryExtension } from "src/editor/contrib/history/history"; export const enum EditorExtensionIDs { @@ -33,6 +34,7 @@ export function getBuiltInExtension(): { id: string, ctor: Constructor Date: Wed, 19 Feb 2025 00:40:32 -0500 Subject: [PATCH 30/65] [Chore] fixes --- src/editor/contrib/slashCommand/slashCommand.ts | 1 - src/editor/view/widget/palette/askAIProvider.ts | 2 +- .../widget/palette/palette.scss} | 5 +++-- src/editor/view/widget/palette/palette.ts | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/editor/{contrib/slashCommand/slashCommand.scss => view/widget/palette/palette.scss} (81%) diff --git a/src/editor/contrib/slashCommand/slashCommand.ts b/src/editor/contrib/slashCommand/slashCommand.ts index 7f8effbdf..087eba213 100644 --- a/src/editor/contrib/slashCommand/slashCommand.ts +++ b/src/editor/contrib/slashCommand/slashCommand.ts @@ -1,4 +1,3 @@ -import "src/editor/contrib/slashCommand/slashCommand.scss"; import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; import { ProseTools } from "src/editor/common/proseUtility"; import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; diff --git a/src/editor/view/widget/palette/askAIProvider.ts b/src/editor/view/widget/palette/askAIProvider.ts index a7eca8a22..43d4a1e64 100644 --- a/src/editor/view/widget/palette/askAIProvider.ts +++ b/src/editor/view/widget/palette/askAIProvider.ts @@ -1,4 +1,4 @@ -import { MenuAction } from "src/base/browser/basic/menu/menuItem"; +import { MenuAction, SimpleMenuAction } from "src/base/browser/basic/menu/menuItem"; import { IEditorWidget } from "src/editor/editorWidget"; import { II18nService } from "src/platform/i18n/browser/i18nService"; diff --git a/src/editor/contrib/slashCommand/slashCommand.scss b/src/editor/view/widget/palette/palette.scss similarity index 81% rename from src/editor/contrib/slashCommand/slashCommand.scss rename to src/editor/view/widget/palette/palette.scss index afe3755f4..68465c30b 100644 --- a/src/editor/contrib/slashCommand/slashCommand.scss +++ b/src/editor/view/widget/palette/palette.scss @@ -1,6 +1,7 @@ -.editor-slash-command.context-menu { + +.editor-palette.context-menu { max-height: 260px; - overflow-y: scroll; + overflow-y: auto; &::-webkit-scrollbar { width: 10px; diff --git a/src/editor/view/widget/palette/palette.ts b/src/editor/view/widget/palette/palette.ts index d3f0059cd..7f5bb81cf 100644 --- a/src/editor/view/widget/palette/palette.ts +++ b/src/editor/view/widget/palette/palette.ts @@ -1,3 +1,4 @@ +import "src/editor/view/widget/palette/palette.scss"; import { AnchorPrimaryAxisAlignment, AnchorVerticalPosition } from "src/base/browser/basic/contextMenu/contextMenu"; import { MenuAction, MenuItemType } from "src/base/browser/basic/menu/menuItem"; import { Disposable, DisposableBucket, IDisposable, safeDisposable } from "src/base/common/dispose"; @@ -122,7 +123,7 @@ class PaletteRenderer extends Disposable { getActions: () => this.options.contentProvider(), getContext: () => undefined, getAnchor: () => ({ x: position.left, y: position.top, height: 24 }), - getExtraContextMenuClassName: () => 'editor-slash-command', + getExtraContextMenuClassName: () => 'editor-palette', primaryAlignment: AnchorPrimaryAxisAlignment.Vertical, verticalPosition: AnchorVerticalPosition.Below, @@ -201,7 +202,7 @@ class PaletteKeyboardController implements IDisposable { return false; } - // escape: destroy the slash command + // escape: destroy the palette if (pressed === KeyCode.Escape) { this.palette.destroy(); } @@ -246,8 +247,7 @@ class PaletteKeyboardController implements IDisposable { }, undefined, Priority.High)); /** - * Whenever current textblock back to empty state, destroy the slash - * command. + * Whenever current textblock back to empty state, destroy the palette. */ bucket.register(this.editorWidget.onDidContentChange(() => { const { $from } = view.state.selection; @@ -258,7 +258,7 @@ class PaletteKeyboardController implements IDisposable { })); /** - * Destroy slash command whenever the selection changes to other blocks. + * Destroy palette whenever the selection changes to other blocks. */ bucket.register(this.editorWidget.onDidSelectionChange(e => { const menu = this.palette; From 4371d4f526cba8eceb7f0d7fe2ebc05b87747c92 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Wed, 19 Feb 2025 00:55:03 -0500 Subject: [PATCH 31/65] [Feat] some test buttons in `AskAI` palette --- src/editor/contrib/askAI/askAI.ts | 3 --- src/editor/view/widget/palette/askAIProvider.ts | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/editor/contrib/askAI/askAI.ts b/src/editor/contrib/askAI/askAI.ts index e89b7b38b..d36df48df 100644 --- a/src/editor/contrib/askAI/askAI.ts +++ b/src/editor/contrib/askAI/askAI.ts @@ -59,9 +59,6 @@ export class EditorAskAIExtension extends EditorExtension implements IEditorAskA return; } - // prevent actual insert '@' character - e.preventDefault(); - // show ask-AI palette const position = view.coordsAtPos(selection.$from.pos); this._palette.render(position); diff --git a/src/editor/view/widget/palette/askAIProvider.ts b/src/editor/view/widget/palette/askAIProvider.ts index 43d4a1e64..f093d034a 100644 --- a/src/editor/view/widget/palette/askAIProvider.ts +++ b/src/editor/view/widget/palette/askAIProvider.ts @@ -11,8 +11,21 @@ export class AskAIProvider { ) {} public getContent(): MenuAction[] { + const contents: MenuAction[] = []; - // TODO - return []; + [ + 'Continue Writing', + 'Make a Summary', + 'Make a Outline', + ] + .forEach(name => { + contents.push(new SimpleMenuAction({ + id: name, + enabled: true, + callback: () => {}, + })); + }); + + return contents; } } \ No newline at end of file From cc8f0a5d2a3261b97a3cceec089be5be63c748d2 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Wed, 19 Feb 2025 16:24:29 -0500 Subject: [PATCH 32/65] [Chore] --- src/editor/editorWidget.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index 5d67b93e3..82c9fb694 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -310,7 +310,7 @@ export class EditorWidget extends Disposable implements IEditorWidget { get initialized(): boolean { return !!this._model; } - get model(): IEditorModel { return assert(this._model); } + get model(): IEditorModel { return this.__assertModel(); } get viewModel(): IEditorViewModel { return assert(this._viewModel); } get view(): IEditorView { return assert(this._view); } @@ -402,15 +402,19 @@ export class EditorWidget extends Disposable implements IEditorWidget { // region - [model] - get source(): URI { return this.__assertModel().source; } + get source(): URI { return this.model.source; } get dirty(): boolean { return assert(this._model).dirty; } public insertAt(textOffset: number, text: string): void { - return this.__assertModel().insertAt(textOffset, text); + return this.model.insertAt(textOffset, text); } public deleteAt(textOffset: number, length: number): void { - return this.__assertModel().deleteAt(textOffset, length); + return this.model.deleteAt(textOffset, length); + } + + public insertAtSelection(text: string): void { + return this.model.insertAtSelection(text); } public destroy(): void { From 178eca1b68b98ce467b24143fb32723bcebd41e0 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Thu, 20 Feb 2025 16:05:31 -0500 Subject: [PATCH 33/65] [Test] enhance `AskAIProvider` to include prompts for menu actions and integrate AI text service --- .../view/widget/palette/askAIProvider.ts | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/editor/view/widget/palette/askAIProvider.ts b/src/editor/view/widget/palette/askAIProvider.ts index f093d034a..fa26afe89 100644 --- a/src/editor/view/widget/palette/askAIProvider.ts +++ b/src/editor/view/widget/palette/askAIProvider.ts @@ -1,28 +1,58 @@ import { MenuAction, SimpleMenuAction } from "src/base/browser/basic/menu/menuItem"; import { IEditorWidget } from "src/editor/editorWidget"; +import { IAITextService } from "src/platform/ai/common/aiText"; import { II18nService } from "src/platform/i18n/browser/i18nService"; - export class AskAIProvider { constructor( private readonly editorWidget: IEditorWidget, @II18nService private readonly i18nService: II18nService, + @IAITextService private readonly aiTextService: IAITextService, ) {} public getContent(): MenuAction[] { const contents: MenuAction[] = []; [ - 'Continue Writing', - 'Make a Summary', - 'Make a Outline', + { name: 'Continue Writing', prompt: 'Continue writing based on the given content.' }, + { name: 'Make a Summary', prompt: 'Make a summary of the given content.' }, + { name: 'Make a Outline', prompt: 'Make a outline of the givne content.' }, ] - .forEach(name => { + .forEach(({ name, prompt }) => { contents.push(new SimpleMenuAction({ id: name, enabled: true, - callback: () => {}, + callback: () => { + + // TEST + const content = this.editorWidget.model.getRawContent(); + + let responseContent = ''; + + (async () => { + await this.aiTextService.sendRequestStream({ + messages: [ + { + role: 'system', + content: prompt, + }, + { + role: 'user', + content: content, + } + ], + model: 'gpt-4o', + stream: true, + }, (response) => { + if (response.primaryMessage.content) { + responseContent += response.primaryMessage.content; + } + }).unwrap(); + + console.log(responseContent); + })(); + }, })); }); From d561cabef86aab2fa4c9de85fd1cb00c57f13ab9 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Thu, 20 Feb 2025 16:53:54 -0500 Subject: [PATCH 34/65] [Chore] log --- src/editor/contrib/inputRule/inputRule.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editor/contrib/inputRule/inputRule.ts b/src/editor/contrib/inputRule/inputRule.ts index 966568f98..74e977cf1 100644 --- a/src/editor/contrib/inputRule/inputRule.ts +++ b/src/editor/contrib/inputRule/inputRule.ts @@ -10,6 +10,7 @@ import { KeyCode } from "src/base/common/keyboard"; import { TokenEnum } from "src/editor/common/markdown"; import { IInputRule, InputRule, registerDefaultInputRules } from "src/editor/contrib/inputRule/editorInputRules"; import { IInstantiationService, IServiceProvider } from "src/platform/instantiation/common/instantiation"; +import { ILogService } from "src/base/common/logger"; /** * Defines the replacement behavior for an input rule. An input rule replacement @@ -117,6 +118,7 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor constructor( editorWidget: IEditorWidget, @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, ) { super(editorWidget); @@ -225,6 +227,7 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor const tr = rule.onMatch(state, match, start, end); if (!tr) { + this.logService.warn('EditorInput', `Unable to achiece replacement for matched input rule: ${rule.id}.`); continue; } From 76bd51d728b8b9e272c9143daa733784767897f8 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Thu, 20 Feb 2025 17:28:49 -0500 Subject: [PATCH 35/65] [Feat] enhance input rule handling to support `mark` replacements --- .../contrib/inputRule/editorInputRules.ts | 66 ++++++++-- src/editor/contrib/inputRule/inputRule.ts | 114 +++++++++++------- 2 files changed, 122 insertions(+), 58 deletions(-) diff --git a/src/editor/contrib/inputRule/editorInputRules.ts b/src/editor/contrib/inputRule/editorInputRules.ts index 2b06d3280..d797835cd 100644 --- a/src/editor/contrib/inputRule/editorInputRules.ts +++ b/src/editor/contrib/inputRule/editorInputRules.ts @@ -1,7 +1,8 @@ import { EditorState, Transaction } from "prosemirror-state"; import { canJoin, findWrapping } from "prosemirror-transform"; -import { TokenEnum } from "src/editor/common/markdown"; -import { IEditorInputRuleExtension, InputRuleReplacement } from "src/editor/contrib/inputRule/inputRule"; +import { panic } from "src/base/common/utilities/panic"; +import { MarkEnum, TokenEnum } from "src/editor/common/markdown"; +import { IEditorInputRuleExtension, InputRuleReplacement, MarkInputRuleReplacement, NodeInputRuleReplacement } from "src/editor/contrib/inputRule/inputRule"; import { CodeBlockAttrs } from "src/editor/model/documentNode/node/codeBlock/codeBlock"; import { HeadingAttrs } from "src/editor/model/documentNode/node/heading"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; @@ -18,9 +19,10 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): // Heading Rule: Matches "#" followed by a space extension.registerRule("headingRule", /^(#{1,6})\s$/, { + type: 'node', nodeType: TokenEnum.Heading, whenReplace: 'type', - getNodeAttribute: (match): HeadingAttrs => { + getAttribute: (match): HeadingAttrs => { return { level: match[1]?.length, }; @@ -32,6 +34,7 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): // Blockquote Rule: Matches ">" followed by a space extension.registerRule("blockquoteRule", /^>\s$/, { + type: 'node', nodeType: TokenEnum.Blockquote, whenReplace: 'type', wrapStrategy: 'WrapBlock' @@ -41,9 +44,10 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): // Code Block Rule: Matches triple backticks extension.registerRule("codeBlockRule", /^```(.*)\s*$/, { + type: 'node', nodeType: TokenEnum.CodeBlock, whenReplace: 'enter', - getNodeAttribute: (match): CodeBlockAttrs => { + getAttribute: (match): CodeBlockAttrs => { const lang = match[1]; return { lang: lang ?? 'Unknown', @@ -55,9 +59,10 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): extension.registerRule("orderedListRule", /^(\d+)\.\s$/, { + type: 'node', nodeType: TokenEnum.List, whenReplace: 'type', - getNodeAttribute: (match) => { + getAttribute: (match) => { if (match && match[1]) { return { ordered: true, start: parseInt(match[1]) }; } @@ -75,6 +80,7 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): extension.registerRule("bulletListRule", /^\s*([-+*])\s$/, { + type: 'node', nodeType: TokenEnum.List, whenReplace: 'type', wrapStrategy: 'WrapBlock' @@ -133,16 +139,23 @@ export class InputRule implements IInputRule { this.pattern = pattern; this.replacement = replacement; - if (typeof this.replacement !== 'string') { + if (typeof this.replacement === 'string') { + this._replacementString = this.replacement; + this.onMatch = this.__onSimpleStringMatch; + } + else if (this.replacement.type === 'node') { this._replacementObject = this.replacement; if (this.replacement.wrapStrategy === 'WrapTextBlock') { this.onMatch = this.__textblockTypeInputRule; } else { this.onMatch = this.__wrappingInputRule; } + } + else if (this.replacement.type === 'mark') { + this._replacementObject = this.replacement; + this.onMatch = this.__markInputRule; } else { - this._replacementString = this.replacement; - this.onMatch = this.__onSimpleStringMatch; + panic(`Invalid replacement type: ${this.replacement['type']}`); } } @@ -167,6 +180,35 @@ export class InputRule implements IInputRule { } return state.tr.insertText(insert, start, end); } + + private __markInputRule( + state: EditorState, + match: RegExpExecArray, + start: number, + end: number + ): Transaction | null { + const replacement = this.replacement as MarkInputRuleReplacement; + const markType = state.schema.marks[replacement.markType]; + if (!markType) { + console.warn(`Mark type "${replacement.markType}" not found`); + return null; + } + + const text = match[1]!; + const tr = state.tr + .delete(start, end) + .insertText(text, start); + + const attrs = replacement.getAttribute?.(match, this.instantiationService); + const mark = markType.create(attrs); + tr.addMark(start, start + text.length, mark); + + if (replacement.preventMarkInheritance) { + tr.setStoredMarks([]); + } + + return tr; + } private __wrappingInputRule( state: EditorState, @@ -174,14 +216,14 @@ export class InputRule implements IInputRule { start: number, end: number ): Transaction | null { - const replacement = this._replacementObject!; + const replacement = this._replacementObject as NodeInputRuleReplacement; const nodeType = state.schema.nodes[replacement.nodeType]; if (!nodeType) { console.warn(`[EditorInputRuleExtension] Node type "${replacement.nodeType}" not found in schema.`); return null; } - const attrs = replacement.getNodeAttribute?.(match, this.instantiationService); + const attrs = replacement.getAttribute?.(match, this.instantiationService); const tr = state.tr.delete(start, end); const $start = tr.doc.resolve(start); const range = $start.blockRange(); @@ -211,14 +253,14 @@ export class InputRule implements IInputRule { start: number, end: number ): Transaction | null { - const replacement = this._replacementObject!; + const replacement = this._replacementObject as NodeInputRuleReplacement; const nodeType = state.schema.nodes[replacement.nodeType]; if (!nodeType) { console.warn(`[EditorInputRuleExtension] Node type "${replacement.nodeType}" not found in schema.`); return null; } - const attrs = replacement.getNodeAttribute?.(match, this.instantiationService); + const attrs = replacement.getAttribute?.(match, this.instantiationService); const $start = state.doc.resolve(start); if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType)) { return null; diff --git a/src/editor/contrib/inputRule/inputRule.ts b/src/editor/contrib/inputRule/inputRule.ts index 74e977cf1..49fb874eb 100644 --- a/src/editor/contrib/inputRule/inputRule.ts +++ b/src/editor/contrib/inputRule/inputRule.ts @@ -19,52 +19,74 @@ import { ILogService } from "src/base/common/logger"; * 2. an object that specifies complicated replacement rule. */ export type InputRuleReplacement = - | string - | { - /** - * Specifies the type of node to create when replacing. - */ - readonly nodeType: string | TokenEnum; - - /** - * Determines the wrapping strategy to use when applying the input rule. - * - `WrapBlock`: Wraps the matched content as a block-level element. - * - `WrapTextBlock`: Wraps the matched content as a text block within a block-level container. - */ - readonly wrapStrategy: 'WrapBlock' | 'WrapTextBlock'; - - /** - * Determines when should the replacement happens. - * - `type`: Any keyboard typing will try to match content. - * - `enter`: Only when pressing the key `enter` will try to match content. - */ - readonly whenReplace: 'type' | 'enter'; - - /** - * @description A function that generates node attributes based on the - * matched text. The attributes will eventually used for constructing - * the node instance ({@link ProseNode}). - * @param matchedText The matched text. - * @returns A dictionary of attributes for the new node. - * - * @note If not defined, the attributes of the generated node would be `null`. - */ - readonly getNodeAttribute?: (matchedText: RegExpExecArray, serviceProvider: IServiceProvider) => Dictionary; - - /** - * @description A predicate function that determines if the new node - * should join with the preceding node. - * @param matchedText The matched text. - * @param prevNode The previous node in the document, used to determine - * if a join is appropriate. - * @returns Returns `true` if the new node should join with `prevNode`, - * otherwise `false`. - * - * @note If not defined, as long as {@link canJoin} returns true, the - * node will be joined with previous node. - */ - readonly shouldJoinWithBefore?: (matchedText: RegExpExecArray, prevNode: ProseNode) => boolean; - }; + | string + | MarkInputRuleReplacement + | NodeInputRuleReplacement; + +type InputRuleReplacementBase = { + readonly type: string; + + readonly whenReplace: 'type' | 'enter'; + + /** + * @description A function that generates node/mark attributes based on the + * matched text. The attributes will eventually used for constructing the + * node/mark instance ({@link ProseNode}). + * @param matchedText The matched text. + * @returns A dictionary of attributes for the new node/mark. + * + * @note If not defined, the attributes of the generated node/mark would be + * `null`. + */ + readonly getAttribute?: (matchedText: RegExpExecArray, serviceProvider: IServiceProvider) => Dictionary; +}; + +export type MarkInputRuleReplacement = InputRuleReplacementBase & { + readonly type: 'mark'; + readonly markType: string; + + /** + * @description After the mark is applied, should the following typed text + * inherit this mark. + */ + readonly preventMarkInheritance: boolean; +}; + +export type NodeInputRuleReplacement = InputRuleReplacementBase & { + readonly type: 'node'; + /** + * Specifies the type of node to create when replacing. + */ + readonly nodeType: string | TokenEnum; + + /** + * Determines the wrapping strategy to use when applying the input rule. + * - `WrapBlock`: Wraps the matched content as a block-level element. + * - `WrapTextBlock`: Wraps the matched content as a text block within a block-level container. + */ + readonly wrapStrategy: 'WrapBlock' | 'WrapTextBlock'; + + /** + * Determines when should the replacement happens. + * - `type`: Any keyboard typing will try to match content. + * - `enter`: Only when pressing the key `enter` will try to match content. + */ + readonly whenReplace: 'type' | 'enter'; + + /** + * @description A predicate function that determines if the new node + * should join with the preceding node. + * @param matchedText The matched text. + * @param prevNode The previous node in the document, used to determine + * if a join is appropriate. + * @returns Returns `true` if the new node should join with `prevNode`, + * otherwise `false`. + * + * @note If not defined, as long as {@link canJoin} returns true, the + * node will be joined with previous node. + */ + readonly shouldJoinWithBefore?: (matchedText: RegExpExecArray, prevNode: ProseNode) => boolean; +}; /** * An interface only for {@link EditorInputRuleExtension}. From 07ccfde42e5927d90b7cc4db7c7bb6c83cb99186 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Thu, 20 Feb 2025 17:29:12 -0500 Subject: [PATCH 36/65] [Feat] add `strongRule` input rule --- src/editor/contrib/inputRule/editorInputRules.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/editor/contrib/inputRule/editorInputRules.ts b/src/editor/contrib/inputRule/editorInputRules.ts index d797835cd..d111184c5 100644 --- a/src/editor/contrib/inputRule/editorInputRules.ts +++ b/src/editor/contrib/inputRule/editorInputRules.ts @@ -86,6 +86,14 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): wrapStrategy: 'WrapBlock' } ); + + extension.registerRule("strongRule", /\*\*(.+?)\*\*$/, { + type: 'mark', + markType: MarkEnum.Strong, + whenReplace: 'type', + preventMarkInheritance: true, + }); + } /** From 909c5b982ccdf6f2c14e5ef38821faedc476fc22 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Thu, 20 Feb 2025 17:51:27 -0500 Subject: [PATCH 37/65] [Feat] add `emphasisRule` input rule --- src/editor/contrib/inputRule/editorInputRules.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/editor/contrib/inputRule/editorInputRules.ts b/src/editor/contrib/inputRule/editorInputRules.ts index d111184c5..fb4ffe35b 100644 --- a/src/editor/contrib/inputRule/editorInputRules.ts +++ b/src/editor/contrib/inputRule/editorInputRules.ts @@ -94,6 +94,13 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): preventMarkInheritance: true, }); + extension.registerRule("emphasisRule", /\*(.+?)\*$/, { + type: 'mark', + markType: MarkEnum.Em, + whenReplace: 'type', + preventMarkInheritance: true, + }); + } /** From 5dc4820f5c1f6d99378075fd75c4cb692401a963 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Thu, 20 Feb 2025 17:51:51 -0500 Subject: [PATCH 38/65] [Feat] add `codespanRule` input rule --- src/editor/contrib/inputRule/editorInputRules.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/editor/contrib/inputRule/editorInputRules.ts b/src/editor/contrib/inputRule/editorInputRules.ts index fb4ffe35b..2f26bf1b1 100644 --- a/src/editor/contrib/inputRule/editorInputRules.ts +++ b/src/editor/contrib/inputRule/editorInputRules.ts @@ -101,6 +101,14 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): preventMarkInheritance: true, }); + extension.registerRule("codespanRule", /`(.+?)`$/, { + type: 'mark', + markType: MarkEnum.Codespan, + whenReplace: 'type', + preventMarkInheritance: true, + }); + + } /** From 05447774ca778277306fcd7d1e7c718c47dfd2c2 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Thu, 20 Feb 2025 18:11:50 -0500 Subject: [PATCH 39/65] [Feat] add `mathBlockRule` input rule with replace block strategy --- .../contrib/inputRule/editorInputRules.ts | 49 ++++++++++++++++++- src/editor/contrib/inputRule/inputRule.ts | 3 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/editor/contrib/inputRule/editorInputRules.ts b/src/editor/contrib/inputRule/editorInputRules.ts index 2f26bf1b1..5272f3bd4 100644 --- a/src/editor/contrib/inputRule/editorInputRules.ts +++ b/src/editor/contrib/inputRule/editorInputRules.ts @@ -2,6 +2,7 @@ import { EditorState, Transaction } from "prosemirror-state"; import { canJoin, findWrapping } from "prosemirror-transform"; import { panic } from "src/base/common/utilities/panic"; import { MarkEnum, TokenEnum } from "src/editor/common/markdown"; +import { ProseNodeSelection } from "src/editor/common/proseMirror"; import { IEditorInputRuleExtension, InputRuleReplacement, MarkInputRuleReplacement, NodeInputRuleReplacement } from "src/editor/contrib/inputRule/inputRule"; import { CodeBlockAttrs } from "src/editor/model/documentNode/node/codeBlock/codeBlock"; import { HeadingAttrs } from "src/editor/model/documentNode/node/heading"; @@ -108,7 +109,14 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): preventMarkInheritance: true, }); - + extension.registerRule("mathBlockRule", /^\$\$$/, + { + type: 'node', + nodeType: TokenEnum.MathBlock, + whenReplace: 'enter', + wrapStrategy: 'ReplaceBlock', + } + ); } /** @@ -170,7 +178,11 @@ export class InputRule implements IInputRule { this._replacementObject = this.replacement; if (this.replacement.wrapStrategy === 'WrapTextBlock') { this.onMatch = this.__textblockTypeInputRule; - } else { + } + else if (this.replacement.wrapStrategy === 'ReplaceBlock') { + this.onMatch = this.__replaceBlockInputRule; + } + else { this.onMatch = this.__wrappingInputRule; } } @@ -293,4 +305,37 @@ export class InputRule implements IInputRule { .delete(start, end) .setBlockType(start, start, nodeType, attrs); } + + private __replaceBlockInputRule( + state: EditorState, + match: RegExpExecArray, + start: number, + end: number + ): Transaction | null { + const replacement = this.replacement as NodeInputRuleReplacement; + const nodeType = state.schema.nodes[replacement.nodeType]; + if (!nodeType) { + console.warn(`Node type "${replacement.nodeType}" not found`); + return null; + } + + const $pos = state.doc.resolve(start); + const blockRange = $pos.blockRange(); + if (!blockRange) { + return null; + } + + let tr = state.tr.delete(blockRange.start, blockRange.end); + + const attrs = replacement.getAttribute?.(match, this.instantiationService); + const newNode = nodeType.create(attrs); + tr = tr.insert(blockRange.start, newNode); + + // select it + const newPos = tr.doc.resolve(blockRange.start); + const selection = ProseNodeSelection.near(newPos, -1); + tr.setSelection(selection); + + return tr; + } } \ No newline at end of file diff --git a/src/editor/contrib/inputRule/inputRule.ts b/src/editor/contrib/inputRule/inputRule.ts index 49fb874eb..57b6f1bcf 100644 --- a/src/editor/contrib/inputRule/inputRule.ts +++ b/src/editor/contrib/inputRule/inputRule.ts @@ -63,8 +63,9 @@ export type NodeInputRuleReplacement = InputRuleReplacementBase & { * Determines the wrapping strategy to use when applying the input rule. * - `WrapBlock`: Wraps the matched content as a block-level element. * - `WrapTextBlock`: Wraps the matched content as a text block within a block-level container. + * - `ReplaceBlock`: Replace the matched block as a new block-level element. */ - readonly wrapStrategy: 'WrapBlock' | 'WrapTextBlock'; + readonly wrapStrategy: 'WrapBlock' | 'WrapTextBlock' | 'ReplaceBlock'; /** * Determines when should the replacement happens. From c1517b7187b36cab573295257a3f15624f6b4cf7 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 11:47:29 -0500 Subject: [PATCH 40/65] [Feat] enhance block and inline math nodes to support draggable, selectable, and atom properties --- src/editor/model/documentNode/mark/mathInline.ts | 3 +++ src/editor/model/documentNode/node/mathBlock.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/editor/model/documentNode/mark/mathInline.ts b/src/editor/model/documentNode/mark/mathInline.ts index 95460509c..732d27285 100644 --- a/src/editor/model/documentNode/mark/mathInline.ts +++ b/src/editor/model/documentNode/mark/mathInline.ts @@ -63,6 +63,9 @@ export class MathInline extends DocumentNode { group: 'inline', inline: true, content: undefined, + draggable: true, + selectable: true, + atom: true, attrs: { text: { default: '' }, }, diff --git a/src/editor/model/documentNode/node/mathBlock.ts b/src/editor/model/documentNode/node/mathBlock.ts index 160edcb06..506163cb8 100644 --- a/src/editor/model/documentNode/node/mathBlock.ts +++ b/src/editor/model/documentNode/node/mathBlock.ts @@ -60,6 +60,7 @@ export class MathBlock extends DocumentNode { content: undefined, draggable: true, selectable: true, + atom: true, attrs: { text: { default: '' }, } satisfies GetProseAttrs, From 6d9db42da9cd35414af465da1f0582be2099511a Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 11:47:47 -0500 Subject: [PATCH 41/65] [Fix] improve block placeholder logic --- .../contrib/blockPlaceHolder/blockPlaceHolder.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/editor/contrib/blockPlaceHolder/blockPlaceHolder.ts b/src/editor/contrib/blockPlaceHolder/blockPlaceHolder.ts index 8f218dabf..94582318a 100644 --- a/src/editor/contrib/blockPlaceHolder/blockPlaceHolder.ts +++ b/src/editor/contrib/blockPlaceHolder/blockPlaceHolder.ts @@ -44,12 +44,14 @@ export class EditorBlockPlaceHolderExtension extends EditorExtension implements return null; } - const isEmptyBlock = ProseTools.Cursor.isOnEmpty(selection); - if (!isEmptyBlock) { + const cursor = selection.$from; + + const anyChild = ProseTools.Node.hasChild(cursor.parent); + if (anyChild) { return null; } - - const blockPos = selection.$from.before(); + + const blockPos = cursor.before(); const blockNode = state.doc.nodeAt(blockPos); if (!blockNode) { return null; From d0c1d92aa0d579fbd9b822910e535e00ec1b9c7a Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 11:57:30 -0500 Subject: [PATCH 42/65] [Fix] remove block display style for `` element --- src/editor/view/media/richTextView.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor/view/media/richTextView.scss b/src/editor/view/media/richTextView.scss index b5494a16f..a13ea649e 100644 --- a/src/editor/view/media/richTextView.scss +++ b/src/editor/view/media/richTextView.scss @@ -103,7 +103,6 @@ & img { max-width: 100%; height: auto; - display: block; margin: 4px 0; } From 6e2de91330b4cfdcf6edf0cb1fb78f394a5735c0 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 11:57:49 -0500 Subject: [PATCH 43/65] [Chore] --- src/editor/common/proseUtility.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/editor/common/proseUtility.ts b/src/editor/common/proseUtility.ts index 6aaa2d3c9..92b4ad52f 100644 --- a/src/editor/common/proseUtility.ts +++ b/src/editor/common/proseUtility.ts @@ -57,6 +57,7 @@ export namespace ProseTools { export const isEmptyTextBlock = __isEmptyTextBlock; export const isInline = __isInline; export const isLeaf = __isLeaf; + export const hasChild = __hasChild; export const iterateChild = __iterateChild; export const getNextValidDefaultNodeTypeAt = __getNextValidDefaultNodeTypeAt; @@ -187,6 +188,10 @@ function __isLeaf(node: ProseNode): boolean { return node.isLeaf; } +function __hasChild(node: ProseNode): boolean { + return !!node.childCount; +} + function *__iterateChild(node: ProseNode): IterableIterator<{ node: ProseNode, offset: number, index: number }> { const fragment = node.content; let offset = 0; From 108fcbebc166fe492e3502a7d0d736810ec61613 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 12:15:24 -0500 Subject: [PATCH 44/65] [Fix] typo --- src/editor/editorWidget.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index 82c9fb694..b7e131974 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -413,10 +413,6 @@ export class EditorWidget extends Disposable implements IEditorWidget { return this.model.deleteAt(textOffset, length); } - public insertAtSelection(text: string): void { - return this.model.insertAtSelection(text); - } - public destroy(): void { return this.dispose(); } From 1d2b84e8222bd68b7a477e92d58b7ac247d80f69 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 16:40:55 -0500 Subject: [PATCH 45/65] [Fix] ensure `EditorInput` handling actually handles the event --- src/editor/contrib/inputRule/inputRule.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/editor/contrib/inputRule/inputRule.ts b/src/editor/contrib/inputRule/inputRule.ts index 57b6f1bcf..3b9d558d8 100644 --- a/src/editor/contrib/inputRule/inputRule.ts +++ b/src/editor/contrib/inputRule/inputRule.ts @@ -151,6 +151,7 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor const handled = this.__handleTextInput(e.view, e.from, e.to, e.text); if (handled) { e.preventDefault(); + return true; } })); @@ -159,6 +160,7 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor const handled = this.__handleEnter(e.view); if (handled) { e.preventDefault(); + return true; } } })); From 69556dee0dd34052c7f652d769dab5191424030d Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 17:02:53 -0500 Subject: [PATCH 46/65] [Fix] enhance input rule handling for inline code and math expressions --- .../contrib/inputRule/editorInputRules.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/editor/contrib/inputRule/editorInputRules.ts b/src/editor/contrib/inputRule/editorInputRules.ts index 5272f3bd4..9f1918635 100644 --- a/src/editor/contrib/inputRule/editorInputRules.ts +++ b/src/editor/contrib/inputRule/editorInputRules.ts @@ -102,7 +102,8 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): preventMarkInheritance: true, }); - extension.registerRule("codespanRule", /`(.+?)`$/, { + const ESCAPE_REGEX = /`(?![`]{2})((?:\\`|[^`])+?)`(?![`]{2})$/; + extension.registerRule("codespanRule", ESCAPE_REGEX, { type: 'mark', markType: MarkEnum.Codespan, whenReplace: 'type', @@ -117,6 +118,18 @@ export function registerDefaultInputRules(extension: IEditorInputRuleExtension): wrapStrategy: 'ReplaceBlock', } ); + + extension.registerRule("mathInlineRule", /\$(.+?)\$$/, { + type: 'node', + nodeType: TokenEnum.MathInline, + whenReplace: 'type', + wrapStrategy: 'ReplaceBlock', + getAttribute: (matched) => { + return { + text: matched[1], + }; + } + }); } /** @@ -319,21 +332,28 @@ export class InputRule implements IInputRule { return null; } + if (nodeType.isInline) { + const attrs = replacement.getAttribute?.(match, this.instantiationService); + const newNode = nodeType.create(attrs); + const tr = state.tr + .delete(start, end) + .insert(start, newNode); + return tr; + } + const $pos = state.doc.resolve(start); const blockRange = $pos.blockRange(); if (!blockRange) { return null; } - let tr = state.tr.delete(blockRange.start, blockRange.end); - const attrs = replacement.getAttribute?.(match, this.instantiationService); const newNode = nodeType.create(attrs); - tr = tr.insert(blockRange.start, newNode); + const tr = state.tr.replaceWith(blockRange.start, blockRange.end, newNode); // select it const newPos = tr.doc.resolve(blockRange.start); - const selection = ProseNodeSelection.near(newPos, -1); + const selection = ProseNodeSelection.near(newPos); tr.setSelection(selection); return tr; From 87bae007baaf5daee3185c9651c263223e244918 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 18:27:27 -0500 Subject: [PATCH 47/65] [Refactor] enrich `EditorViewModel` --- src/editor/common/model.ts | 49 +----- src/editor/common/viewModel.ts | 31 +++- src/editor/contrib/autoSave.ts | 4 +- src/editor/editorWidget.ts | 26 +-- src/editor/model/editorModel.ts | 165 ++++-------------- src/editor/view/editorView.ts | 7 +- .../widget/palette/blockInsertProvider.ts | 2 +- src/editor/viewModel/editorViewModel.ts | 111 ++++++++++-- 8 files changed, 187 insertions(+), 208 deletions(-) diff --git a/src/editor/common/model.ts b/src/editor/common/model.ts index 04e33763d..e192cd588 100644 --- a/src/editor/common/model.ts +++ b/src/editor/common/model.ts @@ -2,12 +2,8 @@ import * as marked from "marked"; import { IDisposable } from "src/base/common/dispose"; import { Register } from "src/base/common/event"; import { URI } from "src/base/common/files/uri"; -import { ProseEditorState, ProseTransaction } from "src/editor/common/proseMirror"; import { AsyncResult } from "src/base/common/result"; -import { IEditorExtension } from "src/editor/common/editorExtension"; -import { EditorSchema } from "src/editor/model/schema"; import { IEditorPosition } from "src/editor/common/position"; -import { IOnDidContentChangeEvent } from "src/editor/view/proseEventBroadcaster"; export type EditorToken = marked.Token; export type EditorTokenGeneric = marked.Tokens.Generic; @@ -49,6 +45,12 @@ export namespace EditorTokens { }; } +// region - IEditorModel + +export interface IModelBuildData { + readonly tokens: EditorToken[]; +} + /** * An interface only for {@link EditorModel}. */ @@ -64,23 +66,8 @@ export interface IEditorModel extends IDisposable { */ readonly dirty: boolean; - /** - * The schema of the editor. - */ - readonly schema: EditorSchema; - - /** - * The state of the model. Returns undefined if the model is not ready yet. - */ - readonly state?: ProseEditorState; - // region - events - /** - * Fires when the model is built for the first time. - */ - readonly onDidBuild: Register; - /** * Fires whenever the file is saved back to the disk successfully. */ @@ -98,24 +85,12 @@ export interface IEditorModel extends IDisposable { */ readonly onDidDirtyChange: Register; - /** - * Fires whenever a transaction to the {@link ProseEditorState} is made - * programmatically. - */ - readonly onTransaction: Register; - - /** - * Fires whenever the state of the view is updated. - */ - readonly onDidStateChange: Register; - // region - general /** * @description Start building the model. - * @note This will trigger `onDidBuild` event. */ - build(extensions: IEditorExtension[]): AsyncResult; + build(): AsyncResult; /** * @description Mark if the model has any unsaved changes. @@ -215,16 +190,6 @@ export interface IEditorModel extends IDisposable { * @param lineOffset The offset relative to the line. */ getCharCodeByLine(lineNumber: number, lineOffset: number): number; - - // region - others - - getRegisteredDocumentNodes(): string[]; - getRegisteredDocumentNodesBlock(): string[]; - getRegisteredDocumentNodesInline(): string[]; - - // region - internal - - __onDidStateChange(event: IOnDidContentChangeEvent): void; } export const enum EndOfLineType { diff --git a/src/editor/common/viewModel.ts b/src/editor/common/viewModel.ts index b9842e7e0..880664713 100644 --- a/src/editor/common/viewModel.ts +++ b/src/editor/common/viewModel.ts @@ -1,15 +1,42 @@ import { Disposable } from "src/base/common/dispose"; import { Register } from "src/base/common/event"; import { ProseEditorState, ProseTransaction } from "src/editor/common/proseMirror"; +import { EditorSchema } from "src/editor/model/schema"; import { IOnDidContentChangeEvent } from "src/editor/view/proseEventBroadcaster"; +// region - IEditorViewModel + +export interface IViewModelBuildData { + readonly state: ProseEditorState; +} + +export interface IViewModelChangeEvent { + readonly tr: ProseTransaction; +} + export interface IEditorViewModel extends Disposable { - readonly onDidChangeModelState: Register; - readonly onDidBuild: Register; + /** + * The schema of the editor. + */ + readonly schema: EditorSchema; + + /** + * The state of the model. Returns undefined if the data is not ready yet. + */ + readonly state?: ProseEditorState; + + readonly onDidContentChange: Register; /** * @description This will be invoked whenever the view content changes. */ onDidViewContentChange(e: IOnDidContentChangeEvent): void; + + + // region - others + + getRegisteredDocumentNodes(): string[]; + getRegisteredDocumentNodesBlock(): string[]; + getRegisteredDocumentNodesInline(): string[]; } \ No newline at end of file diff --git a/src/editor/contrib/autoSave.ts b/src/editor/contrib/autoSave.ts index 2d00d5138..2763f2c22 100644 --- a/src/editor/contrib/autoSave.ts +++ b/src/editor/contrib/autoSave.ts @@ -95,8 +95,8 @@ export class EditorAutoSaveExtension extends EditorExtension implements IEditorA } private __registerEditorStateListener(): void { - this.__register(this._editorWidget.onDidStateChange(() => { - if (this._autoSave) { + this.__register(this._editorWidget.onDidDirtyChange((isDirty) => { + if (isDirty && this._autoSave) { this._scheduler.schedule(undefined, this._autoSaveDelay); } })); diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index b7e131974..b77e4a006 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -32,7 +32,6 @@ export interface IEditorWidget extends Pick()); - public readonly onDidStateChange = this._onDidStateChange.registerListener; - private readonly _onDidDirtyChange = this.__register(RelayEmitter.createPriority()); public readonly onDidDirtyChange = this._onDidDirtyChange.registerListener; @@ -326,11 +322,10 @@ export class EditorWidget extends Disposable implements IEditorWidget { } this.__detachData(); - const extensionList = this._extensions.getExtensions(); - + // model this._model = this.instantiationService.createInstance(EditorModel, source, this._options.getOptions()); - const initState = await this._model.build(extensionList); + const initState = await this._model.build(); // unexpected behavior, we need to let the user know. if (initState.isErr()) { @@ -339,17 +334,24 @@ export class EditorWidget extends Disposable implements IEditorWidget { return err(error); } - const initData = initState.unwrap(); + const modelBuild = initState.unwrap(); + const extensionList = this._extensions.getExtensions(); // view-model - this._viewModel = this.instantiationService.createInstance(EditorViewModel, this._model); + this._viewModel = this.instantiationService.createInstance( + EditorViewModel, + this._model, + extensionList, + ); + + const { state: viewState } = this._viewModel.build(modelBuild); // view this._view = this.instantiationService.createInstance( EditorView, this._container.raw, this._viewModel, - initData, + viewState, extensionList, this._options.getOptions(), ); @@ -452,11 +454,13 @@ export class EditorWidget extends Disposable implements IEditorWidget { private __registerMVVMListeners(model: IEditorModel, viewModel: IEditorViewModel, view: IEditorView): void { // binding to the model - this._onDidStateChange.setInput(model.onDidStateChange); this._onDidDirtyChange.setInput(model.onDidDirtyChange); this._onDidSave.setInput(model.onDidSave); this._onDidSaveError.setInput(model.onDidSaveError); + // binding to the viewModel + // TODO + // binding to the view this._onDidBlur.setInput(this.view.onDidBlur); this._onDidFocus.setInput(this.view.onDidFocus); diff --git a/src/editor/model/editorModel.ts b/src/editor/model/editorModel.ts index 71bf24eeb..527d75c1c 100644 --- a/src/editor/model/editorModel.ts +++ b/src/editor/model/editorModel.ts @@ -2,38 +2,18 @@ import { Disposable } from "src/base/common/dispose"; import { Emitter } from "src/base/common/event"; import { DataBuffer } from "src/base/common/files/buffer"; import { URI } from "src/base/common/files/uri"; -import { defaultLog, ILogService } from "src/base/common/logger"; +import { ILogService } from "src/base/common/logger"; import { AsyncResult, ok } from "src/base/common/result"; -import { assert } from "src/base/common/utilities/panic"; import { EditorOptionsType } from "src/editor/common/editorConfiguration"; -import { IEditorExtension } from "src/editor/common/editorExtension"; -import { IEditorModel } from "src/editor/common/model"; +import { IEditorModel, IModelBuildData } from "src/editor/common/model"; import { IEditorPosition } from "src/editor/common/position"; -import { ProseEditorState, ProseNode, ProseTransaction } from "src/editor/common/proseMirror"; import { IMarkdownLexer, IMarkdownLexerOptions, MarkdownLexer } from "src/editor/model/markdownLexer"; -import { DocumentNodeProvider } from "src/editor/model/documentNode/documentNodeProvider"; -import { DocumentParser, IDocumentParser } from "src/editor/model/parser"; -import { buildSchema, EditorSchema } from "src/editor/model/schema"; -import { MarkdownSerializer } from "src/editor/model/serializer"; import { IFileService } from "src/platform/files/common/fileService"; -import { history } from "prosemirror-history"; -import { IOnDidContentChangeEvent } from "src/editor/view/proseEventBroadcaster"; -import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; -import { TokenEnum } from "src/editor/common/markdown"; export class EditorModel extends Disposable implements IEditorModel { // [events] - private readonly _onDidBuild = this.__register(new Emitter({ onFire: () => this.setDirty(false) })); - public readonly onDidBuild = this._onDidBuild.registerListener; - - private readonly _onTransaction = this.__register(new Emitter({ onFire: () => this.setDirty(true) })); - public readonly onTransaction = this._onTransaction.registerListener; - - private readonly _onDidStateChange = this.__register(new Emitter({ onFire: () => this.setDirty(true) })); - public readonly onDidStateChange = this._onDidStateChange.registerListener; - private readonly _onDidSave = this.__register(new Emitter({ onFire: () => this.setDirty(false) })); public readonly onDidSave = this._onDidSave.registerListener; @@ -45,15 +25,9 @@ export class EditorModel extends Disposable implements IEditorModel { // [fields] - private readonly _options: EditorOptionsType; // The configuration of the editor - private readonly _source: URI; // The source file the model is about to read and parse. - private readonly _schema: EditorSchema; // An object that defines how a view is organized. - private readonly _lexer: IMarkdownLexer; // Responsible for parsing the raw text into tokens. - private readonly _nodeProvider: DocumentNodeProvider; // Stores all the legal document node. - private readonly _docParser: IDocumentParser; // Parser that parses the given token into a legal view based on the schema. - private readonly _docSerializer: MarkdownSerializer; // Serializer that transforms the prosemirror document back to raw string. - - private _editorState?: ProseEditorState; // A reference to the prosemirror state. + private readonly _options: EditorOptionsType; // The configuration of the editor + private readonly _source: URI; // The source file the model is about to read and parse. + private readonly _lexer: IMarkdownLexer; // Responsible for parsing the raw text into tokens. private _dirty: boolean; // Indicates if the file has unsaved changes. Modify this through `this.setDirty()` // [constructor] @@ -63,67 +37,55 @@ export class EditorModel extends Disposable implements IEditorModel { options: EditorOptionsType, @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, - @IInstantiationService instantiationService: IInstantiationService, ) { super(); this._source = source; this._options = options; this._dirty = false; - this._lexer = new MarkdownLexer(this.__initLexerOptions(options)); - - this._nodeProvider = DocumentNodeProvider.create(instantiationService).register(); - this._schema = buildSchema(this._nodeProvider); - this._docParser = this.__register(new DocumentParser(this._schema, this._nodeProvider, /* options */)); - this.__register(this._docParser.onLog(event => defaultLog(logService, event.level, 'EditorView', event.message, event.error, event.additional))); - this._docSerializer = new MarkdownSerializer(this._nodeProvider, { strict: true, escapeExtraCharacters: undefined, }); - - this.__initialization(); - logService.debug('EditorModel', 'Constructed'); } // [getter / setter] get source(): URI { return this._source; } - get schema(): EditorSchema { return this._schema; } - get state(): ProseEditorState | undefined { return this._editorState; } get dirty(): boolean { return this._dirty; } // [public methods] - public build(extensions: IEditorExtension[]): AsyncResult { - return this.__buildModel(this._source, extensions) - .map(state => { - this._editorState = state; - this._onDidBuild.fire(state); - return state; - }); + public build(): AsyncResult { + return this.__buildModel(this._source); } public insertAt(textOffset: number, text: string): void { - const state = assert(this._editorState); - const document = this.__tokenizeAndParse(text); - const newTr = state.tr.insert(textOffset, document); - this._onTransaction.fire(newTr); + // TODO + + // const state = assert(this._editorState); + // const document = this.__tokenizeAndParse(text); + // const newTr = state.tr.insert(textOffset, document); + // this._onTransaction.fire(newTr); } public deleteAt(textOffset: number, length: number): void { - const state = assert(this._editorState); - const newTr = state.tr.delete(textOffset, textOffset + length); - this._onTransaction.fire(newTr); + // TODO + // const state = assert(this._editorState); + // const newTr = state.tr.delete(textOffset, textOffset + length); + // this._onTransaction.fire(newTr); } public getContent(): string[] { - const state = assert(this._editorState); - const raw = this._docSerializer.serialize(state.doc); - return raw.split('\n'); // TODO + // TODO + return []; + // const state = assert(this._editorState); + // const raw = this._docSerializer.serialize(state.doc); + // return raw.split('\n'); } public getRawContent(): string { - const state = assert(this._editorState); - const raw = this._docSerializer.serialize(state.doc); - return raw; // TODO + return ''; + // const state = assert(this._editorState); + // const raw = this._docSerializer.serialize(state.doc); + // return raw; // TODO } public getLine(lineNumber: number): string { @@ -175,8 +137,7 @@ export class EditorModel extends Disposable implements IEditorModel { return AsyncResult.ok(); } - const state = assert(this._editorState); - const serialized = this._docSerializer.serialize(state.doc); + const serialized = this.getRawContent(); const buffer = DataBuffer.fromString(serialized); return this.fileService.writeFile(this._source, buffer, { create: true, overwrite: true, unlock: false }) @@ -190,87 +151,23 @@ export class EditorModel extends Disposable implements IEditorModel { }); } - public getRegisteredDocumentNodes(): string[] { - return this._nodeProvider.getRegisteredNodes().map(each => each.name); - } - - public getRegisteredDocumentNodesBlock(): string[] { - const nodes = this._nodeProvider.getRegisteredNodes(); - const blocks: string[] = []; - for (const node of nodes) { - if (!node.getSchema().inline) { - blocks.push(node.name); - } - } - return blocks; - } - - public getRegisteredDocumentNodesInline(): string[] { - const nodes = this._nodeProvider.getRegisteredNodes(); - const blocks: string[] = []; - for (const node of nodes) { - if (node.getSchema().inline === true) { - blocks.push(node.name); - } - } - return blocks; - } - // [private methods] - public __onDidStateChange(event: IOnDidContentChangeEvent): void { - const newState = event.view.state; - this._editorState = newState; - this._onDidStateChange.fire(); - } - - private __initialization(): void { - /** - * Mapping token: {@link TokenEnum.Space} to {@link TokenEnum.Paragraph} - * Because `space` are just special cases for `paragraph`. - */ - this._docParser.registerMapToken(TokenEnum.Space, (from) => { - return { - type: TokenEnum.Paragraph, - text: '', - raw: '', - tokens: [] - }; - }); - } - - private __tokenizeAndParse(raw: string): ProseNode { - const tokens = this._lexer.lex(raw); - console.log(tokens); // TEST - - const doc = this._docParser.parse(tokens); - console.log(doc); // TEST - - // console.log(this._docSerializer.serialize(doc)); // TEST - return doc; - } - private __initLexerOptions(options: EditorOptionsType): IMarkdownLexerOptions { return { baseURI: options.baseURI.value, }; } - private __buildModel(source: URI, extensions: IEditorExtension[]): AsyncResult { + private __buildModel(source: URI): AsyncResult { this.logService.debug('EditorModel', `Start building at: ${URI.toString(source)}`); return this.__readFileRaw(source) .andThen(raw => { - const document = this.__tokenizeAndParse(raw); - const state = ProseEditorState.create({ - schema: this._schema, - doc: document, - plugins: [ - ...extensions.map(extension => extension.getViewExtension()), - history({ depth: 500 }), - ], + const tokens = this._lexer.lex(raw); + return ok({ + tokens: tokens, }); - return ok(state); }); } diff --git a/src/editor/view/editorView.ts b/src/editor/view/editorView.ts index f38dc8ef6..fcd8c8349 100644 --- a/src/editor/view/editorView.ts +++ b/src/editor/view/editorView.ts @@ -148,11 +148,8 @@ export class EditorView extends Disposable implements IEditorView { private __registerEventFromModel(): void { const viewModel = this._ctx.viewModel; - this.__register(viewModel.onDidBuild(newState => { - this._view.render(newState); - })); - - this.__register(viewModel.onDidChangeModelState(tr => { + this.__register(viewModel.onDidContentChange(event => { + const { tr } = event; this._view.internalView.dispatch(tr); })); } diff --git a/src/editor/view/widget/palette/blockInsertProvider.ts b/src/editor/view/widget/palette/blockInsertProvider.ts index f96f56681..2be58eea3 100644 --- a/src/editor/view/widget/palette/blockInsertProvider.ts +++ b/src/editor/view/widget/palette/blockInsertProvider.ts @@ -73,7 +73,7 @@ export class BlockInsertProvider { // [private methods] private __obtainValidContent(): string[] { - const blocks = this.editorWidget.model.getRegisteredDocumentNodes(); + const blocks = this.editorWidget.viewModel.getRegisteredDocumentNodes(); const ordered = this.__filterContent(blocks, CONTENT_FILTER); return ordered; } diff --git a/src/editor/viewModel/editorViewModel.ts b/src/editor/viewModel/editorViewModel.ts index f4ff4f9b9..db69043da 100644 --- a/src/editor/viewModel/editorViewModel.ts +++ b/src/editor/viewModel/editorViewModel.ts @@ -1,46 +1,135 @@ +import { history } from "prosemirror-history"; import { Disposable } from "src/base/common/dispose"; import { Emitter } from "src/base/common/event"; -import { IEditorModel } from "src/editor/common/model"; -import { ProseEditorState, ProseTransaction } from "src/editor/common/proseMirror"; -import { IEditorViewModel } from "src/editor/common/viewModel"; +import { defaultLog, ILogService } from "src/base/common/logger"; +import { IEditorExtension } from "src/editor/common/editorExtension"; +import { TokenEnum } from "src/editor/common/markdown"; +import { EditorToken, IEditorModel, IModelBuildData } from "src/editor/common/model"; +import { ProseEditorState, ProseNode } from "src/editor/common/proseMirror"; +import { IEditorViewModel, IViewModelBuildData, IViewModelChangeEvent } from "src/editor/common/viewModel"; +import { DocumentNodeProvider } from "src/editor/model/documentNode/documentNodeProvider"; +import { DocumentParser, IDocumentParser } from "src/editor/model/parser"; +import { buildSchema, EditorSchema } from "src/editor/model/schema"; +import { MarkdownSerializer } from "src/editor/model/serializer"; import { IOnDidContentChangeEvent } from "src/editor/view/proseEventBroadcaster"; +import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; export class EditorViewModel extends Disposable implements IEditorViewModel { // [events] - private readonly _onDidChangeModelState = this.__register(new Emitter()); - public readonly onDidChangeModelState = this._onDidChangeModelState.registerListener; - - private readonly _onDidBuild = this.__register(new Emitter()); - public readonly onDidBuild = this._onDidBuild.registerListener; + private readonly _onDidContentChange = this.__register(new Emitter()); + public readonly onDidContentChange = this._onDidContentChange.registerListener; // [fields] private readonly _model: IEditorModel; + private readonly _schema: EditorSchema; // An object that defines how a view is organized. + private readonly _nodeProvider: DocumentNodeProvider; // Stores all the legal document node. + private readonly _docParser: IDocumentParser; // Parser that parses the given token into a legal view based on the schema. + private readonly _docSerializer: MarkdownSerializer; // Serializer that transforms the prosemirror document back to raw string. + // [constructor] constructor( model: IEditorModel, + private readonly extensions: IEditorExtension[], + @IInstantiationService instantiationService: IInstantiationService, + @ILogService logService: ILogService, ) { super(); this._model = model; + this._nodeProvider = DocumentNodeProvider.create(instantiationService).register(); + this._schema = buildSchema(this._nodeProvider); + this._docParser = this.__register(new DocumentParser(this._schema, this._nodeProvider, /* options */)); + this.__register(this._docParser.onLog(event => defaultLog(logService, event.level, 'EditorView', event.message, event.error, event.additional))); + this._docSerializer = new MarkdownSerializer(this._nodeProvider, { strict: true, escapeExtraCharacters: undefined, }); + this.__registerListeners(); } + // [getter] + + get schema(): EditorSchema { return this._schema; } + // [public methods] + public build(e: IModelBuildData): IViewModelBuildData { + const { tokens } = e; + + const document = this.__parse(tokens); + const state = ProseEditorState.create({ + schema: this._schema, + doc: document, + plugins: [ + ...this.extensions.map(extension => extension.getViewExtension()), + history({ depth: 500 }), + ], + }); + + return { + state: state, + }; + } + public onDidViewContentChange(e: IOnDidContentChangeEvent): void { this._model.setDirty(true); - this._model.__onDidStateChange(e); + // TODO + } + + public getRegisteredDocumentNodes(): string[] { + return this._nodeProvider.getRegisteredNodes().map(each => each.name); + } + + public getRegisteredDocumentNodesBlock(): string[] { + const nodes = this._nodeProvider.getRegisteredNodes(); + const blocks: string[] = []; + for (const node of nodes) { + if (!node.getSchema().inline) { + blocks.push(node.name); + } + } + return blocks; + } + + public getRegisteredDocumentNodesInline(): string[] { + const nodes = this._nodeProvider.getRegisteredNodes(); + const blocks: string[] = []; + for (const node of nodes) { + if (node.getSchema().inline === true) { + blocks.push(node.name); + } + } + return blocks; } // [private methods] private __registerListeners(): void { - this.__register(this._model.onDidBuild(state => this._onDidBuild.fire(state))); - this.__register(this._model.onTransaction(tr => this._onDidChangeModelState.fire(tr))); + + /** + * Mapping token: {@link TokenEnum.Space} to {@link TokenEnum.Paragraph} + * Because `space` are just special cases for `paragraph`. + */ + this._docParser.registerMapToken(TokenEnum.Space, (from) => { + return { + type: TokenEnum.Paragraph, + text: '', + raw: '', + tokens: [] + }; + }); + } + + private __parse(tokens: EditorToken[]): ProseNode { + console.log(tokens); // TEST + + const doc = this._docParser.parse(tokens); + console.log(doc); // TEST + + // console.log(this._docSerializer.serialize(doc)); // TEST + return doc; } } \ No newline at end of file From 2b0d2a47378adea98c67acdb5850da38fcc9c4eb Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 18:29:40 -0500 Subject: [PATCH 48/65] [Refactor] remove `insertAt` and `deleteAt` --- src/editor/common/model.ts | 14 -------------- src/editor/editorWidget.ts | 14 ++------------ src/editor/model/editorModel.ts | 16 ---------------- 3 files changed, 2 insertions(+), 42 deletions(-) diff --git a/src/editor/common/model.ts b/src/editor/common/model.ts index e192cd588..0b16a0769 100644 --- a/src/editor/common/model.ts +++ b/src/editor/common/model.ts @@ -104,20 +104,6 @@ export interface IEditorModel extends IDisposable { // region - text-related APIs - /** - * @description Inserts the given text at the given offset. - * @param textOffset The character offset relatives to the whole text model. - * @param text The text to be inserted. - */ - insertAt(textOffset: number, text: string): void; - - /** - * @description Deletes the text with given length at the given offset. - * @param textOffset The character offset relatives to the whole text model. - * @param length The length of text to be deleted. - */ - deleteAt(textOffset: number, length: number): void; - /** * @description Returns all the line contents (without line breaking). * @returns An array of string, each string represents a line content. diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index b77e4a006..361de7176 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -30,14 +30,12 @@ import { IEditorViewModel } from "src/editor/common/viewModel"; export interface IEditorWidget extends IProseEventBroadcaster, Pick + | 'save'> { /** @@ -407,14 +405,6 @@ export class EditorWidget extends Disposable implements IEditorWidget { get source(): URI { return this.model.source; } get dirty(): boolean { return assert(this._model).dirty; } - public insertAt(textOffset: number, text: string): void { - return this.model.insertAt(textOffset, text); - } - - public deleteAt(textOffset: number, length: number): void { - return this.model.deleteAt(textOffset, length); - } - public destroy(): void { return this.dispose(); } diff --git a/src/editor/model/editorModel.ts b/src/editor/model/editorModel.ts index 527d75c1c..35afe058c 100644 --- a/src/editor/model/editorModel.ts +++ b/src/editor/model/editorModel.ts @@ -57,22 +57,6 @@ export class EditorModel extends Disposable implements IEditorModel { return this.__buildModel(this._source); } - public insertAt(textOffset: number, text: string): void { - // TODO - - // const state = assert(this._editorState); - // const document = this.__tokenizeAndParse(text); - // const newTr = state.tr.insert(textOffset, document); - // this._onTransaction.fire(newTr); - } - - public deleteAt(textOffset: number, length: number): void { - // TODO - // const state = assert(this._editorState); - // const newTr = state.tr.delete(textOffset, textOffset + length); - // this._onTransaction.fire(newTr); - } - public getContent(): string[] { // TODO return []; From 3c69b501932e0355dc04989d747c84ec2b2bfa45 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 20:01:09 -0500 Subject: [PATCH 49/65] [Refactor] bring back `PieceTable` in `EditorModel` --- src/editor/editorWidget.ts | 32 +++++++------ src/editor/model/editorModel.ts | 85 ++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 48 deletions(-) diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index 361de7176..2a0445805 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -21,6 +21,7 @@ import { AsyncResult, err, ok, Result } from "src/base/common/result"; import { EditorDragState } from "src/editor/common/cursorDrop"; import { EditorViewModel } from "src/editor/viewModel/editorViewModel"; import { IEditorViewModel } from "src/editor/common/viewModel"; +import { IFileService } from "src/platform/files/common/fileService"; // region - [interface] @@ -30,14 +31,13 @@ import { IEditorViewModel } from "src/editor/common/viewModel"; export interface IEditorWidget extends IProseEventBroadcaster, Pick { - /** * Is the editor initialized. if not, access to model, viewModel and view * will panic. @@ -282,6 +282,7 @@ export class EditorWidget extends Disposable implements IEditorWidget { @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleService private readonly lifecycleService: IBrowserLifecycleService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IFileService private readonly fileService: IFileService, ) { super(); @@ -314,28 +315,31 @@ export class EditorWidget extends Disposable implements IEditorWidget { // region - [public] public async open(source: URI): Promise> { - const currSource = this._model?.source; - if (currSource && URI.equals(source, currSource)) { + // same source, do nothing. + if (this._model?.source && URI.equals(source, this._model.source)) { return ok(); } + // cleanup first this.__detachData(); - + // model - this._model = this.instantiationService.createInstance(EditorModel, source, this._options.getOptions()); - const initState = await this._model.build(); - + this._model = this.instantiationService.createInstance( + EditorModel, + source, + this._options.getOptions(), + ); + const build = await this._model.build(); + // unexpected behavior, we need to let the user know. - if (initState.isErr()) { - const error = new Error(`Editor: Cannot open editor at '${URI.toFsPath(source)}'. ${errorToMessage(initState.unwrapErr(), false)}`); - this._model.dispose(); + if (build.isErr()) { + const error = new Error(`Cannot open editor at '${URI.toFsPath(source)}'. ${errorToMessage(build.unwrapErr(), false)}`); return err(error); } - - const modelBuild = initState.unwrap(); - const extensionList = this._extensions.getExtensions(); + const modelBuild = build.unwrap(); // view-model + const extensionList = this._extensions.getExtensions(); this._viewModel = this.instantiationService.createInstance( EditorViewModel, this._model, diff --git a/src/editor/model/editorModel.ts b/src/editor/model/editorModel.ts index 35afe058c..89acffd49 100644 --- a/src/editor/model/editorModel.ts +++ b/src/editor/model/editorModel.ts @@ -3,11 +3,14 @@ import { Emitter } from "src/base/common/event"; import { DataBuffer } from "src/base/common/files/buffer"; import { URI } from "src/base/common/files/uri"; import { ILogService } from "src/base/common/logger"; -import { AsyncResult, ok } from "src/base/common/result"; +import { AsyncResult, ok, Result } from "src/base/common/result"; +import { Blocker } from "src/base/common/utilities/async"; +import { assert } from "src/base/common/utilities/panic"; import { EditorOptionsType } from "src/editor/common/editorConfiguration"; -import { IEditorModel, IModelBuildData } from "src/editor/common/model"; +import { EditorToken, IEditorModel, IModelBuildData, IPieceTable } from "src/editor/common/model"; import { IEditorPosition } from "src/editor/common/position"; import { IMarkdownLexer, IMarkdownLexerOptions, MarkdownLexer } from "src/editor/model/markdownLexer"; +import { TextBufferBuilder } from "src/editor/model/textBuffer/textBufferBuilder"; import { IFileService } from "src/platform/files/common/fileService"; export class EditorModel extends Disposable implements IEditorModel { @@ -26,8 +29,9 @@ export class EditorModel extends Disposable implements IEditorModel { // [fields] private readonly _options: EditorOptionsType; // The configuration of the editor - private readonly _source: URI; // The source file the model is about to read and parse. + private readonly _source: URI; // The reading target. private readonly _lexer: IMarkdownLexer; // Responsible for parsing the raw text into tokens. + private _textBuffer?: IPieceTable; // The SSOT (Single Source of Truth) for the text. private _dirty: boolean; // Indicates if the file has unsaved changes. Modify this through `this.setDirty()` // [constructor] @@ -35,12 +39,12 @@ export class EditorModel extends Disposable implements IEditorModel { constructor( source: URI, options: EditorOptionsType, - @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, ) { super(); - this._source = source; this._options = options; + this._source = source; this._dirty = false; this._lexer = new MarkdownLexer(this.__initLexerOptions(options)); logService.debug('EditorModel', 'Constructed'); @@ -50,62 +54,59 @@ export class EditorModel extends Disposable implements IEditorModel { get source(): URI { return this._source; } get dirty(): boolean { return this._dirty; } + get textBuffer(): IPieceTable { return assert(this._textBuffer, 'Model not built yet.'); } // [public methods] public build(): AsyncResult { - return this.__buildModel(this._source); + this.logService.debug('EditorModel', `Start building at: ${URI.toString(this.source)}`); + return this.__buildTextBuffer(this.source) + .andThen(textBuffer => this.__tokenizeBuffer(textBuffer)) + .andThen(tokens => ok({ tokens: tokens, })); } public getContent(): string[] { - // TODO - return []; - // const state = assert(this._editorState); - // const raw = this._docSerializer.serialize(state.doc); - // return raw.split('\n'); + return this.textBuffer.getContent(); } public getRawContent(): string { - return ''; - // const state = assert(this._editorState); - // const raw = this._docSerializer.serialize(state.doc); - // return raw; // TODO + return this.textBuffer.getRawContent(); } public getLine(lineNumber: number): string { - return ''; // TODO + return this.textBuffer.getLine(lineNumber); } public getRawLine(lineNumber: number): string { - return ''; // TODO + return this.textBuffer.getRawLine(lineNumber); } public getLineLength(lineNumber: number): number { - return -1; // TODO + return this.textBuffer.getLineLength(lineNumber); } public getRawLineLength(lineNumber: number): number { - return -1; // TODO + return this.textBuffer.getRawLineLength(lineNumber); } public getLineCount(): number { - return -1; // TODO + return this.textBuffer.getLineCount(); } public getOffsetAt(lineNumber: number, lineOffset: number): number { - return -1; // TODO + return this.textBuffer.getOffsetAt(lineNumber, lineOffset); } public getPositionAt(textOffset: number): IEditorPosition { - return undefined!; // TODO + return this.textBuffer.getPositionAt(textOffset); } public getCharCodeByOffset(textOffset: number): number { - return -1; // TODO + return this.textBuffer.getCharcodeByOffset(textOffset); } public getCharCodeByLine(lineNumber: number, lineOffset: number): number { - return -1; // TODO + return this.textBuffer.getCharcodeByLine(lineNumber, lineOffset); } public setDirty(value: boolean): void { @@ -143,20 +144,36 @@ export class EditorModel extends Disposable implements IEditorModel { }; } - private __buildModel(source: URI): AsyncResult { - this.logService.debug('EditorModel', `Start building at: ${URI.toString(source)}`); + private __buildTextBuffer(source: URI): AsyncResult { + return this.fileService + .readFileStream(source, {}) + .andThen(async ready => { + const builder = new TextBufferBuilder(); + const blocker = new Blocker(); + const stream = ready.flow(); - return this.__readFileRaw(source) - .andThen(raw => { - const tokens = this._lexer.lex(raw); - return ok({ - tokens: tokens, + stream.on('data', data => { + builder.receive(data.toString()); }); + + stream.on('end', () => { + stream.destroy(); + builder.build(); + const textBuffer = builder.create(); + blocker.resolve(textBuffer); + }); + + stream.on('error', (error) => { + stream.destroy(); + blocker.reject(error); + }); + + this._textBuffer = await blocker.waiting(); + return this._textBuffer; }); } - private __readFileRaw(source: URI): AsyncResult { - return this.fileService.readFile(source, {}) - .map(buffer => buffer.toString()); + private __tokenizeBuffer(buffer: IPieceTable): Result { + return ok(this._lexer.lex(buffer.getRawContent())); } } \ No newline at end of file From 12a5f49ff56e73fa8d64772da5c7d2df0b5c12d6 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 20:01:55 -0500 Subject: [PATCH 50/65] [Chore] --- src/editor/common/viewModel.ts | 1 + src/editor/editorWidget.ts | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/editor/common/viewModel.ts b/src/editor/common/viewModel.ts index 880664713..89b21161d 100644 --- a/src/editor/common/viewModel.ts +++ b/src/editor/common/viewModel.ts @@ -30,6 +30,7 @@ export interface IEditorViewModel extends Disposable { /** * @description This will be invoked whenever the view content changes. + * // TODO: bad naming, should be described by a specific job, not when the function is called. */ onDidViewContentChange(e: IOnDidContentChangeEvent): void; diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index 2a0445805..2b1e1de2d 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -21,7 +21,6 @@ import { AsyncResult, err, ok, Result } from "src/base/common/result"; import { EditorDragState } from "src/editor/common/cursorDrop"; import { EditorViewModel } from "src/editor/viewModel/editorViewModel"; import { IEditorViewModel } from "src/editor/common/viewModel"; -import { IFileService } from "src/platform/files/common/fileService"; // region - [interface] @@ -282,7 +281,6 @@ export class EditorWidget extends Disposable implements IEditorWidget { @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleService private readonly lifecycleService: IBrowserLifecycleService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService, ) { super(); @@ -336,7 +334,7 @@ export class EditorWidget extends Disposable implements IEditorWidget { const error = new Error(`Cannot open editor at '${URI.toFsPath(source)}'. ${errorToMessage(build.unwrapErr(), false)}`); return err(error); } - const modelBuild = build.unwrap(); + const modelData = build.unwrap(); // view-model const extensionList = this._extensions.getExtensions(); @@ -345,15 +343,14 @@ export class EditorWidget extends Disposable implements IEditorWidget { this._model, extensionList, ); - - const { state: viewState } = this._viewModel.build(modelBuild); + const viewData = this._viewModel.build(modelData); // view this._view = this.instantiationService.createInstance( EditorView, this._container.raw, this._viewModel, - viewState, + viewData.state, extensionList, this._options.getOptions(), ); From 72b673c6e3ba07f7a65d1ca71e91dac2e691bf97 Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 21:36:38 -0500 Subject: [PATCH 51/65] [Chore] --- src/editor/common/viewModel.ts | 6 +++--- src/editor/view/editorView.ts | 2 +- src/editor/viewModel/editorViewModel.ts | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/editor/common/viewModel.ts b/src/editor/common/viewModel.ts index 89b21161d..361daccb6 100644 --- a/src/editor/common/viewModel.ts +++ b/src/editor/common/viewModel.ts @@ -29,10 +29,10 @@ export interface IEditorViewModel extends Disposable { readonly onDidContentChange: Register; /** - * @description This will be invoked whenever the view content changes. - * // TODO: bad naming, should be described by a specific job, not when the function is called. + * @description Takes the changes created from the editor view, adapt it to + * model. */ - onDidViewContentChange(e: IOnDidContentChangeEvent): void; + updateViewChange(e: IOnDidContentChangeEvent): void; // region - others diff --git a/src/editor/view/editorView.ts b/src/editor/view/editorView.ts index fcd8c8349..1f030c9bf 100644 --- a/src/editor/view/editorView.ts +++ b/src/editor/view/editorView.ts @@ -163,7 +163,7 @@ export class EditorView extends Disposable implements IEditorView { * need to inform {@link IEditorModel} to update its state. */ this.__register(this._view.onDidContentChange(e => { - viewModel.onDidViewContentChange(e); + viewModel.updateViewChange(e); })); } } \ No newline at end of file diff --git a/src/editor/viewModel/editorViewModel.ts b/src/editor/viewModel/editorViewModel.ts index db69043da..1ac523b81 100644 --- a/src/editor/viewModel/editorViewModel.ts +++ b/src/editor/viewModel/editorViewModel.ts @@ -24,6 +24,7 @@ export class EditorViewModel extends Disposable implements IEditorViewModel { // [fields] private readonly _model: IEditorModel; + private _viewState: ProseEditorState; private readonly _schema: EditorSchema; // An object that defines how a view is organized. private readonly _nodeProvider: DocumentNodeProvider; // Stores all the legal document node. @@ -40,6 +41,7 @@ export class EditorViewModel extends Disposable implements IEditorViewModel { ) { super(); this._model = model; + this._viewState = new ProseEditorState(); this._nodeProvider = DocumentNodeProvider.create(instantiationService).register(); this._schema = buildSchema(this._nodeProvider); @@ -74,8 +76,9 @@ export class EditorViewModel extends Disposable implements IEditorViewModel { }; } - public onDidViewContentChange(e: IOnDidContentChangeEvent): void { + public updateViewChange(e: IOnDidContentChangeEvent): void { this._model.setDirty(true); + this._viewState = e.view.state; // TODO } From d50d169567e18ae00d690f3b282553d4d316caee Mon Sep 17 00:00:00 2001 From: SIHAN LI <1015813038@qq.com> Date: Fri, 21 Feb 2025 22:24:18 -0500 Subject: [PATCH 52/65] [Fix] initialize view model updates in `RichTextView` --- src/editor/view/richTextView.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/editor/view/richTextView.ts b/src/editor/view/richTextView.ts index e3fc1513e..77db7169f 100644 --- a/src/editor/view/richTextView.ts +++ b/src/editor/view/richTextView.ts @@ -59,6 +59,12 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { this._editorContainer = overlayContainer; this._container = domEventElement; this._context = context; + + // init changes back to viewModel + context.viewModel.updateViewChange({ + view: view, + transaction: view.state.tr, + }); } // [public methods] From ab447277847f0d6a028de0ada601035415f1e0a2 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 22 Feb 2025 15:15:35 -0500 Subject: [PATCH 53/65] [Chore] --- src/editor/common/editorExtension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/common/editorExtension.ts b/src/editor/common/editorExtension.ts index 0a431516e..0c778dd5e 100644 --- a/src/editor/common/editorExtension.ts +++ b/src/editor/common/editorExtension.ts @@ -9,7 +9,7 @@ import { nullable } from "src/base/common/utilities/type"; /** * An interface only for {@link EditorExtension}. */ -export interface IEditorExtension extends Omit { +export interface IEditorExtension extends IProseEventBroadcaster { // [fields] From 19331a29af90db94ffe45faf51f391b683399bad Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 22 Feb 2025 18:45:12 -0500 Subject: [PATCH 54/65] [Chore] --- src/base/common/event.ts | 7 +++++++ src/editor/common/viewModel.ts | 5 ++++- src/editor/view/editorView.ts | 25 +++++++++++++++---------- src/editor/viewModel/editorViewModel.ts | 4 ++-- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/base/common/event.ts b/src/base/common/event.ts index 0d0a66759..b552625f4 100644 --- a/src/base/common/event.ts +++ b/src/base/common/event.ts @@ -599,6 +599,13 @@ export class RelayEmitter exten this._inputUnregister.register(newInputRegister(e => this._relay.fire(e))); } } + + /** + * @description Gives client a chance to emulate event. + */ + public fire(event: T): void { + this._relay.fire(event); + } } // region - NodeEventEmitter diff --git a/src/editor/common/viewModel.ts b/src/editor/common/viewModel.ts index 361daccb6..16a45bca1 100644 --- a/src/editor/common/viewModel.ts +++ b/src/editor/common/viewModel.ts @@ -16,6 +16,8 @@ export interface IViewModelChangeEvent { export interface IEditorViewModel extends Disposable { + // region - field & event + /** * The schema of the editor. */ @@ -28,13 +30,14 @@ export interface IEditorViewModel extends Disposable { readonly onDidContentChange: Register; + // region - methods + /** * @description Takes the changes created from the editor view, adapt it to * model. */ updateViewChange(e: IOnDidContentChangeEvent): void; - // region - others getRegisteredDocumentNodes(): string[]; diff --git a/src/editor/view/editorView.ts b/src/editor/view/editorView.ts index 1f030c9bf..f937cf8f8 100644 --- a/src/editor/view/editorView.ts +++ b/src/editor/view/editorView.ts @@ -18,6 +18,8 @@ export class ViewContext { ) {} } +// region - EditorView + export class EditorView extends Disposable implements IEditorView { // [fields] @@ -99,10 +101,9 @@ export class EditorView extends Disposable implements IEditorView { editorElement.className = 'editor-container'; this._view = this.__register(new RichTextView(editorElement, this._container, context, initState, extensions)); - // forward: start listening events from model - this.__registerEventFromModel(); - this.__registerEventToModel(); - + // forward: start listening events from view model + this.__registerEventFromViewModel(); + this.__registerEventToViewModel(); // render this._container.appendChild(editorElement); @@ -118,6 +119,11 @@ export class EditorView extends Disposable implements IEditorView { return this._view; } + public override dispose(): void { + super.dispose(); + this._container.remove(); + } + public isEditable(): boolean { return this._view.isEditable(); } @@ -145,7 +151,7 @@ export class EditorView extends Disposable implements IEditorView { // [private helper methods] - private __registerEventFromModel(): void { + private __registerEventFromViewModel(): void { const viewModel = this._ctx.viewModel; this.__register(viewModel.onDidContentChange(event => { @@ -154,16 +160,15 @@ export class EditorView extends Disposable implements IEditorView { })); } - private __registerEventToModel(): void { + private __registerEventToViewModel(): void { const viewModel = this._ctx.viewModel; /** * Since in Prosemirror whenever the content of the document changes, * the old {@link ProseEditorState} is no longer valid. Therefore we - * need to inform {@link IEditorModel} to update its state. + * need to inform view model to update its state. */ - this.__register(this._view.onDidContentChange(e => { - viewModel.updateViewChange(e); - })); + this.__register(this._view.onDidContentChange(e => viewModel.updateViewChange(e))); + this.__register(this._view.onDidSelectionChange(e => viewModel.updateViewChange(e))); } } \ No newline at end of file diff --git a/src/editor/viewModel/editorViewModel.ts b/src/editor/viewModel/editorViewModel.ts index 1ac523b81..a507abaf4 100644 --- a/src/editor/viewModel/editorViewModel.ts +++ b/src/editor/viewModel/editorViewModel.ts @@ -46,8 +46,8 @@ export class EditorViewModel extends Disposable implements IEditorViewModel { this._nodeProvider = DocumentNodeProvider.create(instantiationService).register(); this._schema = buildSchema(this._nodeProvider); this._docParser = this.__register(new DocumentParser(this._schema, this._nodeProvider, /* options */)); - this.__register(this._docParser.onLog(event => defaultLog(logService, event.level, 'EditorView', event.message, event.error, event.additional))); this._docSerializer = new MarkdownSerializer(this._nodeProvider, { strict: true, escapeExtraCharacters: undefined, }); + this.__register(this._docParser.onLog(event => defaultLog(logService, event.level, 'EditorView', event.message, event.error, event.additional))); this.__registerListeners(); } @@ -79,7 +79,7 @@ export class EditorViewModel extends Disposable implements IEditorViewModel { public updateViewChange(e: IOnDidContentChangeEvent): void { this._model.setDirty(true); this._viewState = e.view.state; - // TODO + // TODO: convert View changes to EditorModel changes } public getRegisteredDocumentNodes(): string[] { From dcd8627b3c956dabd6893868ed4c5f4b139817df Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 22 Feb 2025 18:59:51 -0500 Subject: [PATCH 55/65] [Refactor] unified event handling of program input and user input by introducing `IEditorInputEmulator` --- src/editor/common/view.ts | 8 +++ src/editor/editorWidget.ts | 112 ++++++++++++++++++++----------- src/editor/view/editorView.ts | 11 +-- src/editor/view/inputEmulator.ts | 21 ++++++ src/editor/view/richTextView.ts | 38 ++++++++++- 5 files changed, 145 insertions(+), 45 deletions(-) create mode 100644 src/editor/view/inputEmulator.ts diff --git a/src/editor/common/view.ts b/src/editor/common/view.ts index cddc30c64..c6cd03c02 100644 --- a/src/editor/common/view.ts +++ b/src/editor/common/view.ts @@ -21,4 +21,12 @@ export interface IEditorView extends IProseEventBroadcaster { * The actual editor instance. */ readonly editor: RichTextView; + + /** + * // TODO + * @param text + * @param from + * @param to + */ + type(text: string, from?: number, to?: number): void; } diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index 2b1e1de2d..52ffd40ef 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -6,7 +6,7 @@ import { ILogService } from "src/base/common/logger"; import { Constructor, isDefined } from "src/base/common/utilities/type"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; import { IBrowserLifecycleService, ILifecycleService } from "src/platform/lifecycle/browser/browserLifecycleService"; -import { IEditorModel } from "src/editor/common/model"; +import { IEditorModel, IModelBuildData } from "src/editor/common/model"; import { EditorType, IEditorView } from "src/editor/common/view"; import { BasicEditorOption, EDITOR_OPTIONS_DEFAULT, EditorOptionsType, IEditorWidgetOptions, toJsonEditorOption } from "src/editor/common/editorConfiguration"; import { EditorModel } from "src/editor/model/editorModel"; @@ -20,7 +20,8 @@ import { assert, errorToMessage } from "src/base/common/utilities/panic"; import { AsyncResult, err, ok, Result } from "src/base/common/result"; import { EditorDragState } from "src/editor/common/cursorDrop"; import { EditorViewModel } from "src/editor/viewModel/editorViewModel"; -import { IEditorViewModel } from "src/editor/common/viewModel"; +import { IEditorViewModel, IViewModelBuildData } from "src/editor/common/viewModel"; +import { IEditorInputEmulator } from "src/editor/view/inputEmulator"; // region - [interface] @@ -35,7 +36,11 @@ export interface IEditorWidget extends | 'onDidDirtyChange' | 'onDidSave' | 'onDidSaveError' - | 'save'> + | 'save' + >, + Pick { /** * Is the editor initialized. if not, access to model, viewModel and view @@ -322,45 +327,24 @@ export class EditorWidget extends Disposable implements IEditorWidget { this.__detachData(); // model - this._model = this.instantiationService.createInstance( - EditorModel, - source, - this._options.getOptions(), - ); - const build = await this._model.build(); + return (await this.__createModel(source)).andThen(([model, modelData]) => { + this._model = model; + const extensions = this._extensions.getExtensions(); - // unexpected behavior, we need to let the user know. - if (build.isErr()) { - const error = new Error(`Cannot open editor at '${URI.toFsPath(source)}'. ${errorToMessage(build.unwrapErr(), false)}`); - return err(error); - } - const modelData = build.unwrap(); + // view-model + this._viewModel = this.__createViewModel(extensions); + const viewModelData = this._viewModel.build(modelData); - // view-model - const extensionList = this._extensions.getExtensions(); - this._viewModel = this.instantiationService.createInstance( - EditorViewModel, - this._model, - extensionList, - ); - const viewData = this._viewModel.build(modelData); + // view + this._view = this.__createView(extensions, viewModelData); - // view - this._view = this.instantiationService.createInstance( - EditorView, - this._container.raw, - this._viewModel, - viewData.state, - extensionList, - this._options.getOptions(), - ); + // listeners + this.__registerMVVMListeners(this._model, this._viewModel, this._view); - // listeners - this.__registerMVVMListeners(this._model, this._viewModel, this._view); - - // cache data - this._editorData = this.__register(new EditorData(this._model, this._viewModel, this._view, undefined)); - return ok(); + // cache data + this._editorData = this.__register(new EditorData(this._model, this._viewModel, this._view, undefined)); + return ok(); + }); } public isOpened(): boolean { @@ -410,6 +394,16 @@ export class EditorWidget extends Disposable implements IEditorWidget { return this.dispose(); } + // region - [viewModel] + + + + // region - [View] + + public type(text: string, from?: number, to?: number): void { + this.view.type(text, from, to); + } + // region - [private] private __detachData(): void { @@ -431,6 +425,48 @@ export class EditorWidget extends Disposable implements IEditorWidget { return assert(this._view, '[EditorWidget] EditorView is not initialized.'); } + private async __createModel(source: URI): Promise> { + const model = this.instantiationService.createInstance( + EditorModel, + source, + this._options.getOptions(), + ); + const build = await model.build(); + + // unexpected behavior, we need to let the user know. + if (build.isErr()) { + const error = new Error(`Cannot open editor at '${URI.toFsPath(source)}'. ${errorToMessage(build.unwrapErr(), false)}`); + return err(error); + } + const modelData = build.unwrap(); + + return ok([model, modelData]); + } + + private __createViewModel(extensions: EditorExtension[]): EditorViewModel { + return this.instantiationService.createInstance( + EditorViewModel, + this.model, + extensions, + ); + } + + private __createView(extensions: EditorExtension[], viewModelData: IViewModelBuildData): EditorView { + const inputEmulator: IEditorInputEmulator = { + type: e => this._onTextInput.fire(e), + }; + + return this.instantiationService.createInstance( + EditorView, + this._container.raw, + this.viewModel, + viewModelData.state, + extensions, + inputEmulator, + this._options.getOptions(), + ); + } + private __registerListeners(): void { this.__register(this.lifecycleService.onBeforeQuit(() => this._options.saveOptions())); diff --git a/src/editor/view/editorView.ts b/src/editor/view/editorView.ts index f937cf8f8..6963121c8 100644 --- a/src/editor/view/editorView.ts +++ b/src/editor/view/editorView.ts @@ -8,6 +8,8 @@ import { IEditorModel } from 'src/editor/common/model'; import { ProseEditorState } from 'src/editor/common/proseMirror'; import { IEditorViewModel } from 'src/editor/common/viewModel'; import { RichTextView } from 'src/editor/view/richTextView'; +import { IEditorInputEmulator } from 'src/editor/view/inputEmulator'; +import { IInstantiationService, InstantiationService } from 'src/platform/instantiation/common/instantiation'; export class ViewContext { constructor( @@ -85,8 +87,10 @@ export class EditorView extends Disposable implements IEditorView { viewModel: IEditorViewModel, initState: ProseEditorState, extensions: IEditorExtension[], + inputEmulator: IEditorInputEmulator, options: EditorOptionsType, @ILogService logService: ILogService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); @@ -99,7 +103,7 @@ export class EditorView extends Disposable implements IEditorView { // the centre that integrates the editor-related functionalities const editorElement = document.createElement('div'); editorElement.className = 'editor-container'; - this._view = this.__register(new RichTextView(editorElement, this._container, context, initState, extensions)); + this._view = this.__register(instantiationService.createInstance(RichTextView, editorElement, this._container, context, initState, extensions, inputEmulator)); // forward: start listening events from view model this.__registerEventFromViewModel(); @@ -144,9 +148,8 @@ export class EditorView extends Disposable implements IEditorView { return this._view.isDestroyed(); } - public override dispose(): void { - super.dispose(); - this._container.remove(); + public type(text: string, from?: number, to?: number): void { + this._view.type(text, from, to); } // [private helper methods] diff --git a/src/editor/view/inputEmulator.ts b/src/editor/view/inputEmulator.ts new file mode 100644 index 000000000..e8a04494f --- /dev/null +++ b/src/editor/view/inputEmulator.ts @@ -0,0 +1,21 @@ +import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; + + +/** + * A delegate to simulate certain user's input programmatically. + * + * Consider the following difference: + * 1. The workflow of user input might be: + * Keyboard Event -> Event Broadcasting -> Extension Handing -> Document Updates + * 2. The workflow of program input: + * function call -> Document Updates + * + * This is a problem for program input, the event is not broadcasting, so the + * extensions and others cannot be notified. + * + * This why we need a delegate to emulate related-functions by also broadcasting + * them. + */ +export interface IEditorInputEmulator { + type(event: IOnTextInputEvent): void; +} \ No newline at end of file diff --git a/src/editor/view/richTextView.ts b/src/editor/view/richTextView.ts index 77db7169f..ac3ad4505 100644 --- a/src/editor/view/richTextView.ts +++ b/src/editor/view/richTextView.ts @@ -4,6 +4,8 @@ import { ProseEditorState, ProseEditorView } from "src/editor/common/proseMirror import { ViewContext } from "src/editor/view/editorView"; import { EditorViewProxy, IEditorViewProxy } from "src/editor/view/editorViewProxy"; import { IEditorExtension } from 'src/editor/common/editorExtension'; +import { IEditorInputEmulator } from 'src/editor/view/inputEmulator'; +import { IOnTextInputEvent } from 'src/editor/view/proseEventBroadcaster'; /** * An interface only for {@link RichTextView}. @@ -33,6 +35,8 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { protected readonly _container: HTMLElement; protected readonly _editorContainer: HTMLElement; protected readonly _context: ViewContext; + + private readonly _inputEmulator: IEditorInputEmulator; // [constructor] @@ -42,6 +46,7 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { context: ViewContext, editorState: ProseEditorState, extensions: IEditorExtension[], + inputEmulator: IEditorInputEmulator, ) { overlayContainer.classList.add('editor-base', 'rich-text'); @@ -59,18 +64,45 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { this._editorContainer = overlayContainer; this._container = domEventElement; this._context = context; + this._inputEmulator = inputEmulator; - // init changes back to viewModel + // send latest data back to viewModel after initialization context.viewModel.updateViewChange({ view: view, transaction: view.state.tr, }); } - // [public methods] - + // [getter] + get container() { return this._container; } get overlayContainer() { return this._editorContainer; } + + // [public methods] + + public type(text: string, from?: number, to?: number): void { + const { state } = this._view; + from ??= state.selection.from; + to ??= state.selection.to; + let prevented = false; + + const event: IOnTextInputEvent = { + view: this._view, + text, + from, + to, + preventDefault: () => prevented = true, + }; + this._inputEmulator.type(event); + if (prevented) { + return; + } + + const tr = state.tr.insertText(text, from, to); + const newState = state.apply(tr); + + this.render(newState); + } // [private helper methods] } From 036936d05e5135d7d4e9b2d3db527aa0354dab12 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 22 Feb 2025 20:25:23 -0500 Subject: [PATCH 56/65] [Feat] prevent actually type `@` during `AskAI` --- src/editor/contrib/askAI/askAI.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/editor/contrib/askAI/askAI.ts b/src/editor/contrib/askAI/askAI.ts index d36df48df..114ed092f 100644 --- a/src/editor/contrib/askAI/askAI.ts +++ b/src/editor/contrib/askAI/askAI.ts @@ -38,25 +38,30 @@ export class EditorAskAIExtension extends EditorExtension implements IEditorAskA )); // show event - this.__register(this.onTextInput(e => this.tryShowAskAI(e))); + this.__register(this.onTextInput(e => { + const handled = this.tryShowAskAI(e); + if (handled) { + e.preventDefault(); + return true; + } + })); } // [public methods] - public tryShowAskAI(e: IOnTextInputEvent): void { - + public tryShowAskAI(e: IOnTextInputEvent): boolean { const { text, view } = e; const { selection } = view.state; const isCursor = ProseTools.Cursor.isCursor(selection); if (!isCursor) { - return; + return false; } const isEmptyBlock = ProseTools.Cursor.isOnEmpty(selection); const isSlash = text === '@'; if (!isEmptyBlock || !isSlash) { - return; + return false; } // show ask-AI palette @@ -65,5 +70,6 @@ export class EditorAskAIExtension extends EditorExtension implements IEditorAskA // re-focus back to editor, not the slash command. view.focus(); + return true; } } \ No newline at end of file From ff2c5bc69687e989bac2964526ffd2cf5a036ff7 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 22 Feb 2025 20:26:57 -0500 Subject: [PATCH 57/65] [Fix] improve `type` --- src/editor/editorWidget.ts | 1 + src/editor/view/inputEmulator.ts | 3 +- src/editor/view/richTextView.ts | 47 +++++++++++++++++-- .../view/widget/palette/askAIProvider.ts | 8 +++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index 52ffd40ef..a387e1460 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -454,6 +454,7 @@ export class EditorWidget extends Disposable implements IEditorWidget { private __createView(extensions: EditorExtension[], viewModelData: IViewModelBuildData): EditorView { const inputEmulator: IEditorInputEmulator = { type: e => this._onTextInput.fire(e), + keydown: e => this._onKeydown.fire(e), }; return this.instantiationService.createInstance( diff --git a/src/editor/view/inputEmulator.ts b/src/editor/view/inputEmulator.ts index e8a04494f..9e91edc0f 100644 --- a/src/editor/view/inputEmulator.ts +++ b/src/editor/view/inputEmulator.ts @@ -1,4 +1,4 @@ -import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; +import { IOnKeydownEvent, IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; /** @@ -18,4 +18,5 @@ import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster"; */ export interface IEditorInputEmulator { type(event: IOnTextInputEvent): void; + keydown(event: IOnKeydownEvent): void; } \ No newline at end of file diff --git a/src/editor/view/richTextView.ts b/src/editor/view/richTextView.ts index ac3ad4505..cf7364455 100644 --- a/src/editor/view/richTextView.ts +++ b/src/editor/view/richTextView.ts @@ -6,6 +6,7 @@ import { EditorViewProxy, IEditorViewProxy } from "src/editor/view/editorViewPro import { IEditorExtension } from 'src/editor/common/editorExtension'; import { IEditorInputEmulator } from 'src/editor/view/inputEmulator'; import { IOnTextInputEvent } from 'src/editor/view/proseEventBroadcaster'; +import { createStandardKeyboardEvent } from 'src/base/common/keyboard'; /** * An interface only for {@link RichTextView}. @@ -81,11 +82,51 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { // [public methods] public type(text: string, from?: number, to?: number): void { + let pointer = 0; + let i = 0; + for (; i < text.length; i++) { + const c = text[i]!; + if (c === ' ' || c === '\n') { + const textBefore = text.slice(pointer, i); + this.__type(textBefore, from, to); + this.__type(c, from, to); + pointer = i + 1; + } + } + + const lastText = text.slice(pointer, i); + if (lastText) { + this.__type(lastText, from, to); + } + } + + // [private helper methods] + + private __type(text: string, from?: number, to?: number): void { + // case 1: typing '\n', we emulate pressing `enter` on keydown. + if (text === '\n') { + this._inputEmulator.keydown({ + view: this._view, + event: createStandardKeyboardEvent(new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13, + which: 13, + bubbles: true, + cancelable: true, + })), + preventDefault: () => {}, + }); + return; + } + + // general case const { state } = this._view; from ??= state.selection.from; to ??= state.selection.to; - let prevented = false; + let prevented = false; const event: IOnTextInputEvent = { view: this._view, text, @@ -98,11 +139,11 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { return; } + // default behavior on typing const tr = state.tr.insertText(text, from, to); const newState = state.apply(tr); + // render it this.render(newState); } - - // [private helper methods] } diff --git a/src/editor/view/widget/palette/askAIProvider.ts b/src/editor/view/widget/palette/askAIProvider.ts index fa26afe89..6ccde42ac 100644 --- a/src/editor/view/widget/palette/askAIProvider.ts +++ b/src/editor/view/widget/palette/askAIProvider.ts @@ -24,8 +24,10 @@ export class AskAIProvider { id: name, enabled: true, callback: () => { - - // TEST + /** + * // TEST + */ + const content = this.editorWidget.model.getRawContent(); let responseContent = ''; @@ -46,7 +48,9 @@ export class AskAIProvider { stream: true, }, (response) => { if (response.primaryMessage.content) { + this.editorWidget.type(response.primaryMessage.content); responseContent += response.primaryMessage.content; + console.log(response.primaryMessage.content); } }).unwrap(); From 908014905251b04d0cc7f3846fdde331bfacf50a Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sat, 22 Feb 2025 20:41:57 -0500 Subject: [PATCH 58/65] [Chore] rename everything to `Snippet` instead of `InputRule` --- src/editor/contrib/builtInExtensionList.ts | 6 +- src/editor/contrib/snippet/snippet.contrib.ts | 128 +++++++++++++ .../inputRule.ts => snippet/snippet.ts} | 61 +++--- .../snippetRule.ts} | 175 +++--------------- src/editor/view/richTextView.ts | 3 + 5 files changed, 189 insertions(+), 184 deletions(-) create mode 100644 src/editor/contrib/snippet/snippet.contrib.ts rename src/editor/contrib/{inputRule/inputRule.ts => snippet/snippet.ts} (80%) rename src/editor/contrib/{inputRule/editorInputRules.ts => snippet/snippetRule.ts} (52%) diff --git a/src/editor/contrib/builtInExtensionList.ts b/src/editor/contrib/builtInExtensionList.ts index 72fdbb085..1d2830cd4 100644 --- a/src/editor/contrib/builtInExtensionList.ts +++ b/src/editor/contrib/builtInExtensionList.ts @@ -2,7 +2,7 @@ import { Constructor } from "src/base/common/utilities/type"; import { EditorExtension } from "src/editor/common/editorExtension"; import { EditorCommandExtension } from "src/editor/contrib/command/command"; import { EditorAutoSaveExtension } from "src/editor/contrib/autoSave"; -import { EditorInputRuleExtension } from "src/editor/contrib/inputRule/inputRule"; +import { EditorSnippetExtension } from "src/editor/contrib/snippet/snippet"; import { EditorDragAndDropExtension } from "src/editor/contrib/dragAndDrop/dragAndDrop"; import { EditorBlockHandleExtension } from "src/editor/contrib/blockHandle/blockHandle"; import { EditorBlockPlaceHolderExtension } from "src/editor/contrib/blockPlaceHolder/blockPlaceHolder"; @@ -13,7 +13,7 @@ import { EditorAskAIExtension } from "src/editor/contrib/askAI/askAI"; export const enum EditorExtensionIDs { Command = 'editor-command-extension', AutoSave = 'editor-autosave-extension', - InputRule = 'editor-inputRule-extension', + Snippet = 'editor-snippet-extension', History = 'editor-history-extension', DragAndDrop = 'editor-drag-and-drop-extension', BlockHandle = 'editor-block-handle-extension', @@ -27,7 +27,7 @@ export const enum EditorExtensionIDs { */ export function getBuiltInExtension(): { id: string, ctor: Constructor }[] { return [ - { id: EditorExtensionIDs.InputRule, ctor: EditorInputRuleExtension }, + { id: EditorExtensionIDs.Snippet, ctor: EditorSnippetExtension }, { id: EditorExtensionIDs.AutoSave, ctor: EditorAutoSaveExtension }, { id: EditorExtensionIDs.Command, ctor: EditorCommandExtension }, { id: EditorExtensionIDs.DragAndDrop, ctor: EditorDragAndDropExtension }, diff --git a/src/editor/contrib/snippet/snippet.contrib.ts b/src/editor/contrib/snippet/snippet.contrib.ts new file mode 100644 index 000000000..c882c980b --- /dev/null +++ b/src/editor/contrib/snippet/snippet.contrib.ts @@ -0,0 +1,128 @@ +import { TokenEnum, MarkEnum } from "src/editor/common/markdown"; +import { IEditorSnippetExtension } from "src/editor/contrib/snippet/snippet"; +import { CodeBlockAttrs } from "src/editor/model/documentNode/node/codeBlock/codeBlock"; +import { HeadingAttrs } from "src/editor/model/documentNode/node/heading"; + +export function registerDefaultSnippet(extension: IEditorSnippetExtension): void { + + extension.registerRule("emDashRule", /--$/, "—"); + extension.registerRule("ellipsisRule", /\.\.\.$/, "…"); + extension.registerRule("openDoubleQuoteRule", /(?:^|[\s{[(<'"\u2018\u201C])(")$/, "“"); + extension.registerRule("closeDoubleQuoteRule", /"$/, "”"); + extension.registerRule("openSingleQuoteRule", /(?:^|[\s{[(<'"\u2018\u201C])(')$/, "‘"); + extension.registerRule("closeSingleQuoteRule", /'$/, "’"); + + // Heading Rule: Matches "#" followed by a space + extension.registerRule("headingRule", /^(#{1,6})\s$/, + { + type: 'node', + nodeType: TokenEnum.Heading, + whenReplace: 'type', + getAttribute: (match): HeadingAttrs => { + return { + level: match[1]?.length, + }; + }, + wrapStrategy: 'WrapTextBlock' + } + ); + + // Blockquote Rule: Matches ">" followed by a space + extension.registerRule("blockquoteRule", /^>\s$/, + { + type: 'node', + nodeType: TokenEnum.Blockquote, + whenReplace: 'type', + wrapStrategy: 'WrapBlock' + } + ); + + // Code Block Rule: Matches triple backticks + extension.registerRule("codeBlockRule", /^```(.*)\s*$/, + { + type: 'node', + nodeType: TokenEnum.CodeBlock, + whenReplace: 'enter', + getAttribute: (match): CodeBlockAttrs => { + const lang = match[1]; + return { + lang: lang ?? 'Unknown', + }; + }, + wrapStrategy: 'WrapTextBlock' + } + ); + + extension.registerRule("orderedListRule", /^(\d+)\.\s$/, + { + type: 'node', + nodeType: TokenEnum.List, + whenReplace: 'type', + getAttribute: (match) => { + if (match && match[1]) { + return { ordered: true, start: parseInt(match[1]) }; + } + return { ordered: true, start: 1, }; + }, + shouldJoinWithBefore: (match, prevNode) => { + if (match && match[1]) { + return prevNode.type.name === TokenEnum.List && prevNode.attrs["order"] + 1 === +match[1]; + } + return false; + }, + wrapStrategy: 'WrapBlock' + } + ); + + extension.registerRule("bulletListRule", /^\s*([-+*])\s$/, + { + type: 'node', + nodeType: TokenEnum.List, + whenReplace: 'type', + wrapStrategy: 'WrapBlock' + } + ); + + extension.registerRule("strongRule", /\*\*(.+?)\*\*$/, { + type: 'mark', + markType: MarkEnum.Strong, + whenReplace: 'type', + preventMarkInheritance: true, + }); + + extension.registerRule("emphasisRule", /\*(.+?)\*$/, { + type: 'mark', + markType: MarkEnum.Em, + whenReplace: 'type', + preventMarkInheritance: true, + }); + + const ESCAPE_REGEX = /`(?![`]{2})((?:\\`|[^`])+?)`(?![`]{2})$/; + extension.registerRule("codespanRule", ESCAPE_REGEX, { + type: 'mark', + markType: MarkEnum.Codespan, + whenReplace: 'type', + preventMarkInheritance: true, + }); + + extension.registerRule("mathBlockRule", /^\$\$$/, + { + type: 'node', + nodeType: TokenEnum.MathBlock, + whenReplace: 'enter', + wrapStrategy: 'ReplaceBlock', + } + ); + + extension.registerRule("mathInlineRule", /\$(.+?)\$$/, { + type: 'node', + nodeType: TokenEnum.MathInline, + whenReplace: 'type', + wrapStrategy: 'ReplaceBlock', + getAttribute: (matched) => { + return { + text: matched[1], + }; + } + }); +} diff --git a/src/editor/contrib/inputRule/inputRule.ts b/src/editor/contrib/snippet/snippet.ts similarity index 80% rename from src/editor/contrib/inputRule/inputRule.ts rename to src/editor/contrib/snippet/snippet.ts index 3b9d558d8..ba06983ee 100644 --- a/src/editor/contrib/inputRule/inputRule.ts +++ b/src/editor/contrib/snippet/snippet.ts @@ -8,22 +8,23 @@ import { Dictionary, isString } from "src/base/common/utilities/type"; import { ProseEditorView, ProseNode, ProseResolvedPos, ProseTextSelection } from "src/editor/common/proseMirror"; import { KeyCode } from "src/base/common/keyboard"; import { TokenEnum } from "src/editor/common/markdown"; -import { IInputRule, InputRule, registerDefaultInputRules } from "src/editor/contrib/inputRule/editorInputRules"; +import { ISnippetRule, SnippetRule } from "src/editor/contrib/snippet/snippetRule"; import { IInstantiationService, IServiceProvider } from "src/platform/instantiation/common/instantiation"; import { ILogService } from "src/base/common/logger"; +import { registerDefaultSnippet } from "src/editor/contrib/snippet/snippet.contrib"; /** - * Defines the replacement behavior for an input rule. An input rule replacement + * Defines the replacement behavior for an snippet rule. An snippet rule replacement * can either be: * 1. a direct string replacement or * 2. an object that specifies complicated replacement rule. */ -export type InputRuleReplacement = +export type SnippetReplacement = | string - | MarkInputRuleReplacement - | NodeInputRuleReplacement; + | MarkSnippetReplacement + | NodeSnippetReplacement; -type InputRuleReplacementBase = { +type SnippetReplacementBase = { readonly type: string; readonly whenReplace: 'type' | 'enter'; @@ -41,7 +42,7 @@ type InputRuleReplacementBase = { readonly getAttribute?: (matchedText: RegExpExecArray, serviceProvider: IServiceProvider) => Dictionary; }; -export type MarkInputRuleReplacement = InputRuleReplacementBase & { +export type MarkSnippetReplacement = SnippetReplacementBase & { readonly type: 'mark'; readonly markType: string; @@ -52,7 +53,7 @@ export type MarkInputRuleReplacement = InputRuleReplacementBase & { readonly preventMarkInheritance: boolean; }; -export type NodeInputRuleReplacement = InputRuleReplacementBase & { +export type NodeSnippetReplacement = SnippetReplacementBase & { readonly type: 'node'; /** * Specifies the type of node to create when replacing. @@ -60,7 +61,7 @@ export type NodeInputRuleReplacement = InputRuleReplacementBase & { readonly nodeType: string | TokenEnum; /** - * Determines the wrapping strategy to use when applying the input rule. + * Determines the wrapping strategy to use when applying the snippet rule. * - `WrapBlock`: Wraps the matched content as a block-level element. * - `WrapTextBlock`: Wraps the matched content as a text block within a block-level container. * - `ReplaceBlock`: Replace the matched block as a new block-level element. @@ -90,14 +91,14 @@ export type NodeInputRuleReplacement = InputRuleReplacementBase & { }; /** - * An interface only for {@link EditorInputRuleExtension}. + * An interface only for {@link EditorSnippetExtension}. */ -export interface IEditorInputRuleExtension extends IEditorExtension { +export interface IEditorSnippetExtension extends IEditorExtension { - readonly id: EditorExtensionIDs.InputRule; + readonly id: EditorExtensionIDs.Snippet; /** - * @description Registers a new input rule. + * @description Registers a new snippet rule. * @param id A unique identifier for the rule. * @param pattern The regular expression pattern that triggers this rule. * @param replacement The replacement behavior when the pattern is matched. @@ -106,34 +107,34 @@ export interface IEditorInputRuleExtension extends IEditorExtension { * @returns Returns `true` if the rule was registered successfully, `false` * if a rule with the same ID or same {@link RegExp} already exists. */ - registerRule(id: string, pattern: RegExp, replacement: InputRuleReplacement): boolean; + registerRule(id: string, pattern: RegExp, replacement: SnippetReplacement): boolean; /** - * @description Un-registers an input rule by its unique identifier. + * @description Un-registers an snippet rule by its unique identifier. * @param id The identifier of the rule to remove. * @returns Returns `true` if the rule was found and removed, otherwise `false`. */ unregisterRule(id: string): boolean; /** - * @description Retrieves a registered input rule by its ID. + * @description Retrieves a registered snippet rule by its ID. * @param id The identifier of the desired rule. */ - getRuleByID(id: string): IInputRule | undefined; + getRuleByID(id: string): ISnippetRule | undefined; /** - * @description Retrieves all registered input rules. + * @description Retrieves all registered snippet rules. */ - getAllRules(): IInputRule[]; + getAllRules(): ISnippetRule[]; } -export class EditorInputRuleExtension extends EditorExtension implements IEditorInputRuleExtension { +export class EditorSnippetExtension extends EditorExtension implements IEditorSnippetExtension { // [field] - public override readonly id = EditorExtensionIDs.InputRule; + public override readonly id = EditorExtensionIDs.Snippet; - private readonly _rules: Map = new Map(); + private readonly _rules: Map = new Map(); private readonly MAX_TEXT_BEFORE = 100; // [constructor] @@ -145,7 +146,7 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor ) { super(editorWidget); - registerDefaultInputRules(this); + registerDefaultSnippet(this); this.__register(this.onTextInput(e => { const handled = this.__handleTextInput(e.view, e.from, e.to, e.text); @@ -168,18 +169,18 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor // [public methods] - public registerRule(id: string, pattern: RegExp, replacement: InputRuleReplacement): boolean { + public registerRule(id: string, pattern: RegExp, replacement: SnippetReplacement): boolean { if (this._rules.has(id)) { - console.warn(`InputRule with id "${id}" already exists.`); + console.warn(`SnippetRule with id "${id}" already exists.`); return false; } if ([...this._rules.values()].some(rule => rule.pattern.source === pattern.source)) { - console.warn(`InputRule with pattern "${pattern}" already exists.`); + console.warn(`SnippetRule with pattern "${pattern}" already exists.`); return false; } - const rule = new InputRule(id, pattern, replacement, this.instantiationService); + const rule = new SnippetRule(id, pattern, replacement, this.instantiationService); this._rules.set(id, rule); return true; } @@ -188,11 +189,11 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor return this._rules.delete(id); } - public getRuleByID(id: string): IInputRule | undefined { + public getRuleByID(id: string): ISnippetRule | undefined { return this._rules.get(id); } - public getAllRules(): IInputRule[] { + public getAllRules(): ISnippetRule[] { return Array.from(this._rules.values()); } @@ -252,7 +253,7 @@ export class EditorInputRuleExtension extends EditorExtension implements IEditor const tr = rule.onMatch(state, match, start, end); if (!tr) { - this.logService.warn('EditorInput', `Unable to achiece replacement for matched input rule: ${rule.id}.`); + this.logService.warn('EditorSnippet', `Unable to achiece replacement for matched snippet rule: ${rule.id}.`); continue; } diff --git a/src/editor/contrib/inputRule/editorInputRules.ts b/src/editor/contrib/snippet/snippetRule.ts similarity index 52% rename from src/editor/contrib/inputRule/editorInputRules.ts rename to src/editor/contrib/snippet/snippetRule.ts index 9f1918635..1f555b27d 100644 --- a/src/editor/contrib/inputRule/editorInputRules.ts +++ b/src/editor/contrib/snippet/snippetRule.ts @@ -1,143 +1,16 @@ import { EditorState, Transaction } from "prosemirror-state"; import { canJoin, findWrapping } from "prosemirror-transform"; import { panic } from "src/base/common/utilities/panic"; -import { MarkEnum, TokenEnum } from "src/editor/common/markdown"; import { ProseNodeSelection } from "src/editor/common/proseMirror"; -import { IEditorInputRuleExtension, InputRuleReplacement, MarkInputRuleReplacement, NodeInputRuleReplacement } from "src/editor/contrib/inputRule/inputRule"; -import { CodeBlockAttrs } from "src/editor/model/documentNode/node/codeBlock/codeBlock"; -import { HeadingAttrs } from "src/editor/model/documentNode/node/heading"; +import { SnippetReplacement, MarkSnippetReplacement, NodeSnippetReplacement } from "src/editor/contrib/snippet/snippet"; import { IInstantiationService } from "src/platform/instantiation/common/instantiation"; -export function registerDefaultInputRules(extension: IEditorInputRuleExtension): void { - - extension.registerRule("emDashRule", /--$/, "—"); - extension.registerRule("ellipsisRule", /\.\.\.$/, "…"); - extension.registerRule("openDoubleQuoteRule", /(?:^|[\s{[(<'"\u2018\u201C])(")$/, "“"); - extension.registerRule("closeDoubleQuoteRule", /"$/, "”"); - extension.registerRule("openSingleQuoteRule", /(?:^|[\s{[(<'"\u2018\u201C])(')$/, "‘"); - extension.registerRule("closeSingleQuoteRule", /'$/, "’"); - - // Heading Rule: Matches "#" followed by a space - extension.registerRule("headingRule", /^(#{1,6})\s$/, - { - type: 'node', - nodeType: TokenEnum.Heading, - whenReplace: 'type', - getAttribute: (match): HeadingAttrs => { - return { - level: match[1]?.length, - }; - }, - wrapStrategy: 'WrapTextBlock' - } - ); - - // Blockquote Rule: Matches ">" followed by a space - extension.registerRule("blockquoteRule", /^>\s$/, - { - type: 'node', - nodeType: TokenEnum.Blockquote, - whenReplace: 'type', - wrapStrategy: 'WrapBlock' - } - ); - - // Code Block Rule: Matches triple backticks - extension.registerRule("codeBlockRule", /^```(.*)\s*$/, - { - type: 'node', - nodeType: TokenEnum.CodeBlock, - whenReplace: 'enter', - getAttribute: (match): CodeBlockAttrs => { - const lang = match[1]; - return { - lang: lang ?? 'Unknown', - }; - }, - wrapStrategy: 'WrapTextBlock' - } - ); - - extension.registerRule("orderedListRule", /^(\d+)\.\s$/, - { - type: 'node', - nodeType: TokenEnum.List, - whenReplace: 'type', - getAttribute: (match) => { - if (match && match[1]) { - return { ordered: true, start: parseInt(match[1]) }; - } - return { ordered: true, start: 1, }; - }, - shouldJoinWithBefore: (match, prevNode) => { - if (match && match[1]) { - return prevNode.type.name === TokenEnum.List && prevNode.attrs["order"] + 1 === +match[1]; - } - return false; - }, - wrapStrategy: 'WrapBlock' - } - ); - - extension.registerRule("bulletListRule", /^\s*([-+*])\s$/, - { - type: 'node', - nodeType: TokenEnum.List, - whenReplace: 'type', - wrapStrategy: 'WrapBlock' - } - ); - - extension.registerRule("strongRule", /\*\*(.+?)\*\*$/, { - type: 'mark', - markType: MarkEnum.Strong, - whenReplace: 'type', - preventMarkInheritance: true, - }); - - extension.registerRule("emphasisRule", /\*(.+?)\*$/, { - type: 'mark', - markType: MarkEnum.Em, - whenReplace: 'type', - preventMarkInheritance: true, - }); - - const ESCAPE_REGEX = /`(?![`]{2})((?:\\`|[^`])+?)`(?![`]{2})$/; - extension.registerRule("codespanRule", ESCAPE_REGEX, { - type: 'mark', - markType: MarkEnum.Codespan, - whenReplace: 'type', - preventMarkInheritance: true, - }); - - extension.registerRule("mathBlockRule", /^\$\$$/, - { - type: 'node', - nodeType: TokenEnum.MathBlock, - whenReplace: 'enter', - wrapStrategy: 'ReplaceBlock', - } - ); - - extension.registerRule("mathInlineRule", /\$(.+?)\$$/, { - type: 'node', - nodeType: TokenEnum.MathInline, - whenReplace: 'type', - wrapStrategy: 'ReplaceBlock', - getAttribute: (matched) => { - return { - text: matched[1], - }; - } - }); -} - /** - * Represents an individual input rule. + * Represents an individual snippet rule. */ -export interface IInputRule { +export interface ISnippetRule { /** - * Unique identifier for the input rule. + * Unique identifier for the snippet rule. */ readonly id: string; @@ -151,23 +24,23 @@ export interface IInputRule { * Defines the replacement strategy, either as a string or as a configuration * object that specifies the `nodeType` to wrap around the matched text. */ - readonly replacement: InputRuleReplacement; + readonly replacement: SnippetReplacement; } /** * @internal - * Internal representation of an input rule. + * Internal representation of an snippet rule. */ -export class InputRule implements IInputRule { +export class SnippetRule implements ISnippetRule { // [fields] public readonly id: string; public readonly pattern: RegExp; - public readonly replacement: InputRuleReplacement; + public readonly replacement: SnippetReplacement; private readonly _replacementString?: string; - private readonly _replacementObject?: Exclude; + private readonly _replacementObject?: Exclude; public readonly onMatch: ( state: EditorState, @@ -178,7 +51,7 @@ export class InputRule implements IInputRule { // [constructor] - constructor(id: string, pattern: RegExp, replacement: InputRuleReplacement, private readonly instantiationService: IInstantiationService) { + constructor(id: string, pattern: RegExp, replacement: SnippetReplacement, private readonly instantiationService: IInstantiationService) { this.id = id; this.pattern = pattern; this.replacement = replacement; @@ -190,18 +63,18 @@ export class InputRule implements IInputRule { else if (this.replacement.type === 'node') { this._replacementObject = this.replacement; if (this.replacement.wrapStrategy === 'WrapTextBlock') { - this.onMatch = this.__textblockTypeInputRule; + this.onMatch = this.__textblockTypeSnippetRule; } else if (this.replacement.wrapStrategy === 'ReplaceBlock') { - this.onMatch = this.__replaceBlockInputRule; + this.onMatch = this.__replaceBlockSnippetRule; } else { - this.onMatch = this.__wrappingInputRule; + this.onMatch = this.__wrappingSnippetRule; } } else if (this.replacement.type === 'mark') { this._replacementObject = this.replacement; - this.onMatch = this.__markInputRule; + this.onMatch = this.__markSnippetRule; } else { panic(`Invalid replacement type: ${this.replacement['type']}`); } @@ -229,13 +102,13 @@ export class InputRule implements IInputRule { return state.tr.insertText(insert, start, end); } - private __markInputRule( + private __markSnippetRule( state: EditorState, match: RegExpExecArray, start: number, end: number ): Transaction | null { - const replacement = this.replacement as MarkInputRuleReplacement; + const replacement = this.replacement as MarkSnippetReplacement; const markType = state.schema.marks[replacement.markType]; if (!markType) { console.warn(`Mark type "${replacement.markType}" not found`); @@ -258,16 +131,16 @@ export class InputRule implements IInputRule { return tr; } - private __wrappingInputRule( + private __wrappingSnippetRule( state: EditorState, match: RegExpExecArray, start: number, end: number ): Transaction | null { - const replacement = this._replacementObject as NodeInputRuleReplacement; + const replacement = this._replacementObject as NodeSnippetReplacement; const nodeType = state.schema.nodes[replacement.nodeType]; if (!nodeType) { - console.warn(`[EditorInputRuleExtension] Node type "${replacement.nodeType}" not found in schema.`); + console.warn(`[EditorSnippetExtension] Node type "${replacement.nodeType}" not found in schema.`); return null; } @@ -295,16 +168,16 @@ export class InputRule implements IInputRule { return tr; } - private __textblockTypeInputRule( + private __textblockTypeSnippetRule( state: EditorState, match: RegExpExecArray, start: number, end: number ): Transaction | null { - const replacement = this._replacementObject as NodeInputRuleReplacement; + const replacement = this._replacementObject as NodeSnippetReplacement; const nodeType = state.schema.nodes[replacement.nodeType]; if (!nodeType) { - console.warn(`[EditorInputRuleExtension] Node type "${replacement.nodeType}" not found in schema.`); + console.warn(`[EditorSnippetExtension] Node type "${replacement.nodeType}" not found in schema.`); return null; } @@ -319,13 +192,13 @@ export class InputRule implements IInputRule { .setBlockType(start, start, nodeType, attrs); } - private __replaceBlockInputRule( + private __replaceBlockSnippetRule( state: EditorState, match: RegExpExecArray, start: number, end: number ): Transaction | null { - const replacement = this.replacement as NodeInputRuleReplacement; + const replacement = this.replacement as NodeSnippetReplacement; const nodeType = state.schema.nodes[replacement.nodeType]; if (!nodeType) { console.warn(`Node type "${replacement.nodeType}" not found`); diff --git a/src/editor/view/richTextView.ts b/src/editor/view/richTextView.ts index cf7364455..5eb95b55d 100644 --- a/src/editor/view/richTextView.ts +++ b/src/editor/view/richTextView.ts @@ -82,6 +82,9 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { // [public methods] public type(text: string, from?: number, to?: number): void { + + // FIX: after snippet is based on state machine, refactor this code. + let pointer = 0; let i = 0; for (; i < text.length; i++) { From ab6d1fe52350a0b2ab2c099dbec002311e1ce79d Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sun, 23 Feb 2025 14:10:04 -0500 Subject: [PATCH 59/65] [Feat] add `keydown` event emulation and browser key code mapping --- src/base/common/keyboard.ts | 29 +++++++++++++++++-- src/editor/common/view.ts | 19 ++++++++++--- src/editor/editorWidget.ts | 5 ++++ src/editor/view/editorView.ts | 5 ++++ src/editor/view/richTextView.ts | 49 +++++++++++++++++++++++---------- 5 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/base/common/keyboard.ts b/src/base/common/keyboard.ts index 05d70d37e..af616c712 100644 --- a/src/base/common/keyboard.ts +++ b/src/base/common/keyboard.ts @@ -71,6 +71,20 @@ export namespace Keyboard { } } + /** + * @description Returns the {@link KeyCode} corresponding browser key code. + * @param strKeyOrKeyCode The string form of the keycode or {@link KeyCode}. + */ + export function toKeyCodeBrowser(strKeyOrKeyCode: string | number): number { + + const keyCode = typeof strKeyOrKeyCode === 'string' + ? keyCodeStringMap.getKeyCode(strKeyOrKeyCode) + : strKeyOrKeyCode; + + const keyCodeBrowser = keyNumberMap.map[keyCode]; + return keyCodeBrowser ?? -1; + } + /** * @description Determines if two {@link IStandardKeyboardEvent} are the same. */ @@ -158,6 +172,9 @@ export function createStandardKeyboardEvent(event: KeyboardEvent): IStandardKeyb /** * The standard key code used to represent the keyboard pressing which may from * different operating systems. + * + * @note This is NOT the same keycode used in browser event. For example, + * `KeyCode.Enter` is `60`, but browser uses `13` to represent `Enter`. */ export const enum KeyCode { @@ -271,12 +288,19 @@ export const enum KeyCode { } /** - * @internal A mapping from the numerical value to {@link KeyCode}. + * @internal A mapping from the browser keycode to {@link KeyCode}. */ class KeyCodeMap { public map: { [keyCode: number]: KeyCode } = new Array(250); } +/** + * @internal A mapping from our {@link KeyCode} to browser keycode. + */ +class KeyNumberMap { + public map: { [keyCode: number]: number } = new Array(250); +} + /** * @internal A mapping either from {@link KeyCode} to the string form of keycode * OR string to {@link KeyCode}. @@ -302,8 +326,8 @@ class KeyCodeStringMap { } } -/** @internal */ const keyCodeMap = new KeyCodeMap(); +const keyNumberMap = new KeyNumberMap(); const keyCodeStringMap = new KeyCodeStringMap(); /** @internal */ @@ -406,6 +430,7 @@ for (const [keycode, keycodeNum, keycodeStr] of <[number, number, string][]> [KeyCode.ContextMenu, 93, 'ContextMenu'], ]) { keyCodeMap.map[keycodeNum] = keycode; + keyNumberMap.map[keycode] = keycodeNum; keyCodeStringMap.set(keycode, keycodeStr); } diff --git a/src/editor/common/view.ts b/src/editor/common/view.ts index c6cd03c02..8208a5b7a 100644 --- a/src/editor/common/view.ts +++ b/src/editor/common/view.ts @@ -1,3 +1,4 @@ +import { KeyCode } from "src/base/common/keyboard"; import { IProseEventBroadcaster } from "src/editor/view/proseEventBroadcaster"; import { RichTextView } from "src/editor/view/richTextView"; @@ -23,10 +24,20 @@ export interface IEditorView extends IProseEventBroadcaster { readonly editor: RichTextView; /** - * // TODO - * @param text - * @param from - * @param to + * @description Programmatically emulate user typing action. + * - If `from` or `to` not provided, it will be treated as simple insertion + * at the cursor. + * @param text The text for replacement, insertion. + * @param from The start absolute position points to ProseMirror document. + * @param to The end absolute position points to ProseMirror document. */ type(text: string, from?: number, to?: number): void; + + /** + * @description Programmatically trigger a keydown event. This emulates + * low-level keyboard interactions like navigation, shortcuts, or special + * key behaviors in the editor. + * @param code The physical key code to emulate. + */ + keydown(code: KeyCode, alt?: boolean, shift?: boolean, ctrl?: boolean, meta?: boolean): void; } diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index a387e1460..6f8219eb4 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -22,6 +22,7 @@ import { EditorDragState } from "src/editor/common/cursorDrop"; import { EditorViewModel } from "src/editor/viewModel/editorViewModel"; import { IEditorViewModel, IViewModelBuildData } from "src/editor/common/viewModel"; import { IEditorInputEmulator } from "src/editor/view/inputEmulator"; +import { KeyCode } from "src/base/common/keyboard"; // region - [interface] @@ -404,6 +405,10 @@ export class EditorWidget extends Disposable implements IEditorWidget { this.view.type(text, from, to); } + public keydown(code: KeyCode, alt?: boolean, shift?: boolean, ctrl?: boolean, meta?: boolean): void { + this.view.keydown(code, alt, shift, ctrl, meta); + } + // region - [private] private __detachData(): void { diff --git a/src/editor/view/editorView.ts b/src/editor/view/editorView.ts index 6963121c8..bcd3cc8d4 100644 --- a/src/editor/view/editorView.ts +++ b/src/editor/view/editorView.ts @@ -10,6 +10,7 @@ import { IEditorViewModel } from 'src/editor/common/viewModel'; import { RichTextView } from 'src/editor/view/richTextView'; import { IEditorInputEmulator } from 'src/editor/view/inputEmulator'; import { IInstantiationService, InstantiationService } from 'src/platform/instantiation/common/instantiation'; +import { KeyCode } from 'src/base/common/keyboard'; export class ViewContext { constructor( @@ -152,6 +153,10 @@ export class EditorView extends Disposable implements IEditorView { this._view.type(text, from, to); } + public keydown(code: KeyCode, alt?: boolean, shift?: boolean, ctrl?: boolean, meta?: boolean): void { + this._view.keydown(code, alt, shift, ctrl, meta); + } + // [private helper methods] private __registerEventFromViewModel(): void { diff --git a/src/editor/view/richTextView.ts b/src/editor/view/richTextView.ts index 5eb95b55d..c52f58558 100644 --- a/src/editor/view/richTextView.ts +++ b/src/editor/view/richTextView.ts @@ -6,7 +6,7 @@ import { EditorViewProxy, IEditorViewProxy } from "src/editor/view/editorViewPro import { IEditorExtension } from 'src/editor/common/editorExtension'; import { IEditorInputEmulator } from 'src/editor/view/inputEmulator'; import { IOnTextInputEvent } from 'src/editor/view/proseEventBroadcaster'; -import { createStandardKeyboardEvent } from 'src/base/common/keyboard'; +import { createStandardKeyboardEvent, Keyboard, KeyCode } from 'src/base/common/keyboard'; /** * An interface only for {@link RichTextView}. @@ -103,24 +103,45 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { } } + public keydown(code: KeyCode, alt?: boolean, shift?: boolean, ctrl?: boolean, meta?: boolean): void { + const key = Keyboard.toString(code); + const keyCode = Keyboard.toKeyCodeBrowser(code); + + const browserEvent = new KeyboardEvent('keydown', { + key: key, + code: key, + + keyCode: keyCode, + charCode: keyCode, + which: keyCode, + + altKey: alt, + shiftKey: shift, + ctrlKey: ctrl, + metaKey: meta, + + bubbles: true, + cancelable: true, + }); + + this._inputEmulator.keydown({ + view: this._view, + event: createStandardKeyboardEvent(browserEvent), + preventDefault: () => {}, + }); + } + // [private helper methods] private __type(text: string, from?: number, to?: number): void { + // case 0: typing nothing. + if (text === '') { + return; + } + // case 1: typing '\n', we emulate pressing `enter` on keydown. if (text === '\n') { - this._inputEmulator.keydown({ - view: this._view, - event: createStandardKeyboardEvent(new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - keyCode: 13, - charCode: 13, - which: 13, - bubbles: true, - cancelable: true, - })), - preventDefault: () => {}, - }); + this.keydown(KeyCode.Enter); return; } From cd249d85b16a518e1c7b19f23c19d4f73e55b71e Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Sun, 23 Feb 2025 14:48:40 -0500 Subject: [PATCH 60/65] [Chore] --- src/editor/contrib/snippet/snippet.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/editor/contrib/snippet/snippet.ts b/src/editor/contrib/snippet/snippet.ts index ba06983ee..aafcba554 100644 --- a/src/editor/contrib/snippet/snippet.ts +++ b/src/editor/contrib/snippet/snippet.ts @@ -13,6 +13,8 @@ import { IInstantiationService, IServiceProvider } from "src/platform/instantiat import { ILogService } from "src/base/common/logger"; import { registerDefaultSnippet } from "src/editor/contrib/snippet/snippet.contrib"; +// region - replacement + /** * Defines the replacement behavior for an snippet rule. An snippet rule replacement * can either be: @@ -27,6 +29,11 @@ export type SnippetReplacement = type SnippetReplacementBase = { readonly type: string; + /** + * Determines when should the replacement happens. + * - `type`: Any keyboard typing will try to match content. + * - `enter`: Only when pressing the key `enter` will try to match content. + */ readonly whenReplace: 'type' | 'enter'; /** @@ -44,11 +51,15 @@ type SnippetReplacementBase = { export type MarkSnippetReplacement = SnippetReplacementBase & { readonly type: 'mark'; + + /** + * Specifies the type of mark to create when replacing. + */ readonly markType: string; /** - * @description After the mark is applied, should the following typed text - * inherit this mark. + * After the mark is applied, should the following typed text inherit this + * mark. */ readonly preventMarkInheritance: boolean; }; @@ -68,13 +79,6 @@ export type NodeSnippetReplacement = SnippetReplacementBase & { */ readonly wrapStrategy: 'WrapBlock' | 'WrapTextBlock' | 'ReplaceBlock'; - /** - * Determines when should the replacement happens. - * - `type`: Any keyboard typing will try to match content. - * - `enter`: Only when pressing the key `enter` will try to match content. - */ - readonly whenReplace: 'type' | 'enter'; - /** * @description A predicate function that determines if the new node * should join with the preceding node. @@ -128,6 +132,8 @@ export interface IEditorSnippetExtension extends IEditorExtension { getAllRules(): ISnippetRule[]; } +// region - snippet + export class EditorSnippetExtension extends EditorExtension implements IEditorSnippetExtension { // [field] From 046cd01a4a567597ab28f9d023fcb1330bda0e5d Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 25 Feb 2025 12:35:16 -0500 Subject: [PATCH 61/65] [Fix] typo --- src/editor/common/proseMirror.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/common/proseMirror.ts b/src/editor/common/proseMirror.ts index 9f50bea7d..3085758c3 100644 --- a/src/editor/common/proseMirror.ts +++ b/src/editor/common/proseMirror.ts @@ -145,7 +145,7 @@ declare module 'prosemirror-model' { * console.log(pos.before(1)); // Returns 8 (before the second paragraph starts) * ``` */ - after(depth?: number | null): number; + before(depth?: number | null): number; /** * @description Returns the absolute position immediately **after** the @@ -173,7 +173,7 @@ declare module 'prosemirror-model' { * console.log(pos.after(1)); // Returns 16 (after the paragraph has closed) * ``` */ - before(depth?: number | null): number; + after(depth?: number | null): number; /** * @description The exact same API as {@link getParentNodeAt}. Except From 90f0a5bd55c5aa5ccd7ae1e763dd306f6f9c75c9 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 25 Feb 2025 12:37:11 -0500 Subject: [PATCH 62/65] [Chore] --- test/editor/{view => }/contrib/editorCommands.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/editor/{view => }/contrib/editorCommands.test.ts (100%) diff --git a/test/editor/view/contrib/editorCommands.test.ts b/test/editor/contrib/editorCommands.test.ts similarity index 100% rename from test/editor/view/contrib/editorCommands.test.ts rename to test/editor/contrib/editorCommands.test.ts From b33c25e10fa9302a9c7225df6ef82fd0e41ea3ec Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 25 Feb 2025 12:56:07 -0500 Subject: [PATCH 63/65] [Refactor] simplify typing logic in `RichTextView` --- src/editor/common/view.ts | 3 ++- src/editor/view/richTextView.ts | 20 ++------------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/editor/common/view.ts b/src/editor/common/view.ts index 8208a5b7a..ac4281650 100644 --- a/src/editor/common/view.ts +++ b/src/editor/common/view.ts @@ -27,7 +27,8 @@ export interface IEditorView extends IProseEventBroadcaster { * @description Programmatically emulate user typing action. * - If `from` or `to` not provided, it will be treated as simple insertion * at the cursor. - * @param text The text for replacement, insertion. + * @param text The text for replacement, insertion. Multi-character supported. + * Each character will be typed in sequence. * @param from The start absolute position points to ProseMirror document. * @param to The end absolute position points to ProseMirror document. */ diff --git a/src/editor/view/richTextView.ts b/src/editor/view/richTextView.ts index c52f58558..f04a358b2 100644 --- a/src/editor/view/richTextView.ts +++ b/src/editor/view/richTextView.ts @@ -82,24 +82,8 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { // [public methods] public type(text: string, from?: number, to?: number): void { - - // FIX: after snippet is based on state machine, refactor this code. - - let pointer = 0; - let i = 0; - for (; i < text.length; i++) { - const c = text[i]!; - if (c === ' ' || c === '\n') { - const textBefore = text.slice(pointer, i); - this.__type(textBefore, from, to); - this.__type(c, from, to); - pointer = i + 1; - } - } - - const lastText = text.slice(pointer, i); - if (lastText) { - this.__type(lastText, from, to); + for (const c of text) { + this.__type(c, from, to); } } From 7d0dc75aa1a02ae2c206c71365907d1d4523d1fb Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 25 Feb 2025 14:21:09 -0500 Subject: [PATCH 64/65] [Fix] `CodeBlock` has to work with `setBlockType` if it's not textblock --- src/editor/model/documentNode/node/codeBlock/codeBlock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor/model/documentNode/node/codeBlock/codeBlock.ts b/src/editor/model/documentNode/node/codeBlock/codeBlock.ts index 48b891d57..4f6b8ebcc 100644 --- a/src/editor/model/documentNode/node/codeBlock/codeBlock.ts +++ b/src/editor/model/documentNode/node/codeBlock/codeBlock.ts @@ -36,6 +36,7 @@ export class CodeBlock extends DocumentNode { marks: '', // disallow any marks code: true, defining: true, + content: 'text*', attrs: >{ view: { default: CodeBlock.createView('') }, lang: { default: '' }, From ff9070745391a9c3cedf4da5beef4d6f16414d63 Mon Sep 17 00:00:00 2001 From: SIHAN LI Date: Tue, 25 Feb 2025 15:03:37 -0500 Subject: [PATCH 65/65] [Doc] --- src/editor/view/richTextView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor/view/richTextView.ts b/src/editor/view/richTextView.ts index f04a358b2..c31275c22 100644 --- a/src/editor/view/richTextView.ts +++ b/src/editor/view/richTextView.ts @@ -82,6 +82,7 @@ export class RichTextView extends EditorViewProxy implements IRichTextView { // [public methods] public type(text: string, from?: number, to?: number): void { + // TODO: perf for (const c of text) { this.__type(c, from, to); }