diff --git a/package-lock.json b/package-lock.json index 7fe4508..1880d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.13.6", "license": "MIT", "dependencies": { + "@acode.app/canvas-view": "^0.0.3", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.0", "@codemirror/lang-angular": "^0.1.4", @@ -32,18 +33,18 @@ "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.3", - "@codemirror/language": "^6.12.4", + "@codemirror/language": "^6.13.1", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.3", "@codemirror/lint": "^6.9.7", "@codemirror/lsp-client": "file:codemirror-lsp-client", "@codemirror/search": "^6.7.1", - "@codemirror/state": "^6.7.1", + "@codemirror/state": "^6.7.6", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.9", + "@codemirror/view": "^6.43.14", "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.1", - "@lezer/common": "^1.5.2", + "@lezer/common": "^1.5.4", "@lezer/highlight": "^1.2.5", "@lezer/lr": "^1.4.10", "@ungap/custom-elements": "^1.3.0", @@ -74,10 +75,10 @@ "markdown-it-github-alerts": "^1.0.1", "markdown-it-task-lists": "^2.1.1", "markdown-it-texmath": "^1.0.0", - "mermaid": "^11.14.0", "mime-types": "^3.0.2", "motion": "^12.43.0", "mustache": "^4.2.0", + "pele": "^1.10.0", "picomatch": "^4.0.7", "tough-cookie": "^6.0.2", "url-parse": "^1.5.10", @@ -159,17 +160,18 @@ "node": ">= 18" } }, - "node_modules/@antfu/install-pkg": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz", - "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==", + "node_modules/@acode.app/canvas-view": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@acode.app/canvas-view/-/canvas-view-0.0.3.tgz", + "integrity": "sha512-OqiaTijJYsAyhYfUXmd8bZuWJvc6n2GiG5AO4LNsl1qSRYfTx0UgpVI9DUNeVooDCFempuVBuZiOIh4sdUb/Hg==", "license": "MIT", "dependencies": { - "package-manager-detector": "^1.8.0", - "tinyexec": "^1.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "@codemirror/language": "^6.13.1", + "@codemirror/state": "^6.7.6", + "@codemirror/view": "^6.43.14", + "@lezer/common": "^1.5.4", + "@lezer/highlight": "^1.2.5", + "style-mod": "^4.1.2" } }, "node_modules/@babel/code-frame": { @@ -2675,9 +2677,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -2695,9 +2694,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -2715,9 +2711,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -2735,9 +2728,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -2781,18 +2771,6 @@ "node": ">=14.21.3" } }, - "node_modules/@braintree/sanitize-url": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", - "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", - "license": "MIT" - }, - "node_modules/@chevrotain/types": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", - "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", - "license": "Apache-2.0" - }, "node_modules/@codemirror/autocomplete": { "version": "6.20.3", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", @@ -3114,12 +3092,13 @@ } }, "node_modules/@codemirror/language": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", - "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.13.1.tgz", + "integrity": "sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", + "@codemirror/streamparser": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", @@ -3202,6 +3181,12 @@ "@marijn/find-cluster-break": "^1.0.0" } }, + "node_modules/@codemirror/streamparser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/streamparser/-/streamparser-6.0.0.tgz", + "integrity": "sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==", + "license": "MIT" + }, "node_modules/@codemirror/theme-one-dark": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", @@ -3215,9 +3200,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.43.13", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.13.tgz", - "integrity": "sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==", + "version": "6.43.15", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.15.tgz", + "integrity": "sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.7.0", @@ -3318,23 +3303,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz", - "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==", - "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^2.0.1", - "@iconify/types": "^2.0.0", - "import-meta-resolve": "^4.2.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3398,9 +3366,9 @@ } }, "node_modules/@lezer/common": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz", - "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.4.tgz", + "integrity": "sha512-xt3Q4z64xM+xhg4M7NXqw1N//1O74bUFo5kz6fwMeC4qwxSuRdwkYSmjcqCEEKYkNw5e67f2hcNqBoNERAEU0w==", "license": "MIT" }, "node_modules/@lezer/cpp": { @@ -3626,15 +3594,6 @@ "testtool": "bin/testtool.js" } }, - "node_modules/@mermaid-js/parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", - "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", - "license": "MIT", - "dependencies": { - "@chevrotain/types": "~11.1.2" - } - }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -3643,9 +3602,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3812,9 +3768,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3836,9 +3789,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3860,9 +3810,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3884,9 +3831,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3908,9 +3852,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3932,9 +3873,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4100,9 +4038,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4120,9 +4055,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4140,9 +4072,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4160,9 +4089,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4180,9 +4106,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4200,9 +4123,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4362,9 +4282,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4379,9 +4296,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4396,9 +4310,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4413,9 +4324,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4430,9 +4338,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4447,9 +4352,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4464,9 +4366,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4481,9 +4380,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4498,9 +4394,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4515,9 +4408,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4532,9 +4422,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4549,9 +4436,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4566,9 +4450,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4718,9 +4599,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4735,9 +4613,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4752,9 +4627,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4769,9 +4641,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4786,9 +4655,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4803,9 +4669,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4820,9 +4683,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4837,9 +4697,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4990,259 +4847,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", - "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", - "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", - "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -5264,12 +4868,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, "node_modules/@types/jsesc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", @@ -5363,16 +4961,6 @@ "integrity": "sha512-f4q/s76+8nOy+fhrNHyetuoPDR01lmlZB5czfCG+OOnBw/Wf+x48DcCDPmMQY7oL8xYFL8qfenMoiS8DUkKBUw==", "license": "ISC" }, - "node_modules/@upsetjs/venn.js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", - "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", - "license": "MIT", - "optionalDependencies": { - "d3-selection": "^3.0.0", - "d3-transition": "^3.0.1" - } - }, "node_modules/@vitest/expect": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", @@ -6348,15 +5936,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, - "node_modules/cose-base": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", - "license": "MIT", - "dependencies": { - "layout-base": "^1.0.0" - } - }, "node_modules/cosmiconfig": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", @@ -6439,599 +6018,82 @@ "node": ">=4" } }, - "node_modules/cytoscape": { - "version": "3.34.3", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", - "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" }, - "node_modules/cytoscape-cose-bilkent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { - "cose-base": "^1.0.0" + "ms": "^2.1.3" }, - "peerDependencies": { - "cytoscape": "^3.2.0" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/cytoscape-fcose": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, "license": "MIT", - "dependencies": { - "cose-base": "^2.2.0" + "engines": { + "node": ">=10" }, - "peerDependencies": { - "cytoscape": "^3.2.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cytoscape-fcose/node_modules/cose-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", - "dependencies": { - "layout-base": "^2.0.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/layout-base": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", - "license": "MIT" - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, "engines": { - "node": ">=12" + "node": ">= 0.8" } }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-sankey": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "1 - 2", - "d3-shape": "^1.2.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre-d3-es": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", - "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "lodash-es": "^4.17.21" - } - }, - "node_modules/dayjs": { - "version": "1.11.23", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", - "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delaunator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", - "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" + "node": ">=0.3.1" } }, "node_modules/dompurify": { @@ -7174,18 +6236,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-toolkit": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", - "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks", - "tests/types", - "tests/browser-compat" - ] - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7325,15 +6375,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fastdom": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastdom/-/fastdom-1.0.12.tgz", - "integrity": "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==", - "license": "MIT", - "dependencies": { - "strictdom": "^1.0.1" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -7567,12 +6608,6 @@ "license": "ISC", "peer": true }, - "node_modules/hachure-fill": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", - "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", - "license": "MIT" - }, "node_modules/happy-dom": { "version": "20.14.5", "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz", @@ -7652,18 +6687,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -7711,6 +6734,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7735,15 +6759,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7989,17 +7004,6 @@ "katex": "cli.js" } }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" - }, - "node_modules/layout-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", - "license": "MIT" - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -8152,9 +7156,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8176,9 +7177,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8200,9 +7198,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8224,9 +7219,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8339,12 +7331,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -8464,18 +7450,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/mdurl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", @@ -8490,36 +7464,6 @@ "license": "MIT", "peer": true }, - "node_modules/mermaid": { - "version": "11.17.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.17.2.tgz", - "integrity": "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==", - "license": "MIT", - "dependencies": { - "@braintree/sanitize-url": "^7.1.2", - "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.2.1", - "@types/d3": "^7.4.3", - "@upsetjs/venn.js": "^2.0.0", - "cytoscape": "^3.34.0", - "cytoscape-cose-bilkent": "^4.1.0", - "cytoscape-fcose": "^2.2.0", - "d3": "^7.9.0", - "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.14", - "dayjs": "^1.11.21", - "dompurify": "^3.3.3", - "es-toolkit": "^1.45.1", - "fastdom": "1.0.12", - "katex": "^0.16.47", - "khroma": "^2.1.0", - "marked": "^16.3.0", - "roughjs": "^4.6.6", - "stylis": "^4.3.6", - "ts-dedent": "^2.2.0", - "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -9040,12 +7984,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-manager-detector": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", - "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", - "license": "MIT" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -9133,12 +8071,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-data-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", - "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", - "license": "MIT" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9163,6 +8095,15 @@ "dev": true, "license": "MIT" }, + "node_modules/pele": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/pele/-/pele-1.12.0.tgz", + "integrity": "sha512-3OdOK6rvkaTB569R4/I8U0H82MqPRxj7vfkbowa2rRhgSBPFhvWlfFy5eae0jP+2A6hE8abE17nsdyxJz2VBiQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9182,22 +8123,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/points-on-curve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", - "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", - "license": "MIT" - }, - "node_modules/points-on-path": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", - "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", - "license": "MIT", - "dependencies": { - "path-data-parser": "0.1.0", - "points-on-curve": "0.2.0" - } - }, "node_modules/postcss": { "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", @@ -9642,12 +8567,6 @@ "node": ">=4" } }, - "node_modules/robust-predicates": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", - "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", - "license": "Unlicense" - }, "node_modules/rolldown": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", @@ -9760,36 +8679,12 @@ } } }, - "node_modules/roughjs": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", - "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", - "license": "MIT", - "dependencies": { - "hachure-fill": "^0.5.2", - "path-data-parser": "^0.1.0", - "points-on-curve": "^0.2.0", - "points-on-path": "^0.2.1" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/sass": { "version": "1.105.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.105.1.tgz", @@ -10107,12 +9002,6 @@ "dev": true, "license": "MIT" }, - "node_modules/strictdom": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz", - "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", - "license": "MIT" - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -10189,12 +9078,6 @@ "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==", "license": "MIT" }, - "node_modules/stylis": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", - "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", - "license": "MIT" - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -10289,6 +9172,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10384,15 +9268,6 @@ "node": ">=16" } }, - "node_modules/ts-dedent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", - "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", - "license": "MIT", - "engines": { - "node": ">=6.10" - } - }, "node_modules/ts-loader": { "version": "9.6.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", @@ -10565,19 +9440,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/uuid": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", - "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/vanilla-picker": { "version": "2.12.3", "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.3.tgz", diff --git a/package.json b/package.json index 6624f06..e29e7f0 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "ws": "^8.21.3" }, "dependencies": { + "@acode.app/canvas-view": "^0.0.3", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.0", "@codemirror/lang-angular": "^0.1.4", @@ -98,18 +99,18 @@ "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.3", - "@codemirror/language": "^6.12.4", + "@codemirror/language": "^6.13.1", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.3", "@codemirror/lint": "^6.9.7", "@codemirror/lsp-client": "file:codemirror-lsp-client", "@codemirror/search": "^6.7.1", - "@codemirror/state": "^6.7.1", + "@codemirror/state": "^6.7.6", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.9", + "@codemirror/view": "^6.43.14", "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.1", - "@lezer/common": "^1.5.2", + "@lezer/common": "^1.5.4", "@lezer/highlight": "^1.2.5", "@lezer/lr": "^1.4.10", "@ungap/custom-elements": "^1.3.0", @@ -140,10 +141,10 @@ "markdown-it-github-alerts": "^1.0.1", "markdown-it-task-lists": "^2.1.1", "markdown-it-texmath": "^1.0.0", - "mermaid": "^11.14.0", "mime-types": "^3.0.2", "motion": "^12.43.0", "mustache": "^4.2.0", + "pele": "^1.10.0", "picomatch": "^4.0.7", "tough-cookie": "^6.0.2", "url-parse": "^1.5.10", @@ -157,12 +158,12 @@ "overrides": { "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.0", - "@codemirror/language": "^6.12.4", + "@codemirror/language": "^6.13.1", "@codemirror/lint": "^6.9.7", "@codemirror/search": "^6.7.1", - "@codemirror/state": "^6.7.1", - "@codemirror/view": "^6.43.9", - "@lezer/common": "^1.5.2", + "@codemirror/state": "^6.7.6", + "@codemirror/view": "^6.43.14", + "@lezer/common": "^1.5.4", "@lezer/lr": "^1.4.10", "tar": "^7.5.22" }, diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java b/platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java index 15eb015..06cf847 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java @@ -1678,8 +1678,26 @@ private void getAppInfo(Callback callback) { } private void openInBrowser(String src, Callback callback) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(src)); - activity.startActivity(browserIntent); + if (src == null || src.isEmpty()) { + callback.error("No URL provided."); + return; + } + + Uri uri = Uri.parse(src); + if ("file".equalsIgnoreCase(uri.getScheme())) { + // file:// URIs can't leave the app (FileUriExposedException). + callback.error("Cannot open file URI in browser: " + src); + return; + } + + try { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri); + activity.startActivity(browserIntent); + callback.success(); + } catch (Exception e) { + Log.e("System", "Failed to open URL: " + src, e); + callback.error(e.getMessage()); + } } private void launchApp( diff --git a/src/cm/baseExtensions.ts b/src/cm/baseExtensions.ts index d55a6c7..c429b20 100644 --- a/src/cm/baseExtensions.ts +++ b/src/cm/baseExtensions.ts @@ -7,6 +7,7 @@ import { import { history } from "@codemirror/commands"; import { bracketMatching, + codeFolding as codeFoldingState, defaultHighlightStyle, foldGutter, indentOnInput, @@ -31,6 +32,17 @@ import indentationFolding from "./indentationFolding"; export interface BaseExtensionOptions { autoIndent?: boolean; codeFolding?: boolean; + /** + * Show CodeMirror's fold gutter with code folding. The canvas renderer + * draws its own fold markers, so it only needs the folding state. + */ + foldGutter?: boolean; + /** + * The canvas renderer draws the selection, cursors, drop cursor, active + * line (and its gutter) and special characters itself, so those + * extensions are left out. + */ + canvas?: boolean; autoCloseBrackets?: boolean; bracketMatching?: boolean; highlightActiveLine?: boolean; @@ -46,17 +58,26 @@ export default function createBaseExtensions( const { autoIndent = true, codeFolding = true, + foldGutter: showFoldGutter = true, + canvas = false, autoCloseBrackets = true, bracketMatching: enableBracketMatching = true, highlightActiveLine: enableHighlightActiveLine = true, highlightSelectionMatches: enableHighlightSelectionMatches = true, } = options; - const extensions: Extension[] = [highlightSpecialChars(), history()]; + const extensions: Extension[] = [history()]; + if (!canvas) extensions.push(highlightSpecialChars()); - if (enableHighlightActiveLine) extensions.push(highlightActiveLineGutter()); - if (codeFolding) extensions.push(foldGutter(), indentationFolding); - extensions.push(drawSelection()); - extensions.push(dropCursor()); + if (enableHighlightActiveLine && !canvas) { + extensions.push(highlightActiveLineGutter()); + } + if (codeFolding) { + extensions.push( + showFoldGutter ? foldGutter() : codeFoldingState(), + indentationFolding, + ); + } + if (!canvas) extensions.push(drawSelection(), dropCursor()); extensions.push(EditorState.allowMultipleSelections.of(true)); if (autoIndent) extensions.push(indentOnInput()); extensions.push( @@ -66,7 +87,9 @@ export default function createBaseExtensions( if (autoCloseBrackets) extensions.push(closeBrackets()); extensions.push(rectangularSelection()); extensions.push(crosshairCursor()); - if (enableHighlightActiveLine) extensions.push(highlightActiveLine()); + if (enableHighlightActiveLine && !canvas) { + extensions.push(highlightActiveLine()); + } if (enableHighlightSelectionMatches) { extensions.push(highlightSelectionMatches()); } diff --git a/src/cm/canvasOptions.ts b/src/cm/canvasOptions.ts new file mode 100644 index 0000000..fdaaac5 --- /dev/null +++ b/src/cm/canvasOptions.ts @@ -0,0 +1,351 @@ +import { + type CanvasThemeSpec, + CanvasView, + type CanvasViewConfig, + type FontSpec, + type TokenStyleSpec, +} from "@acode.app/canvas-view"; +import { HighlightStyle } from "@codemirror/language"; +import { oneDark } from "@codemirror/theme-one-dark"; +import { tags as t } from "@lezer/highlight"; +import { getRainbowBracketColors } from "cm/rainbowBrackets"; +import { getThemeById, getThemeConfig, getThemeExtensions } from "cm/themes"; + +/** + * Maps Acode's editor settings and themes to @acode.app/canvas-view options. + * + * The canvas renderer draws line numbers, fold markers, the active line, + * indent guides, rainbow brackets, whitespace, wrapping and scroll room + * itself, so editorManager leaves those CodeMirror extensions out of the + * state and passes the options built here instead. + */ + +export type CanvasDisplayOptions = Parameters[0]; + +type Settings = Record; + +interface ThemeConfig { + dark?: boolean; + background?: string; + foreground?: string; + selection?: string; + cursor?: string; + activeLine?: string; + lineNumber?: string; + lineNumberActive?: string; + matchingBracket?: string; + selectionMatch?: string; + [color: string]: unknown; +} + +const SCROLL_PAST_END: Record = { + none: 0, + small: 0.25, + medium: 0.5, + full: 1, +}; + +const SCROLL_SPEED: Record = { + SLOW: "slow", + NORMAL: "normal", + FAST: "fast", + FAST_X2: "fastX2", +}; + +const GUTTER_MARKERS = ["shared", "columns", "overlay"] as const; + +/** A finite number within [min, max], else the fallback. */ +function clampSetting( + value: unknown, + min: number, + max: number, + fallback: number, +): number { + const number = Number(value); + if (value == null || value === "" || !Number.isFinite(number)) { + return fallback; + } + return Math.min(Math.max(Math.round(number), min), max); +} + +/** + * Find the HighlightStyle a theme passes to `syntaxHighlighting()`, by + * walking its extension tree (arrays, precedence wrappers, compartments, + * `facet.of(value)` providers). Returns null for themes that compute their + * highlighter (`themeType`) or don't have one. + */ +export function findHighlightStyle( + extension: unknown, + seen = new Set(), +): HighlightStyle | null { + if (!extension || typeof extension !== "object" || seen.has(extension)) { + return null; + } + seen.add(extension); + if (extension instanceof HighlightStyle) return extension; + if (Array.isArray(extension)) { + for (const item of extension) { + const found = findHighlightStyle(item, seen); + if (found) return found; + } + return null; + } + const node = extension as { + value?: unknown; + inner?: unknown; + extension?: unknown; + }; + return ( + findHighlightStyle(node.value, seen) || + findHighlightStyle(node.inner, seen) || + findHighlightStyle(node.extension, seen) + ); +} + +/** Token colors from a theme config, for themes without a HighlightStyle. */ +function highlightFromConfig(config: ThemeConfig): TokenStyleSpec[] { + const color = (key: string) => + typeof config[key] === "string" ? (config[key] as string) : undefined; + const specs: TokenStyleSpec[] = [ + { tag: t.variableName, color: color("variable") }, + { tag: [t.keyword, t.modifier], color: color("keyword") }, + { tag: [t.string, t.special(t.string)], color: color("string") }, + { tag: [t.number, t.bool, t.null], color: color("number") }, + { tag: t.comment, color: color("comment"), fontStyle: "italic" }, + { + tag: [t.function(t.variableName), t.function(t.propertyName)], + color: color("function"), + }, + { tag: t.typeName, color: color("type") }, + { tag: t.className, color: color("class") ?? color("type") }, + { + tag: [t.constant(t.variableName), t.standard(t.variableName)], + color: color("constant"), + }, + { tag: t.operator, color: color("operator") }, + { tag: t.invalid, color: color("invalid") }, + ]; + return specs.filter((spec) => spec.color); +} + +/** + * Decoration colors the canvas draws natively, as the themes' CSS has them: + * selection matches use the theme's `selectionMatch`, else its selection. + */ +function themeClasses(config: ThemeConfig): CanvasThemeSpec["classes"] { + const classes: NonNullable = {}; + if (config.matchingBracket) { + classes["cm-matchingBracket"] = { background: config.matchingBracket }; + } + const selectionMatch = config.selectionMatch || config.selection; + if (selectionMatch) { + classes["cm-selectionMatch"] = { background: selectionMatch }; + } + return Object.keys(classes).length ? classes : undefined; +} + +/** + * The canvas theme for an Acode editor theme: colors from its config, syntax + * colors from the HighlightStyle in its extensions. The theme's + * EditorView.theme stays in the state, where it styles the DOM UI (tooltips, + * panels, autocomplete). + */ +export function getCanvasTheme(themeId: string): CanvasThemeSpec { + // Resolve ids like getThemeExtensions does ("one-dark" -> "one_dark"). + const theme = + getThemeById(themeId) || getThemeById(String(themeId).replace(/-/g, "_")); + const config = (theme?.config || + getThemeConfig(themeId) || + {}) as ThemeConfig; + const highlight = + findHighlightStyle(getThemeExtensions(themeId, [oneDark])) || + highlightFromConfig(config); + return { + dark: config.dark !== false, + background: config.background || "#282c34", + foreground: config.foreground || "#abb2bf", + selection: config.selection, + cursor: config.cursor, + activeLine: config.activeLine, + // Like the DOM themes, which give the active gutter line no background. + activeLineGutter: config.background, + gutterForeground: config.lineNumber, + gutterActiveForeground: config.lineNumberActive, + // Acode draws its own (draggable) scrollbars over the editor. + scrollbar: "transparent", + highlight, + classes: themeClasses(config), + }; +} + +/** A CSS font size ("12px", "1rem", "10pt"...) in CSS pixels. */ +export function fontSizeToPx(value: unknown): number { + const match = /^([\d.]+)\s*([a-z]*)$/i.exec(String(value ?? "").trim()); + if (!match) return 12; + const size = Number.parseFloat(match[1]); + if (!Number.isFinite(size) || size <= 0) return 12; + const unit = match[2].toLowerCase(); + const rootSize = () => + Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || + 16; + switch (unit) { + case "pt": + return (size * 4) / 3; + case "pc": + return size * 16; + case "in": + return size * 96; + case "mm": + return (size * 96) / 25.4; + case "em": + case "rem": + return size * rootSize(); + default: + return size; + } +} + +/** Same font size and line height as `makeFontTheme` gives the DOM editor. */ +export function getCanvasFont(settings: Settings, family: string): FontSpec { + const lineHeight = Number(settings.lineHeight); + return { + family, + size: fontSizeToPx(settings.fontSize || "12px"), + lineHeight: + Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : 1.4, + }; +} + +/** Display options from Acode's editor settings (the live ones). */ +export function getCanvasDisplayOptions( + settings: Settings, + { + themeId, + onLineNumberClick, + }: { + themeId: string; + onLineNumberClick?: CanvasViewConfig["onLineNumberClick"]; + }, +): CanvasDisplayOptions { + const { linenumbers = true, relativeLineNumbers = false } = settings; + const cursorWidth = Number(settings.cursorWidth); + const showSpaces = !!settings.showSpaces; + const rainbow = settings.rainbowBrackets ?? true; + return { + lineNumbers: linenumbers + ? relativeLineNumbers + ? "relative" + : true + : false, + onLineNumberClick, + foldMarkers: settings.fadeFoldWidgets ? "fade" : "always", + highlightActiveLine: settings.highlightActiveLine !== false, + cursorWidth: + Number.isFinite(cursorWidth) && cursorWidth > 0 + ? Math.min(cursorWidth, 10) + : 2, + // 0 keeps the cursor steady. + cursorBlinkRate: clampSetting(settings.cursorBlinkRate, 0, 10000, 1200), + gutterMarkers: GUTTER_MARKERS.includes(settings.canvasGutterMarkers) + ? settings.canvasGutterMarkers + : "shared", + lineWrapping: !!settings.textWrap, + wrapIndent: settings.wrappingIndent ?? "same", + textDirection: settings.rtlText ? "auto" : "ltr", + horizontalScrollPastEnd: Number(settings.leftMargin ?? 50) || 0, + scrollPastEnd: SCROLL_PAST_END[settings.scrollPastEnd || "medium"] ?? 1, + scrollSpeed: SCROLL_SPEED[settings.scrollSpeed] ?? "normal", + // Unlike the DOM editor's guides, the block around the cursor is + // highlighted: it's one rect per frame on the canvas. + indentGuides: settings.indentGuides ? { active: true } : false, + rainbowBrackets: rainbow + ? { colors: getRainbowBracketColors(getThemeConfig(themeId)) } + : false, + showWhitespace: showSpaces + ? { spaces: true, trailing: true, lineBreaks: true } + : false, + stickyScroll: settings.stickyScroll + ? { maxLines: clampSetting(settings.stickyScrollMaxLines, 1, 10, 5) } + : false, + }; +} + +const CANVAS_BACKENDS = ["auto", "webgl2", "webgpu"] as const; + +/** The GPU API for new canvas views ("webgpu" falls back to WebGL2). */ +export function getCanvasBackend( + settings: Settings, +): CanvasViewConfig["backend"] { + const value = settings.canvasBackend; + return CANVAS_BACKENDS.includes(value) ? value : "auto"; +} + +interface CanvasSyncState { + display?: string; + font?: string; + theme?: string; +} + +const synced = new WeakMap(); +// Acode replaces `setTheme` on editor instances with its own (taking a theme +// id), so the view's method is called through the prototype. +const setViewTheme = CanvasView.prototype.setTheme; + +/** + * Bring a canvas view in line with the settings. Each part is applied only + * when it changed: reconfiguring resets the drawn scene, and a new font or a + * dark/light switch rebuilds the glyph atlas. + */ +export function syncCanvasView( + view: CanvasView, + { + display, + font, + themeId, + }: { display?: CanvasDisplayOptions; font?: FontSpec; themeId?: string }, +): void { + let state = synced.get(view); + if (!state) { + state = {}; + synced.set(view, state); + } + if (display) { + // Functions (onLineNumberClick) are left out of the key: they read + // settings when called. + const key = JSON.stringify(display); + if (key !== state.display) { + state.display = key; + view.reconfigure(display); + } + } + if (font) { + const key = JSON.stringify(font); + if (key !== state.font) { + state.font = key; + view.setFont(font); + } + } + if (themeId != null) { + const key = String(themeId); + if (key !== state.theme) { + state.theme = key; + setViewTheme.call(view, getCanvasTheme(key)); + } + } +} + +/** Remember what a new view was created with, so syncing skips it. */ +export function markCanvasViewSynced( + view: CanvasView, + { + display, + font, + themeId, + }: { display: CanvasDisplayOptions; font: FontSpec; themeId: string }, +): void { + synced.set(view, { + display: JSON.stringify(display), + font: JSON.stringify(font), + theme: String(themeId), + }); +} diff --git a/src/cm/colorChip.ts b/src/cm/colorChip.ts index 4d29c4f..8b02429 100644 --- a/src/cm/colorChip.ts +++ b/src/cm/colorChip.ts @@ -1,3 +1,4 @@ +import type { CanvasWidgetSpec } from "@acode.app/canvas-view"; import type { LSPClient } from "@codemirror/lsp-client"; import type { Range } from "@codemirror/state"; import { Decoration, EditorView, WidgetType } from "@codemirror/view"; @@ -73,6 +74,27 @@ export class ColorChipWidget extends WidgetType { return el; } + /** + * How a canvas view (@acode.app/canvas-view) draws the chip: the box + * `toDOM` styles, without making its element. The view only calls + * `toDOM` when the chip is tapped, so the color picker still opens. + */ + toCanvas(view: EditorView): CanvasWidgetSpec { + const cell = view.defaultCharacterWidth || 8; + const em = (view as unknown as { fontSizePx?: number }).fontSizePx || 14; + return { + text: "", + cells: Math.max(0.5, Math.round(((0.9 * em) / cell) * 4) / 4), + padLeft: 2 / cell, + padRight: 2 / cell, + style: { + background: this.payload.css, + border: "rgba(0,0,0,0.2)", + rounded: true, + }, + }; + } + ignoreEvent(): boolean { return false; } @@ -88,9 +110,9 @@ export function colorChipDecoration( } export function isViewEditable(view: EditorView): boolean { - const readOnly = view.contentDOM.ariaReadOnly === "true"; - const editable = view.contentDOM.contentEditable === "true"; - return !readOnly && editable; + // From the state, not contentDOM's attributes: a canvas view's + // contentDOM is its hidden input element, which has neither. + return view.state.facet(EditorView.editable) && !view.state.readOnly; } export function hasLspColorProvider(view: EditorView): boolean { diff --git a/src/cm/createEditorView.ts b/src/cm/createEditorView.ts new file mode 100644 index 0000000..bf3a2b1 --- /dev/null +++ b/src/cm/createEditorView.ts @@ -0,0 +1,63 @@ +import { CanvasView, type CanvasViewConfig } from "@acode.app/canvas-view"; +import type { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; + +/** + * The editor renderers Acode can create. "dom" is CodeMirror's own + * EditorView; "canvas" is @acode.app/canvas-view, which draws the text with + * the GPU on CodeMirror's state (opt-in, experimental). + */ +export type EditorRenderer = "dom" | "canvas"; + +export type CanvasEditorOptions = Omit; + +interface CreateEditorViewOptions { + state: EditorState; + parent: HTMLElement; + /** Options for a canvas view; without them a DOM EditorView is created. */ + canvas?: CanvasEditorOptions | null; +} + +let canvasSupport: boolean | null = null; + +/** + * Whether this WebView can run the canvas renderer (it needs WebGL2). The + * probe context is released right away: pages only get ~16 of them. + */ +export function isCanvasRendererSupported(): boolean { + if (canvasSupport != null) return canvasSupport; + try { + const gl = document.createElement("canvas").getContext("webgl2"); + canvasSupport = !!gl; + gl?.getExtension("WEBGL_lose_context")?.loseContext(); + } catch { + canvasSupport = false; + } + return canvasSupport; +} + +export function isCanvasView(view: unknown): view is CanvasView { + return view instanceof CanvasView; +} + +/** + * Create the editor view for a pane. Code after construction treats both + * kinds the same: CanvasView implements the EditorView members Acode and + * CodeMirror's commands use (`state`, `dispatch`, `setState`, `scrollDOM`, + * `contentDOM`, `coordsAtPos`, `requestMeasure`...). + */ +export default function createEditorView({ + state, + parent, + canvas = null, +}: CreateEditorViewOptions): EditorView { + if (canvas) { + // `view instanceof EditorView` holds for canvas views too. + return new CanvasView({ + ...canvas, + state, + parent, + }) as unknown as EditorView; + } + return new EditorView({ state, parent }); +} diff --git a/src/cm/editorReadOnly.ts b/src/cm/editorReadOnly.ts index 767b740..07f5e15 100644 --- a/src/cm/editorReadOnly.ts +++ b/src/cm/editorReadOnly.ts @@ -7,6 +7,7 @@ import { Transaction, } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; +import { isCanvasView } from "./createEditorView"; interface PointerTapSnapshot { pointerId: number; @@ -75,10 +76,29 @@ const readOnlyCursor = [ }), ]; +export interface EditorReadOnlyOptions { + /** The extension is for a canvas view (@acode.app/canvas-view). */ + canvas?: boolean; +} + /** * Keep CodeMirror's document and DOM editability in sync. */ -export function createEditorReadOnlyExtension(readOnly: boolean): Extension { +export function createEditorReadOnlyExtension( + readOnly: boolean, + { canvas = false }: EditorReadOnlyOptions = {}, +): Extension { + // A canvas view handles read-only files itself: it ignores typing, + // cut, paste and drops, takes focus without opening the keyboard (so + // its cursor shows and hardware-keyboard shortcuts work), and drops + // focus when the editor becomes read-only. It stays editable; only the + // filter for Acode's own editing commands applies. + if (canvas) { + return [ + EditorState.readOnly.of(readOnly), + ...(readOnly ? [readOnlyUserChangeFilter] : []), + ]; + } return [ EditorState.readOnly.of(readOnly), EditorView.editable.of(!readOnly), @@ -103,7 +123,8 @@ export function blurEditorIfReadOnly( view: EditorView, readOnly: boolean, ): void { - if (!readOnly) return; + // A read-only canvas view keeps focus without a keyboard. + if (!readOnly || isCanvasView(view)) return; const activeElement = document.activeElement; if ( activeElement instanceof HTMLElement && @@ -121,7 +142,9 @@ export function reconfigureEditorReadOnly( readOnly: boolean, ): void { view.dispatch({ - effects: compartment.reconfigure(createEditorReadOnlyExtension(readOnly)), + effects: compartment.reconfigure( + createEditorReadOnlyExtension(readOnly, { canvas: isCanvasView(view) }), + ), }); blurEditorIfReadOnly(view, readOnly); } @@ -129,7 +152,10 @@ export function reconfigureEditorReadOnly( /** Focus editable editors and actively settle read-only editors unfocused. */ export function focusEditorIfEditable(view: EditorView): boolean { if (view.state.readOnly) { - blurEditorIfReadOnly(view, true); + // A canvas view focuses without opening the keyboard. (Checked on + // `unknown`: the type guard would narrow an EditorView to never.) + if (isCanvasView(view as unknown)) view.focus(); + else blurEditorIfReadOnly(view, true); return false; } view.focus(); diff --git a/src/cm/lsp/diagnostics.ts b/src/cm/lsp/diagnostics.ts index 7877d40..b13e08f 100644 --- a/src/cm/lsp/diagnostics.ts +++ b/src/cm/lsp/diagnostics.ts @@ -543,10 +543,15 @@ export function lspDiagnosticsClientExtension(): { }; } -export function lspDiagnosticsUiExtension(includeGutter = true): Extension[] { - const diagnosticsMarkerFilter = isCoarsePointerDevice() - ? () => [] - : undefined; +export function lspDiagnosticsUiExtension( + includeGutter = true, + { canvas = false }: { canvas?: boolean } = {}, +): Extension[] { + // Touch devices get no in-text marks in the DOM editor. Canvas views + // (@acode.app/canvas-view) draw them natively (wavy underlines, lint + // points) at no layout cost, so they keep them; tooltips stay off. + const diagnosticsMarkerFilter = + isCoarsePointerDevice() && !canvas ? () => [] : undefined; const diagnosticsTooltipFilter = isCoarsePointerDevice() ? () => [] : undefined; diff --git a/src/cm/lsp/tooltipExtensions.ts b/src/cm/lsp/tooltipExtensions.ts index 7cab0e1..2e73a78 100644 --- a/src/cm/lsp/tooltipExtensions.ts +++ b/src/cm/lsp/tooltipExtensions.ts @@ -27,6 +27,7 @@ import { type ViewUpdate, } from "@codemirror/view"; import { highlightCode } from "@lezer/highlight"; +import { navigateToReference } from "components/referencesPanel/utils"; import type { HoverParams, ServerCapabilities, @@ -348,6 +349,41 @@ function closeHoverIfNeeded(view: EditorView): void { } } +// Matches `#12`, `#L12`, `#L12,5`, `#12:5` style line/column fragments. +const FILE_LINK_FRAGMENT = /^#L?(\d+)(?:[,:Cc](\d+))?/; + +/** + * Open `file://` links from LSP documentation in the editor instead of + * letting them bubble to the global handler, which would hand them to + * the system browser. + */ +function handleDocumentationLinks(dom: HTMLElement, view: EditorView): void { + dom.addEventListener("click", (event) => { + const link = (event.target as Element | null)?.closest?.("a[href]"); + if (!link) return; + const href = link.getAttribute("href") || ""; + if (!/^file:/i.test(href)) return; + + event.preventDefault(); + event.stopPropagation(); + + const hashIndex = href.indexOf("#"); + const uri = hashIndex === -1 ? href : href.slice(0, hashIndex); + const match = + hashIndex === -1 ? null : FILE_LINK_FRAGMENT.exec(href.slice(hashIndex)); + const position = { + line: match ? Math.max(0, Number(match[1]) - 1) : 0, + character: match?.[2] ? Math.max(0, Number(match[2]) - 1) : 0, + }; + + closeHoverIfNeeded(view); + void navigateToReference({ + uri, + range: { start: position, end: position }, + }); + }); +} + function hoverRequest(plugin: LSPPlugin, pos: number) { if (!clientHasCapability(plugin.client, "hoverProvider")) { return Promise.resolve(null); @@ -417,6 +453,7 @@ function lspTooltipSource( results[index].result.contents, ); } + handleDocumentationLinks(dom, view); return { dom }; }, above: true, @@ -618,6 +655,7 @@ function drawSignatureTooltip( const docs = dom.appendChild(document.createElement("div")); docs.className = "cm-lsp-signature-documentation cm-lsp-documentation"; docs.innerHTML = plugin.docToHTML(signature.documentation); + handleDocumentationLinks(docs, view); } } diff --git a/src/cm/modelist.ts b/src/cm/modelist.ts index 64038b7..bebd133 100644 --- a/src/cm/modelist.ts +++ b/src/cm/modelist.ts @@ -5,6 +5,8 @@ export type LanguageExtensionProvider = () => Extension | Promise; export interface AddModeOptions { aliases?: string[]; filenameMatchers?: RegExp[]; + /** Mode whose formatter (and Prettier parser) this mode reuses. */ + formatAs?: string; } export interface ModesByName { @@ -352,6 +354,15 @@ export function getMode(name: string): Mode | null { return modesByName[normalizeModeKey(name)] || null; } +/** + * Name of the mode that `name` formats as, with aliases resolved, or `null`. + */ +export function getFormatAsMode(name: string): string | null { + const formatAs = getMode(name)?.formatAs; + if (!formatAs) return null; + return getMode(formatAs)?.name ?? formatAs; +} + export class Mode { extensions: string; caption: string; @@ -360,6 +371,7 @@ export class Mode { aliases: string[]; extRe: RegExp | null; filenameMatchers: RegExp[]; + formatAs: string | null; languageExtension: LanguageExtensionProvider | null; constructor( @@ -377,10 +389,19 @@ export class Mode { this.mode = name; // CodeMirror uses different mode naming this.extensions = extensions; this.caption = caption || this.name.replace(/_/g, " "); - this.aliases = normalizeAliases(options.aliases, this.name); - this.filenameMatchers = Array.isArray(options.filenameMatchers) - ? options.filenameMatchers.filter((matcher) => matcher instanceof RegExp) + // Plugins may pass anything here; ignore what isn't the expected shape + const { aliases, filenameMatchers, formatAs } = + options && typeof options === "object" ? options : {}; + this.aliases = Array.isArray(aliases) + ? normalizeAliases(aliases, this.name) + : []; + this.filenameMatchers = Array.isArray(filenameMatchers) + ? filenameMatchers.filter((matcher) => matcher instanceof RegExp) : []; + this.formatAs = + typeof formatAs === "string" && normalizeModeKey(formatAs) + ? normalizeModeKey(formatAs) + : null; this.languageExtension = languageExtension; let re = ""; diff --git a/src/cm/touchSelectionMenu.js b/src/cm/touchSelectionMenu.js index 7304711..c31c209 100644 --- a/src/cm/touchSelectionMenu.js +++ b/src/cm/touchSelectionMenu.js @@ -176,6 +176,17 @@ export default function createTouchSelectionMenu(view, options = {}) { return new TouchSelectionMenuController(view, options); } +/** + * The selection menu for a canvas editor (@acode.app/canvas-view). The view owns + * the touch selection gestures and handles, and asks for the menu through its + * `touchSelection.onMenu` hook, which goes to `handleCanvasMenu()`. Only the + * menu UI (items, layout, actions) is shared with the DOM editor's controller, + * so it listens to no pointer or selection events of its own. + */ +export function createCanvasSelectionMenu(view, options = {}) { + return new TouchSelectionMenuController(view, { ...options, canvas: true }); +} + class TouchSelectionMenuController { #view; #container; @@ -200,9 +211,11 @@ class TouchSelectionMenuController { #menuAnimation = null; #viewAnimations = []; #overflowExpanded = false; + #canvas = false; constructor(view, options = {}) { this.#view = view; + this.#canvas = !!options.canvas; this.#container = options.container || view.dom.closest(".editor-container") || view.dom; this.#getActiveFile = options.getActiveFile || (() => null); @@ -217,11 +230,19 @@ class TouchSelectionMenuController { #bindEvents() { const root = this.#view.dom; - root.addEventListener("contextmenu", this.#onContextMenu, true); - document.addEventListener("pointerdown", this.#onGlobalPointerDown, true); - document.addEventListener("pointermove", this.#onGlobalPointerMove, true); - document.addEventListener("pointerup", this.#onGlobalPointerUp, true); - document.addEventListener("pointercancel", this.#onGlobalPointerUp, true); + if (this.#canvas) { + document.addEventListener( + "pointerdown", + this.#onCanvasOutsidePointerDown, + true, + ); + } else { + root.addEventListener("contextmenu", this.#onContextMenu, true); + document.addEventListener("pointerdown", this.#onGlobalPointerDown, true); + document.addEventListener("pointermove", this.#onGlobalPointerMove, true); + document.addEventListener("pointerup", this.#onGlobalPointerUp, true); + document.addEventListener("pointercancel", this.#onGlobalPointerUp, true); + } this.#tooltipObserver = new MutationObserver((mutations) => { const relevant = mutations.some((m) => @@ -231,9 +252,13 @@ class TouchSelectionMenuController { (n.matches?.(".cm-tooltip") || n.querySelector?.(".cm-tooltip")), ), ); - if (!relevant || !this.#menuActive || !this.#shouldShowMenu()) { + if (!relevant || !this.#menuActive) return; + if (this.#canvas) { + // Keep the menu where the view asked for it, clear of the tooltip. + if (this.#menuAnchor) this.#positionMenu(this.#menuAnchor); return; } + if (!this.#shouldShowMenu()) return; this.#showMenuDeferred(); }); this.#tooltipObserver.observe(this.#view.dom, { @@ -244,6 +269,11 @@ class TouchSelectionMenuController { destroy() { const root = this.#view.dom; + document.removeEventListener( + "pointerdown", + this.#onCanvasOutsidePointerDown, + true, + ); root.removeEventListener("contextmenu", this.#onContextMenu, true); document.removeEventListener( "pointerdown", @@ -285,10 +315,15 @@ class TouchSelectionMenuController { cancelAnimationFrame(this.#stateSyncRaf); this.#stateSyncRaf = 0; this.#hideMenu(true); + if (this.#canvas) this.#view.touchSelection?.hideMenu(); } setSelection(value) { if (!this.#enabled) return; + if (this.#canvas) { + if (value) this.#view.touchSelection?.openMenu(); + return; + } if (value) { this.#dismissedSelection = null; this.#menuRequested = true; @@ -302,6 +337,11 @@ class TouchSelectionMenuController { setMenu(value) { this.#menuRequested = !!value; if (!this.#enabled) return; + if (this.#canvas) { + if (value) this.#view.touchSelection?.openMenu(); + else this.#view.touchSelection?.hideMenu(); + return; + } if (!value) { this.#clearMenuShowTimer(); this.#hideMenu(); @@ -316,7 +356,8 @@ class TouchSelectionMenuController { } onScrollStart() { - if (!this.#enabled) return; + // A canvas view hides and re-shows the menu around its own scrolls. + if (!this.#enabled || this.#canvas) return; if (this.#isScrolling) return; this.#clearMenuShowTimer(); this.#isScrolling = true; @@ -325,13 +366,15 @@ class TouchSelectionMenuController { } onScrollEnd() { - if (!this.#enabled || !this.#isScrolling) return; + if (!this.#enabled || this.#canvas || !this.#isScrolling) return; this.#isScrolling = false; if (this.#shouldShowMenu()) this.#scheduleMenuShow(MENU_SHOW_DELAY); } onStateChanged(meta = {}) { - if (!this.#enabled) return; + // A canvas view drops its menu itself on keyboard input and outside + // selection changes. + if (!this.#enabled || this.#canvas) return; if (meta.selectionChanged) this.#cancelReadOnlyTap(); if ( meta.selectionChanged && @@ -364,8 +407,44 @@ class TouchSelectionMenuController { this.#isScrolling = false; this.#clearMenuShowTimer(); this.#hideMenu(true); + if (this.#canvas) this.#view.touchSelection?.hideMenu(); + } + + /** + * A canvas view's `touchSelection.onMenu` event: show the menu over the + * selection (or caret) it reports, or hide it. Hides during scrolls and + * handle drags are transient; the view sends "show" again when they end. + */ + handleCanvasMenu(event) { + if (!this.#canvas) return; + if (event?.type !== "show") { + this.#hideMenu(); + return; + } + if (!this.#enabled) return; + const rect = event.selectionRect; + if (!rect) return; + this.#showMenu({ + x: rect.left + rect.width / 2, + top: rect.top, + bottom: rect.bottom, + hasSelection: event.trigger === "selection", + }); } + #onCanvasOutsidePointerDown = (event) => { + if (!this.#menuActive) return; + const target = event.target; + if ( + target instanceof Node && + (this.$menu.contains(target) || this.#view.dom.contains(target)) + ) { + return; + } + this.#hideMenu(); + this.#view.touchSelection?.hideMenu(); + }; + #onContextMenu = (event) => { if (!this.#enabled) return; if (this.#isIgnoredPointerTarget(event.target)) return; @@ -829,6 +908,7 @@ class TouchSelectionMenuController { this.#menuRequested = false; this.#dismissedSelection = this.#selectionSignature(); this.#hideMenu(); + if (this.#canvas) this.#view.touchSelection?.hideMenu(); focusEditorIfEditable(this.#view); } }; diff --git a/src/components/fileTree/index.js b/src/components/fileTree/index.js index caf20bd..6296b67 100644 --- a/src/components/fileTree/index.js +++ b/src/components/fileTree/index.js @@ -35,6 +35,9 @@ export default class FileTree { this.isLoading = false; this.childTrees = new Map(); // Track child trees for cleanup this.depth = options._depth || 0; // Internal: nesting depth + // Bumped on every load and on destroy so a slow load that finishes + // late can tell it's stale and skip rendering. + this.generation = 0; } /** @@ -45,11 +48,14 @@ export default class FileTree { if (this.isLoading) return; this.isLoading = true; this.currentUrl = url; + const generation = ++this.generation; try { this.clear(); const entries = await this.options.getEntries(url); + // Destroyed (e.g. the folder collapsed) while listing. + if (generation !== this.generation) return; this.entries = helpers.sortDir(entries, { sortByName: true, showHiddenFiles: true, @@ -341,6 +347,7 @@ export default class FileTree { * Destroy the file tree and cleanup */ destroy() { + this.generation++; this.clear(); this.container.classList.remove("file-tree"); } diff --git a/src/components/scrollbar/index.js b/src/components/scrollbar/index.js index 1f5799d..5317fb3 100644 --- a/src/components/scrollbar/index.js +++ b/src/components/scrollbar/index.js @@ -192,11 +192,22 @@ export default function ScrollBar(options) { if (height < 0) height = 0; if (width < 0) width = 0; - if (render && height && width) setValue(scroll); + if (render && trackLength()) setValue(scroll); + } + + /** + * Length the thumb travels along the scrollbar's axis. The cross axis is + * always 0 (the bar is thinner than the thumb), so only this one counts. + */ + function trackLength() { + return isVertical ? height : width; } function setValue(val) { - if (!height || !width) resize(false); + // Measure each time, as the parent can change size without a window + // resize (e.g. the sidebar opening). A detached bar has an empty rect; + // show() measures it once it's back on the page. + if ($scrollbar.isConnected) resize(false); //Make sure value is between 0 and 1 if (val < 0) val = 0; @@ -230,6 +241,9 @@ export default function ScrollBar(options) { $scrollbar.classList.remove("hide"); if (!$scrollbar.isConnected) { options.parent.append($scrollbar); + // The value may have been set while detached, against an empty + // rect; measure again so the thumb isn't painted at 0. + resize(); if (typeof onshow === "function") onshow(); } } diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js index 670ea19..4859685 100644 --- a/src/components/sidebar/index.js +++ b/src/components/sidebar/index.js @@ -735,6 +735,12 @@ preventSlideTests.push((target) => { } }); +// Let the app icon rail scroll when plugins add more icons than fit +preventSlideTests.push((target) => { + const $rail = target.closest?.(".app-icons-container"); + return !!$rail && $rail.scrollHeight > $rail.clientHeight; +}); + preventSlideTests.push((target) => { return ( target instanceof HTMLInputElement || diff --git a/src/components/sidebar/style.scss b/src/components/sidebar/style.scss index 8927015..92b6dbb 100644 --- a/src/components/sidebar/style.scss +++ b/src/components/sidebar/style.scss @@ -192,9 +192,20 @@ body.no-animation { } } - // Special styling for the favorites/sponsors icon + // Unread dot, drawn as a background so ::before/::after stay free + body.has-unread-notifications &[data-id="notification"] { + background-image: radial-gradient( + circle at 28px 12px, + var(--active-color, #3399ff) 3px, + transparent 3.5px + ); + background-size: 100% 100%; + background-position: 0 0; + } + + // Special styling for the favorites/sponsors icon, pinned above the profile &.favorite { - margin-top: 8px; + margin: 4px auto 8px; &::after { display: none; diff --git a/src/components/toast/style.scss b/src/components/toast/style.scss index 4f2654c..32dfeea 100644 --- a/src/components/toast/style.scss +++ b/src/components/toast/style.scss @@ -1,12 +1,17 @@ #toast { position: fixed; bottom: 10px; + bottom: calc(10px + env(safe-area-inset-bottom, 0px)); left: 0; right: 0; margin: 0 auto; + // Follow the theme's popup surface so toasts match menus and dialogs background-color: rgba(9, 14, 29, 0.9); - box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.5); + background-color: var(--popup-background-color); + border: 1px solid var(--border-color, rgba(122, 122, 122, 0.2)); + box-shadow: 0 4px 16px var(--box-shadow-color, rgba(0, 0, 0, 0.3)); color: white; + color: var(--popup-text-color); font-size: 0.95rem; font-weight: 300; display: flex; @@ -17,7 +22,7 @@ width: fit-content; overflow: auto; z-index: 9999; - border-radius: 4px; + border-radius: 8px; pointer-events: none; will-change: transform, opacity; @@ -26,7 +31,7 @@ } .message { - padding: 10px; + padding: 10px 14px; } .icon { diff --git a/src/dialogs/prompt.js b/src/dialogs/prompt.js index f9db7f9..3c4407f 100644 --- a/src/dialogs/prompt.js +++ b/src/dialogs/prompt.js @@ -36,7 +36,8 @@ export default function prompt( return new Promise((resolve) => { const inputType = type === "textarea" ? "textarea" : "input"; - type = type === "filename" ? "text" : type; + const isFilename = type === "filename"; + type = isFilename ? "text" : type; const messageSpan = tag("span", { textContent: message, @@ -150,7 +151,11 @@ export default function prompt( if (input.value) { try { const col = input.value.length; - input.setSelectionRange(col, col); + // Select the name without its extension (or the whole name when + // there is none), so typing replaces it + const dot = isFilename ? input.value.lastIndexOf(".") : -1; + if (isFilename) input.setSelectionRange(0, dot > 0 ? dot : col); + else input.setSelectionRange(col, col); } catch (error) { // ignore } diff --git a/src/dialogs/select.js b/src/dialogs/select.js index c34567f..71b1f8b 100644 --- a/src/dialogs/select.js +++ b/src/dialogs/select.js @@ -162,6 +162,7 @@ function select(title, items, options = {}) { $item.setAttribute("aria-label", itemOptions.title); } if (itemOptions.disabled) $item.classList.add("disabled"); + if (itemOptions.value === "delete") $item.classList.add("destructive"); if (options.default === itemOptions.value) { $item.classList.add("selected"); $defaultVal = $item; diff --git a/src/dialogs/style.scss b/src/dialogs/style.scss index 5219956..9ca5102 100644 --- a/src/dialogs/style.scss +++ b/src/dialogs/style.scss @@ -67,7 +67,7 @@ &.alert, &.app-icon-dialog { .title { - text-transform: uppercase; + text-transform: none; } .confirm-checkbox { @@ -94,6 +94,13 @@ justify-content: center; } + .tile.destructive { + &, + .icon { + color: #e5534b; + } + } + .no-text-transform { li { text-transform: initial !important; @@ -273,8 +280,26 @@ .message { overflow: auto; + overflow-wrap: anywhere; font-size: 0.9em; + .error-details { + margin-top: 12px; + font-size: 0.85em; + opacity: 0.75; + + summary { + cursor: pointer; + } + + code { + display: block; + margin-top: 6px; + white-space: pre-wrap; + word-break: break-word; + } + } + &.color-picker { position: relative; @@ -488,5 +513,19 @@ .button-container { margin-top: 20px; + padding: 0 8px 8px; + gap: 4px; + box-sizing: border-box; + + > button { + min-height: 40px; + border-radius: 6px; + text-transform: capitalize; + font-weight: 500; + + &:last-child { + font-weight: 600; + } + } } } diff --git a/src/lang/ar-ye.json b/src/lang/ar-ye.json index 0c71752..2282d8e 100644 --- a/src/lang/ar-ye.json +++ b/src/lang/ar-ye.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/az-az.json b/src/lang/az-az.json index a06465d..7ad749f 100644 --- a/src/lang/az-az.json +++ b/src/lang/az-az.json @@ -889,5 +889,29 @@ "plugin-review:error-max-length": "Rəy ən çox {max} simvoldan ibarət olmalıdır.", "plugin-review:error-downvote-comment": "Nəyin yaxşılaşdırıla biləcəyini tərtibatçıya bildir.", "plugin-review:error-empty": "Şərh əlavə et və ya «Bəyən» seçimini et.", - "plugin-review:unrated": "Qiymətləndirilməyib" + "plugin-review:unrated": "Qiymətləndirilməyib", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/be-by.json b/src/lang/be-by.json index 4fb6e11..2cb5f96 100644 --- a/src/lang/be-by.json +++ b/src/lang/be-by.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/bn-bd.json b/src/lang/bn-bd.json index a261b49..e90c325 100644 --- a/src/lang/bn-bd.json +++ b/src/lang/bn-bd.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/cs-cz.json b/src/lang/cs-cz.json index af8f6a4..16ce2a0 100644 --- a/src/lang/cs-cz.json +++ b/src/lang/cs-cz.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/de-de.json b/src/lang/de-de.json index 13298d4..7cbb477 100644 --- a/src/lang/de-de.json +++ b/src/lang/de-de.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/en-us.json b/src/lang/en-us.json index 99ef3c8..c545365 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -20,7 +20,7 @@ "delete": "Delete", "dependencies": "Dependencies", "delay": "Time in milliseconds", - "editor settings": "Editor settings", + "editor settings": "Editor Settings", "editor theme": "Editor theme", "enter file name": "Enter file name", "enter folder name": "Enter folder name", @@ -51,7 +51,7 @@ "loading": "Loading", "my profile": "My profile", "new file": "New file", - "new folder": "New Folder", + "new folder": "New folder", "no": "No", "no editor message": "Open or create new file and folder from menu", "not set": "Not set", @@ -61,7 +61,7 @@ "open files and folders": "Open files and folders", "open folder": "Open folder", "open recent": "Open recent", - "ok": "ok", + "ok": "OK", "overwrite": "Overwrite", "paste": "Paste", "preview mode": "Preview mode", @@ -233,7 +233,7 @@ "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", - "app settings": "App settings", + "app settings": "App Settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "clipboard not available": "Clipboard is not available.", @@ -262,7 +262,7 @@ "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", - "scroll settings": "Scroll settings", + "scroll settings": "Scroll Settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", @@ -292,7 +292,7 @@ "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", - "preview settings": "Preview settings", + "preview settings": "Preview Settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme, installed plugins and key bindings. It will not backup your FTP/SFTP or App state.", "host": "Host", @@ -315,7 +315,7 @@ "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", - "quicktools trigger mode": "Quicktools trigger mode", + "quicktools trigger mode": "Quick tools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails.", @@ -332,7 +332,7 @@ "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", - "info-quickTools": "Show or hide quick tools.", + "info-quickTools": "Choose how tall the quick tools bar is, or hide it.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-listFiles": "Automatically list all files when a folder is opened", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", @@ -551,7 +551,7 @@ "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", - "language servers": "Language servers", + "language servers": "Language Servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", @@ -651,7 +651,7 @@ "settings-category-discover-apps": "Discover more apps", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", - "settings-category-core": "Core settings", + "settings-category-core": "Core Settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", @@ -779,7 +779,7 @@ "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", - "app icon": "App icon", + "app icon": "App Icon", "app icon changed": "App icon changed", "app icon change warning": "The app will exit after the app icon is changed.", "settings-info-app-font-family": "Choose the font used across the app interface.", @@ -889,5 +889,29 @@ "plugin-review:error-max-length": "Review must be {max} characters or less.", "plugin-review:error-downvote-comment": "Tell the developer what could be improved.", "plugin-review:error-empty": "Add a comment or choose thumbs up.", - "plugin-review:unrated": "Unrated" + "plugin-review:unrated": "Unrated", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/es-sv.json b/src/lang/es-sv.json index f65da3e..3c92900 100644 --- a/src/lang/es-sv.json +++ b/src/lang/es-sv.json @@ -1,17 +1,17 @@ { - "lang": "Español (by DouZerr)", + "lang": "Español", "about": "Información", "active files": "Archivos Activos", "alert": "Alerta", - "app theme": "Tema de App", + "app theme": "Tema de la Aplicación", "autocorrect": "¿Habilitar Autocorrección?", - "autosave": "Guardar Automáticamente", + "autosave": "Auto guardado", "cancel": "Cancelar", "change language": "Cambiar Idioma", "choose color": "Elegir color", "clear": "Limpiar", "close app": "¿Cerrar la Aplicación?", - "commit message": "Commitear mensaje", + "commit message": "Mensaje de Commit", "console": "Consola", "conflict error": "¡Conflicto! Por favor espera antes de otro commit.", "copy": "Copiar", @@ -82,10 +82,10 @@ "show line numbers": "Mostrar números de línea", "show hidden files": "Mostrar archivos ocultos", "show spaces": "Mostrar espacios", - "soft tab": "Pestaña suave", + "soft tab": "Tabulador suave", "sort by name": "Ordenar por nombre", "success": "Éxito", - "tab size": "Tamaño de pestaña", + "tab size": "Tamaño de tabulador", "text wrap": "Ajuste de línea", "theme": "Tema", "title-listfiles": "Listar archivos", @@ -100,7 +100,7 @@ "unsaved file": "Este archivo no se guardado, ¿cerrar de todos modos?", "warning": "Advertencia", "use emmet": "Usar emmet", - "use quick tools": "Usa herramientas rápidas", + "use quick tools": "Usar herramientas rápidas", "yes": "Si", "encoding": "Codificación de Texto", "syntax highlighting": "Resaltado de sintaxis", @@ -125,11 +125,11 @@ "unsupported device": "Su dispositivo no es compatible con el tema.", "vibrate on tap": "Vibrar al tocar", "copy command is not supported by ftp.": "Comando de copia no es compatible con FTP.", - "support title": "Apoye Acode", + "support title": "Apoya a Acode", "fullscreen": "pantalla completa", - "animation": "animación", - "backup": "respaldo", - "restore": "restaurar", + "animation": "Animación", + "backup": "Respaldar", + "restore": "Restaurar", "backup successful": "Respaldo exitoso", "invalid backup file": "Archivo de respaldo inválido", "add path": "Añadir dirección", @@ -146,9 +146,9 @@ "login": "Iniciar Sesión", "scrollbar size": "Tamaño de barra de scroll", "cursor controller size": "Tamaño del controlador de cursor", - "none": "ninguno", - "small": "pequeño", - "large": "grande", + "none": "Ninguno", + "small": "Pequeño", + "large": "Grande", "confirm on exit": "Confirmar al salir", "show console": "Mostrar consola", "image": "Imagen", @@ -156,11 +156,11 @@ "insert color": "Insertar color", "powersave mode warning": "Desactive el modo de ahorro de energía para obtener una vista previa en un navegador externo", "exit": "Salir", - "custom": "personalizado", + "custom": "Personalizado", "reset warning": "¿Seguro que quieres restablecer el tema?", "theme type": "Tipo de tema", - "light": "claro", - "dark": "oscuro", + "light": "Claro", + "dark": "Oscuro", "file browser": "Explorador de archivos", "operation not permitted": "Operación no permitida", "no such file or directory": "El fichero o directorio no existe", @@ -213,10 +213,10 @@ "app settings": "Ajustes de aplicacion", "disable in-app-browser caching": "Desactivar el almacenamiento en caché de la aplicación en el navegador", "copied to clipboard": "Copiado al portapapeles", - "clipboard not available": "Clipboard is not available.", + "clipboard not available": "El portapapeles no está disponible.", "remember opened files": "Recordar archivos abiertos", "remember opened folders": "Recordar carpetas abiertas", - "no suggestions": "No hay sugerencias", + "no suggestions": "Sin sugerencias", "no suggestions aggressive": "Ninguna sugerencia, agresivo", "install": "Instalar", "installing": "Instalando...", @@ -251,8 +251,8 @@ "security type": "Tipo de seguridad", "connection mode": "Modo de conexión", "port": "Puerto", - "key file": "Archivo de claves", - "select key file": "Seleccionar archivo clave", + "key file": "Archivo de llaves", + "select key file": "Seleccionar archivo de llave", "passphrase": "Frase de acceso", "connecting...": "Conectando...", "type filename": "Escriba el nombre del archivo", @@ -270,8 +270,8 @@ "restore default settings": "Restablecer la configuración predeterminada", "server port": "Puerto del servidor", "preview settings": "Ajustes de previsualización", - "preview settings note": "Si el puerto de previsualización y el puerto del servidor son diferentes, la aplicación no arrancará el servidor y en su lugar abrirá https://: en el navegador o en el navegador de la aplicación. Esto es útil cuando se está ejecutando un servidor en otro lugar.", - "backup/restore note": "Sólo hará una copia de seguridad de tu configuración, tema personalizado y atajos de teclado. No hará copia de seguridad de su FPT/SFTP.", + "preview settings note": "Si el puerto de previsualización y el puerto del servidor son diferentes, la aplicación no arrancará el servidor y en su lugar abrirá https://: en el navegador o en el navegador de la aplicación. Esto es útil cuando se está ejecutando un servidor en otro lugar.", + "backup/restore note": "Sólo hará una copia de seguridad de tu configuración, tema personalizado y atajos de teclado. No hará copia de seguridad de su FTP/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Reintentar ftp/sftp cuando falla", "more": "Más", @@ -285,13 +285,13 @@ "relative line numbers": "Números de línea relativos", "elastic tabstops": "Tabuladores elásticos", "line based rtl switching": "Conmutación RTL basada en líneas", - "hard wrap": "ajuste de línea rígido", + "hard wrap": "Ajuste de línea rígido", "spellcheck": "Corrector ortográfico", "wrap method": "Método de ajuste de línea", "use textarea for ime": "Utilizar área de texto para IME", - "invalid plugin": "Extensión inválida", + "invalid plugin": "Complemento inválido", "type command": "Escriba el comando", - "plugin": "Extensión", + "plugin": "Complemento", "quicktools trigger mode": "Modo de activación de herramientas rápidas", "print margin": "Margen de impresión", "touch move threshold": "Umbral de movimiento táctil", @@ -308,29 +308,29 @@ "info-scroll-settings": "Esta configuración contiene los ajustes de desplazamiento, incluido el ajuste de línea de texto.", "info-animation": "Si la aplicación va lenta, desactiva la animación.", "info-quicktoolstriggermode": "Si el botón de las herramientas rápidas no funciona, intente cambiar este valor.", - "info-checkForAppUpdates": "Check for app updates automatically.", - "info-quickTools": "Show or hide quick tools.", - "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", + "info-checkForAppUpdates": "Buscar actualizaciones de la aplicación automáticamente.", + "info-quickTools": "Mostrar u ocultar las herramientas rápidas.", + "info-showHiddenFiles": "Mostrar archivos y carpetas ocultos. (Comienzan con .)", "info-listFiles": "Listar automáticamente todos los archivos al abrir una carpeta", - "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", - "info-fontSize": "The font size used to render text.", - "info-fontFamily": "The font family used to render text.", - "info-theme": "The color theme of the terminal.", - "info-cursorStyle": "The style of the cursor when the terminal is focused.", - "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", - "info-fontWeight": "The font weight used to render non-bold text.", - "info-cursorBlink": "Whether the cursor blinks.", - "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", - "info-tabStopWidth": "The size of tab stops in the terminal.", - "info-letterSpacing": "The spacing in whole pixels between characters.", - "info-imageSupport": "Whether images are supported in the terminal.", - "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", - "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", - "info-backup": "Creates a backup of the terminal installation.", - "info-restore": "Restores a backup of the terminal installation.", - "info-uninstall": "Uninstalls the terminal installation.", - "owned": "En propiedad", - "api_error": "El servidor API no funciona, inténtelo después de un rato.", + "info-all_file_access": "Otorgar el acceso a /sdcard y /storage en la terminal.", + "info-fontSize": "El tamaño de fuente utilizado para mostrar el texto.", + "info-fontFamily": "La familia tipográfica utilizada para mostrar el texto.", + "info-theme": "La paleta de colores de la terminal.", + "info-cursorStyle": "El estilo del cursor cuando la terminal está enfocada.", + "info-cursorInactiveStyle": "El estilo del cursor cuando la terminal no está enfocada.", + "info-fontWeight": "El grosor de la fuente utilizado para mostrar texto que no esté en negrita.", + "info-cursorBlink": "Parpadeo del cursor.", + "info-scrollback": "Cantidad de desplazamiento hacia atrás en la terminal. El desplazamiento hacia atrás es la cantidad de filas que se conservan cuando las líneas se desplazan más allá del área visible inicial.", + "info-tabStopWidth": "El tamaño de las tabulaciones en la terminal.", + "info-letterSpacing": "El espaciado (en píxeles) entre carácteres.", + "info-imageSupport": "Admitir imagenes en la terminal.", + "info-fontLigatures": "Indica si las ligaduras de texto están habilitadas en la terminal.", + "info-confirmTabClose": "Solicita confirmación antes de cerrar las pestañas de la terminal.", + "info-backup": "Crea una copia de seguridad de la instalación de la terminal.", + "info-restore": "Restaura una copia de seguridad de la instalación de la terminal.", + "info-uninstall": "Desinstala la instalación de la terminal.", + "owned": "Comprados", + "api_error": "El servidor API no funciona, inténtalo más tarde.", "installed": "Instalado", "all": "Todo", "medium": "Medio", @@ -385,11 +385,11 @@ "quicktools:exclamation": "Insertar exclamación", "quicktools:alt-key": "Tecla Alt", "quicktools:meta-key": "Tecla Windows", - "quicktools:search-prev": "Previous Match", - "quicktools:search-next": "Next Match", - "quicktools:search-settings": "Search Settings", - "quicktools:search-replace": "Replace", - "quicktools:search-replace-all": "Replace All", + "quicktools:search-prev": "Coincidencia anterior", + "quicktools:search-next": "Siguiente coincidencia", + "quicktools:search-settings": "Configuración de búsqueda", + "quicktools:search-replace": "Reemplazar", + "quicktools:search-replace-all": "Reemplazar todo", "info-quicktoolssettings": "Personalice los botones de acceso rápido y las teclas del teclado en el contenedor de herramientas rápidas situado debajo del editor para mejorar su experiencia de codificación.", "info-excludefolders": "Utilice el patrón **/node_modules/** para ignorar todos los archivos de la carpeta node_modules. Esto excluirá los archivos de la lista y también evitará que se incluyan en las búsquedas de archivos.", "missed files": "Se han escaneado {count} archivos después de iniciarse la búsqueda y no se incluirán en ella.", @@ -406,446 +406,446 @@ "confirm": "Confirmar", "problems": "Problemas", "show side buttons": "Mostrar botones laterales", - "bug_report": "Submit a Bug Report", + "bug_report": "Reportar un Error", "verified publisher": "Publicador verificado", - "most_downloaded": "Most Downloaded", - "newly_added": "Newly Added", - "top_rated": "Top Rated", - "rename not supported": "Rename on termux dir isn't supported", - "compress": "Compress", - "copy uri": "Copy Uri", - "delete entries": "Are you sure you want to delete {count} items?", - "deleting items": "Deleting {count} items...", - "import project zip": "Import Project(zip)", - "changelog": "Change Log", - "notifications": "Notifications", - "no_unread_notifications": "No unread notifications", - "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", - "fade fold widgets": "Fade Fold Widgets", - "quicktools:home-key": "Home Key", - "quicktools:end-key": "End Key", - "quicktools:pageup-key": "PageUp Key", - "quicktools:pagedown-key": "PageDown Key", - "quicktools:delete-key": "Delete Key", - "quicktools:tilde": "Insert tilde symbol", - "quicktools:backtick": "Insert backtick", - "quicktools:hash": "Insert Hash symbol", - "quicktools:dollar": "Insert dollar symbol", - "quicktools:modulo": "Insert modulo/percent symbol", - "quicktools:caret": "Insert caret symbol", - "plugin_enabled": "Plugin enabled", - "plugin_disabled": "Plugin disabled", - "enable_plugin": "Enable this Plugin", - "disable_plugin": "Disable this Plugin", - "open_source": "Open Source", - "terminal settings": "Terminal Settings", - "font ligatures": "Font Ligatures", - "letter spacing": "Letter Spacing", - "terminal:tab stop width": "Tab Stop Width", - "terminal:scrollback": "Scrollback Lines", - "terminal:cursor blink": "Cursor Blink", - "terminal:font weight": "Font Weight", - "terminal:cursor inactive style": "Cursor Inactive Style", - "terminal:cursor style": "Cursor Style", - "terminal:font family": "Font Family", - "terminal:convert eol": "Convert EOL", - "terminal:confirm tab close": "Confirm terminal tab close", - "terminal:image support": "Image support", + "most_downloaded": "Más Descargados", + "newly_added": "Recientemente Añadidos", + "top_rated": "Mejor Calificados", + "rename not supported": "No hay soporte de rename en el directorio de Termux", + "compress": "Comprimir", + "copy uri": "Copiar Uri", + "delete entries": "¿Estás seguro de que deseas eliminar {count} elementos?", + "deleting items": "Eliminando {count} elementos...", + "import project zip": "Importar proyecto (zip)", + "changelog": "Historial de cambios", + "notifications": "Notificaciones", + "no_unread_notifications": "No hay notificaciones sin leer", + "should_use_current_file_for_preview": "Usar el archivo actual para la vista previa en lugar del predeterminado (index.html).", + "fade fold widgets": "Desvanecer Iconos de Plegado", + "quicktools:home-key": "Tecla Home", + "quicktools:end-key": "Tecla End", + "quicktools:pageup-key": "Tecla de Pagina Arriba", + "quicktools:pagedown-key": "Tecla de Pagina Abajo", + "quicktools:delete-key": "Tecla de borrado", + "quicktools:tilde": "Insertar tilde", + "quicktools:backtick": "Insertar comilla invertida", + "quicktools:hash": "Insertar símbolo de hash", + "quicktools:dollar": "Insertar símbolo de dólar", + "quicktools:modulo": "Insertar símbolo de módulo/porcentaje", + "quicktools:caret": "Insertar símbolo de intercalación", + "plugin_enabled": "Plugin activado", + "plugin_disabled": "Plugin desactivado", + "enable_plugin": "Activar este Plugin", + "disable_plugin": "Desactivar este Plugin", + "open_source": "Codigo Abierto", + "terminal settings": "Ajustes de la Terminal", + "font ligatures": "Ligaduras de Texto", + "letter spacing": "Espaciado de Letras", + "terminal:tab stop width": "Ancho del Tabulador", + "terminal:scrollback": "Líneas de desplazamiento hacia atrás", + "terminal:cursor blink": "Parpadeo del Cursor", + "terminal:font weight": "Grosor de Fuente", + "terminal:cursor inactive style": "Estilo del Cursor Inactivo", + "terminal:cursor style": "Estilo del Cursor", + "terminal:font family": "Familia de Fuentes", + "terminal:convert eol": "Convertir EOL", + "terminal:confirm tab close": "Confirmar el cierre de la pestaña del terminal", + "terminal:image support": "Soporte de imágenes", "terminal": "Terminal", - "terminal not installed prompt": "Terminal no está instalado. ¿Desea inicializarlo ahora?", - "terminal first launch prompt": "El entorno del terminal no está instalado. Permite ejecutar comandos, SSH y scripting. ¿Instalar ahora?", - "allFileAccess": "All file access", - "fonts": "Fonts", - "sponsor": "Patrocinador", - "downloads": "downloads", - "reviews": "reviews", - "overview": "Overview", - "contributors": "Contributors", - "quicktools:hyphen": "Insert hyphen symbol", - "check for app updates": "Check for app updates", - "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", - "keywords": "Keywords", - "author": "Author", - "filtered by": "Filtered by", - "clean install state": "Clean Install State", - "backup created": "Backup created", - "restore completed": "Restore completed", - "restore will include": "This will restore", - "restore warning": "This action cannot be undone. Continue?", - "reload to apply": "Reload to apply changes?", - "reload app": "Reload app", - "preparing backup": "Preparing backup", - "collecting settings": "Collecting settings", - "collecting key bindings": "Collecting key bindings", - "collecting plugins": "Collecting plugin information", - "creating backup": "Creating backup file", - "validating backup": "Validating backup", - "restoring key bindings": "Restoring key bindings", - "restoring plugins": "Restoring plugins", - "restoring settings": "Restoring settings", - "legacy backup warning": "This is an older backup format. Some features may be limited.", - "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", - "plugin not found": "Plugin not found in registry", - "paid plugin skipped": "Paid plugin - purchase not found", - "source not found": "Source file no longer exists", - "restored": "Restored", - "skipped": "Skipped", - "backup not valid object": "Backup file is not a valid object", - "backup no data": "Backup file contains no data to restore", - "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", - "backup missing metadata": "Missing backup metadata - some info may be unavailable", - "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", - "backup checksum verify failed": "Could not verify checksum", - "backup invalid settings": "Invalid settings format", - "backup invalid keybindings": "Invalid keyBindings format", - "backup invalid plugins": "Invalid installedPlugins format", - "issues found": "Issues found", - "error details": "Error details", - "active tools": "Active tools", - "available tools": "Available tools", - "recent": "Recent Files", - "command palette": "Open Command Palette", - "change theme": "Change Theme", - "documentation": "Documentation", - "open in terminal": "Open in Terminal", - "developer mode": "Developer Mode", - "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", - "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", - "developer mode disabled": "Developer mode disabled", - "copy relative path": "Copy Relative Path", - "shortcut request sent": "Shortcut request opened. Tap Add to finish.", - "add to home screen": "Add to home screen", - "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", - "save file before home shortcut": "Save the file before adding it to the home screen.", - "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", - "shift click selection": "Shift + tap/click selection", - "earn ad-free time": "Earn ad-free time", - "indent guides": "Indent guides", - "language servers": "Language servers", - "lint gutter": "Show lint gutter", - "rainbow brackets": "Rainbow brackets", - "lsp-add-custom-server": "Add custom server", - "lsp-binary-args": "Binary args (JSON array)", - "lsp-binary-command": "Binary command", - "lsp-binary-path-optional": "Binary path (optional)", - "lsp-check-command-optional": "Check command (optional override)", - "lsp-checking-installation-status": "Checking installation status...", - "lsp-configured": "Configured", - "lsp-custom-server-added": "Custom server added", - "lsp-default": "Default", - "lsp-details-line": "Details: {details}", - "lsp-edit-initialization-options": "Edit initialization options", - "lsp-empty": "Empty", - "lsp-enabled": "Enabled", - "lsp-error-add-server-failed": "Failed to add server", - "lsp-error-args-must-be-array": "Arguments must be a JSON array", - "lsp-error-binary-command-required": "Binary command is required", - "lsp-error-language-id-required": "At least one language ID is required", - "lsp-error-package-required": "At least one package is required", - "lsp-error-server-id-required": "Server ID is required", - "lsp-feature-completion": "Code completion", - "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", - "lsp-feature-diagnostics": "Diagnostics", - "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", - "lsp-feature-formatting": "Formatting", - "lsp-feature-formatting-info": "Enable code formatting from the language server.", - "lsp-feature-hover": "Hover information", - "lsp-feature-hover-info": "Show type information and documentation on hover.", - "lsp-feature-inlay-hints": "Inlay hints", - "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", - "lsp-feature-signature": "Signature help", + "terminal not installed prompt": "La terminal no está instalada. ¿Desea inicializarla ahora?", + "terminal first launch prompt": "El entorno de la terminal no está instalado. Permite ejecutar comandos, SSH y scripting. ¿Instalar ahora?", + "allFileAccess": "Acceso a todos los archivos", + "fonts": "Fuentes", + "sponsor": "Patrocinar", + "downloads": "Descargas", + "reviews": "Reseñas", + "overview": "Descripción general", + "contributors": "Contribuidores", + "quicktools:hyphen": "Insertar símbolo de guion", + "check for app updates": "Buscar Actualizaciones de la Aplicación", + "prompt update check consent message": "Acode puede buscar nuevas actualizaciones cuando estés conectado a internet. ¿Activar la busqueda de actualizaciones?", + "keywords": "Palabras Clave", + "author": "Autor", + "filtered by": "Filtrado por", + "clean install state": "Limpiar Install State", + "backup created": "Copia de seguridad creada", + "restore completed": "Restauración completada", + "restore will include": "Esto restaurará", + "restore warning": "Esta acción no se puede deshacer. ¿Continuar?", + "reload to apply": "¿Recargar para aplicar los cambios?", + "reload app": "Recargar Aplicación", + "preparing backup": "Preparando la copia de seguridad", + "collecting settings": "Recopilando Configuraciones", + "collecting key bindings": "Recopilando de combinaciones de teclas", + "collecting plugins": "Recopilando de información del plugin", + "creating backup": "Creando copia de seguridad", + "validating backup": "Validando la copia de seguridad", + "restoring key bindings": "Restaurar las combinaciones de teclas", + "restoring plugins": "Restaurando plugins", + "restoring settings": "Restaurando la configuración", + "legacy backup warning": "Este es un formato de copia de seguridad antiguo. Algunas funciones pueden estar limitadas.", + "checksum mismatch": "Error de suma de comprobación: es posible que el archivo de copia de seguridad haya sido modificado o esté dañado.", + "plugin not found": "El plugin no se encontró en el registro", + "paid plugin skipped": "Complemento de pago: no se encontró la compra.", + "source not found": "El archivo fuente ya no existe", + "restored": "Restaurado", + "skipped": "Ignorado", + "backup not valid object": "El archivo de copia de seguridad no es un objeto válido.", + "backup no data": "El archivo de copia de seguridad no contiene datos para restaurar.", + "backup legacy warning": "Este es un formato de copia de seguridad antiguo (v1). Algunas funciones pueden estar limitadas.", + "backup missing metadata": "Faltan metadatos de copia de seguridad: es posible que parte de la información no esté disponible.", + "backup checksum mismatch": "Error de suma de comprobación: el archivo de copia de seguridad podría haber sido modificado o estar dañado. Proceda con precaución.", + "backup checksum verify failed": "No se pudo verificar la suma de comprobación.", + "backup invalid settings": "Formato de configuración no válido", + "backup invalid keybindings": "Formato de asignación de teclas no válido", + "backup invalid plugins": "Formato de installedPlugins no válido", + "issues found": "Problemas encontrados", + "error details": "Detalles del error", + "active tools": "Herramientas activas", + "available tools": "Herramientas disponibles", + "recent": "Archivos recientes", + "command palette": "Abrir la paleta de comandos", + "change theme": "Cambiar tema", + "documentation": "Documentación", + "open in terminal": "Abrir en la terminal", + "developer mode": "Modo desarrollador", + "info-developermode": "Habilita las herramientas para desarrolladores (Eruda) para depurar complementos e inspeccionar el estado de la aplicación. El inspector se inicializará al iniciar la aplicación.", + "developer mode enabled": "Modo desarrollador activado. Usa la paleta de comandos para activar o desactivar el inspector. (Ctrl+Shift+I).", + "developer mode disabled": "Modo desarrollador desactivado", + "copy relative path": "Copiar ruta relativa", + "shortcut request sent": "Se ha abierto la solicitud de acceso directo. Pulsa Añadir para finalizar.", + "add to home screen": "Agregar a la pantalla de inicio", + "pin shortcuts not supported": "Los accesos directos a la pantalla de inicio no son compatibles con este dispositivo.", + "save file before home shortcut": "Guarda el archivo antes de agregarlo a la pantalla de inicio.", + "terminal_required_message_for_lsp": "La terminal no está instalada. Instale la terminal primero para poder usar los servidores LSP.", + "shift click selection": "Shift + pulsar/clic para seleccionar", + "earn ad-free time": "Gana tiempo sin anuncios", + "indent guides": "Guías de Indentación", + "language servers": "Servidores de Lenguaje", + "lint gutter": "Mostrar lint gutter", + "rainbow brackets": "Corchetes arcoíris", + "lsp-add-custom-server": "Agregar servidor personalizado", + "lsp-binary-args": "Parámetros binarios (array JSON)", + "lsp-binary-command": "Comando binario", + "lsp-binary-path-optional": "Ruta al binario (opcional)", + "lsp-check-command-optional": "Comando de verificación (reemplazo opcional)", + "lsp-checking-installation-status": "Comprobando el estado de la instalación...", + "lsp-configured": "Configurado", + "lsp-custom-server-added": "Servidor personalizado añadido", + "lsp-default": "Por defecto", + "lsp-details-line": "Detalles: {details}", + "lsp-edit-initialization-options": "Editar opciones de inicialización", + "lsp-empty": "Vacío", + "lsp-enabled": "Activado", + "lsp-error-add-server-failed": "No se pudo agregar el servidor.", + "lsp-error-args-must-be-array": "Los argumentos deben ser un array JSON.", + "lsp-error-binary-command-required": "Se requiere un comando binario.", + "lsp-error-language-id-required": "Se requiere al menos un id de lenguaje.", + "lsp-error-package-required": "Se requiere al menos un paquete.", + "lsp-error-server-id-required": "Se requiere el ID del servidor.", + "lsp-feature-completion": "Autocompletado de código", + "lsp-feature-completion-info": "Habilitar las sugerencias de autocompletado del servidor.", + "lsp-feature-diagnostics": "Diagnósticos", + "lsp-feature-diagnostics-info": "Muestra los errores y advertencias del servidor.", + "lsp-feature-formatting": "Formato", + "lsp-feature-formatting-info": "Habilitar el formato de código desde el servidor.", + "lsp-feature-hover": "Información al pasar el cursor", + "lsp-feature-hover-info": "Muestra información sobre el tipo y la documentación al pasar el cursor por encima.", + "lsp-feature-inlay-hints": "Incrustaciones", + "lsp-feature-inlay-hints-info": "Mostrar sugerencias de tipo en línea en el editor.", + "lsp-feature-signature": "Ayuda de parámetros", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", - "lsp-initialization-options": "Initialization options", - "lsp-initialization-options-json": "Initialization options (JSON)", - "lsp-initialization-options-updated": "Initialization options updated", - "lsp-install-command": "Install command", - "lsp-install-command-unavailable": "Install command not available", - "lsp-install-info-check-failed": "Acode could not verify the installation status.", - "lsp-install-info-missing": "Language server is not installed in the terminal environment.", - "lsp-install-info-ready": "Language server is installed and ready.", - "lsp-install-info-unknown": "Installation status could not be checked automatically.", - "lsp-install-info-version-available": "Version {version} is available.", + "lsp-initialization-options": "Opciones de inicialización", + "lsp-initialization-options-json": "Opciones de inicialización (JSON)", + "lsp-initialization-options-updated": "Opciones de inicialización actualizadas", + "lsp-install-command": "Comando de instalación", + "lsp-install-command-unavailable": "El comando de instalación no está disponible.", + "lsp-install-info-check-failed": "Acode no pudo verificar el estado de la instalación.", + "lsp-install-info-missing": "El servidor no está instalado en el entorno de la terminal.", + "lsp-install-info-ready": "El servidor está instalado y listo.", + "lsp-install-info-unknown": "No se pudo comprobar automáticamente el estado de la instalación.", + "lsp-install-info-version-available": "Version {version} disponible.", "lsp-install-notification": "{server} no está instalado. Toque para instalar.", - "lsp-install-method-apk": "APK package", + "lsp-install-method-apk": "paquete APK", "lsp-install-method-cargo": "Cargo crate", - "lsp-install-method-manual": "Manual binary", - "lsp-install-method-npm": "npm package", - "lsp-install-method-pip": "pip package", - "lsp-install-method-shell": "Custom shell", - "lsp-install-method-title": "Install method", - "lsp-install-repair": "Install / repair", - "lsp-installation-status": "Installation status", - "lsp-installed": "Installed", - "lsp-invalid-timeout": "Invalid timeout value", - "lsp-language-ids": "Language IDs (comma separated)", - "lsp-packages-prompt": "{method} packages (comma separated)", - "lsp-remove-installed-files": "Remove installed files for {server}?", - "lsp-server-disabled-toast": "Server disabled", - "lsp-server-enabled-toast": "Server enabled", - "lsp-server-id": "Server ID", - "lsp-server-label": "Server label", - "lsp-server-not-found": "Server not found", - "lsp-server-uninstalled": "Server uninstalled", - "lsp-startup-timeout": "Startup timeout", - "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", - "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", - "lsp-state-disabled": "disabled", - "lsp-state-enabled": "enabled", - "lsp-status": "Status", - "lsp-status-check-failed": "Check failed", - "lsp-status-installed": "Installed", - "lsp-status-installed-version": "Installed ({version})", - "lsp-status-line": "Status: {status}", - "lsp-status-not-installed": "Not installed", - "lsp-status-unknown": "Unknown", + "lsp-install-method-manual": "Binario manual", + "lsp-install-method-npm": "paquete npm", + "lsp-install-method-pip": "paquete pip", + "lsp-install-method-shell": "Shell personalizado", + "lsp-install-method-title": "Método de instalación", + "lsp-install-repair": "Instalar/ Reparar", + "lsp-installation-status": "Estado de la instalación", + "lsp-installed": "Instalado", + "lsp-invalid-timeout": "Valor de tiempo de espera no válido", + "lsp-language-ids": "IDs de lenguajes (separados por comas)", + "lsp-packages-prompt": "paquetes de {method} (separados por comas)", + "lsp-remove-installed-files": "¿Eliminar archivos instalados para {server}?", + "lsp-server-disabled-toast": "Servidor desactivado", + "lsp-server-enabled-toast": "Servidor activado", + "lsp-server-id": "ID del servidor", + "lsp-server-label": "Etiqueta del servidor", + "lsp-server-not-found": "Servidor no encontrado", + "lsp-server-uninstalled": "Servidor desinstalado", + "lsp-startup-timeout": "Tiempo de espera de inicio", + "lsp-startup-timeout-ms": "Tiempo de espera de inicio (milisegundos)", + "lsp-startup-timeout-set": "Tiempo de espera de inicio establecido en: {timeout} ms", + "lsp-state-disabled": "desactivado", + "lsp-state-enabled": "activado", + "lsp-status": "Estado", + "lsp-status-check-failed": "Revisión fallida", + "lsp-status-installed": "Instalado", + "lsp-status-installed-version": "Instalado ({version})", + "lsp-status-line": "Estado: {status}", + "lsp-status-not-installed": "No instalado", + "lsp-status-unknown": "Desconocido", "lsp-timeout-ms": "{timeout} ms", - "lsp-uninstall-command-unavailable": "Uninstall command not available", + "lsp-uninstall-command-unavailable": "Comando para desinstalar no disponible", "lsp-uninstall-notification": "Toque para desinstalar {server}.", - "lsp-uninstall-server": "Uninstall server", - "lsp-update-command-optional": "Update command (optional)", - "lsp-update-command-unavailable": "Update command not available", - "lsp-update-server": "Update server", + "lsp-uninstall-server": "Desinstalar Servidor", + "lsp-update-command-optional": "Comando de actualización (opcional)", + "lsp-update-command-unavailable": "Comando de actualización no disponible", + "lsp-update-server": "Actualizar Servidor", "lsp-version-line": "Version: {version}", - "lsp-view-initialization-options": "View initialization options", - "settings-category-about-acode": "About Acode", + "lsp-view-initialization-options": "Ver opciones de inicialización", + "settings-category-about-acode": "Acerca de Acode", "settings-category-discover-apps": "Descubre más aplicaciones", - "settings-category-advanced": "Advanced", - "settings-category-assistance": "Assistance", - "settings-category-core": "Core settings", + "settings-category-advanced": "Avanzado", + "settings-category-assistance": "Asistencia", + "settings-category-core": "Configuración Principal", "settings-category-cursor": "Cursor", - "settings-category-cursor-selection": "Cursor & selection", - "settings-category-custom-servers": "Custom servers", + "settings-category-cursor-selection": "Cursor y Selección", + "settings-category-custom-servers": "Servidores Personalizados", "settings-category-customization": "Personalización", "settings-category-tools": "Herramientas", - "settings-category-display": "Display", - "settings-category-editing": "Editing", - "settings-category-features": "Features", - "settings-category-files-sessions": "Files & sessions", - "settings-category-fonts": "Fonts", + "settings-category-display": "Visualización", + "settings-category-editing": "Edición", + "settings-category-features": "Funciones", + "settings-category-files-sessions": "Archivos y Sesiones", + "settings-category-fonts": "Fuentes", "settings-category-general": "General", - "settings-category-guides-indicators": "Guides & indicators", - "settings-category-installation": "Installation", - "settings-category-interface": "Interface", - "settings-category-maintenance": "Maintenance", - "settings-category-permissions": "Permissions", - "settings-category-preview": "Preview", - "settings-category-scrolling": "Scrolling", - "settings-category-server": "Server", - "settings-category-servers": "Servers", - "settings-category-session": "Session", - "settings-category-support-acode": "Support Acode", - "settings-category-text-layout": "Text & layout", - "settings-info-app-animation": "Control transition animations across the app.", - "settings-info-app-check-files": "Refresh editors when files change outside Acode.", - "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", - "settings-info-app-confirm-on-exit": "Ask before closing the app.", - "settings-info-app-console": "Choose which debug console integration Acode uses.", - "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", - "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", - "settings-info-app-floating-button": "Show the floating quick actions button.", - "settings-info-app-font-manager": "Install, manage, or remove app fonts.", - "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", - "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", - "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", - "settings-info-app-language": "Choose the app language and translated labels.", - "settings-info-app-open-file-list-position": "Choose where the active files list appears.", - "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", - "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", - "settings-info-app-remember-files": "Reopen the files that were open last time.", - "settings-info-app-remember-folders": "Reopen folders from the previous session.", - "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", - "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", - "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", - "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", - "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", - "settings-info-editor-autosave": "Save changes automatically after a delay.", - "settings-info-editor-color-preview": "Preview color values inline in the editor.", - "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", - "settings-info-editor-font-family": "Choose the typeface used in the editor.", - "settings-info-editor-font-size": "Set the editor text size.", - "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", - "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", - "settings-info-editor-indent-guides": "Show indentation guide lines.", - "settings-info-editor-line-height": "Adjust vertical spacing between lines.", - "settings-info-editor-line-numbers": "Show line numbers in the gutter.", - "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", - "settings-info-editor-live-autocomplete": "Show suggestions while you type.", - "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", - "settings-info-editor-relative-line-numbers": "Show distance from the current line.", - "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", - "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", - "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", + "settings-category-guides-indicators": "Guías e indicadores", + "settings-category-installation": "Instalación", + "settings-category-interface": "Interfaz", + "settings-category-maintenance": "Mantenimiento", + "settings-category-permissions": "Permisos", + "settings-category-preview": "Vista Previa", + "settings-category-scrolling": "Desplazamiento", + "settings-category-server": "Servidor", + "settings-category-servers": "Servidores", + "settings-category-session": "Sesión", + "settings-category-support-acode": "Apoya a Acode", + "settings-category-text-layout": "Texto y Distribución", + "settings-info-app-animation": "Controla las animaciones en la aplicación.", + "settings-info-app-check-files": "Recarga el editor cuando los archivos cambian fuera de Acode.", + "settings-info-app-clean-install-state": "Limpiar el estado de instalación usado por los flujos de incorporación y configuración.", + "settings-info-app-confirm-on-exit": "Preguntar antes de cerrar la aplicación.", + "settings-info-app-console": "Elige qué integración de consola de debug usa Acode.", + "settings-info-app-default-file-encoding": "Codificación por defecto al abrir o crear archivos.", + "settings-info-app-exclude-folders": "Ignorar carpetas o patrones al buscar o escanear.", + "settings-info-app-floating-button": "Mostrar el botón flotante que abre las herramientas rápidas..", + "settings-info-app-font-manager": "Instala, administra o quita fuentes de la aplicación.", + "settings-info-app-fullscreen": "Esconde la barra de estado al usa Acode.", + "settings-info-app-keybindings": "Edita las combinaciones de teclas o reinicia los atajos del editor.", + "settings-info-app-keyboard-mode": "Elige cómo se comporta el teclado al escribir.", + "settings-info-app-language": "Elige el lenguaje de la aplicación y las etiquetas traducidas.", + "settings-info-app-open-file-list-position": "Elige dónde aparece la lista de archivos activos.", + "settings-info-app-quick-tools-settings": "Reorganiza y personaliza los botones de herramientas rápidas.", + "settings-info-app-quick-tools-trigger-mode": "Elige si las herramientas rápidas se activan mediante clic o toque.", + "settings-info-app-remember-files": "Reabrir archivos que estaban abiertos la última vez.", + "settings-info-app-remember-folders": "Reabrir carpetas de la sesión anterior.", + "settings-info-app-retry-remote-fs": "Reintentar operaciones de archivos remotos después de una transferencia fallida.", + "settings-info-app-side-buttons": "Mostrar botones extra en el margen del editor.", + "settings-info-app-sponsor-sidebar": "Mostrar la opción de sponsor en la barra lateral.", + "settings-info-app-touch-move-threshold": "Movimiento mínimo para que un toque de arrastre sea detectado.", + "settings-info-app-vibrate-on-tap": "Activar la retroalimentación háptica al tocar y para los controles.", + "settings-info-editor-autosave": "Guardar los cambios en un archivo después de cierto tiempo.", + "settings-info-editor-color-preview": "Mostrar vista previa de colores en el editor.", + "settings-info-editor-fade-fold-widgets": "Desvanece los marcadores de plegado hasta que sean necesarios.", + "settings-info-editor-font-family": "Elige el tipo de cara para la fuente del editor", + "settings-info-editor-font-size": "Establece el tamaño de fuente del editor.", + "settings-info-editor-format-on-save": "Ejecutar el formateador cada vez que guardas un archivo.", + "settings-info-editor-hard-wrap": "Insertar saltos de línea reales en lugar de solo ajustar el texto visualmente.", + "settings-info-editor-indent-guides": "Mostrar guías de indentación.", + "settings-info-editor-line-height": "Ajustar espaciado vertical entre líneas.", + "settings-info-editor-line-numbers": "Muestra los números de línea en el editor.", + "settings-info-editor-lint-gutter": "Mostrar diagnósticos y marcadores de error en el lateral del editor.", + "settings-info-editor-live-autocomplete": "Mostrar sugerencias mientras escribes.", + "settings-info-editor-rainbow-brackets": "Llaves del mismo color basado en la profundidad de anidación.", + "settings-info-editor-relative-line-numbers": "Mostrar distancia a la linea actual.", + "settings-info-editor-rtl-text": "Intercambia el comportamiento de las líneas a derecha-a-izquierda", + "settings-info-editor-scroll-settings": "Ajusta el tamaño, velocidad y comportamiento de gestos de la barra de desplazamiento.", + "settings-info-editor-shift-click-selection": "Extender la selección manteniendo shift y tocando o dando click en el editor.", "settings-info-editor-show-spaces": "Mostrar marcadores visibles de espacios en blanco y saltos de línea.", - "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", - "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", - "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", - "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", - "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", - "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", - "settings-info-lsp-install-server": "Install or repair this language server.", - "settings-info-lsp-server-enabled": "Enable or disable this language server.", - "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", - "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", - "settings-info-lsp-update-server": "Update this language server if an update flow is available.", - "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", - "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", - "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", - "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", - "settings-info-main-changelog": "See recent updates and release notes.", - "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", - "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", - "settings-info-main-formatter": "Choose a formatter for each supported language.", - "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", - "settings-info-main-plugins": "Manage installed plugins and their available actions.", - "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", - "settings-info-main-rateapp": "Rate Acode on Google Play.", - "settings-info-main-remove-ads": "Unlock permanent ad-free access.", - "settings-info-main-reset": "Reset Acode to its default configuration.", - "settings-info-main-sponsors": "Support ongoing Acode development.", - "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", - "settings-info-main-theme": "App theme, contrast, and custom colors.", - "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", - "settings-info-preview-host": "Hostname used when opening the preview URL.", - "settings-info-preview-mode": "Choose where preview opens when you launch it.", - "settings-info-preview-preview-port": "Port used by the live preview server.", - "settings-info-preview-server-port": "Port used by the internal app server.", - "settings-info-preview-show-console-toggler": "Show the console button in preview.", - "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", - "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", - "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", - "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. They work best with folders that Acode's built-in terminal can access directly, such as terminal/public folders. SAF, external, and remote locations are skipped by default because they may use cached or virtual paths; enable the compatibility option only if you want to try them.", - "search result label singular": "result", - "search result label plural": "results", - "pin tab": "Pin tab", - "unpin tab": "Unpin tab", - "pinned tab": "Pinned tab", - "unpin tab before closing": "Unpin the tab before closing it.", - "app font": "App font", + "settings-info-editor-soft-tab": "Inserta espacios en lugar de tabuladores.", + "settings-info-editor-tab-size": "Establece cuántos espacios ocupa un tabulador.", + "settings-info-editor-teardrop-size": "Establece el tamaño del cursor touch al editar.", + "settings-info-editor-text-wrap": "Ajusta las líneas largas dentro del editor.", + "settings-info-lsp-add-custom-server": "Registra un nuevo servidor de lenguaje con comandos de instalación, actualización e inicialización.", + "settings-info-lsp-edit-init-options": "Editar las opciones de inicialización como JSON.", + "settings-info-lsp-install-server": "Instalar o reparar este servidor.", + "settings-info-lsp-server-enabled": "Activa o desactiva este servidor.", + "settings-info-lsp-startup-timeout": "Establece por cuánto tiempo Acode espera a que inicie este servidor.", + "settings-info-lsp-uninstall-server": "Remover los paquetes o binarios instalados para este servidor.", + "settings-info-lsp-update-server": "Actualiza este servidor si algún flujo de actualización está disponible.", + "settings-info-lsp-view-init-options": "Mira las opciones de inicialización aplicadas en formato JSON.", + "settings-info-main-ad-rewards": "Mira anuncios para desactivar los anuncios en la aplicación de forma temporal.", + "settings-info-main-app-settings": "Lenguaje, comportamiento de la aplicación y acceso a herramientas rápidas.", + "settings-info-main-backup-restore": "Exporta tu configuración a un respaldo o restaurala más tarde.", + "settings-info-main-changelog": "Mira las actualizaciones recientes y las notas release de la aplicación", + "settings-info-main-edit-settings": "Edita el archivo de settings.json directamente.", + "settings-info-main-editor-settings": "Fuentes, tabuladores, sugerencias y muestreo del editor", + "settings-info-main-formatter": "Elige un formateador para cada lenguaje soportado.", + "settings-info-main-lsp-settings": "Configurar servidores de lenguaje e inteligencia del editor.", + "settings-info-main-plugins": "Administra los plugins instalados y sus funciones disponibles.", + "settings-info-main-preview-settings": "Modo de vista previa, puertos del servidor y comportamiento del navegador.", + "settings-info-main-rateapp": "Califica Acode en Google Play.", + "settings-info-main-remove-ads": "Desbloquea la experiencia sin anuncios de forma permanente.", + "settings-info-main-reset": "Reiniciar Acode a su configuración por defecto.", + "settings-info-main-sponsors": "Apoyar al desarrollo continuo de Acode.", + "settings-info-main-terminal-settings": "Tema de la terminal, fuente, cursor y comportamiento de la sesión.", + "settings-info-main-theme": "Tema de la aplicación, contraste y colores personalizados.", + "settings-info-preview-disable-cache": "Siempre recargar el contenido en el navegador de la aplicación.", + "settings-info-preview-host": "Nombre de host usado al abrir la URL de la vista previa.", + "settings-info-preview-mode": "Elige dónde se abre la vista previa cuando la inicias.", + "settings-info-preview-preview-port": "Puerto usado por el servidor de vista previa en tiempo real.", + "settings-info-preview-server-port": "Puerto usado por el servidor interno de la aplicación.", + "settings-info-preview-show-console-toggler": "Mostrar el botón de la consola en la vista previa.", + "settings-info-preview-use-current-file": "Usar el archivo actual para iniciar la vista previa.", + "settings-info-terminal-convert-eol": "Convertir los finales de linea al pegar o renderizar la salida de la terminal.", + "settings-note-formatter-settings": "Asigna un formateador a cada lenguaje. Instala plugins de formateado para desbloquear más opciones.", + "settings-note-lsp-settings": "Los servidores de lenguaje agregan funciones de auto completado, diagnósticos, detalles de hover y más. Funcionan mejor con carpetas a las que la terminal de Acode puede acceder directamente, como la carpeta terminal/public. Las ubicaciones SAF, externas y remotas son ignoradas automáticamente ya que podrían usar rutas cacheadas o virtuales; activa la opción de compatibilidad solo si quieres probarlas.", + "search result label singular": "resultado", + "search result label plural": "resultados", + "pin tab": "Fijar ventana", + "unpin tab": "Dejar de fijar ventana", + "pinned tab": "Ventana fijada", + "unpin tab before closing": "Deja de fijar la ventana antes de cerrarla.", + "app font": "Fuente de la Aplicación", "app icon": "Icono de la aplicación", - "app icon changed": "App icon changed", - "app icon change warning": "The app will exit after the app icon is changed.", - "settings-info-app-font-family": "Choose the font used across the app interface.", + "app icon changed": "El icono de la aplicación ha sido cambiado", + "app icon change warning": "La aplicación se cerrará después de cambiar el icono de aplicación.", + "settings-info-app-font-family": "Elige la fuente que se usará en la interfaz de la aplicación.", "settings-info-app-icon": "Elige el icono de la aplicación que se muestra en tu dispositivo.", - "lsp-transport-method-stdio": "STDIO (launch a binary command)", - "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", - "lsp-websocket-url": "WebSocket URL", - "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", - "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", - "lsp-error-websocket-url-required": "WebSocket URL is required", - "lsp-remove-custom-server": "Remove custom server", - "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", - "lsp-custom-server-removed": "Custom server removed", - "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode.", - "unsaved selected tabs warning": "Some selected tabs are not saved. Choose what to do.", - "save selected tabs": "Save selected tabs", - "close selected tabs": "Close selected tabs", - "save selected tabs warning": "Are you sure you want to save and close the selected tabs?", - "close selected tabs warning": "Are you sure you want to close the selected tabs? You will lose the unsaved changes and this action cannot be reversed.", - "close tabs to right": "Close Right", - "close tabs to left": "Close Left", - "close other tabs": "Close Others", - "auto close tags": "Auto close tags", - "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", - "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface.", - "plugin-not-supported": "Plugin not supported", - "plugin-not-supported-info": "The plugin was created for older version of Acode.", - "login-to-view": "Please login to view this...", - "show share button": "Show share button", - "settings-info-editor-show-share-button": "Show share button in selection menu.", + "lsp-transport-method-stdio": "STDIO (ejecutar un binario)", + "lsp-transport-method-websocket": "WebSocket (conectar a una URL ws/wss)", + "lsp-websocket-url": "URL del WebSocket", + "lsp-websocket-server-managed-externally": "Este servidor es manejado de forma externa por medio de WebSocket.", + "lsp-error-websocket-url-invalid": "La URL del WebSocket debe iniciar con: ws:// o wss://", + "lsp-error-websocket-url-required": "La URL del WebSocket es requerida", + "lsp-remove-custom-server": "Remover servidor personalizado", + "lsp-remove-custom-server-confirm": "¿Remover el servidor personalizado \"{server}\"?", + "lsp-custom-server-removed": "Servidor personalizado removido", + "settings-info-lsp-remove-custom-server": "Remover este servidor de lenguaje personalizado de Acode.", + "unsaved selected tabs warning": "Algunas de las pestañas seleccionadas no se han guardado. Elige que hacer", + "save selected tabs": "Guardar Pestañas Seleccionadas", + "close selected tabs": "Cerrar Pestañas Seleccionadas", + "save selected tabs warning": "¿Estás seguro de guardar y cerrar las pestañas seleccionadas?", + "close selected tabs warning": "¿Estás seguro de que quieres cerrar las pestañas seleccionadas? Perderás los cambios no guardados, esto no es reversible.", + "close tabs to right": "Cerrar Ventanas a la Derecha", + "close tabs to left": "Cerrar Ventanas a la Izquierda", + "close other tabs": "Cerrar Otras Pestañas", + "auto close tags": "Etiquetas de cierre automáticas", + "settings-info-editor-auto-close-tags": "Insertar automáticamente las etiquetas de cierre en: HTML, XML, Vue, Angular y PHP.", + "ui zoom": "Zoom de la IU", + "settings-info-app-ui-zoom": "Escalar el texto en la interfaz de Acode.", + "plugin-not-supported": "Plugin no soportado", + "plugin-not-supported-info": "Este plugin fue creado para una versión antigua de Acode.", + "login-to-view": "Por favor inicia sesión para ver esto...", + "show share button": "Mostrar boton \"compartir\"", + "settings-info-editor-show-share-button": "Mostrar el botón de compartir en el menú de selección.", "iap-plugin-purchase-warning": "Los plugins comprados no se sincronizan con tu cuenta de Acode. Usa la misma cuenta de Google en Play Store para restaurar tu compra.", "iap-pro-purchase-warning": "Esta compra no se sincronizará con tu cuenta de Acode. Usa la misma cuenta de Google en Play Store para restaurar tu compra.", "iap-pro-purchase-warning-ios": "This purchase will not be synced to your Acode account. Use Restore purchases with the same Apple Account to restore it.", "confirm-login": "No has iniciado sesión en Acode, ¿iniciar sesión ahora?", - "local word completion": "Local word completion", - "language package completion": "Language package completion", - "settings-info-editor-local-word-completion": "Suggest words from the current file.", - "settings-info-editor-language-completion": "Suggest keywords, snippets, and other completions from CodeMirror language packages.", - "terminal:failsafe": "FailSafe mode", - "terminal:failsafe-info": "Start terminal with system shell", - "auto rename tags": "Auto rename tags", - "settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags.", - "copying items": "Copying {count} items...", - "cannot paste folder into itself": "Cannot paste a folder into itself", - "folder already exists": "Folder already exists", - "file already exists force named": "'{name}' already exists. Overwrite?", - "lsp-allow-non-terminal-workspace": "Run LSP on external folders", - "settings-info-lsp-allow-non-terminal-workspace": "Allow language servers for SAF, external, and remote locations. LSP runs in Acode's built-in terminal, which cannot access those locations, so features may not work correctly. As it will connect to a virtual path or cache location.", - "don't ask again": "Don't ask again", - "scrollbar height": "Scrollbar height", - "full": "Full", - "scroll past end": "Scroll past end", - "settings-info-scroll-past-end": "Adds extra empty space at the bottom of the editor, allowing you to scroll the last line to the top.", + "local word completion": "Completado con palabras locales", + "language package completion": "Completado de paquetes de lenguaje", + "settings-info-editor-local-word-completion": "Sugerir palabras presentes en el documento actual.", + "settings-info-editor-language-completion": "Sugerir palabras clave, snippets y otras opciones de los paquetes de lenguaje de CodeMirror.", + "terminal:failsafe": "Modo Seguro", + "terminal:failsafe-info": "Iniciar la terminal con el shell del sistema", + "auto rename tags": "Auto renombrar etiquetas", + "settings-info-editor-auto-rename-tags": "Renombrar la etiqueta de apertura o cierre correspondiente al renombrar etiquetas de estilo HTML.", + "copying items": "Copiando {count} objetos...", + "cannot paste folder into itself": "No puedes pegar una carpeta dentro de ella misma.", + "folder already exists": "Esta carpeta ya existe", + "file already exists force named": "'{name}' ya existe, ¿deseas reemplazarlo?", + "lsp-allow-non-terminal-workspace": "Correr LSPs en carpetas externas", + "settings-info-lsp-allow-non-terminal-workspace": "Permitir servidores de lenguaje para ubicaciones SAF, externas y remotas. El LSP se ejecuta en la terminal integrada de Acode, la cual no puede acceder a esas ubicaciones, por lo que algunas funciones podrían no funcionar correctamente, ya que se conectará a una ruta virtual o a una ubicación en caché.", + "don't ask again": "No volver a preguntar", + "scrollbar height": "Altura de la barra de desplazamiento", + "full": "Completo", + "scroll past end": "Desplazar más allá de la última línea.", + "settings-info-scroll-past-end": "Agrega espacio vacío extra al fondo del editor, permitiendo desplazar la última línea hasta arriba.", "compact": "Compacto", "off": "Apagado", "quick tools height": "Altura de herramientas rápidas", "quick tools toggler": "Alternador de herramientas rápidas", - "running processes": "Running processes", - "recommend extensions": "Recommend extensions", - "settings-info-editor-recommend-extensions": "Suggest plugins when a file opens as plain text because no language mode is available.", - "extension recommendation title": "Extensions available for {extension}", - "extension recommendation message": "No syntax mode is installed for {extension}. Search plugins for \"{keyword}\".", - "extension request title": "No extension found for {extension}", - "extension request message": "Ask for a plugin so Acode can highlight this file type.", - "search plugins": "Search plugins", - "request plugin": "Request plugin", - "terminal:block": "Block", - "terminal:underline": "Underline", - "terminal:bar": "Bar", - "terminal:inactive block": "Block", - "terminal:inactive underline": "Underline", - "terminal:inactive bar": "Bar", - "terminal:inactive none": "None", + "running processes": "Procesos en Ejecución", + "recommend extensions": "Recomendar Extensiones.", + "settings-info-editor-recommend-extensions": "Sugerir extensiones cuando un archivo se abre como texto plano porque no hay un resaltador de texto disponible.", + "extension recommendation title": "Hay extensiones disponibles para {extension}.", + "extension recommendation message": "No hay un resaltador de sintaxis instalado para {extension}. Buscar plugins para \"{keyword}\".", + "extension request title": "No se encontró una extensión para: {extension}", + "extension request message": "Solicita un plugin para que acode pueda colorear este tipo de archivo.", + "search plugins": "Buscar Plugins", + "request plugin": "Solicitar Plugin", + "terminal:block": "Bloque", + "terminal:underline": "Subrayado", + "terminal:bar": "Barra", + "terminal:inactive block": "Bloque", + "terminal:inactive underline": "Subrayado", + "terminal:inactive bar": "Barra", + "terminal:inactive none": "Ninguno", "terminal:normal": "Normal", - "terminal:bold": "Bold", + "terminal:bold": "Negrita", "quicktools-trigger:click": "Click", - "quicktools-trigger:touch": "Touch", - "settings-info-terminal-uninstall": "Are you sure you want to uninstall the terminal?", - "terminal:inactive outline": "Outline", - "restored successfully": "Terminal restored successfully", - "uninstalled successfully": "Terminal uninstalled successfully", - "enter plugin source": "Enter plugin source", - "enter keywords": "Enter keywords", - "enter author name": "Enter author name", - "update available": "Update Available", - "update available info": "Acode {version} is now available! Click here to check out.", - "plugin updates": "Plugin Updates", - "plugin updates singular": "1 plugin has a new version available.", - "plugin updates plural": "{count} plugins have new versions available.", - "just now": "Just now", - "day singular": "{count} day ago", - "hour singular": "{count} hour ago", - "min singular": "{count} minute ago", - "day plural": "{count} days ago", - "hour plural": "{count} hours ago", - "min plural": "{count} minutes ago", - "settings-category-builtin-servers": "Built-in servers", - "settings-category-plugin-servers": "Plugin servers", - "new name": "New name", - "enter new name": "Enter new name", - "code actions": "Code Actions", - "active processes": "Active Processes", - "total memory": "Total Memory", - "uptime": "Uptime", - "note": "Note", - "acode main process": "Acode main process", - "ppid parent": "PPID (Parent)", - "unknown": "Unknown", - "just started": "Just Started", - "diff days": "{days}d {hr}h ago", - "diff hr": "{hr}h {min}m ago", - "diff min": "{min}m {sec}s ago", - "diff sec": "{sec}s ago", - "command": "Command", - "managed": "Managed", - "acode service": "Acode Service", - "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar", - "apply exclusions when copying": "Apply exclusions when copying", - "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations.", - "horizontal scroll margin": "Horizontal scroll margin", - "settings-info-horizontal-scroll-margin": "Adds empty space after the longest line when word wrap is off. Enter any non-negative whole-pixel value.", + "quicktools-trigger:touch": "Toque", + "settings-info-terminal-uninstall": "¿Estás seguro de desinstalar la terminal?", + "terminal:inactive outline": "Contorno", + "restored successfully": "La terminal se restauró correctamente.", + "uninstalled successfully": "La terminal se desinstaló correctamente.", + "enter plugin source": "Ingresa el origen del plugin", + "enter keywords": "Ingresa palabras clave", + "enter author name": "Ingresa nombre del autor", + "update available": "Actualización Disponible", + "update available info": "¡Acode {version} ya está disponible! Haz click aquí para echar un vistazo.", + "plugin updates": "Actualizaciones de Plugins", + "plugin updates singular": "1 plugin tiene una nueva version disponible.", + "plugin updates plural": "{count} plugins tienen nuevas versiones disponibles.", + "just now": "Justo ahora", + "day singular": "hace {count} día", + "hour singular": "hace {count} hora", + "min singular": "hace {count} minuto", + "day plural": "hace {count} días", + "hour plural": "hace {count} horas", + "min plural": "hace {count} minutos", + "settings-category-builtin-servers": "Servidores nativos", + "settings-category-plugin-servers": "Servidores de plugins", + "new name": "Nuevo nombre", + "enter new name": "Ingresa el nuevo nombre", + "code actions": "Acciones de Código", + "active processes": "Procesos Activos", + "total memory": "Memoria Total", + "uptime": "Tiempo ejecutándose", + "note": "Nota", + "acode main process": "Proceso principal de Acode", + "ppid parent": "PPID (Padre)", + "unknown": "Desconocido", + "just started": "Recien Iniciado", + "diff days": "hace {days}d {hr}h", + "diff hr": "hace {hr}h {min}m", + "diff min": "hace {min}m {sec}s", + "diff sec": "hace {sec}s", + "command": "Comando", + "managed": "Administrado por Acode", + "acode service": "Servicio de Acode", + "info-terminal-show-scrollbar": "Mostrar la barra de desplazamiento xterm al lado de la terminal.", + "terminal:show scrollbar": "Mostrar Barra de Desplazamiento", + "apply exclusions when copying": "Aplicar exclusiones al copiar.", + "settings-info-app-use-file-operation-exclusions": "Ignora los archivos y carpetas que coincidan con los patrones de exclusión al realizar operaciones de copiar y pegar.", + "horizontal scroll margin": "Margen de desplazamiento horizontal", + "settings-info-horizontal-scroll-margin": "Agrega espacio vacío después de la linea más larga cuando el ajuste de texto está desactivado. Inserta cualquier valor no negativo (pixeles).", "confirm app icon reward": "¿Ver un anuncio con recompensa para aplicar este icono de la aplicación?", "rewarded ad unavailable": "Los anuncios con recompensa no están disponibles en este momento. Inténtalo de nuevo.", "rewarded ad failed": "No se pudo completar el anuncio. Inténtalo de nuevo.", @@ -880,14 +880,38 @@ "plugin-review:recommended": "Recomendó este complemento", "plugin-review:error-max-length": "La reseña debe tener {max} caracteres o menos.", "plugin-review:error-downvote-comment": "Dile al desarrollador qué podría mejorar.", - "plugin-review:error-empty": "Agrega un comentario o elige Me gusta.", - "plugin-review:unrated": "Sin calificar", - "wrapped line indent": "Word wrap indentation", - "wrap-indent-same": "Same", - "wrap-indent-indent": "Indent (+1 level)", - "wrap-indent-deep": "Deep indent (+2 levels)", - "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", - "icon pack": "Icon pack", - "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "plugin-review:error-empty": "Agrega un comentario o elige \"Me gusta\".", + "plugin-review:unrated": "Sin calificación.", + "wrapped line indent": "Indentación del Ajuste de Texto", + "wrap-indent-same": "Igualar", + "wrap-indent-indent": "Indentar (+1 nivel)", + "wrap-indent-deep": "Indentar profundo (+2 niveles)", + "settings-info-editor-wrapping-indent": "Elige si las líneas ajustadas comienzan desde el borde izquierdo, mantienen la indentación de la línea original o se indentan más. Cada nivel extra usa el tamaño del tabulador.", + "icon pack": "Paquete de Íconos", + "settings-info-icon-pack": "Elige el paquete de iconos usado para archivos y carpetas en el explorador y listas de archivos en la aplicación.", + "unavailable": "no disponible", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/fr-fr.json b/src/lang/fr-fr.json index 377963a..a55d850 100644 --- a/src/lang/fr-fr.json +++ b/src/lang/fr-fr.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/he-il.json b/src/lang/he-il.json index ee5597b..a29685f 100644 --- a/src/lang/he-il.json +++ b/src/lang/he-il.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/hi-in.json b/src/lang/hi-in.json index ec319db..54bc438 100644 --- a/src/lang/hi-in.json +++ b/src/lang/hi-in.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/hu-hu.json b/src/lang/hu-hu.json index b8096cd..4e45d6f 100644 --- a/src/lang/hu-hu.json +++ b/src/lang/hu-hu.json @@ -90,7 +90,7 @@ "theme": "Téma", "title-listfiles": "Fájlok listázása", "unable to delete file": "Nem lehet törölni a fájlt", - "document plugin required": "Telepítse vagy engedélyezze a Docs Workspace bővítményt a Bővítmények között, majd nyissa meg újra a dokumentumot.", + "document plugin required": "Telepítse vagy engedélyezze a Docs Workspace bővítményt a bővítmények közül, majd nyissa meg újra a dokumentumot.", "shared files unavailable": "Ezeket a fájlokat nem sikerült megnyitni. Lehet, hogy nem támogatottak, vagy már nem érhetők el. Próbálja meg újra megosztani őket.", "invalid shared file": "Érvénytelen megosztott fájl", "unable to open file": "Nem lehet megnyitni a fájlt", @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Válassza ki, hogy a tördelt szövegek a képernyő bal szélétől kezdődjenek-e, az eredeti sor behúzását kövessék-e, vagy még jobban legyenek-e behúzva. Minden további behúzási szint a tabulátor méretét veszi alapul.", "icon pack": "Ikoncsomag", "settings-info-icon-pack": "Válassza ki a fájlokhoz és a mappákhoz használandó ikonokat a fájlkezelőben és az alkalmazás egészében megjelenő fájllistákban.", - "unavailable": "nem érhető el" + "unavailable": "nem érhető el", + "editor renderer": "Megjelenítő", + "editor renderer dom": "DOM (alapértelmezett)", + "editor renderer canvas": "Canvas (kísérleti)", + "settings-info-editor-renderer": "A Canvas a GPU segítségével rajzolja ki a kódot a nagy fájlok gördülékenyebb görgetéséhez. Kísérleti funkció: előfordulhat, hogy egyes bővítmények nem működnek vele. Újratöltés után lép érvénybe.", + "canvas backend": "Canvas háttérprogram", + "canvas backend auto": "Automatikus (WebGL2)", + "settings-info-canvas-backend": "A GPU API, amellyel a Canvas megjelenítő rajzol. A WebGPU ott, ahol nem érhető el, WebGL2-re vált vissza. Újratöltés után lép érvénybe.", + "sticky scroll": "Rögzített görgetés", + "settings-info-sticky-scroll": "Görgetés közben a felső sor körüli függvények, osztályok és blokkok első sorait a képernyő tetejére rögzíti.", + "sticky scroll max lines": "Rögzített görgetés sorai", + "settings-info-sticky-scroll-max-lines": "A rögzített görgetés által a tetején tartott fejlécsorok maximális száma (1–10).", + "gutter markers": "Margójelölők", + "gutter markers shared": "Egy közös oszlop", + "gutter markers columns": "Külön oszlop margónként", + "gutter markers overlay": "Sorszámok felett", + "settings-info-gutter-markers": "Hova kerüljenek a szintaxisellenőrző és egyéb margójelölők: egy közös oszlopba, külön oszlopba mindegyiknek, vagy a sorszámok fölé a helytakarékosság érdekében.", + "cursor blink rate": "Kurzor villogási sebessége", + "settings-info-cursor-blink-rate": "Egy kurzorvillanás időtartama ezredmásodpercben. A 0 értéknél a kurzor nem villog.", + "settings-info-scroll-speed": "A görgetés mértéke egy lendítés vagy az egérkerék elfordításakor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/id-id.json b/src/lang/id-id.json index 3d085b8..852769a 100644 --- a/src/lang/id-id.json +++ b/src/lang/id-id.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Pilih apakah teks terbungkus dimulai dari tepi kiri, mengikuti indentasi baris asli, atau di-indentasi lebih jauh. Setiap level tambahan menggunakan ukuran tab Anda.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/index.d.ts b/src/lang/index.d.ts index 0a579b1..4d8e3e0 100644 --- a/src/lang/index.d.ts +++ b/src/lang/index.d.ts @@ -893,6 +893,30 @@ declare type LangStrings = { "plugin-review:error-downvote-comment": string; "plugin-review:error-empty": string; "plugin-review:unrated": string; + "editor renderer": string; + "editor renderer dom": string; + "editor renderer canvas": string; + "settings-info-editor-renderer": string; + "canvas backend": string; + "canvas backend auto": string; + "settings-info-canvas-backend": string; + "sticky scroll": string; + "settings-info-sticky-scroll": string; + "sticky scroll max lines": string; + "settings-info-sticky-scroll-max-lines": string; + "gutter markers": string; + "gutter markers shared": string; + "gutter markers columns": string; + "gutter markers overlay": string; + "settings-info-gutter-markers": string; + "cursor blink rate": string; + "settings-info-cursor-blink-rate": string; + "settings-info-scroll-speed": string; + "clear all": string; + "error-permission-revoked": string; + "error-not-found": string; + "error-no-space": string; + "error-network": string; }; declare var strings: LangStrings; diff --git a/src/lang/ir-fa.json b/src/lang/ir-fa.json index cf6caba..b7f8539 100644 --- a/src/lang/ir-fa.json +++ b/src/lang/ir-fa.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/it-it.json b/src/lang/it-it.json index a3c9c41..dc8b33b 100644 --- a/src/lang/it-it.json +++ b/src/lang/it-it.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/ja-jp.json b/src/lang/ja-jp.json index 593943f..ee31f34 100644 --- a/src/lang/ja-jp.json +++ b/src/lang/ja-jp.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/ko-kr.json b/src/lang/ko-kr.json index 132fa7e..9c1dec6 100644 --- a/src/lang/ko-kr.json +++ b/src/lang/ko-kr.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/ln-ln.json b/src/lang/ln-ln.json index 9f3a6d7..6f94ac1 100644 --- a/src/lang/ln-ln.json +++ b/src/lang/ln-ln.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/ml-in.json b/src/lang/ml-in.json index 434dc87..6d8d02f 100644 --- a/src/lang/ml-in.json +++ b/src/lang/ml-in.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/mm-unicode.json b/src/lang/mm-unicode.json index f96f7e3..8d6f594 100644 --- a/src/lang/mm-unicode.json +++ b/src/lang/mm-unicode.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/mm-zawgyi.json b/src/lang/mm-zawgyi.json index 1f24bc3..480f63d 100644 --- a/src/lang/mm-zawgyi.json +++ b/src/lang/mm-zawgyi.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/pl-pl.json b/src/lang/pl-pl.json index 067e0d6..8ca6ca5 100644 --- a/src/lang/pl-pl.json +++ b/src/lang/pl-pl.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/pt-br.json b/src/lang/pt-br.json index db188e1..7924442 100644 --- a/src/lang/pt-br.json +++ b/src/lang/pt-br.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/pu-in.json b/src/lang/pu-in.json index 88ca3df..0a1c863 100644 --- a/src/lang/pu-in.json +++ b/src/lang/pu-in.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/ru-ru.json b/src/lang/ru-ru.json index 7919c17..6634f88 100644 --- a/src/lang/ru-ru.json +++ b/src/lang/ru-ru.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/tl-ph.json b/src/lang/tl-ph.json index 54c3d05..bb19633 100644 --- a/src/lang/tl-ph.json +++ b/src/lang/tl-ph.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/tr-tr.json b/src/lang/tr-tr.json index 393368e..85d1162 100644 --- a/src/lang/tr-tr.json +++ b/src/lang/tr-tr.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/uk-ua.json b/src/lang/uk-ua.json index 906b67e..97750a8 100644 --- a/src/lang/uk-ua.json +++ b/src/lang/uk-ua.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/uz-uz.json b/src/lang/uz-uz.json index e4ca618..baecf5f 100644 --- a/src/lang/uz-uz.json +++ b/src/lang/uz-uz.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/vi-vn.json b/src/lang/vi-vn.json index 3480d12..34f495a 100644 --- a/src/lang/vi-vn.json +++ b/src/lang/vi-vn.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index e9a07d8..0830cbb 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "选择折行后的文本是从行首靠左对齐、保持原行的缩进,还是进一步向内缩进。每增加一级缩进将遵循您设定的制表符宽度。", "icon pack": "图标包", "settings-info-icon-pack": "选择资源管理器及整个应用文件列表中文件和文件夹所使用的图标。", - "unavailable": "不可用" + "unavailable": "不可用", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/zh-hant.json b/src/lang/zh-hant.json index 6a0bf4f..870872d 100644 --- a/src/lang/zh-hant.json +++ b/src/lang/zh-hant.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "選擇換行後的文字是靠左對齊、保持原行的縮排,還是進一步向內縮排。每增加一級縮排將遵循您設定的製表符寬度。", "icon pack": "圖標包", "settings-info-icon-pack": "選擇檔案總管及整個應用程式檔案列表中檔案和文件夾所使用的圖標。", - "unavailable": "不可用" + "unavailable": "不可用", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 43aa00d..dac9fd8 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -889,5 +889,29 @@ "settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.", "icon pack": "Icon pack", "settings-info-icon-pack": "Choose the icons used for files and folders in the explorer and file lists across the app.", - "unavailable": "unavailable" + "unavailable": "unavailable", + "editor renderer": "Renderer", + "editor renderer dom": "DOM (default)", + "editor renderer canvas": "Canvas (experimental)", + "settings-info-editor-renderer": "Canvas draws code with the GPU for smoother scrolling in large files. It is experimental: some plugins may not work with it. Applies after a reload.", + "canvas backend": "Canvas backend", + "canvas backend auto": "Auto (WebGL2)", + "settings-info-canvas-backend": "The GPU API the canvas renderer draws with. WebGPU falls back to WebGL2 where it isn't available. Applies after a reload.", + "sticky scroll": "Sticky scroll", + "settings-info-sticky-scroll": "Keep the first lines of the functions, classes and blocks around the top line pinned while scrolling.", + "sticky scroll max lines": "Sticky scroll lines", + "settings-info-sticky-scroll-max-lines": "The most header lines sticky scroll keeps pinned at the top (1–10).", + "gutter markers": "Gutter markers", + "gutter markers shared": "One column", + "gutter markers columns": "Column per gutter", + "gutter markers overlay": "Over line numbers", + "settings-info-gutter-markers": "Where lint and other gutter markers go: one shared column, a column for each gutter, or over the line numbers to save width.", + "cursor blink rate": "Cursor blink rate", + "settings-info-cursor-blink-rate": "How long one cursor blink takes, in milliseconds. 0 keeps the cursor steady.", + "settings-info-scroll-speed": "How far a fling or a mouse wheel turn scrolls the editor.", + "clear all": "Clear all", + "error-permission-revoked": "Acode can no longer access this location. Open the folder again to grant access.", + "error-not-found": "This file or folder no longer exists. It may have been moved, renamed or deleted.", + "error-no-space": "There isn't enough storage space left on this device.", + "error-network": "Couldn't reach the server. Check your connection and try again." } diff --git a/src/lib/acode.js b/src/lib/acode.js index 3a9b114..2beac4b 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -22,6 +22,7 @@ import { getLoadedLspClient, setLspOptions } from "cm/lsp/clientLoader"; import { registerLspFormatter } from "cm/lsp/formatter"; import { addMode, + getFormatAsMode, getModeForPath, getModes, getModesByName, @@ -282,9 +283,10 @@ class Acode { // Preferred CodeMirror language registration API for plugins const editorLanguages = { // name: string, extensions: string|Array, caption?: string, - // loader?: () => Extension | Promise - register: (name, extensions, caption, loader) => - addMode(name, extensions, caption, loader), + // loader?: () => Extension | Promise, + // options?: { aliases?, filenameMatchers?, formatAs? } + register: (name, extensions, caption, loader, options) => + addMode(name, extensions, caption, loader, options), unregister: (name) => removeMode(name), add: (name, extensions, caption, loader) => addMode(name, extensions, caption, loader), @@ -859,7 +861,9 @@ class Acode { } const modeName = resolvedMode || "text"; const formatterMap = appSettings.value.formatter || {}; - const formatterId = formatterMap[modeName]; + const formatAs = getFormatAsMode(modeName); + const formatterId = + formatterMap[modeName] ?? (formatAs && formatterMap[formatAs]); const formatter = this.#formatter.find(({ id }) => id === formatterId); if (!formatter) { diff --git a/src/lib/actionStack.js b/src/lib/actionStack.js index e5d2ae6..62ba740 100644 --- a/src/lib/actionStack.js +++ b/src/lib/actionStack.js @@ -81,7 +81,7 @@ export default { if (appSettings.value.confirmOnExit) { let closeMessage = acode.exitAppMessage || strings["close app"].capitalize(0); - confirmation = await confirm(strings.warning.toUpperCase(), closeMessage); + confirmation = await confirm(strings.warning, closeMessage); } if (confirmation) { diff --git a/src/lib/checkFiles.js b/src/lib/checkFiles.js index 93751f1..584ac11 100644 --- a/src/lib/checkFiles.js +++ b/src/lib/checkFiles.js @@ -87,7 +87,7 @@ export default async function checkFiles() { if (!alreadyWarnedConflict) { await new Promise((resolve) => { alert( - strings.warning.toUpperCase(), + strings.warning, `${file.filename} changed on disk while you have unsaved edits. Saving now may overwrite the external changes.`, resolve, ); @@ -108,7 +108,7 @@ export default async function checkFiles() { if (!currentDoc?.eq?.(diskDoc)) { try { const confirmation = await confirm( - strings.warning.toUpperCase(), + strings.warning, file.filename + strings["file changed"], ); diff --git a/src/lib/commands.js b/src/lib/commands.js index a00d055..55a3997 100644 --- a/src/lib/commands.js +++ b/src/lib/commands.js @@ -331,9 +331,7 @@ const commands = { }, async files() { const FileBrowser = await loadFileBrowser(); - FileBrowser("both", strings["file browser"]) - .then(FileBrowser.open) - .catch(FileBrowser.openError); + FileBrowser("both").then(FileBrowser.open).catch(FileBrowser.openError); }, find() { actions("search"); @@ -678,7 +676,7 @@ const commands = { file = file || editorManager.activeFile; if (file.SAFMode === "single") { - alert(strings.info.toUpperCase(), strings["unable to rename"]); + alert(strings.info, strings["unable to rename"]); return; } diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index 1851543..f40d8f6 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -1382,7 +1382,7 @@ export default class EditorFile { } if (!force && isUnsaved) { const confirmation = await confirm( - strings.warning.toUpperCase(), + strings.warning, strings["unsaved file"], ); if (!confirmation) return false; diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index d771790..d9f63db 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -21,6 +21,14 @@ import { placeholder, } from "@codemirror/view"; import createBaseExtensions from "cm/baseExtensions"; +import { + getCanvasBackend, + getCanvasDisplayOptions, + getCanvasFont, + getCanvasTheme, + markCanvasViewSynced, + syncCanvasView, +} from "cm/canvasOptions"; import { setKeyBindings as applyKeyBindings, executeCommand, @@ -30,6 +38,10 @@ import { registerExternalCommand, removeExternalCommand, } from "cm/commandRegistry"; +import createEditorView, { + isCanvasRendererSupported, + isCanvasView, +} from "cm/createEditorView"; import { blurEditorIfReadOnly, createEditorReadOnlyExtension, @@ -69,7 +81,9 @@ import { getModesByName, initModes, } from "cm/modelist"; -import createTouchSelectionMenu from "cm/touchSelectionMenu"; +import createTouchSelectionMenu, { + createCanvasSelectionMenu, +} from "cm/touchSelectionMenu"; import "cm/supportedModes"; import { onProviderRegistered } from "fileSystem"; import { autocompletion } from "@codemirror/autocomplete"; @@ -963,9 +977,20 @@ async function EditorManager($header, $body) { const languageCompartment = new Compartment(); // Compartment for LSP extensions so we can swap per file const lspCompartment = new Compartment(); + // The renderer is chosen at startup ("Renderer" in editor settings applies + // on reload). The canvas renderer draws line numbers, folding, guides, + // whitespace, wrapping and scroll room itself, so the option specs below + // leave those extensions out and canvas views get options instead. + let canvasRenderer = appSettings.value.editorRenderer === "canvas"; + if (canvasRenderer && !isCanvasRendererSupported()) { + canvasRenderer = false; + console.warn("Canvas renderer needs WebGL2; using the DOM editor."); + } const diagnosticsClientExt = lspDiagnosticsClientExtension(); const buildDiagnosticsUiExt = () => - lspDiagnosticsUiExtension(appSettings?.value?.lintGutter !== false); + lspDiagnosticsUiExtension(appSettings?.value?.lintGutter !== false, { + canvas: canvasRenderer, + }); const UNTITLED_URI_PREFIX = "untitled://acode/"; function getEditorFontFamily() { @@ -1014,10 +1039,13 @@ async function EditorManager($header, $body) { function makeFontTheme() { const fontSize = appSettings?.value?.fontSize || "12px"; - const lineHeight = appSettings?.value?.lineHeight || 1.6; + const lineHeight = appSettings?.value?.lineHeight || 1.4; const fontFamily = getEditorFontFamily(); return EditorView.theme({ - "&": { fontSize, lineHeight: String(lineHeight) }, + "&": { fontSize }, + // On the scroller, where CodeMirror's base theme sets 1.4: a line + // height on the editor itself never reached the lines or gutters. + ".cm-scroller": { lineHeight: String(lineHeight) }, ".cm-content": { fontFamily }, ".cm-gutter": { fontFamily }, ".cm-tooltip, .cm-tooltip *": { fontFamily }, @@ -1104,12 +1132,21 @@ async function EditorManager($header, $body) { function getBaseExtensionOptions() { const values = appSettings?.value || {}; - return Object.fromEntries( - Object.entries(baseExtensionDefaults).map(([key, defaultValue]) => [ - key, - values[key] ?? defaultValue, - ]), - ); + return { + ...Object.fromEntries( + Object.entries(baseExtensionDefaults).map(([key, defaultValue]) => [ + key, + values[key] ?? defaultValue, + ]), + ), + // Canvas views draw fold markers themselves; CodeMirror's fold + // gutter would only rebuild its (unused) markers on every update. + foldGutter: !canvasRenderer, + canvas: canvasRenderer, + // Off by default for the DOM editor (its cost per selection + // change); canvas views draw the matches cheaply. + ...(canvasRenderer ? { highlightSelectionMatches: true } : {}), + }; } function createConfiguredBaseExtensions() { @@ -1118,9 +1155,12 @@ async function EditorManager($header, $body) { function getBaseExtensionSignature() { const options = getBaseExtensionOptions(); - return JSON.stringify( - baseExtensionSettings.map((key) => [key, options[key]]), - ); + // The renderer is part of it: states built for canvas views leave out + // what the canvas draws, so a DOM fallback must not reuse them. + return JSON.stringify([ + ...baseExtensionSettings.map((key) => [key, options[key]]), + ["canvas", options.canvas], + ]); } function makeRainbowBracketExtension() { @@ -1165,6 +1205,7 @@ async function EditorManager($header, $body) { keys: ["linenumbers", "relativeLineNumbers"], compartments: [lineNumberCompartment], build() { + if (canvasRenderer) return []; return makeLineNumberExtension(); }, }, @@ -1172,6 +1213,7 @@ async function EditorManager($header, $body) { keys: ["rainbowBrackets"], compartments: [rainbowCompartment], build() { + if (canvasRenderer) return []; return makeRainbowBracketExtension(); }, }, @@ -1180,7 +1222,7 @@ async function EditorManager($header, $body) { compartments: [indentGuidesCompartment], build() { const enabled = appSettings?.value?.indentGuides ?? false; - if (!enabled) return []; + if (!enabled || canvasRenderer) return []; return indentGuides({ highlightActiveGuide: false, hideOnBlankLines: false, @@ -1205,6 +1247,7 @@ async function EditorManager($header, $body) { keys: ["textWrap", "wrappingIndent"], compartments: [wrapCompartment], build() { + if (canvasRenderer) return []; return makeWrapExtension(); }, }, @@ -1220,6 +1263,7 @@ async function EditorManager($header, $body) { keys: ["rtlText"], compartments: [rtlCompartment], build() { + if (canvasRenderer) return []; const rtl = !!appSettings?.value?.rtlText; return EditorView.theme({ "&": { direction: rtl ? "rtl" : "ltr" }, @@ -1231,7 +1275,7 @@ async function EditorManager($header, $body) { compartments: [whitespaceCompartment], build() { const show = !!appSettings?.value?.showSpaces; - return show + return show && !canvasRenderer ? [ highlightWhitespace(), highlightTrailingWhitespace(), @@ -1245,7 +1289,7 @@ async function EditorManager($header, $body) { compartments: [lineBreakMarkerCompartment], build() { const showSpaces = !!appSettings?.value?.showSpaces; - return showSpaces ? lineBreakMarker : []; + return showSpaces && !canvasRenderer ? lineBreakMarker : []; }, }, { @@ -1253,7 +1297,7 @@ async function EditorManager($header, $body) { compartments: [foldThemeCompartment], build() { const fade = !!appSettings?.value?.fadeFoldWidgets; - if (!fade) return []; + if (!fade || canvasRenderer) return []; return EditorView.theme({ ".cm-gutter.cm-foldGutter .cm-gutterElement": { opacity: 0, @@ -1297,7 +1341,7 @@ async function EditorManager($header, $body) { compartments: [scrollPastEndCompartment], build() { const value = appSettings?.value?.scrollPastEnd || "medium"; - if (value === "none") { + if (value === "none" || canvasRenderer) { return []; } const factorMap = { @@ -1412,9 +1456,109 @@ async function EditorManager($header, $body) { } target.dispatch({ effects }); } + if (isCanvasView(target)) syncCanvasOptions(target); } } + function getCanvasDisplayOptionsFromSettings() { + return getCanvasDisplayOptions(appSettings.value, { + themeId: appSettings.value.editorTheme || "one_dark", + onLineNumberClick: handleCanvasLineNumberClick, + }); + } + + /** Apply the current settings to a canvas view (only what changed). */ + function syncCanvasOptions(target) { + syncCanvasView(target, { + display: getCanvasDisplayOptionsFromSettings(), + font: getCanvasFont(appSettings.value, getEditorFontFamily()), + }); + } + + function syncAllCanvasViews() { + for (const pane of panes) { + if (isCanvasView(pane.editor)) syncCanvasOptions(pane.editor); + } + } + + /** + * Taps on a canvas view's line numbers select the line, like the DOM + * editor's line number gutter. Its fold markers sit in a narrow column + * right of the numbers: taps there fall through to the fold toggle. + */ + function handleCanvasLineNumberClick(line, event, view) { + const rect = view.scrollDOM.getBoundingClientRect(); + const foldColumn = view.gutterWidth - view.cellWidth * 1.5; + if (event.clientX - rect.left >= foldColumn) return false; + const docLine = view.state.doc.line(line); + return handleLineNumberClick(view, view.lineBlockAt(docLine.from), event, { + shiftClickSelection: appSettings.value.shiftClickSelection !== false, + }); + } + + /** + * Create a pane's editor view with the configured renderer. Canvas views + * get the settings as options, and drive the selection menu through their + * touch selection hooks. + */ + function createPaneEditorView(pane, state, parent) { + if (!canvasRenderer) return createEditorView({ state, parent }); + const themeId = appSettings.value.editorTheme || "one_dark"; + const display = getCanvasDisplayOptionsFromSettings(); + const font = getCanvasFont(appSettings.value, getEditorFontFamily()); + try { + const view = createEditorView({ + state, + parent, + canvas: { + ...display, + font, + theme: getCanvasTheme(themeId), + backend: getCanvasBackend(appSettings.value), + input: appSettings.value.useTextareaForIME ? "textarea" : "auto", + touchSelection: { + onMenu: (event) => + pane.touchSelectionController?.handleCanvasMenu?.(event), + onHaptic() { + if (appSettings.value.vibrateOnTap) { + navigator.vibrate?.(config.VIBRATION_TIME); + } + }, + }, + }, + }); + markCanvasViewSynced(view, { display, font, themeId }); + return view; + } catch (error) { + // Use the DOM editor for the rest of the session, with the + // extensions the canvas would have drawn. + canvasRenderer = false; + for (const leftover of parent.querySelectorAll(":scope > .cv-editor")) { + leftover.remove(); + } + warnRecoverable( + "Canvas renderer failed to start; using the DOM editor.", + error, + "canvas-renderer", + ); + // Panes start with an empty state; rebuild it with the DOM + // editor's extensions. Files' states follow on their next show, + // since the renderer is in their extension signature. + const view = createEditorView({ + state: createEmptyEditorState(), + parent, + }); + applyOptions(null, view); + return view; + } + } + + function createPaneTouchSelectionMenu(view, options) { + return isCanvasView(view) + ? createCanvasSelectionMenu(view, options) + : createTouchSelectionMenu(view, options); + } + function setThemeForEditors(themeId) { panes.forEach((pane) => { if (pane.editor) { @@ -1730,7 +1874,7 @@ async function EditorManager($header, $body) { searchExtension: search(), // Ensure read-only can be toggled later via compartment readOnlyExtension: readOnlyCompartment.of( - createEditorReadOnlyExtension(false), + createEditorReadOnlyExtension(false, { canvas: canvasRenderer }), ), // Editor options driven by settings via compartments optionExtensions: getBaseExtensionsFromOptions(), @@ -1740,10 +1884,7 @@ async function EditorManager($header, $body) { const editorState = createEmptyEditorState(); - editor = new EditorView({ - state: editorState, - parent: $container, - }); + editor = createPaneEditorView(primaryPane, editorState, $container); editor.__editorPane = primaryPane; primaryPane.editor = editor; activePane = primaryPane; @@ -1789,7 +1930,7 @@ async function EditorManager($header, $body) { }, }); - touchSelectionController = createTouchSelectionMenu(editor, { + touchSelectionController = createPaneTouchSelectionMenu(editor, { container: $container, getActiveFile: () => manager?.activeFile || null, isShiftSelectionActive, @@ -1827,6 +1968,9 @@ async function EditorManager($header, $body) { const id = String(themeId || ""); const ext = getThemeExtensions(id, [oneDark]); targetEditor.dispatch({ effects: themeCompartment.reconfigure(ext) }); + if (isCanvasView(targetEditor)) { + syncCanvasView(targetEditor, { themeId: id }); + } return true; } catch (_) { return false; @@ -2462,15 +2606,16 @@ async function EditorManager($header, $body) { } async function createPaneEditor(pane) { - const paneEditor = new EditorView({ - state: createEmptyEditorState(), - parent: pane.editorContainer, - }); + const paneEditor = createPaneEditorView( + pane, + createEmptyEditorState(), + pane.editorContainer, + ); pane.editor = paneEditor; paneEditor.__editorPane = pane; applyEditorCompatibility(paneEditor); await applyKeyBindings(paneEditor); - pane.touchSelectionController = createTouchSelectionMenu(paneEditor, { + pane.touchSelectionController = createPaneTouchSelectionMenu(paneEditor, { container: pane.editorContainer, getActiveFile: () => pane.activeFile || null, isShiftSelectionActive, @@ -2891,7 +3036,9 @@ async function EditorManager($header, $body) { ...getBaseExtensionsFromOptions(), languageCompartment.of([]), lspCompartment.of([]), - readOnlyCompartment.of(createEditorReadOnlyExtension(true)), + readOnlyCompartment.of( + createEditorReadOnlyExtension(true, { canvas: canvasRenderer }), + ), ...(text === undefined ? [placeholder(`Loading ${file.filename || "file"}...`)] : []), @@ -3034,7 +3181,11 @@ async function EditorManager($header, $body) { // Apply read-only state based on file.editable/loading using Compartment try { const ro = !file.editable || !!file.loading; - exts.push(readOnlyCompartment.of(createEditorReadOnlyExtension(ro))); + exts.push( + readOnlyCompartment.of( + createEditorReadOnlyExtension(ro, { canvas: canvasRenderer }), + ), + ); } catch (e) { // safe to ignore; editor will remain editable by default } @@ -3664,7 +3815,9 @@ async function EditorManager($header, $body) { appSettings.on("update:lintGutter", function (value) { setLspOptions({ - diagnosticsUiExtension: lspDiagnosticsUiExtension(value !== false), + diagnosticsUiExtension: lspDiagnosticsUiExtension(value !== false, { + canvas: canvasRenderer, + }), }); const active = manager.activeFile; if (active?.type === "editor") { @@ -3712,6 +3865,15 @@ async function EditorManager($header, $body) { applyOptions(["indentGuides"]); }); + // Options only the canvas renderer applies live; the DOM editor picks up + // highlightActiveLine when the state is recreated. + appSettings.on("update:highlightActiveLine", syncAllCanvasViews); + appSettings.on("update:scrollSpeed", syncAllCanvasViews); + appSettings.on("update:stickyScroll", syncAllCanvasViews); + appSettings.on("update:stickyScrollMaxLines", syncAllCanvasViews); + appSettings.on("update:cursorBlinkRate", syncAllCanvasViews); + appSettings.on("update:canvasGutterMarkers", syncAllCanvasViews); + // Keep file.session and cache in sync on every edit function getDocSyncListener() { return EditorView.updateListener.of((update) => { diff --git a/src/lib/notificationManager.js b/src/lib/notificationManager.js index 6ff6036..0ec79be 100644 --- a/src/lib/notificationManager.js +++ b/src/lib/notificationManager.js @@ -262,6 +262,7 @@ class NotificationManager { const index = this.notifications.findIndex((n) => n.id === id); if (index > -1) { this.notifications.splice(index, 1); + if (!this.notifications.length) this.#setUnread(false); this.renderNotifications(); } } @@ -293,6 +294,7 @@ class NotificationManager { } this.renderNotifications(); + if (!this.#isPanelVisible()) this.#setUnread(true); // show toast notification const $toastEl = this.createToastNotification(notification); @@ -372,8 +374,29 @@ class NotificationManager { return date.toLocaleDateString(); } + /** + * Clears the unread dot on the notifications sidebar icon + */ + markAllRead() { + this.#setUnread(false); + } + + #isPanelVisible() { + const $panel = sidebarApps.get("notification"); + return !!($panel?.isConnected && $panel.closest("#sidebar")?.activated); + } + + /** + * @param {boolean} unread + */ + #setUnread(unread) { + // Kept on so it applies even before the sidebar icon exists + document.body.classList.toggle("has-unread-notifications", unread); + } + clearAll() { this.notifications = []; + this.#setUnread(false); this.renderNotifications(); if (this.timeUpdateInterval) { clearInterval(this.timeUpdateInterval); diff --git a/src/lib/openFile.js b/src/lib/openFile.js index 1382da4..d6866fc 100644 --- a/src/lib/openFile.js +++ b/src/lib/openFile.js @@ -460,7 +460,7 @@ export default async function openFile(file, options = {}) { if (options.external) throw Object.assign(new Error("File too large"), { filename: name }); return alert( - strings.error.toUpperCase(), + strings.error, strings["file too large"].replace( "{size}", settings.maxFileSize + "MB", diff --git a/src/lib/openFolder.js b/src/lib/openFolder.js index 6942ef2..b825f69 100644 --- a/src/lib/openFolder.js +++ b/src/lib/openFolder.js @@ -1,5 +1,6 @@ import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; +import fileSelection from "sidebarApps/files/selection"; import collapsableList from "components/collapsableList"; import FileTree from "components/fileTree"; import Sidebar from "components/sidebar"; @@ -11,8 +12,10 @@ import confirm from "dialogs/confirm"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import escapeStringRegexp from "escape-string-regexp"; +import tag from "html-tag-js"; import fileIcons from "lib/fileIcons"; import copyEntry from "utils/copyEntry"; +import { isExcludedFileOperationPath } from "utils/fileOperationExclusions"; import haptic from "utils/haptic"; import helpers from "utils/helpers"; import Path from "utils/Path"; @@ -101,10 +104,15 @@ const convertToProotPath = (url = "") => { */ /** - * @typedef {object} ClipBoard + * @typedef {object} ClipBoardItem * @property {string} url - * @property {HTMLElement} $el - * @property {"cut"|"copy"} action + * @property {"file"|"dir"} type + */ + +/** + * @typedef {object} ClipBoard + * @property {ClipBoardItem[]} items + * @property {"cut"|"copy"|null} action */ /** @@ -123,6 +131,11 @@ const convertToProotPath = (url = "") => { /**@type {Folder[]} */ export const addedFolder = []; +/** + * Shared by every open folder so entries can be pasted across roots. + * @type {ClipBoard} + */ +const clipBoard = { items: [], action: null }; const ACODE_PLUGIN_MANIFEST_FILE = "plugin.json"; /** * Open a folder in the sidebar @@ -150,9 +163,23 @@ function openFolder(_path, opts = {}) { const $root = collapsableList(title, "folder", { allCaps: true, + tail: createRootActions(() => $root), ontoggle: () => { setFolderLeadIcon($root, title, { isRoot: true }); expandList($root); + // Folders also expand without a header tap (saved state, header + // actions, drag hover), so size them here rather than on click only. + // Only on expand: sizing collapses the other folders, and doing it on + // collapse would undo the folder that just opened. + // Imported lazily: the files app reads `strings` when it loads. + if ($root.unclasped) { + import("sidebarApps/files") + .then(({ fixHeight }) => { + // It may have closed again while the module loaded. + if ($root.unclasped) fixHeight($root); + }) + .catch(console.error); + } }, }); setFolderLeadIcon($root, title, { isRoot: true }); @@ -189,7 +216,7 @@ function openFolder(_path, opts = {}) { url: _path, $node: $root, id: opts.id, - clipBoard: {}, + clipBoard, reload() { $root.collapse(); $root.expand(); @@ -237,6 +264,89 @@ function openFolder(_path, opts = {}) { } } +/** + * Quick actions on an open folder's header. With a mouse they appear on + * hover; on touch a single button opens the folder's menu (the same one as a + * long press), so idle headers stay clean. + * @param {() => Collapsible} getRoot + * @returns {HTMLElement} + */ +function createRootActions(getRoot) { + const $actions = tag("span", { className: "root-actions" }); + const buttons = [ + ["new file", "document-add", strings["new file"]], + ["new folder", "folder-add", strings["new folder"]], + ["refresh", "autorenew", strings.refresh], + ["collapse-all", "expand_less", strings["collapse all"] || "Collapse all"], + ["menu", "more_vert more-actions", strings.more], + ]; + + for (const [action, icon, title] of buttons) { + // tile children only receive clicks when they carry data-action. + const $button = tag("span", { className: `icon ${icon}`, title }); + $button.dataset.action = action; + $actions.append($button); + } + + $actions.addEventListener("click", (e) => { + const $button = e.target.closest("[data-action]"); + if (!$button) return; + e.preventDefault(); + e.stopPropagation(); + + const $root = getRoot(); + const { action } = $button.dataset; + if (action === "menu") { + const { url, name } = $root.$title.dataset; + handleContextmenu("root", url, name, $root.$title); + return; + } + + runRootAction($root, action, $button).catch(helpers.error); + }); + + return $actions; +} + +/** + * @param {Collapsible} $root + * @param {string} action + * @param {HTMLElement} [$button] + */ +async function runRootAction($root, action, $button) { + const { url, name } = $root.$title.dataset; + + switch (action) { + case "new file": + case "new folder": + if ($root.collapsed) $root.expand(); + return execOperation("root", action, url, $root.$title, name); + + case "refresh": { + if ($root.collapsed) { + $root.expand(); + return; + } + $button?.classList.add("spinning"); + try { + await getLoadedFileTree($root.$title)?.refresh(); + } finally { + $button?.classList.remove("spinning"); + } + return; + } + + case "collapse-all": { + // Deepest first, so each folder records its own collapsed state. + const $open = Array.from( + $root.$ul.querySelectorAll(".list.collapsible:not(.hidden)"), + ).reverse(); + for (const $list of $open) $list.collapse(); + return; + } + } +} + /** * Expand the list * @param {Collapsible} $list @@ -251,20 +361,21 @@ async function expandList($list) { if (!$ul) return; - // Cleanup existing file tree - if ($ul._fileTree) { - $ul._fileTree.destroy(); - $ul._fileTree = null; - } + // Cleanup existing file tree, including one still loading: toggling while + // it loads would otherwise render both trees into the same list. + for (const tree of [$ul._fileTree, $ul._pendingFileTree]) tree?.destroy(); + $ul._fileTree = null; + $ul._pendingFileTree = null; $ul.innerHTML = ""; if (saveState) listState[url] = $list.unclasped; if (!$list.unclasped) return; + let fileTree; try { startLoading(); - const fileTree = new FileTree($ul, { + fileTree = new FileTree($ul, { getEntries: (dirUrl) => fsOperation(dirUrl).lsDir(), expandedState: listState, onExpandedChange: (folderUrl, isExpanded) => { @@ -278,9 +389,16 @@ async function expandList($list) { }, }); + $ul._pendingFileTree = fileTree; await fileTree.load(url); + // A later toggle replaced this tree while it loaded. + if ($ul._pendingFileTree !== fileTree) return; + $ul._pendingFileTree = null; $ul._fileTree = fileTree; } catch (err) { + // A newer toggle owns the list now; don't collapse it. + if ($ul._pendingFileTree !== fileTree) return; + $ul._pendingFileTree = null; $list.collapse(); if (err?.includes?.("Invalid message length")) { console.error(err); @@ -289,7 +407,8 @@ async function expandList($list) { } helpers.error(err); } finally { - stopLoading(); + // Leave the loading bar to a newer load that is still running. + if (!$ul._pendingFileTree) stopLoading(); } } @@ -324,8 +443,10 @@ async function handleContextmenu(type, url, name, $target) { if (appSettings.value.vibrateOnTap) { haptic(config.VIBRATION_TIME); } - const { clipBoard, $node } = openFolder.find(url); - const cancel = `${strings.cancel}${clipBoard ? ` (${strings[clipBoard.action]})` : ""}`; + const { $node } = openFolder.find(url); + const hasClipboard = clipBoard.items.length > 0; + const cancel = `${strings.cancel}${hasClipboard ? ` (${strings[clipBoard.action]})` : ""}`; + const SELECT = ["select", strings.select, "check_circle_outline"]; const COPY = ["copy", strings.copy, "copy"]; const CUT = ["cut", strings.cut, "cut"]; const COPY_RELATIVE_PATH = [ @@ -335,13 +456,19 @@ async function handleContextmenu(type, url, name, $target) { ]; const REMOVE = ["delete", strings.delete, "delete"]; const RENAME = ["rename", strings.rename, "edit"]; - const PASTE = ["paste", strings.paste, "paste", !!clipBoard]; + const PASTE = ["paste", strings.paste, "paste", hasClipboard]; const NEW_FILE = ["new file", strings["new file"], "document-add"]; const NEW_FOLDER = ["new folder", strings["new folder"], "folder-add"]; const CANCEL = ["cancel", cancel, "clearclose"]; const OPEN_FOLDER = ["open-folder", strings["open folder"], "folder"]; const INSERT_FILE = ["insert-file", strings["insert file"], "file_copy"]; const CLOSE_FOLDER = ["close", strings["close"], "folder-remove"]; + const REFRESH = ["refresh", strings.refresh, "autorenew"]; + const COLLAPSE_ALL = [ + "collapse-all", + strings["collapse all"] || "Collapse all", + "expand_less", + ]; const INSTALL_PLUGIN = [ "install-plugin", strings["install as plugin"] || "Install as Plugin", @@ -356,7 +483,7 @@ async function handleContextmenu(type, url, name, $target) { let options; if (helpers.isFile(type)) { - options = [COPY, CUT, COPY_RELATIVE_PATH, RENAME, REMOVE]; + options = [SELECT, COPY, CUT, COPY_RELATIVE_PATH, RENAME, REMOVE]; if ( url.toLowerCase().endsWith(".zip") && (await fsOperation( @@ -366,9 +493,9 @@ async function handleContextmenu(type, url, name, $target) { options.push(INSTALL_PLUGIN); } } else if (helpers.isDir(type)) { - options = [COPY, CUT, COPY_RELATIVE_PATH, REMOVE, RENAME]; + options = [SELECT, COPY, CUT, COPY_RELATIVE_PATH, REMOVE, RENAME]; - if (clipBoard.url != null) { + if (hasClipboard) { options.push(PASTE); } @@ -387,7 +514,7 @@ async function handleContextmenu(type, url, name, $target) { } else if (type === "root") { options = []; - if (clipBoard.url != null) { + if (hasClipboard) { options.push(PASTE); } @@ -404,7 +531,7 @@ async function handleContextmenu(type, url, name, $target) { options.push(OPEN_SSH_TERMINAL); } - options.push(CLOSE_FOLDER); + options.push(REFRESH, COLLAPSE_ALL, CLOSE_FOLDER); } if (clipBoard.action) options.push(CANCEL); @@ -422,17 +549,20 @@ async function handleContextmenu(type, url, name, $target) { /** * @param {"dir"|"file"|"root"} type - * @param {"copy"|"cut"|"delete"|"rename"|"paste"|"new file"|"new folder"|"cancel"|"open-folder"|"install-plugin"|"open-in-terminal"|"open-ssh-terminal"|"copy-relative-path"} action + * @param {"select"|"copy"|"cut"|"delete"|"rename"|"paste"|"new file"|"new folder"|"cancel"|"open-folder"|"install-plugin"|"open-in-terminal"|"open-ssh-terminal"|"copy-relative-path"} action * @param {string} url target url * @param {HTMLElement} $target target element * @param {string} name Name of file or folder */ function execOperation(type, action, url, $target, name) { - const { clipBoard, $node, remove, url: rootUrl } = openFolder.find(url); + const { $node, remove, url: rootUrl } = openFolder.find(url); const startLoading = () => $node.$title.classList.add("loading"); const stopLoading = () => $node.$title.classList.remove("loading"); switch (action) { + case "select": + return fileSelection.start($target); + case "copy": case "cut": return clipBoardAction(); @@ -471,6 +601,10 @@ function execOperation(type, action, url, $target, name) { case "open-ssh-terminal": return openSshTerminal(); + case "refresh": + case "collapse-all": + return runRootAction($node, action); + case "copy-relative-path": return copyRelativePath(); } @@ -504,42 +638,14 @@ function execOperation(type, action, url, $target, name) { return; } - let relativePath; - - // Try using Url.pathname for protocol-based URLs - const rootPath = Url.pathname(rootUrl); - const targetPath = Url.pathname(url); - - if (rootPath && targetPath) { - // Both pathnames extracted successfully - relativePath = Path.convertToRelative(rootPath, targetPath); - } else { - // Fallback: Use simple string comparison for URIs where pathname extraction fails - const cleanRoot = rootUrl.endsWith("/") - ? rootUrl.slice(0, -1) - : rootUrl; - const cleanTarget = url.endsWith("/") ? url.slice(0, -1) : url; - - // Check if target URL starts with root URL - if (cleanTarget.startsWith(cleanRoot)) { - relativePath = cleanTarget.slice(cleanRoot.length + 1); - } else { - // If not a child path, just use basename - relativePath = Url.basename(url); - } - } + const relativePath = getRelativePath(url, rootUrl); if (!relativePath) { console.error("Unable to calculate relative path"); return; } - if (Bridge.clipboard) { - Bridge.clipboard.copy(relativePath); - } else { - console.error("Clipboard not available"); - toast("Clipboard not available"); - } + copyText(relativePath); } catch (error) { console.error("Failed to copy relative path:", error); } @@ -595,49 +701,8 @@ function execOperation(type, action, url, $target, name) { const confirmation = await confirm(strings.warning, msg); if (!confirmation) return; startLoading(); - if (!(await fsOperation(url).exists())) return; - // await fsOperation(url).delete(); - recents.removeFile(url); - if (helpers.isFile(type)) { - await fsOperation(url).delete(); - removeEntryFromOpenFolder(url); - const file = editorManager.getFile(url, "uri"); - if (file) file.uri = null; - editorManager.onupdate("delete-file"); - editorManager.emit("update", "delete-file"); - } else { - if (isTerminalSafUri(url)) { - const fs = fsOperation(url); - const entries = await fs.lsDir(); - if (entries.length === 0) { - await fs.delete(); - } else { - const deleteRecursively = async (currentUrl) => { - const currentFs = fsOperation(currentUrl); - const currentEntries = await currentFs.lsDir(); - for (const entry of currentEntries) { - if (entry.isDirectory) { - await deleteRecursively(entry.url); - } else { - await fsOperation(entry.url).delete(); - } - } - await currentFs.delete(); - }; - await deleteRecursively(url); - } - } else { - await fsOperation(url).delete(); - } - recents.removeFolder(url); - helpers.updateUriOfAllActiveFiles(url, null); - removeEntryFromOpenFolder(url); - editorManager.onupdate("delete-folder"); - editorManager.emit("update", "delete-folder"); - } - + if (!(await deleteEntry(url, type))) return; toast(strings.success); - FileList.remove(url); } async function renameFile() { @@ -645,10 +710,15 @@ function execOperation(type, action, url, $target, name) { alert(strings.warning, strings["rename not supported"]); return; } - let newName = await prompt(strings.rename, name, "text", { - match: config.FILE_NAME_REGEX, - required: true, - }); + let newName = await prompt( + strings.rename, + name, + helpers.isFile(type) ? "filename" : "text", + { + match: config.FILE_NAME_REGEX, + required: true, + }, + ); newName = helpers.fixFilename(newName); if (!newName || newName === name) return; @@ -728,124 +798,27 @@ function execOperation(type, action, url, $target, name) { } async function paste() { - if (clipBoard.url == null) { + if (!clipBoard.items.length) { alert(strings.warning, "Nothing to paste"); return; } - // Prevent pasting a folder into itself or its subdirectories - if (helpers.isDir(clipBoard.$el.dataset.type)) { - const sourceUrl = Url.parse(clipBoard.url).url; - const targetUrl = Url.parse(url).url; - - // Check if trying to paste folder into itself - if (sourceUrl === targetUrl) { - alert(strings.warning, "Cannot paste a folder into itself"); - return; - } - - // Check if trying to paste folder into one of its subdirectories - if ( - targetUrl.startsWith(sourceUrl + "/") || - targetUrl.startsWith(sourceUrl + "\\") - ) { - alert(strings.warning, "Cannot paste a folder into its subdirectory"); - return; - } - } - - const srcType = clipBoard.$el.dataset.type; - const IS_FILE = helpers.isFile(srcType); - const IS_DIR = helpers.isDir(srcType); + const { action: pasteAction, items } = clipBoard; startLoading(); try { - const fs = fsOperation(clipBoard.url); - const itemName = Url.basename(clipBoard.url); - const possibleConflictUrl = Url.join(url, itemName); - const doesExist = await fsOperation(possibleConflictUrl).exists(); - if (doesExist) { - let confirmation = await confirm( - strings.warning, - strings["already exists"] - ? strings["already exists"].replace("{name}", itemName) - : `"${itemName}" already exists in this location.`, - ); - if (!confirmation) return; - } - let newUrl; - if (clipBoard.action === "cut") { - // Special handling for SAF folders backed by terminal providers - move manually due to SAF limitations - if (isTerminalSafUri(clipBoard.url) && IS_DIR) { - const moveRecursively = async (sourceUrl, targetParentUrl) => { - const sourceFs = fsOperation(sourceUrl); - const sourceName = Url.basename(sourceUrl); - const targetUrl = Url.join(targetParentUrl, sourceName); - - // Create target folder - await fsOperation(targetParentUrl).createDirectory(sourceName); - - // Get all entries in source folder - const entries = await sourceFs.lsDir(); - - // Move all files and folders recursively - for (const entry of entries) { - if (entry.isDirectory) { - await moveRecursively(entry.url, targetUrl); - } else { - const fileContent = await fsOperation(entry.url).readFile(); - const fileName = entry.name || Url.basename(entry.url); - await fsOperation(targetUrl).createFile(fileName, fileContent); - await fsOperation(entry.url).delete(); - } - } - - // Delete the now-empty source folder - await sourceFs.delete(); - return targetUrl; - }; - - newUrl = await moveRecursively(clipBoard.url, url); - } else { - newUrl = await fs.moveTo(url); - } - } else { - if (appSettings.value.useFileOperationExclusions) { - const result = await copyEntry(clipBoard.url, url, { - excludePatterns: appSettings.value.excludeFolders, - }); - newUrl = result.url; - if (!newUrl) { - toast(strings.skipped); - clearClipboard(); - return; - } - } else { - newUrl = await fs.copyTo(url); - } - } - stopLoading(); + const { pasted, remaining, error } = await pasteEntries( + items, + url, + pasteAction, + ); - if (clipBoard.action === "cut") { - if (IS_FILE) { - const file = editorManager.getFile(clipBoard.url, "uri"); - if (file) file.uri = newUrl; - } else if (IS_DIR) { - helpers.updateUriOfAllActiveFiles(clipBoard.url, newUrl); - migrateOpenFolderStateUrls(clipBoard.url, newUrl); - } - removeEntryFromOpenFolder(clipBoard.url); - FileList.rename(clipBoard.url, newUrl); - } else { - FileList.append(url, newUrl); - } + // Keep skipped or failed entries so the paste can be retried. + if (remaining.length) setClipboard(remaining, pasteAction); + else clearClipboard(); - appendEntryToOpenFolder(url, newUrl, IS_DIR ? "folder" : "file"); - toast(strings.success); - clearClipboard(); - } catch (error) { - console.error(error); - helpers.error(error); + if (error) helpers.error(error); + else if (pasted) toast(strings.success); } finally { stopLoading(); } @@ -872,12 +845,7 @@ function execOperation(type, action, url, $target, name) { } async function clipBoardAction() { - clipBoard.url = url; - clipBoard.action = action; - clipBoard.$el = $target; - - if (action === "cut") $target.classList.add("cut"); - else $target.classList.remove("cut"); + setClipboard([{ url, type }], action); } async function open() { @@ -889,17 +857,466 @@ function execOperation(type, action, url, $target, name) { } function cancelAction() { - clipBoard.$el.classList.remove("cut"); clearClipboard(); } +} + +/** + * Delete a file or folder and update every view that references it. + * @param {string} url + * @param {"file"|"dir"} type + * @returns {Promise} false when the entry no longer exists + */ +async function deleteEntry(url, type) { + if (!(await fsOperation(url).exists())) return false; + recents.removeFile(url); + await deleteFromDisk(url, type); + if (helpers.isFile(type)) { + removeEntryFromOpenFolder(url); + const file = editorManager.getFile(url, "uri"); + if (file) file.uri = null; + editorManager.onupdate("delete-file"); + editorManager.emit("update", "delete-file"); + } else { + recents.removeFolder(url); + helpers.updateUriOfAllActiveFiles(url, null); + removeEntryFromOpenFolder(url); + editorManager.onupdate("delete-folder"); + editorManager.emit("update", "delete-folder"); + } - function clearClipboard() { - clipBoard.$el = null; - clipBoard.url = null; - clipBoard.action = null; + FileList.remove(url); + return true; +} + +/** + * Remove a file or folder from its file system without touching any views. + * @param {string} url + * @param {"file"|"dir"} type + */ +async function deleteFromDisk(url, type) { + if (!helpers.isDir(type) || !isTerminalSafUri(url)) { + await fsOperation(url).delete(); + return; } + + // Terminal SAF providers can't delete non-empty folders. + const deleteRecursively = async (currentUrl) => { + const currentFs = fsOperation(currentUrl); + const currentEntries = await currentFs.lsDir(); + for (const entry of currentEntries) { + if (entry.isDirectory) { + await deleteRecursively(entry.url); + } else { + await fsOperation(entry.url).delete(); + } + } + await currentFs.delete(); + }; + await deleteRecursively(url); } +/** + * Paste entries one by one so a failure doesn't stop the rest. + * @param {ClipBoardItem[]} items + * @param {string} targetUrl + * @param {"cut"|"copy"} action + * @returns {Promise<{pasted: number, remaining: ClipBoardItem[], error?: Error}>} + * `remaining` holds the entries that were skipped or failed. + */ +async function pasteEntries(items, targetUrl, action) { + const remaining = []; + let pasted = 0; + let error; + + for (const item of items) { + try { + if (await pasteEntry(item, targetUrl, action)) { + pasted++; + continue; + } + } catch (err) { + console.error(err); + error = err; + } + remaining.push(item); + } + + return { pasted, remaining, error }; +} + +/** + * Identify the connection a url belongs to. Providers can only move or copy + * within their own connection (e.g. one SFTP server or one SAF tree). + * @param {string} url + * @returns {string} + */ +function getConnectionKey(url) { + const { url: parsedUrl } = Url.parse(url); + const protocol = Url.getProtocol(parsedUrl); + + if (!protocol || protocol === "file:") return "file:"; + + if (protocol === "content:") { + try { + return `content:${Uri.parse(parsedUrl).rootUri}`; + } catch (error) { + return parsedUrl; + } + } + + const { username, hostname, port } = Url.decodeUrl(parsedUrl); + return `${protocol}//${username || ""}@${hostname}:${port || ""}`; +} + +/** + * Paste a clipboard entry into a folder. + * @param {ClipBoardItem} item + * @param {string} targetUrl + * @param {"cut"|"copy"} action + * @returns {Promise} New url, or null when skipped + */ +async function pasteEntry({ url: srcUrl, type: srcType }, targetUrl, action) { + const IS_FILE = helpers.isFile(srcType); + const IS_DIR = helpers.isDir(srcType); + + // Prevent pasting a folder into itself or its subdirectories + if (IS_DIR) { + const sourcePath = Url.parse(srcUrl).url; + const targetPath = Url.parse(targetUrl).url; + + if (sourcePath === targetPath) { + alert(strings.warning, "Cannot paste a folder into itself"); + return null; + } + + if ( + targetPath.startsWith(sourcePath + "/") || + targetPath.startsWith(sourcePath + "\\") + ) { + alert(strings.warning, "Cannot paste a folder into its subdirectory"); + return null; + } + } + + const fs = fsOperation(srcUrl); + const itemName = Url.basename(srcUrl); + const possibleConflictUrl = Url.join(targetUrl, itemName); + const doesExist = await fsOperation(possibleConflictUrl).exists(); + if (doesExist) { + const confirmation = await confirm( + strings.warning, + strings["already exists"] + ? strings["already exists"].replace("{name}", itemName) + : `"${itemName}" already exists in this location.`, + ); + if (!confirmation) return null; + } + + const excludePatterns = + action === "copy" && appSettings.value.useFileOperationExclusions + ? appSettings.value.excludeFolders + : null; + + let newUrl; + if (getConnectionKey(srcUrl) !== getConnectionKey(targetUrl)) { + // Different servers or storage: read from the source and write to the + // target, since the source provider can't reach the target. + // Check exclusions first so a skipped entry never replaces anything. + if ( + excludePatterns && + isExcludedFileOperationPath(srcUrl, excludePatterns) + ) { + toast(strings.skipped); + return null; + } + + if (doesExist && IS_FILE) { + // copyEntry only creates new files, so overwrite the confirmed one. + await fsOperation(possibleConflictUrl).writeFile(await fs.readFile()); + newUrl = possibleConflictUrl; + } else { + if (doesExist) { + // The user confirmed replacing what's there. + const { isDirectory } = await fsOperation(possibleConflictUrl).stat(); + await deleteFromDisk(possibleConflictUrl, isDirectory ? "dir" : "file"); + } + const result = await copyEntry(srcUrl, targetUrl, { + excludePatterns: excludePatterns || [], + }); + newUrl = result.url; + } + if (newUrl && action === "cut") await deleteFromDisk(srcUrl, srcType); + } else if (action === "cut") { + // Special handling for SAF folders backed by terminal providers - move manually due to SAF limitations + if (isTerminalSafUri(srcUrl) && IS_DIR) { + const moveRecursively = async (sourceUrl, targetParentUrl) => { + const sourceFs = fsOperation(sourceUrl); + const sourceName = Url.basename(sourceUrl); + const targetUrl = Url.join(targetParentUrl, sourceName); + + // Create target folder + await fsOperation(targetParentUrl).createDirectory(sourceName); + + // Get all entries in source folder + const entries = await sourceFs.lsDir(); + + // Move all files and folders recursively + for (const entry of entries) { + if (entry.isDirectory) { + await moveRecursively(entry.url, targetUrl); + } else { + const fileContent = await fsOperation(entry.url).readFile(); + const fileName = entry.name || Url.basename(entry.url); + await fsOperation(targetUrl).createFile(fileName, fileContent); + await fsOperation(entry.url).delete(); + } + } + + // Delete the now-empty source folder + await sourceFs.delete(); + return targetUrl; + }; + + newUrl = await moveRecursively(srcUrl, targetUrl); + } else { + newUrl = await fs.moveTo(targetUrl); + } + } else if (excludePatterns) { + const result = await copyEntry(srcUrl, targetUrl, { excludePatterns }); + newUrl = result.url; + } else { + newUrl = await fs.copyTo(targetUrl); + } + + if (!newUrl) { + toast(strings.skipped); + return null; + } + + if (action === "cut") { + if (IS_FILE) { + const file = editorManager.getFile(srcUrl, "uri"); + if (file) file.uri = newUrl; + } else if (IS_DIR) { + helpers.updateUriOfAllActiveFiles(srcUrl, newUrl); + migrateOpenFolderStateUrls(srcUrl, newUrl); + } + removeEntryFromOpenFolder(srcUrl); + FileList.rename(srcUrl, newUrl); + } else { + FileList.append(targetUrl, newUrl); + } + + appendEntryToOpenFolder(targetUrl, newUrl, IS_DIR ? "folder" : "file"); + return newUrl; +} + +/** + * Get the elements currently rendered for an entry. + * @param {string} url + * @returns {HTMLElement[]} + */ +function getRenderedEntries(url) { + const filesApp = sidebarApps.get("files"); + return Array.from(filesApp.getAll(`[data-url="${CSS.escape(url)}"]`)); +} + +/** + * @param {ClipBoardItem[]} items + * @param {"cut"|"copy"} action + */ +function setClipboard(items, action) { + clearClipboard(); + clipBoard.items = items.map(({ url, type }) => ({ url, type })); + clipBoard.action = action; + + if (action !== "cut") return; + for (const { url } of clipBoard.items) { + for (const $el of getRenderedEntries(url)) $el.classList.add("cut"); + } +} + +function clearClipboard() { + for (const { url } of clipBoard.items) { + for (const $el of getRenderedEntries(url)) $el.classList.remove("cut"); + } + clipBoard.items = []; + clipBoard.action = null; +} + +/** + * Get the path of an entry relative to its open folder. + * @param {string} url + * @param {string} rootUrl + * @returns {string} + */ +function getRelativePath(url, rootUrl) { + // Try using Url.pathname for protocol-based URLs + const rootPath = Url.pathname(rootUrl); + const targetPath = Url.pathname(url); + + if (rootPath && targetPath) { + // Both pathnames extracted successfully + return Path.convertToRelative(rootPath, targetPath); + } + + // Fallback: Use simple string comparison for URIs where pathname extraction fails + const cleanRoot = rootUrl.endsWith("/") ? rootUrl.slice(0, -1) : rootUrl; + const cleanTarget = url.endsWith("/") ? url.slice(0, -1) : url; + + // Check if target URL starts with root URL + if (cleanTarget.startsWith(cleanRoot)) { + return cleanTarget.slice(cleanRoot.length + 1); + } + + // If not a child path, just use basename + return Url.basename(url); +} + +/** + * @param {string} text + * @returns {boolean} Whether the text was copied + */ +function copyText(text) { + if (Bridge.clipboard) { + Bridge.clipboard.copy(text); + return true; + } + + console.error("Clipboard not available"); + toast("Clipboard not available"); + return false; +} + +/** + * Drop entries that are already covered by a selected parent folder. + * @template {ClipBoardItem} T + * @param {T[]} items + * @returns {T[]} + */ +function removeNestedItems(items) { + return items.filter( + (item) => + !items.some( + (other) => + other !== item && + helpers.isDir(other.type) && + !areSameOpenFolderUrl(other.url, item.url) && + isInsideOpenFolder(item.url, other.url), + ), + ); +} + +/** + * Run an action from the file selection bar. + * @param {import("sidebarApps/files/selection").SelectionAction} action + * @param {import("sidebarApps/files/selection").SelectedEntry[]} items + * @param {string} [targetUrl] Destination folder for "move" + * @returns {Promise} false keeps the selection open + */ +async function handleSelectionAction(action, items, targetUrl) { + // Entries inside a selected folder are handled with the folder, but open + // and copy path still act on everything that was picked. + if (["copy", "cut", "delete", "move"].includes(action)) { + items = removeNestedItems(items); + } + + switch (action) { + case "copy": + case "cut": + setClipboard(items, action); + return; + + case "delete": + return deleteEntries(items); + + case "move": + return moveEntries(items, targetUrl); + + case "copy-path": { + const paths = items.map(({ url }) => + getRelativePath(url, openFolder.find(url)?.url || url), + ); + if (copyText(paths.join("\n"))) toast(strings["copied to clipboard"]); + return; + } + + case "rename": { + const [{ type, url, $el, name }] = items; + return execOperation(type, "rename", url, $el, name); + } + + case "open": { + const files = items.filter(({ type }) => helpers.isFile(type)); + for (const [index, { url }] of files.entries()) { + await openFile(url, { render: index === files.length - 1 }); + } + Sidebar.hide(); + return; + } + } +} + +/** + * @param {ClipBoardItem[]} items + * @returns {Promise} false when cancelled + */ +async function deleteEntries(items) { + const msg = strings["delete entries"].replace("{count}", items.length); + const confirmation = await confirm(strings.warning, msg); + if (!confirmation) return false; + + const $titles = new Set( + items.map(({ url }) => openFolder.find(url)?.$node.$title).filter(Boolean), + ); + for (const $title of $titles) $title.classList.add("loading"); + + let failed = 0; + let lastError; + try { + for (const { url, type } of items) { + try { + await deleteEntry(url, type); + } catch (error) { + console.error(error); + failed++; + lastError = error; + } + } + } finally { + for (const $title of $titles) $title.classList.remove("loading"); + } + + if (failed) helpers.error(lastError); + else toast(strings.success); + return true; +} + +/** + * Move entries into a folder, skipping those already there. + * @param {ClipBoardItem[]} items + * @param {string} targetUrl + */ +async function moveEntries(items, targetUrl) { + const movable = items.filter( + ({ url }) => !areSameOpenFolderUrl(getParentUrl(url), targetUrl), + ); + if (!movable.length) return; + + const $title = openFolder.find(targetUrl)?.$node.$title; + $title?.classList.add("loading"); + try { + const { pasted, error } = await pasteEntries(movable, targetUrl, "cut"); + if (error) helpers.error(error); + else if (pasted) toast(strings.success); + } finally { + $title?.classList.remove("loading"); + } +} + +fileSelection.onAction(handleSelectionAction); + /** * * @param {"file"|"dir"|"root"} type @@ -1205,7 +1622,7 @@ fileIcons.onChange(() => { */ function createFileTile(name, url) { const $tile = tile({ - lead: , + lead: tag("span", { className: helpers.getIconForFile(name) }), text: name, }); $tile.dataset.url = url; diff --git a/src/lib/prettierFormatter.js b/src/lib/prettierFormatter.js index 7200e56..e95014c 100644 --- a/src/lib/prettierFormatter.js +++ b/src/lib/prettierFormatter.js @@ -1,6 +1,7 @@ import fsOperation from "fileSystem"; import { parse } from "acorn"; import { getDocText } from "cm/editorUtils"; +import { getFormatAsMode } from "cm/modelist"; import toast from "components/toast"; import appSettings from "lib/settings"; import prettierPluginBabel from "prettier/plugins/babel"; @@ -77,7 +78,9 @@ export async function formatActiveFileWithPrettier() { if (!file || file.type !== "editor" || !editor) return false; const modeName = (file.currentMode || "text").toLowerCase(); - const parser = getParserForMode(modeName); + const formatAs = getFormatAsMode(modeName); + const parser = + getParserForMode(modeName) || (formatAs && getParserForMode(formatAs)); if (!parser) { toast("Prettier does not support this file type yet"); return false; diff --git a/src/lib/settings.js b/src/lib/settings.js index de8d3b1..1fbf774 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -133,6 +133,12 @@ class Settings { fontSize: "12px", cursorWidth: 2, editorTheme: "one_dark", + editorRenderer: "dom", + canvasBackend: "auto", + canvasGutterMarkers: "shared", + stickyScroll: false, + stickyScrollMaxLines: 5, + cursorBlinkRate: 1200, textWrap: false, wrappingIndent: "same", softTab: true, @@ -169,7 +175,8 @@ class Settings { scrollbarHeight: 50, showSpaces: false, confirmOnExit: true, - lineHeight: 2, + lineHeight: 1.4, + lineHeightMigrated: true, leftMargin: 50, checkFiles: true, checkForAppUpdates: false, @@ -247,6 +254,15 @@ class Settings { const settings = helpers.parseJSON(await fs.readFile("utf8")); if (settings) { + // The old default line height (2) never reached the editor's lines + // (CodeMirror's 1.4 won), so nobody saw it. Now that it applies, + // keep what the editor showed instead of doubling line spacing. + if ( + settings.lineHeightMigrated === undefined && + settings.lineHeight === 2 + ) + settings.lineHeight = 1.4; + // make sure that all the settings are present Object.keys(this.#defaultSettings).forEach((setting) => { const value = settings[setting]; diff --git a/src/pages/customTheme/customTheme.js b/src/pages/customTheme/customTheme.js index 6234516..8131a7c 100644 --- a/src/pages/customTheme/customTheme.js +++ b/src/pages/customTheme/customTheme.js @@ -62,7 +62,7 @@ export default function CustomThemeInclude() { if (action === "reset-theme") { const confirmation = await confirm( - strings["info"].toUpperCase(), + strings["info"], strings["reset warning"], ); if (!confirmation) return; diff --git a/src/pages/fileBrowser/fileBrowser.hbs b/src/pages/fileBrowser/fileBrowser.hbs index bf3f196..f35c89c 100644 --- a/src/pages/fileBrowser/fileBrowser.hbs +++ b/src/pages/fileBrowser/fileBrowser.hbs @@ -1,4 +1,4 @@
-
ⓘ {{info}}
+ {{#info}}
ⓘ {{info}}
{{/info}}
\ No newline at end of file diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index a4e1b3a..f7ed5b6 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -81,12 +81,10 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { let bypassSelectionForContextMenu = false; let $selectAllCheckbox = null; - if (!info) { - if (mode !== "both") { - info = IS_FOLDER_MODE ? strings["open folder"] : strings["open file"]; - } else { - info = strings["file browser"]; - } + // In "both" mode the page title already says "File browser", so the + // info row is only shown when the caller passes its own note. + if (!info && mode !== "both") { + info = IS_FOLDER_MODE ? strings["open folder"] : strings["open file"]; } return new Promise((resolve, reject) => { @@ -211,6 +209,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (IS_FOLDER_MODE) { $openFolder = tag("button", { className: "floating icon check", + title: strings["select folder"], + ariaLabel: strings["select folder"], style: { bottom: "10px", top: "auto", @@ -1344,7 +1344,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { currentDir.url === "/" ? strings.remove : strings.delete; const options = [ ["delete", deleteText, "delete"], - ["rename", strings.rename, "text_format"], + ["rename", strings.rename, "edit"], ]; if (/s?ftp/.test(storageType)) { @@ -1385,9 +1385,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } case "rename": { - let newname = await prompt(strings.rename, name, "text", { - match: config.FILE_NAME_REGEX, - }); + let newname = await prompt( + strings.rename, + name, + helpers.isFile(type) ? "filename" : "text", + { + match: config.FILE_NAME_REGEX, + }, + ); newname = helpers.fixFilename(newname); if (!newname || newname === name) break; diff --git a/src/pages/fileBrowser/index.js b/src/pages/fileBrowser/index.js index bd46f59..deed906 100644 --- a/src/pages/fileBrowser/index.js +++ b/src/pages/fileBrowser/index.js @@ -49,7 +49,7 @@ FileBrowser.openFile = (res) => { }; FileBrowser.openFileError = (err) => { - const ERROR = strings.error.toUpperCase(); + const ERROR = strings.error; const message = `${strings["unable to open file"]}. ${helpers.errorMessage(err.code)}`; if (err.code) { alert(ERROR, message); @@ -78,7 +78,7 @@ FileBrowser.openFolder = async (res) => { }; FileBrowser.openFolderError = (err) => { - const ERROR = strings.error.toUpperCase(); + const ERROR = strings.error; const message = `${strings["unable to open folder"]}. ${helpers.errorMessage(err.code)}`; if (err.code) { alert(ERROR, message); diff --git a/src/pages/markdownPreview/index.js b/src/pages/markdownPreview/index.js index af48aaa..fc80db8 100644 --- a/src/pages/markdownPreview/index.js +++ b/src/pages/markdownPreview/index.js @@ -17,30 +17,9 @@ import { } from "./renderer"; let previewController = null; -let mermaidModulePromise = null; -let mermaidThemeSignature = ""; +let peleModulePromise = null; let mathStylesPromise = null; -function getThemeColor(name, fallback) { - const value = getComputedStyle(document.documentElement) - .getPropertyValue(name) - .trim(); - return value || fallback; -} - -function isDarkColor(color) { - const normalized = color.replace(/\s+/g, ""); - const match = normalized.match(/^#([0-9a-f]{6})$/i); - if (!match) return true; - - const value = match[1]; - const r = Number.parseInt(value.slice(0, 2), 16); - const g = Number.parseInt(value.slice(2, 4), 16); - const b = Number.parseInt(value.slice(4, 6), 16); - const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - return luminance < 0.5; -} - function escapeHtml(text) { return String(text ?? "") .replace(/&/g, "&") @@ -75,17 +54,15 @@ function getOffsetTopWithinContainer(target, container) { return top; } -async function getMermaid() { - if (!mermaidModulePromise) { - mermaidModulePromise = import("mermaid") - .then(({ default: mermaid }) => mermaid) - .catch((error) => { - mermaidModulePromise = null; - throw error; - }); +async function getPele() { + if (!peleModulePromise) { + peleModulePromise = import("pele/lazy").catch((error) => { + peleModulePromise = null; + throw error; + }); } - return mermaidModulePromise; + return peleModulePromise; } async function ensureMathStyles() { @@ -102,76 +79,6 @@ async function ensureMathStyles() { return mathStylesPromise; } -function getMermaidThemeConfig() { - const backgroundColor = getThemeColor("--background-color", "#1e1e1e"); - const panelColor = getThemeColor("--popup-background-color", "#2a2f3a"); - const borderColor = getThemeColor("--border-color", "#4a4f5a"); - const primaryTextColor = getThemeColor("--primary-text-color", "#f5f5f5"); - const accentColor = getThemeColor("--link-text-color", "#4ba3ff"); - const activeColor = getThemeColor("--active-color", accentColor); - - return { - startOnLoad: false, - securityLevel: "strict", - htmlLabels: false, - theme: "base", - flowchart: { - htmlLabels: false, - }, - themeVariables: { - darkMode: isDarkColor(backgroundColor), - background: backgroundColor, - mainBkg: panelColor, - primaryColor: panelColor, - mainContrastColor: primaryTextColor, - textColor: primaryTextColor, - primaryTextColor, - primaryBorderColor: borderColor, - lineColor: primaryTextColor, - secondaryColor: accentColor, - secondaryBorderColor: borderColor, - secondaryTextColor: primaryTextColor, - tertiaryColor: backgroundColor, - tertiaryBorderColor: borderColor, - tertiaryTextColor: primaryTextColor, - clusterBkg: panelColor, - clusterBorder: borderColor, - nodeBorder: borderColor, - nodeTextColor: primaryTextColor, - titleColor: primaryTextColor, - defaultLinkColor: activeColor, - actorTextColor: primaryTextColor, - labelTextColor: primaryTextColor, - loopTextColor: primaryTextColor, - noteTextColor: primaryTextColor, - sectionBkgColor: panelColor, - sectionBkgColor2: backgroundColor, - sectionTitleColor: primaryTextColor, - sequenceNumberColor: primaryTextColor, - signalTextColor: primaryTextColor, - taskTextColor: primaryTextColor, - taskTextDarkColor: primaryTextColor, - taskTextOutsideColor: primaryTextColor, - edgeLabelBackground: backgroundColor, - pieTitleTextColor: primaryTextColor, - pieLegendTextColor: primaryTextColor, - pieSectionTextColor: primaryTextColor, - git0: panelColor, - git1: backgroundColor, - git2: accentColor, - git3: activeColor, - }, - }; -} - -function initializeMermaid(mermaid) { - const config = getMermaidThemeConfig(); - const signature = JSON.stringify(config); - if (signature === mermaidThemeSignature) return; - mermaid.initialize(config); - mermaidThemeSignature = signature; -} - async function copyText(text) { if (Bridge?.clipboard) { Bridge.clipboard.copy(text); @@ -437,17 +344,17 @@ function createMarkdownPreview(file) { ); if (!mermaidBlocks.length) return; - const mermaid = await getMermaid(); + const { renderAsync } = await getPele(); if (previewState.disposed || version !== previewState.renderVersion) return; - initializeMermaid(mermaid); - let index = 0; await Promise.all( - mermaidBlocks.map(async (block) => { + mermaidBlocks.map(async (block, index) => { const source = block.textContent || ""; - const id = `acode-markdown-mermaid-${Date.now()}-${version}-${index++}`; try { - const { svg, bindFunctions } = await mermaid.render(id, source); + const { svg } = await renderAsync(source, { + idPrefix: `acode-markdown-mermaid-${version}-${index}`, + maxWidth: block.clientWidth || undefined, + }); if ( previewState.disposed || version !== previewState.renderVersion || @@ -456,13 +363,10 @@ function createMarkdownPreview(file) { return; } - const sanitizedSvg = DOMPurify.sanitize(svg, { + block.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true }, - ADD_TAGS: ["style"], - ADD_ATTR: ["data-et", "data-id", "data-node", "data-zoom", "class"], + ADD_ATTR: ["data-id", "class"], }); - block.innerHTML = sanitizedSvg; - bindFunctions?.(block); } catch (error) { if (!block.isConnected) return; block.classList.add("mermaid-error"); diff --git a/src/pages/markdownPreview/style.scss b/src/pages/markdownPreview/style.scss index 4e666c1..0c1caaa 100644 --- a/src/pages/markdownPreview/style.scss +++ b/src/pages/markdownPreview/style.scss @@ -85,6 +85,15 @@ justify-content: center; overflow-x: auto; padding: 12px 0; + --pele-bg: var(--primary-color); + --pele-fg: var(--primary-text-color); + --pele-muted: var(--secondary-text-color); + --pele-line: var(--primary-text-color); + --pele-surface: var(--popup-background-color); + --pele-surface-alt: var(--secondary-color); + --pele-border: var(--border-color); + --pele-accent: var(--link-text-color); + --pele-error: var(--error-text-color); } .mermaid svg { @@ -92,22 +101,6 @@ height: auto; } - .mermaid svg text, - .mermaid svg tspan, - .mermaid .label, - .mermaid .nodeLabel, - .mermaid .edgeLabel, - .mermaid .cluster-label, - .mermaid .flowchart-label { - fill: var(--primary-text-color) !important; - color: var(--primary-text-color) !important; - } - - .mermaid svg foreignObject, - .mermaid svg foreignObject * { - color: var(--primary-text-color) !important; - } - @supports not (gap: 1px) { section.eqno > * + * { margin-left: 12px; } } diff --git a/src/pages/sponsor/sponsor.js b/src/pages/sponsor/sponsor.js index 7fa5dda..0c7f178 100644 --- a/src/pages/sponsor/sponsor.js +++ b/src/pages/sponsor/sponsor.js @@ -103,7 +103,7 @@ export default function Sponsor(onclose) { if (res.error) { helpers.error(res.error); } else { - dialog(strings.info.toUpperCase(), msg); + dialog(strings.info, msg); localStorage.removeItem(`sponsor_${productId}`); $page.hide(); } @@ -117,7 +117,7 @@ export default function Sponsor(onclose) { }, (err) => { if (err !== iap.USER_CANCELED) { - alert(strings.error.toUpperCase(), err); + alert(strings.error, err); } loader.removeTitleLoader(); }, diff --git a/src/settings/backupRestore.js b/src/settings/backupRestore.js index 2f534c3..ca9d54d 100644 --- a/src/settings/backupRestore.js +++ b/src/settings/backupRestore.js @@ -319,12 +319,12 @@ function backupRestore() { `${strings.plugins || "Plugins"}: ${installedPlugins.length}`, ].join("
"); - alert(strings.success.toUpperCase(), message); + alert(strings.success, message); } catch (error) { loaderDialog.destroy(); console.error("Backup error:", error); alert( - strings.error.toUpperCase(), + strings.error, `${strings["error details"] || "Error"}: ${error.message || error}`, ); } @@ -375,7 +375,7 @@ backupRestore.restore = async function (url) { backup = JSON.parse(backupContent); } catch (error) { loaderDialog.destroy(); - alert(strings.error.toUpperCase(), strings["invalid backup file"]); + alert(strings.error, strings["invalid backup file"]); return; } @@ -391,7 +391,7 @@ backupRestore.restore = async function (url) { `${strings["issues found"]}:`, ...validation.errors.map((e) => `• ${e}`), ].join("\n"); - alert(strings.error.toUpperCase(), errorMessage); + alert(strings.error, errorMessage); return; } @@ -697,10 +697,7 @@ backupRestore.restore = async function (url) { } catch (err) { loaderDialog.destroy(); console.error("Restore error:", err); - alert( - strings.error.toUpperCase(), - `${strings["error details"]}: ${err.message || err}`, - ); + alert(strings.error, `${strings["error details"]}: ${err.message || err}`); } }; diff --git a/src/settings/editorSettings.js b/src/settings/editorSettings.js index ec4c34b..0ba895e 100644 --- a/src/settings/editorSettings.js +++ b/src/settings/editorSettings.js @@ -1,12 +1,36 @@ import settingsPage from "components/settingsPage"; +import confirm from "dialogs/confirm"; import config from "lib/config"; import fonts from "lib/fonts"; import appSettings from "lib/settings"; import scrollSettings from "./scrollSettings"; +/** Settings only canvas editors use; shown with the canvas renderer. */ +const CANVAS_ONLY_SETTINGS = [ + "canvasBackend", + "stickyScroll", + "stickyScrollMaxLines", + "canvasGutterMarkers", + "cursorBlinkRate", +]; + export default function editorSettings() { const title = strings["editor settings"]; const values = appSettings.value; + const rendererOptions = [ + ["dom", strings["editor renderer dom"]], + ["canvas", strings["editor renderer canvas"]], + ]; + const canvasBackendOptions = [ + ["auto", strings["canvas backend auto"]], + ["webgl2", "WebGL2"], + ["webgpu", "WebGPU"], + ]; + const gutterMarkerOptions = [ + ["shared", strings["gutter markers shared"]], + ["columns", strings["gutter markers columns"]], + ["overlay", strings["gutter markers overlay"]], + ]; const wrappingIndentOptions = [ ["none", strings.none], ["same", strings["wrap-indent-same"]], @@ -29,6 +53,50 @@ export default function editorSettings() { category: categories.scrolling, chevron: true, }, + { + key: "editorRenderer", + text: strings["editor renderer"], + value: values.editorRenderer ?? "dom", + valueText: (value) => + rendererOptions.find(([key]) => key === value)?.[1] ?? + rendererOptions[0][1], + select: rendererOptions, + info: strings["settings-info-editor-renderer"], + category: categories.textLayout, + }, + { + key: "canvasBackend", + text: strings["canvas backend"], + value: values.canvasBackend ?? "auto", + valueText: (value) => + canvasBackendOptions.find(([key]) => key === value)?.[1] ?? + canvasBackendOptions[0][1], + select: canvasBackendOptions, + info: strings["settings-info-canvas-backend"], + category: categories.textLayout, + }, + { + key: "stickyScroll", + text: strings["sticky scroll"], + checkbox: values.stickyScroll ?? false, + info: strings["settings-info-sticky-scroll"], + category: categories.textLayout, + }, + { + key: "stickyScrollMaxLines", + text: strings["sticky scroll max lines"], + value: values.stickyScrollMaxLines ?? 5, + prompt: strings["sticky scroll max lines"], + promptType: "number", + promptOptions: { + test(value) { + value = Number.parseInt(value); + return value >= 1 && value <= 10; + }, + }, + info: strings["settings-info-sticky-scroll-max-lines"], + category: categories.textLayout, + }, { key: "editorFont", text: strings["editor font"], @@ -205,6 +273,17 @@ export default function editorSettings() { info: strings["settings-info-editor-lint-gutter"], category: categories.guidesIndicators, }, + { + key: "canvasGutterMarkers", + text: strings["gutter markers"], + value: values.canvasGutterMarkers ?? "shared", + valueText: (value) => + gutterMarkerOptions.find(([key]) => key === value)?.[1] ?? + gutterMarkerOptions[0][1], + select: gutterMarkerOptions, + info: strings["settings-info-gutter-markers"], + category: categories.guidesIndicators, + }, { key: "showSpaces", text: strings["show spaces"], @@ -247,6 +326,21 @@ export default function editorSettings() { info: strings["settings-info-editor-shift-click-selection"], category: categories.cursorSelection, }, + { + key: "cursorBlinkRate", + text: strings["cursor blink rate"], + value: values.cursorBlinkRate ?? 1200, + prompt: strings["cursor blink rate"] + " (ms)", + promptType: "number", + promptOptions: { + test(value) { + value = Number.parseInt(value); + return value >= 0 && value <= 10000; + }, + }, + info: strings["settings-info-cursor-blink-rate"], + category: categories.cursorSelection, + }, { key: "rtlText", text: strings["line based rtl switching"], @@ -256,7 +350,13 @@ export default function editorSettings() { }, ]; - return settingsPage(title, items, callback, undefined, { + for (const item of items) { + if (CANVAS_ONLY_SETTINGS.includes(item.key)) { + item.hidden = !isCanvasSettingVisible(item.key); + } + } + + const page = settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", @@ -264,17 +364,55 @@ export default function editorSettings() { valueInTail: true, }); + // The page is created once and kept, so follow the renderer from here. + appSettings.on("update:editorRenderer", updateCanvasSettings); + appSettings.on("update:stickyScroll", updateCanvasSettings); + + return page; + + function isCanvasSettingVisible(key) { + const { editorRenderer, stickyScroll } = appSettings.value; + if (editorRenderer !== "canvas") return false; + return key !== "stickyScrollMaxLines" || !!stickyScroll; + } + + function updateCanvasSettings() { + for (const key of CANVAS_ONLY_SETTINGS) { + page.setItemVisibility(key, isCanvasSettingVisible(key)); + } + } + /** * Callback for settings page when an item is clicked * @param {string} key * @param {string} value */ - function callback(key, value) { + async function callback(key, value) { switch (key) { case "scroll-settings": appSettings.uiSettings[key].show(); return; + case "editorRenderer": + case "canvasBackend": + // Editors are created with their renderer and backend: reload + // to switch (a backend change only matters to canvas editors). + if (value === appSettings.value[key]) return; + await appSettings.update({ [key]: value }); + if ( + (key === "editorRenderer" || + appSettings.value.editorRenderer === "canvas") && + (await confirm(strings.info, strings["reload to apply"])) + ) { + await reloadApp(); + } + return; + + case "stickyScrollMaxLines": + case "cursorBlinkRate": + appSettings.update({ [key]: Number.parseInt(value) }); + break; + case "editorFont": fonts.setFont(value); @@ -285,4 +423,11 @@ export default function editorSettings() { break; } } + + /** Reload once open files' edits are cached and the session is saved. */ + async function reloadApp() { + await window.editorManager?.flushCacheWrites?.(); + window.acode?.exec("save-state"); + location.reload(); + } } diff --git a/src/settings/formatterSettings.js b/src/settings/formatterSettings.js index 604caec..b7d97ae 100644 --- a/src/settings/formatterSettings.js +++ b/src/settings/formatterSettings.js @@ -1,4 +1,4 @@ -import { getModes } from "cm/modelist"; +import { getFormatAsMode, getMode, getModes } from "cm/modelist"; import settingsPage from "components/settingsPage"; import appSettings from "lib/settings"; @@ -21,8 +21,20 @@ export default function formatterSettings(languageName) { const extList = String(extensions) .split("|") .filter((e) => e && !e.startsWith("^")); - const options = acode.getFormatterFor(extList); + // Also offer formatters of the mode this one formats as + const baseMode = getMode(getFormatAsMode(name) || ""); + const baseExts = String(baseMode?.extensions || "") + .split("|") + .filter((e) => e && !e.startsWith("^")); + const options = acode.getFormatterFor([...extList, ...baseExts]); const sampleExt = extList[0] || name; + // Unset falls back to the base mode's formatter, so say which one + const inheritedText = () => { + const inherited = formatters.find( + ({ id }) => id === values.formatter[baseMode.name], + ); + return inherited ? `${inherited.name} (${baseMode.caption})` : null; + }; return { key: name, @@ -34,9 +46,13 @@ export default function formatterSettings(languageName) { if (formatter) { return formatter.name; } - return strings.none; + return (baseMode && inheritedText()) || strings.none; + }, + // Read when the picker opens, so the inherited label stays current + get select() { + if (baseMode) options[0] = [null, inheritedText() || strings.none]; + return options; }, - select: options, chevron: true, category: languagesLabel, }; diff --git a/src/settings/scrollSettings.js b/src/settings/scrollSettings.js index dbb72f4..cb5eb49 100644 --- a/src/settings/scrollSettings.js +++ b/src/settings/scrollSettings.js @@ -7,18 +7,21 @@ export default function scrollSettings() { const title = strings["scroll settings"]; const items = [ - /*{ + { key: "scrollSpeed", text: strings["scroll speed"], value: values.scrollSpeed, valueText: getScrollSpeedString, select: [ - [constants.SCROLL_SPEED_FAST_X2, `${strings.fast} x2`], - [constants.SCROLL_SPEED_FAST, strings.fast], - [constants.SCROLL_SPEED_NORMAL, strings.normal], - [constants.SCROLL_SPEED_SLOW, strings.slow], + [config.SCROLL_SPEED_FAST_X2, `${strings.fast} x2`], + [config.SCROLL_SPEED_FAST, strings.fast], + [config.SCROLL_SPEED_NORMAL, strings.normal], + [config.SCROLL_SPEED_SLOW, strings.slow], ], - },*/ + info: strings["settings-info-scroll-speed"], + // Only canvas editors use it. + hidden: values.editorRenderer !== "canvas", + }, /*{ key: "reverseScrolling", text: strings["reverse scrolling"], @@ -86,7 +89,7 @@ export default function scrollSettings() { }, ]; - return settingsPage(title, items, callback, undefined, { + const page = settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", @@ -95,6 +98,13 @@ export default function scrollSettings() { groupByDefault: true, }); + // The page is created once and kept, so follow the renderer from here. + appSettings.on("update:editorRenderer", (renderer) => { + page.setItemVisibility("scrollSpeed", renderer === "canvas"); + }); + + return page; + function callback(key, value) { appSettings.update({ [key]: key === "leftMargin" ? Number(value) : value, diff --git a/src/settings/terminalSettings.js b/src/settings/terminalSettings.js index 246d131..babc658 100644 --- a/src/settings/terminalSettings.js +++ b/src/settings/terminalSettings.js @@ -325,10 +325,7 @@ export default function terminalSettings() { Terminal.uninstall() .then(() => { loader.removeTitleLoader(); - alert( - strings.success.toUpperCase(), - `${strings["uninstalled successfully"]}.`, - ); + alert(strings.success, `${strings["uninstalled successfully"]}.`); }) .catch((error) => { loader.removeTitleLoader(); @@ -383,7 +380,7 @@ export default function terminalSettings() { console.error, ); loader.removeTitleLoader(); - alert(strings.success.toUpperCase(), `${strings["backup successful"]}.`); + alert(strings.success, `${strings["backup successful"]}.`); } catch (error) { loader.removeTitleLoader(); console.error("Terminal backup failed:", error); @@ -417,10 +414,7 @@ export default function terminalSettings() { await clearBackup(); loader.removeTitleLoader(); - alert( - strings.success.toUpperCase(), - `${strings["restored successfully"]}.`, - ); + alert(strings.success, `${strings["restored successfully"]}.`); }, (error) => { if (helpers.isCancelled(error)) return; diff --git a/src/sidebarApps/files/dragMove.js b/src/sidebarApps/files/dragMove.js new file mode 100644 index 0000000..592d9b5 --- /dev/null +++ b/src/sidebarApps/files/dragMove.js @@ -0,0 +1,615 @@ +import "./dragMove.scss"; +import tag from "html-tag-js"; +import config from "lib/config"; +import appSettings from "lib/settings"; +import haptic from "utils/haptic"; +import selection from "./selection"; + +// Movement allowed before a touch counts as a scroll instead of a press. +const TOUCH_SLOP = 8; +// After a long press a resting finger still wobbles, so a drag needs a +// clearly deliberate move. +const DRAG_SLOP = 14; +const MOUSE_SLOP = 5; +// Holding still this long after the long press opens the menu without +// waiting for the finger to lift. +const MENU_HOLD_MS = 300; +// Distance from a scroll edge where the list starts scrolling on its own. +const SCROLL_EDGE = 48; +const MAX_SCROLL_SPEED = 14; +// Hovering a closed folder this long opens it. +const SPRING_OPEN_MS = 650; +// Fraction of the remaining distance the preview covers each frame. +const FOLLOW = 0.35; +const SETTLE_MS = 240; + +/** @type {HTMLElement} */ +let container; +/** + * Press that may turn into a drag. + * @type {{$tile: HTMLElement, x: number, y: number, pointer: "touch"|"mouse", armed: boolean, menuOpened?: boolean, menuTimer?: number, $target?: Element, $touchTarget?: EventTarget} | null} + */ +let press = null; +/** @type {DragState|null} */ +let drag = null; +let suppressClick = false; +/** Context menu events re-dispatched after a long press ends without moving. */ +const replayed = new WeakSet(); +/** Touch events already handled, since they can reach two listeners. */ +const handled = new WeakSet(); + +/** + * @typedef {object} DragState + * @property {import("./selection").SelectedEntry[]} items + * @property {HTMLElement[]} $sources + * @property {HTMLElement} $ghost + * @property {DOMRect} origin + * @property {"touch"|"mouse"} pointer + * @property {number} x Pointer position + * @property {number} y + * @property {number} gx Preview position + * @property {number} gy + * @property {number} tilt + * @property {HTMLElement|null} $list Folder the entries would drop into + * @property {HTMLElement|null} $scroller + * @property {number} raf + * @property {number} springTimer + */ + +/** + * Let entries in the files sidebar be dragged onto folders. + * Must run before the selection handlers so a long press can become a drag. + * @param {HTMLElement} $container + */ +function init($container) { + container = $container; + container.addEventListener("touchstart", onTouchStart, { + capture: true, + passive: true, + }); + container.addEventListener("mousedown", onMouseDown, true); + container.addEventListener("contextmenu", onContextMenu, true); + container.addEventListener("click", onClick, true); +} + +/** + * @param {TouchEvent} e + */ +function onTouchStart(e) { + // A drag that lost its touch stream ends with the next touch. + if (drag) finishDrag(false); + endPress(); + if (e.touches.length !== 1) { + endPress(); + return; + } + + const $tile = selection.getEntry(e.target); + if (!$tile) return; + + const { clientX, clientY } = e.touches[0]; + press = { + $tile, + x: clientX, + y: clientY, + pointer: "touch", + armed: false, + $touchTarget: e.target, + }; + // Touch events keep going to the touched element even after it leaves the + // DOM (e.g. its folder collapses mid-drag), where they no longer reach the + // window, so listen on both. + for (const $el of [window, e.target]) { + $el.addEventListener("touchmove", onTouchMove, { + capture: true, + passive: false, + }); + $el.addEventListener("touchend", onTouchEnd, true); + $el.addEventListener("touchcancel", onTouchCancel, true); + } +} + +/** + * @param {TouchEvent} e + */ +function onTouchMove(e) { + if (isHandled(e)) return; + const touch = e.touches[0]; + if (!touch) return; + + if (drag) { + consume(e); + moveDrag(touch.clientX, touch.clientY); + return; + } + + if (!press) return; + + if (!press.armed) { + // Moving before the long press is a scroll. + if (distance(press, touch) > TOUCH_SLOP) endPress(); + return; + } + + // Held long enough: keep the list (and the sidebar) still. + consume(e); + if (press.menuOpened) return; + if (distance(press, touch) > DRAG_SLOP) { + clearTimeout(press.menuTimer); + startDrag(touch.clientX, touch.clientY); + } +} + +/** + * @param {TouchEvent} e + */ +function onTouchEnd(e) { + if (isHandled(e)) return; + if (drag) { + const touch = e.changedTouches[0]; + if (touch) moveDrag(touch.clientX, touch.clientY); + finishDrag(true); + endPress(); + return; + } + + // A long press released in place is a normal context menu request. + if (press?.armed && !press.menuOpened) replayContextMenu(press); + endPress(); +} + +function onTouchCancel(e) { + if (isHandled(e)) return; + if (drag) finishDrag(false); + endPress(); +} + +/** + * Hold the long-press context menu until it's clear whether this is a drag. + * @param {MouseEvent} e + */ +function onContextMenu(e) { + if (replayed.has(e)) return; + if (press?.pointer !== "touch" || press.armed) return; + if (selection.getEntry(e.target) !== press.$tile) return; + + consume(e); + press.armed = true; + press.$target = e.target; + press.$tile.classList.add("drag-armed"); + vibrate(); + + // Holding still means the menu was wanted, not a drag. + const current = press; + current.menuTimer = setTimeout(() => { + if (press !== current || drag) return; + current.menuOpened = true; + current.$tile.classList.remove("drag-armed"); + replayContextMenu(current); + }, MENU_HOLD_MS); +} + +/** + * @param {MouseEvent} e + */ +function onMouseDown(e) { + suppressClick = false; + if (drag || e.button !== 0) return; + + const $tile = selection.getEntry(e.target); + if (!$tile) return; + + press = { + $tile, + x: e.clientX, + y: e.clientY, + pointer: "mouse", + armed: true, + }; + window.addEventListener("mousemove", onMouseMove, true); + window.addEventListener("mouseup", onMouseUp, true); +} + +/** + * @param {MouseEvent} e + */ +function onMouseMove(e) { + if (drag) { + e.preventDefault(); + moveDrag(e.clientX, e.clientY); + return; + } + + if (press && distance(press, e) > MOUSE_SLOP) { + e.preventDefault(); + startDrag(e.clientX, e.clientY); + } +} + +function onMouseUp() { + if (drag) { + // The click that follows mouseup must not open or toggle the entry. + suppressClick = true; + finishDrag(true); + } + endPress(); +} + +/** + * @param {MouseEvent} e + */ +function onClick(e) { + if (!suppressClick) return; + suppressClick = false; + consume(e); +} + +/** + * @param {KeyboardEvent} e + */ +function onKeydown(e) { + if (e.key === "Escape" && drag) { + finishDrag(false); + endPress(); + } +} + +/** + * Any new touch during a drag means its own touch stream was lost. + */ +function onStrayTouch() { + if (drag) finishDrag(false); + endPress(); +} + +/** + * @param {number} x + * @param {number} y + */ +function startDrag(x, y) { + const { $tile, pointer } = press; + const { url, type, name } = $tile.dataset; + + // Dragging a selected entry carries the whole selection along. + const items = selection.isSelected(url) + ? selection.getItems() + : [{ url, type, name, $el: $tile }]; + const $sources = items.flatMap(({ url }) => findTiles(url)); + const origin = $tile.getBoundingClientRect(); + + $tile.classList.remove("drag-armed"); + for (const $el of $sources) $el.classList.add("drag-source"); + document.body.classList.add("file-dragging"); + document.addEventListener("keydown", onKeydown, true); + document.addEventListener("touchstart", onStrayTouch, true); + + drag = { + items, + $sources, + $ghost: createGhost($tile, items), + origin, + pointer, + x, + y, + gx: origin.left, + gy: origin.top, + tilt: 0, + $list: null, + $scroller: null, + raf: 0, + springTimer: 0, + }; + + vibrate(); + updateTarget(); + drag.raf = requestAnimationFrame(frame); +} + +/** + * @param {number} x + * @param {number} y + */ +function moveDrag(x, y) { + drag.x = x; + drag.y = y; + updateTarget(); +} + +/** + * Ease the preview toward the pointer, tilting slightly with its motion, + * and scroll the list when the pointer nears an edge. + */ +function frame() { + if (!drag) return; + + // Keep the preview clear of the finger; beside the cursor for mice. + const isTouch = drag.pointer === "touch"; + const targetX = drag.x + (isTouch ? 16 : 14); + const targetY = drag.y + (isTouch ? -52 : 10); + const dx = targetX - drag.gx; + + drag.gx += dx * FOLLOW; + drag.gy += (targetY - drag.gy) * FOLLOW; + drag.tilt += (clamp(dx * 0.12, -6, 6) - drag.tilt) * 0.2; + drag.$ghost.style.transform = `translate3d(${drag.gx}px, ${drag.gy}px, 0) rotate(${drag.tilt}deg)`; + + autoScroll(); + drag.raf = requestAnimationFrame(frame); +} + +function autoScroll() { + const $scroller = drag.$scroller; + if (!$scroller) return; + + const { top, bottom } = $scroller.getBoundingClientRect(); + let speed = 0; + if (drag.y < top + SCROLL_EDGE) { + speed = + -MAX_SCROLL_SPEED * + Math.min(1, (top + SCROLL_EDGE - drag.y) / SCROLL_EDGE); + } else if (drag.y > bottom - SCROLL_EDGE) { + speed = + MAX_SCROLL_SPEED * + Math.min(1, (drag.y - bottom + SCROLL_EDGE) / SCROLL_EDGE); + } + if (!speed) return; + + const before = $scroller.scrollTop; + $scroller.scrollTop += speed; + if ($scroller.scrollTop !== before) updateTarget(); +} + +/** + * Find the folder under the pointer and highlight it if it can take the drop. + */ +function updateTarget() { + const $el = document.elementFromPoint(drag.x, drag.y); + const $scroller = getScroller($el); + if ($scroller) drag.$scroller = $scroller; + + let $list = getDropList($el); + if ($list && !canDropInto($list)) $list = null; + if ($list === drag.$list) return; + + clearTarget(); + drag.$list = $list; + if (!$list) return; + + $list.classList.add("drop-target"); + + // Spring-loaded folders: hovering a closed one opens it. + if ( + $list.classList.contains("hidden") && + typeof $list.expand === "function" + ) { + $list.classList.add("drop-pending"); + drag.springTimer = setTimeout(() => { + $list.classList.remove("drop-pending"); + if (drag?.$list === $list) $list.expand(); + }, SPRING_OPEN_MS); + } +} + +function clearTarget() { + clearTimeout(drag.springTimer); + drag.$list?.classList.remove("drop-target", "drop-pending"); +} + +/** + * @param {boolean} drop Whether to move the entries into the hovered folder + */ +function finishDrag(drop) { + const state = drag; + drag = null; + cancelAnimationFrame(state.raf); + clearTimeout(state.springTimer); + state.$list?.classList.remove("drop-target", "drop-pending"); + document.body.classList.remove("file-dragging"); + document.removeEventListener("keydown", onKeydown, true); + document.removeEventListener("touchstart", onStrayTouch, true); + for (const $el of state.$sources) $el.classList.remove("drag-source"); + + const $ghost = state.$ghost; + const $title = drop ? state.$list?.firstElementChild : null; + const targetUrl = $title?.dataset.url; + $ghost.classList.add("settling"); + + if (targetUrl) { + // Shrink into the folder it was dropped on. + const rect = $title.getBoundingClientRect(); + const y = rect.top + rect.height / 2 - $ghost.offsetHeight / 2; + $ghost.style.transform = `translate3d(${rect.left + 12}px, ${y}px, 0) scale(0.3)`; + $title.classList.add("drop-flash"); + setTimeout(() => $title.classList.remove("drop-flash"), 600); + vibrate(); + selection.perform("move", state.items, targetUrl); + } else { + // Nowhere to drop: return to where it came from. + const { left, top } = state.origin; + $ghost.style.transform = `translate3d(${left}px, ${top}px, 0)`; + } + + setTimeout(() => $ghost.remove(), SETTLE_MS); +} + +function endPress() { + clearTimeout(press?.menuTimer); + press?.$tile.classList.remove("drag-armed"); + for (const $el of [window, press?.$touchTarget]) { + $el?.removeEventListener("touchmove", onTouchMove, true); + $el?.removeEventListener("touchend", onTouchEnd, true); + $el?.removeEventListener("touchcancel", onTouchCancel, true); + } + press = null; + window.removeEventListener("mousemove", onMouseMove, true); + window.removeEventListener("mouseup", onMouseUp, true); +} + +/** + * Open the context menu the long press would have opened. + * @param {{x: number, y: number, $target?: Element}} state + */ +function replayContextMenu({ x, y, $target }) { + if (!$target?.isConnected) return; + const event = new MouseEvent("contextmenu", { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + }); + replayed.add(event); + $target.dispatchEvent(event); +} + +/** + * @param {HTMLElement} $tile + * @param {import("./selection").SelectedEntry[]} items + */ +function createGhost($tile, items) { + const $icon = tag("span", { + className: $tile.firstElementChild?.className || "", + }); + const $chip = tag("div", { + className: "chip", + children: [ + $icon, + tag("span", { className: "name", textContent: items[0].name }), + ], + }); + + if (items.length > 1) { + $chip.append( + tag("span", { className: "badge", textContent: String(items.length) }), + ); + } + + const $ghost = tag("div", { + className: `file-drag-ghost${items.length > 1 ? " multi" : ""}`, + children: [$chip], + }); + const { left, top } = $tile.getBoundingClientRect(); + $ghost.style.transform = `translate3d(${left}px, ${top}px, 0)`; + document.body.append($ghost); + return $ghost; +} + +/** + * @param {Element|null} $el + * @returns {HTMLElement|null} The folder list an entry would be dropped into + */ +function getDropList($el) { + if (!$el || !container.contains($el)) return null; + + const $tile = $el.closest(".tile[data-type]"); + const type = $tile?.dataset.type; + const $list = + type === "dir" || type === "root" + ? $tile.parentElement + : $el.closest(".list.collapsible"); + + if (!$list || !container.contains($list)) return null; + if ($list.classList.contains("file-list")) return null; + return $list; +} + +/** + * @param {HTMLElement} $list + */ +function canDropInto($list) { + const url = $list.firstElementChild?.dataset.url; + if (!url) return false; + + for (const $source of drag.$sources) { + if ($source.dataset.url === url) return false; + // A folder can't go inside itself. + if ( + $source.dataset.type === "dir" && + $source.parentElement.contains($list) + ) { + return false; + } + } + + // Nothing to do if every entry is already in this folder. + return !drag.items.every(({ $el }) => getParentUrl($el) === url); +} + +/** + * @param {HTMLElement} [$el] + * @returns {string|undefined} + */ +function getParentUrl($el) { + if (!$el?.isConnected) return undefined; + const $own = $el.dataset.type === "dir" ? $el.parentElement : $el; + return $own.parentElement?.closest(".list.collapsible")?.firstElementChild + ?.dataset.url; +} + +/** + * @param {Element|null} $el + * @returns {HTMLElement|null} + */ +function getScroller($el) { + while ($el && $el !== container.parentElement) { + if ($el.scrollHeight > $el.clientHeight + 1) { + const { overflowY } = getComputedStyle($el); + if (overflowY === "auto" || overflowY === "scroll") return $el; + } + $el = $el.parentElement; + } + return null; +} + +/** + * @param {string} url + * @returns {HTMLElement[]} + */ +function findTiles(url) { + return Array.from( + container.querySelectorAll(`.tile[data-url="${CSS.escape(url)}"]`), + ).filter(($el) => selection.getEntry($el)); +} + +/** + * @param {{x: number, y: number}} from + * @param {{clientX: number, clientY: number}} to + */ +function distance(from, to) { + return Math.hypot(to.clientX - from.x, to.clientY - from.y); +} + +/** + * @param {Event} e + * @returns {boolean} Whether another listener already handled the event + */ +function isHandled(e) { + if (handled.has(e)) return true; + handled.add(e); + return false; +} + +/** + * @param {number} value + * @param {number} min + * @param {number} max + */ +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +/** + * @param {Event} e + */ +function consume(e) { + if (e.cancelable) e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); +} + +function vibrate() { + if (appSettings.value.vibrateOnTap) { + haptic(config.VIBRATION_TIME); + } +} + +export default { init }; diff --git a/src/sidebarApps/files/dragMove.scss b/src/sidebarApps/files/dragMove.scss new file mode 100644 index 0000000..52e961e --- /dev/null +++ b/src/sidebarApps/files/dragMove.scss @@ -0,0 +1,179 @@ +// Written without inset, flex gap or unguarded color-mix() so it still works +// on old Android WebViews. +$ease: cubic-bezier(0.2, 0, 0, 1); +$spring: cubic-bezier(0.34, 1.56, 0.64, 1); + +body.file-dragging { + cursor: grabbing; + user-select: none; + -webkit-user-select: none; +} + +#sidebar .container.files { + .tile[data-type="file"], + .tile[data-type="dir"] { + transition: + opacity 160ms ease, + transform 160ms $ease, + background-color 180ms ease; + + // Long press recognised: the row lifts slightly, ready to be dragged. + &.drag-armed { + transform: scale(0.97); + background-color: rgba(127, 127, 127, 0.14); + } + + &.drag-source { + opacity: 0.35; + } + } + + // The whole folder the entries would land in lights up. + .list.collapsible.drop-target { + border-radius: 8px; + background-color: rgba(127, 127, 127, 0.1); + background-color: color-mix(in srgb, var(--active-color) 10%, transparent); + box-shadow: inset 0 0 0 1px rgba(127, 127, 127, 0.35); + box-shadow: inset 0 0 0 1px + color-mix(in srgb, var(--active-color) 45%, transparent); + + > .tile:first-child { + border-radius: 8px; + } + } + + // Closed folder about to spring open. + .list.collapsible.drop-pending > .tile:first-child { + animation: file-drop-pending 650ms ease-in-out; + } + + .tile.drop-flash { + animation: file-drop-flash 600ms ease-out; + } +} + +.file-drag-ghost { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + pointer-events: none; + will-change: transform; + + &.settling { + opacity: 0; + transition: + transform 240ms $ease, + opacity 240ms ease; + } + + .chip { + position: relative; + display: flex; + align-items: center; + box-sizing: border-box; + height: 34px; + max-width: 220px; + padding: 0 10px 0 6px; + border-radius: 10px; + border: 1px solid var(--popup-border-color, var(--border-color)); + background-color: var(--popup-background-color); + color: var(--popup-text-color); + font-size: 13px; + box-shadow: + 0 14px 30px -8px rgba(0, 0, 0, 0.45), + 0 2px 6px rgba(0, 0, 0, 0.2); + animation: file-drag-pick 260ms $spring; + + > :first-child { + flex: none; + width: 22px; + height: 22px; + min-width: 0; + margin-right: 6px; + font-size: 15px; + line-height: 22px; + text-align: center; + background-size: 16px; + background-position: center; + background-repeat: no-repeat; + } + } + + .name { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .badge { + flex: none; + min-width: 18px; + height: 18px; + box-sizing: border-box; + margin-left: 8px; + padding: 0 5px; + border-radius: 9px; + background-color: var(--active-color); + color: var(--active-text-color); + font-size: 11px; + font-weight: 600; + line-height: 18px; + text-align: center; + } + + // Several entries: cards stacked behind the chip. + &.multi .chip::before, + &.multi .chip::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + box-sizing: border-box; + border-radius: 10px; + border: 1px solid var(--popup-border-color, var(--border-color)); + background-color: var(--popup-background-color); + } + + &.multi .chip::before { + transform: translate(4px, 4px) rotate(2deg); + opacity: 0.7; + } + + &.multi .chip::after { + transform: translate(8px, 8px) rotate(4deg); + opacity: 0.4; + } +} + +@keyframes file-drag-pick { + from { + transform: scale(0.85); + opacity: 0; + } +} + +@keyframes file-drop-pending { + 50% { + background-color: rgba(127, 127, 127, 0.22); + } +} + +@keyframes file-drop-flash { + from { + background-color: rgba(127, 127, 127, 0.3); + background-color: color-mix(in srgb, var(--active-color) 35%, transparent); + } +} + +body.no-animation { + .file-drag-ghost, + .file-drag-ghost * { + animation: none !important; + transition: none !important; + } +} diff --git a/src/sidebarApps/files/index.js b/src/sidebarApps/files/index.js index 4baca7c..f07e9a1 100644 --- a/src/sidebarApps/files/index.js +++ b/src/sidebarApps/files/index.js @@ -2,6 +2,8 @@ import "./style.scss"; import Sidebar from "components/sidebar"; import fileIcons from "lib/fileIcons"; import settings from "lib/settings"; +import dragMove from "./dragMove"; +import selection from "./selection"; /**@type {HTMLElement} */ let container; @@ -25,6 +27,10 @@ function initApp(el) { container.setAttribute("data-msg", strings["open folder"]); container.style.overflowX = "auto"; container.addEventListener("click", clickHandler); + // Drag listens first so a long press can become a drag before selection + // or the context menu sees it. + dragMove.init(container); + selection.init(container); editorManager.on( ["new-file", "int-open-file-list", "remove-file"], (position) => { @@ -49,6 +55,12 @@ function onSelected(el) { // Phone sidebars and inactive sidebar apps are detached, so document-wide // icon updates cannot reach them until this view is shown again. fileIcons.refreshRenderedIcons(container); + selection.syncBar(); + // Heights measured while the sidebar was detached come out wrong. + const $openFolder = container.querySelector( + ":scope > .list:not(.hidden):not(.file-list)", + ); + if ($openFolder) fixHeight($openFolder); const $scrollableLists = container.getAll(":scope .scroll[data-scroll-top]"); $scrollableLists.forEach(($el) => { $el.scrollTop = $el.dataset.scrollTop; @@ -79,8 +91,13 @@ function clickHandler(e) { export function fixHeight(target) { const lists = Array.from(container.getAll(":scope > div")); const ITEM_HEIGHT = 30; + // Headers are taller than rows (36px, 40px on wide screens); measure them so + // the expanded list doesn't overflow below the sidebar. + const headerHeight = + container.querySelector(":scope > .list > .tile")?.offsetHeight || + ITEM_HEIGHT; - let height = (lists.length - 1) * ITEM_HEIGHT; + let height = (lists.length - 1) * headerHeight; let activeFileList; if (settings.value.openFileListPos === settings.OPEN_FILE_LIST_POS_SIDEBAR) { @@ -88,9 +105,9 @@ export function fixHeight(target) { if (firstList.classList.contains("file-list")) { activeFileList = firstList; if (firstList.unclasped) { - const heightOffset = height - ITEM_HEIGHT; + const heightOffset = height - headerHeight; const totalHeight = - ITEM_HEIGHT * activeFileList.$ul.children.length + ITEM_HEIGHT; + ITEM_HEIGHT * activeFileList.$ul.children.length + headerHeight; const maxHeight = lists.length === 1 || !lists.slice(1).find((list) => list.unclasped) ? window.innerHeight @@ -99,7 +116,7 @@ export function fixHeight(target) { activeFileList.style.maxHeight = `${minHeight}px`; activeFileList.style.height = `${minHeight}px`; - height += minHeight - ITEM_HEIGHT; + height += minHeight - headerHeight; } } } diff --git a/src/sidebarApps/files/selection.js b/src/sidebarApps/files/selection.js new file mode 100644 index 0000000..b13e211 --- /dev/null +++ b/src/sidebarApps/files/selection.js @@ -0,0 +1,529 @@ +import "./selection.scss"; +import select from "dialogs/select"; +import tag from "html-tag-js"; +import config from "lib/config"; +import appSettings from "lib/settings"; +import haptic from "utils/haptic"; +import helpers from "utils/helpers"; + +/** + * @typedef {"copy"|"cut"|"delete"|"copy-path"|"rename"|"open"|"move"} SelectionAction + */ + +/** + * @typedef {object} SelectedEntry + * @property {string} url + * @property {"file"|"dir"} type + * @property {string} name + * @property {string} [rootUrl] Url of the open folder containing the entry + * @property {HTMLElement} [$el] + */ + +const ACTION_STACK_ID = "file-selection"; +const ENTRY_SELECTOR = + '.tile[data-url][data-type="file"], .tile[data-url][data-type="dir"]'; +// Bar widths below which the label and then the buttons are compacted. +const COMPACT_WIDTH = 270; +const TIGHT_WIDTH = 210; +// Range selections fill in as a short wave instead of all at once. +const WAVE_STEP_MS = 14; +const WAVE_MAX_MS = 180; + +/** @type {Map} */ +const selected = new Map(); +/** @type {HTMLElement} */ +let container; +/** @type {string|null} */ +let anchorUrl = null; +/** @type {(action: SelectionAction, items: SelectedEntry[], targetUrl?: string) => Promise} */ +let actionHandler; +/** @type {MutationObserver} */ +let observer; +/** @type {HTMLElement} */ +let $bar; +/** @type {HTMLElement} */ +let $count; +let busy = false; + +/** + * Attach selection handling to the files sidebar container. + * @param {HTMLElement} $container + */ +function init($container) { + container = $container; + // Capture phase runs before the tree's own tile listeners, so a tap can + // select instead of opening the file or toggling the folder. + container.addEventListener("click", onClick, true); + container.addEventListener("contextmenu", onContextMenu, true); + observer = new MutationObserver(onTreeMutation); + editorManager.on("remove-folder", onRemoveFolder); +} + +/** + * @param {(action: SelectionAction, items: SelectedEntry[], targetUrl?: string) => Promise} handler + * Return false from the handler to keep the selection open. + */ +function onAction(handler) { + actionHandler = handler; +} + +function isActive() { + return !!container?.classList.contains("selecting"); +} + +/** + * Enter selection mode, optionally selecting an entry. + * @param {HTMLElement} [$tile] + */ +function start($tile) { + if (!container) return; + + if (!isActive()) { + container.classList.add("selecting"); + observer.observe(container, { childList: true, subtree: true }); + document.addEventListener("keydown", onKeydown); + actionStack.push({ id: ACTION_STACK_ID, action: exit }); + syncBar(); + updateBarSize(); + vibrate(); + } + + if ($tile && getEntry($tile)) { + setSelected($tile, true); + anchorUrl = $tile.dataset.url; + } + + update(1); +} + +function exit() { + if (!isActive()) return; + + container.classList.remove("selecting"); + for (const $el of container.querySelectorAll(".tile.selected")) { + markEntry($el, false); + } + + selected.clear(); + anchorUrl = null; + observer.disconnect(); + document.removeEventListener("keydown", onKeydown); + actionStack.remove(ACTION_STACK_ID); +} + +/** + * Keep the bar right after the files container so the CSS sibling rule can + * show it only while the files app is the visible sidebar app. + */ +function syncBar() { + if (!container?.isConnected) return; + if (!$bar) createBar(); + if ($bar.previousElementSibling !== container) container.after($bar); +} + +/** + * @param {MouseEvent} e + */ +function onClick(e) { + const $tile = getEntry(e.target); + if (!$tile) return; + + const additive = e.ctrlKey || e.metaKey; + + if (!isActive()) { + if (!additive && !e.shiftKey) return; + consume(e); + start($tile); + return; + } + + // The folder icon keeps expanding folders so nested entries can be reached. + const isPlainTap = !additive && !e.shiftKey; + if (isPlainTap && helpers.isDir($tile.dataset.type) && isOnLead($tile, e)) { + return; + } + + consume(e); + if (e.shiftKey) selectRange($tile); + else toggle($tile); +} + +/** + * @param {PointerEvent} e + */ +function onContextMenu(e) { + if (!isActive()) return; + + const $tile = getEntry(e.target); + if (!$tile) return; + + consume(e); + + // Right click acts on the selection; long press extends it. + if (e.pointerType === "mouse") { + if (!selected.has($tile.dataset.url)) { + setSelected($tile, true); + anchorUrl = $tile.dataset.url; + update(1); + } + showMore(); + return; + } + + vibrate(); + if (anchorUrl && anchorUrl !== $tile.dataset.url) selectRange($tile); + else toggle($tile); +} + +/** + * @param {KeyboardEvent} e + */ +function onKeydown(e) { + if (e.key === "Escape") exit(); +} + +/** + * Re-apply selection to entries re-rendered by the tree (e.g. a folder + * collapsed and expanded again). + * @param {MutationRecord[]} records + */ +function onTreeMutation(records) { + for (const { addedNodes } of records) { + for (const node of addedNodes) { + if (!(node instanceof Element)) continue; + const $tiles = node.matches(ENTRY_SELECTOR) + ? [node] + : node.querySelectorAll(ENTRY_SELECTOR); + for (const $tile of $tiles) { + if (selected.has($tile.dataset.url)) markEntry($tile, true); + } + } + } +} + +/** + * Drop entries of an open folder that was closed. + * @param {{url: string}} folder + */ +function onRemoveFolder({ url } = {}) { + if (!isActive()) return; + for (const [key, entry] of selected) { + if (entry.rootUrl === url) selected.delete(key); + } + update(-1); +} + +/** + * @param {HTMLElement} $tile + */ +function toggle($tile) { + const { url } = $tile.dataset; + const value = !selected.has(url); + setSelected($tile, value); + anchorUrl = url; + update(value ? 1 : -1); +} + +/** + * Select every visible entry between the anchor and the given entry. + * @param {HTMLElement} $tile + */ +function selectRange($tile) { + const $tiles = getVisibleEntries(); + const to = $tiles.indexOf($tile); + const from = $tiles.findIndex(($el) => $el.dataset.url === anchorUrl); + + if (from === -1 || to === -1) { + toggle($tile); + return; + } + + const step = from < to ? 1 : -1; + const range = []; + for (let i = from; i !== to + step; i += step) range.push($tiles[i]); + selectWave(range); +} + +function selectAll() { + const roots = new Set( + Array.from(container.querySelectorAll(".tile.selected"), getRoot), + ); + const $tiles = getVisibleEntries().filter( + ($el) => !roots.size || roots.has(getRoot($el)), + ); + selectWave($tiles); +} + +/** + * Select entries in order with a slight stagger. + * @param {HTMLElement[]} $tiles + */ +function selectWave($tiles) { + let delay = 0; + for (const $tile of $tiles) { + if (selected.has($tile.dataset.url)) continue; + $tile.style.setProperty("--select-delay", `${delay}ms`); + setSelected($tile, true); + delay = Math.min(delay + WAVE_STEP_MS, WAVE_MAX_MS); + setTimeout(() => $tile.style.removeProperty("--select-delay"), 400); + } + update(1); +} + +/** + * @param {HTMLElement} $tile + * @param {boolean} value + */ +function setSelected($tile, value) { + const { url, type, name } = $tile.dataset; + const rootUrl = getRoot($tile).firstElementChild?.dataset.url; + if (value) selected.set(url, { url, type, name, rootUrl }); + else selected.delete(url); + + for (const $el of findEntries(url)) markEntry($el, value); +} + +/** + * @param {HTMLElement} $tile + * @param {boolean} value + */ +function markEntry($tile, value) { + $tile.classList.toggle("selected", value); + // Lets CSS tint a selected folder's contents without :has(). + if (helpers.isDir($tile.dataset.type)) { + $tile.parentElement?.classList.toggle("has-selected-title", value); + } +} + +/** + * Sync the bar with the selection, leaving selection mode once it's empty. + * @param {1|-1} direction Whether the count went up or down + */ +function update(direction) { + if (!selected.size) { + exit(); + return; + } + + if (!$bar) return; + const count = String(selected.size); + if ($count.textContent === count) return; + + $count.textContent = count; + $count.classList.remove("up", "down"); + void $count.offsetWidth; // restart the animation + $count.classList.add(direction > 0 ? "up" : "down"); +} + +/** + * @param {SelectionAction} action + */ +async function run(action) { + if (!selected.size) return; + await perform(action, getItems()); +} + +/** + * Run an action on the given entries, then leave selection mode. + * @param {SelectionAction} action + * @param {SelectedEntry[]} items + * @param {string} [targetUrl] + */ +async function perform(action, items, targetUrl) { + if (busy || !actionHandler || !items.length) return; + + busy = true; + $bar?.classList.add("busy"); + try { + const result = await actionHandler(action, items, targetUrl); + if (result !== false) exit(); + } catch (error) { + console.error(error); + helpers.error(error); + } finally { + busy = false; + $bar?.classList.remove("busy"); + } +} + +/** + * @returns {SelectedEntry[]} Selected entries with their rendered elements + */ +function getItems() { + return Array.from(selected.values(), (item) => ({ + ...item, + $el: findEntries(item.url)[0], + })); +} + +async function showMore() { + const items = Array.from(selected.values()); + const files = items.filter(({ type }) => helpers.isFile(type)).length; + const openText = strings.open || "Open"; + const options = [ + ["select-all", strings["select all"], "library_add_check"], + ["copy-path", strings["copy relative path"], "attach_file"], + ]; + + if (items.length === 1) { + options.push(["rename", strings.rename, "edit"]); + } + + if (files) { + const text = files > 1 ? `${openText} (${files})` : openText; + options.push(["open", text, "launchopen_in_new"]); + } + + const action = await select(getLabel(), options); + if (!isActive()) return; + if (action === "select-all") selectAll(); + else run(action); +} + +function createBar() { + $count = tag("span", { className: "count" }); + $bar = tag("div", { + className: "file-selection-bar", + children: [ + barButton("exit", "clearclose", strings.cancel), + tag("div", { + className: "label", + children: [ + $count, + tag("span", { + className: "unit", + textContent: strings.selected || "selected", + }), + ], + }), + barButton("copy", "copy", strings.copy), + barButton("cut", "cut", strings.cut), + barButton("delete", "delete danger", strings.delete), + barButton("more", "more_vert", strings.more || "More"), + ], + }); + + $bar.setAttribute("role", "toolbar"); + if (window.ResizeObserver) { + new ResizeObserver(updateBarSize).observe($bar); + } + $bar.addEventListener("click", (e) => { + const action = e.target.closest("[data-action]")?.dataset.action; + if (!action) return; + if (action === "exit") exit(); + else if (action === "more") showMore(); + else run(action); + }); +} + +/** + * Stand-in for container queries, which old WebViews don't support. + */ +function updateBarSize() { + if (!$bar) return; + const width = $bar.clientWidth; + if (!width) return; + $bar.classList.toggle("compact", width < COMPACT_WIDTH); + $bar.classList.toggle("tight", width < TIGHT_WIDTH); +} + +/** + * @param {string} action + * @param {string} icon + * @param {string} title + */ +function barButton(action, icon, title) { + const $button = tag("button", { type: "button", className: `icon ${icon}` }); + $button.dataset.action = action; + $button.title = title; + $button.setAttribute("aria-label", title); + return $button; +} + +function getLabel() { + return `${selected.size} ${strings.selected || "selected"}`; +} + +/** + * @param {EventTarget} target + * @returns {HTMLElement|null} The selectable tree entry for the target + */ +function getEntry(target) { + if (!(target instanceof Element)) return null; + const $tile = target.closest(ENTRY_SELECTOR); + if (!$tile || !container.contains($tile)) return null; + if ($tile.closest(".file-list")) return null; + return $tile; +} + +/** + * @param {string} url + * @returns {HTMLElement[]} + */ +function findEntries(url) { + return Array.from( + container.querySelectorAll(`.tile[data-url="${CSS.escape(url)}"]`), + ).filter(($el) => getEntry($el)); +} + +/** + * Entries in display order, skipping those inside collapsed folders. + * @returns {HTMLElement[]} + */ +function getVisibleEntries() { + return Array.from(container.querySelectorAll(ENTRY_SELECTOR)).filter( + ($el) => getEntry($el) && $el.getClientRects().length, + ); +} + +/** + * @param {HTMLElement} $el + * @returns {HTMLElement} The top level open folder containing the entry + */ +function getRoot($el) { + while ($el.parentElement && $el.parentElement !== container) { + $el = $el.parentElement; + } + return $el; +} + +/** + * @param {HTMLElement} $tile + * @param {MouseEvent} e + */ +function isOnLead($tile, e) { + const $lead = $tile.firstElementChild; + if (!$lead) return false; + const { left, right } = $lead.getBoundingClientRect(); + return e.clientX >= left && e.clientX <= right; +} + +/** + * @param {Event} e + */ +function consume(e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); +} + +function vibrate() { + if (appSettings.value.vibrateOnTap) { + haptic(config.VIBRATION_TIME); + } +} + +export default { + init, + onAction, + start, + exit, + syncBar, + perform, + getItems, + getEntry, + isSelected: (url) => selected.has(url), + get active() { + return isActive(); + }, +}; diff --git a/src/sidebarApps/files/selection.scss b/src/sidebarApps/files/selection.scss new file mode 100644 index 0000000..c8e0937 --- /dev/null +++ b/src/sidebarApps/files/selection.scss @@ -0,0 +1,273 @@ +// Avoids :is(), :has(), color-mix() without fallback, container queries and +// flex gap so the styles still work on old Android WebViews. +$ease: cubic-bezier(0.2, 0, 0, 1); +$spring: cubic-bezier(0.34, 1.56, 0.64, 1); +$delay: var(--select-delay, 0ms); + +@mixin entries { + > .list:not(.file-list) { + .tile[data-type="file"], + .tile[data-type="dir"] { + @content; + } + } +} + +#sidebar .container.files { + transition: padding-bottom 220ms $ease; + + &.selecting { + // Keeps the last entries reachable above the floating bar. + padding-bottom: 64px; + } + + @include entries { + transition: background-color 180ms ease $delay; + + &.selected { + background-color: rgba(127, 127, 127, 0.18); + background-color: color-mix(in srgb, var(--active-color) 18%, transparent); + } + } + + &.selecting { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; + + @include entries { + &:active { + background-color: rgba(127, 127, 127, 0.1); + } + + &.selected:active { + background-color: rgba(127, 127, 127, 0.26); + background-color: color-mix( + in srgb, + var(--active-color) 26%, + transparent + ); + } + } + + // Entries inside a selected folder are included implicitly. + .list.collapsible.has-selected-title > ul { + .tile[data-type="file"]:not(.selected), + .tile[data-type="dir"]:not(.selected) { + background-color: rgba(127, 127, 127, 0.08); + background-color: color-mix( + in srgb, + var(--active-color) 8%, + transparent + ); + } + } + } +} + +#sidebar .file-selection-bar { + position: absolute; + z-index: 111; + left: 60px; + right: 10px; + bottom: 12px; + bottom: calc(12px + env(safe-area-inset-bottom, 0px)); + height: 44px; + box-sizing: border-box; + display: flex; + align-items: center; + padding: 0 5px; + border-radius: 14px; + border: 1px solid var(--popup-border-color, var(--border-color)); + background-color: var(--popup-background-color); + color: var(--popup-text-color); + box-shadow: + 0 10px 28px -8px var(--box-shadow-color), + 0 2px 6px var(--box-shadow-color); + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateY(16px) scale(0.96); + transform-origin: 50% 100%; + transition: + opacity 140ms ease, + transform 200ms $ease, + visibility 0s linear 200ms; + + .label { + flex: 1; + min-width: 0; + display: flex; + align-items: baseline; + padding: 0 4px; + overflow: hidden; + white-space: nowrap; + font-size: 0.9em; + } + + .count { + display: inline-block; + font-weight: 600; + font-variant-numeric: tabular-nums; + + &.up { + animation: file-selection-count-up 260ms $spring; + } + + &.down { + animation: file-selection-count-down 260ms $spring; + } + } + + .unit { + margin-left: 4px; + opacity: 0.6; + overflow: hidden; + text-overflow: ellipsis; + } + + button { + flex: none; + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + border: none; + border-radius: 10px; + background-color: transparent; + color: inherit; + font-size: 1.15em; + cursor: pointer; + margin-left: 2px; + transition: + transform 160ms $ease, + background-color 120ms ease, + color 120ms ease; + + &:active { + transform: scale(0.86); + background-color: rgba(127, 127, 127, 0.2); + } + + &:focus-visible { + outline: 2px solid var(--active-color); + outline-offset: -2px; + } + + &.danger:active { + color: var(--danger-color); + background-color: rgba(127, 127, 127, 0.2); + background-color: color-mix(in srgb, var(--danger-color) 16%, transparent); + } + } + + @media (hover: hover) { + button:hover { + background-color: rgba(127, 127, 127, 0.12); + } + + button.danger:hover { + color: var(--danger-color); + background-color: rgba(127, 127, 127, 0.12); + background-color: color-mix(in srgb, var(--danger-color) 12%, transparent); + } + } + + &.busy { + overflow: hidden; + pointer-events: none; + + &::after { + content: ""; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 2px; + background-color: var(--active-color); + animation: bar-loader-animation 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite; + } + + button { + opacity: 0.45; + } + } + + // Size classes are set from JS (container queries need newer WebViews). + // Too narrow for "3 selected": keep just the number. + &.compact .unit { + display: none; + } + + // Minimum sidebar width: tighten the buttons so they still fit. + &.tight button { + width: 28px; + } +} + +#sidebar .container.files.selecting + .file-selection-bar { + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: none; + transition: + opacity 180ms ease, + transform 340ms $spring, + visibility 0s; + + // Bar contents settle in one after another. + > * { + animation: file-selection-item-in 300ms $spring backwards; + } + + @for $i from 2 through 6 { + > :nth-child(#{$i}) { + animation-delay: #{($i - 1) * 25}ms; + } + } +} + +@keyframes file-selection-item-in { + from { + opacity: 0; + transform: translateY(5px); + } +} + +@keyframes file-selection-count-up { + from { + opacity: 0; + transform: translateY(70%); + } +} + +@keyframes file-selection-count-down { + from { + opacity: 0; + transform: translateY(-70%); + } +} + +body.no-animation #sidebar { + .container.files, + .container.files *::before, + .container.files *::after, + .file-selection-bar, + .file-selection-bar * { + transition: none !important; + animation: none !important; + } +} + +@media (prefers-reduced-motion: reduce) { + #sidebar .container.files *::before, + #sidebar .container.files *::after, + #sidebar .file-selection-bar, + #sidebar .file-selection-bar * { + transition-duration: 1ms !important; + animation: none !important; + } +} diff --git a/src/sidebarApps/files/style.scss b/src/sidebarApps/files/style.scss index 4b3a8b7..2c740d1 100644 --- a/src/sidebarApps/files/style.scss +++ b/src/sidebarApps/files/style.scss @@ -104,3 +104,77 @@ } } } + +// Open folder header actions: on hover with a mouse, a single menu button on +// touch, and nothing on collapsed folders. +$root-actions-ease: cubic-bezier(0.2, 0, 0, 1); + +#sidebar .container.files > .list > .tile[data-type="root"] .root-actions { + display: flex; + flex: none; + align-items: center; + margin: 0 4px 0 auto; + + .icon { + display: none; + width: 28px; + min-width: 28px; + height: 28px; + border-radius: 8px; + font-size: 1em; + line-height: 28px; + opacity: 0.55; + transition: + opacity 120ms ease, + transform 160ms $root-actions-ease, + background-color 120ms ease; + + &:active { + opacity: 1; + transform: scale(0.86); + background-color: rgba(127, 127, 127, 0.2); + } + + &.spinning { + animation: root-action-spin 700ms linear infinite; + } + } + + .icon.more-actions { + display: block; + } + + @media (hover: hover) and (pointer: fine) { + opacity: 0; + transition: opacity 120ms ease; + + .icon { + display: block; + + &:hover { + opacity: 1; + background-color: rgba(127, 127, 127, 0.14); + } + } + + .icon.more-actions { + display: none; + } + } +} + +@media (hover: hover) and (pointer: fine) { + #sidebar .container.files > .list > .tile[data-type="root"]:hover .root-actions { + opacity: 1; + } +} + +#sidebar .container.files > .list.hidden > .tile[data-type="root"] .root-actions { + display: none; +} + +@keyframes root-action-spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/sidebarApps/index.js b/src/sidebarApps/index.js index e97cc22..10fd4ac 100644 --- a/src/sidebarApps/index.js +++ b/src/sidebarApps/index.js @@ -115,7 +115,8 @@ function setSponsorSidebarAppVisibility(visible) { onclick={Sponsors} /> ); - $apps.append($sponsorIcon); + // Keep it out of the app list so it isn't tapped by mistake (#2866) + $apps.after($sponsorIcon); return; } diff --git a/src/sidebarApps/notification/index.js b/src/sidebarApps/notification/index.js index d987606..0719604 100644 --- a/src/sidebarApps/notification/index.js +++ b/src/sidebarApps/notification/index.js @@ -23,9 +23,11 @@ const $header = ( @@ -52,6 +54,8 @@ function initApp(el) { * @param {HTMLElement} el */ function onSelected(el) { + // Sidebar "show" fires for every app; only mark read when this one is open + if (container.isConnected) notificationManager.markAllRead(); const $scrollableLists = container.getAll(":scope .scroll[data-scroll-top]"); $scrollableLists.forEach(($el) => { $el.scrollTop = $el.dataset.scrollTop; diff --git a/src/sidebarApps/searchInFiles/index.js b/src/sidebarApps/searchInFiles/index.js index 9de507b..5729f1e 100644 --- a/src/sidebarApps/searchInFiles/index.js +++ b/src/sidebarApps/searchInFiles/index.js @@ -37,6 +37,7 @@ const $caseSensitive = Ref(); const $useIndex = Ref(); const $btnReplaceAll = Ref(); const $resultOverview = Ref(); +const $resultHeader = Ref(); const $error = Reactive(); const $progress = Reactive(); const $indexStatus = Reactive(""); @@ -285,7 +286,7 @@ export default [ /> -
+
{" "} ({$progress}%)
@@ -618,6 +619,8 @@ function onInput(e) { clearNativeResultQueue(); searchResult.setValue(""); removeEvents(); + // Keep the result summary hidden until there is something to search for + $resultHeader.classList.toggle("idle", !$search.value); if (!$search.value) { searchResult.removeGhostText(); return; diff --git a/src/sidebarApps/searchInFiles/styles.scss b/src/sidebarApps/searchInFiles/styles.scss index d5cf3f1..32e08d1 100644 --- a/src/sidebarApps/searchInFiles/styles.scss +++ b/src/sidebarApps/searchInFiles/styles.scss @@ -171,6 +171,10 @@ } } + .search-result-header.idle { + display: none; + } + .search-result-header { flex-shrink: 0; padding: 8px 10px; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index b04529e..cbc8f91 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -140,6 +140,36 @@ export default { const message = typeof err === "string" ? err : err.message; return /cancel/i.test(message || ""); }, + /** + * Maps common native errors (e.g. revoked SAF permissions) to a message + * people can act on. Returns null when the error isn't recognised. + * @param {string} msg + * @returns {string|null} + */ + friendlyErrorMessage(msg) { + if (typeof msg !== "string") return null; + if ( + /Permission Denial|ACTION_OPEN_DOCUMENT|SecurityException|EACCES/i.test( + msg, + ) + ) { + return strings["error-permission-revoked"]; + } + if (/FileNotFoundException|ENOENT|No such file or directory/i.test(msg)) { + return strings["error-not-found"]; + } + if (/ENOSPC|No space left on device/i.test(msg)) { + return strings["error-no-space"]; + } + if ( + /UnknownHostException|ConnectException|SocketTimeoutException|ECONNREFUSED|ETIMEDOUT/i.test( + msg, + ) + ) { + return strings["error-network"]; + } + return null; + }, /** * * @param {Error} err @@ -157,7 +187,16 @@ export default { if (hide) hide(); }; - const msg = this.errorMessage(err, ...args); + let msg = this.errorMessage(err, ...args); + const base = this.errorMessage(err); + const friendly = this.friendlyErrorMessage(base); + if (friendly) { + // Keep the raw native message for bug reports, but out of the way + const raw = document.createElement("div"); + raw.textContent = base; + const extra = msg.slice(base.length); + msg = `${friendly}${extra}
${strings["error details"]}${raw.innerHTML}
`; + } alert(strings.error, msg, onhide); return new Promise((resolve) => { diff --git a/src/views/menu.hbs b/src/views/menu.hbs index 36cb71a..eca060c 100644 --- a/src/views/menu.hbs +++ b/src/views/menu.hbs @@ -8,15 +8,15 @@
  • {{save as}} - +
  • {{files}} - +
  • {{close file}} - +
  • {{open recent}} diff --git a/tests/helpers/queueSelectionChange.ts b/tests/helpers/queueSelectionChange.ts new file mode 100644 index 0000000..6259ef0 --- /dev/null +++ b/tests/helpers/queueSelectionChange.ts @@ -0,0 +1,23 @@ +import { afterAll, beforeAll } from "vitest"; + +// happy-dom dispatches selectionchange synchronously from Selection methods, +// while browsers queue it as a task. CodeMirror's view flushes on every +// selectionchange, so a synchronous one fired while the view syncs its own +// DOM selection re-enters EditorView.update. Queue it like a browser does. +export function queueSelectionChange() { + const dispatchDocumentEvent = document.dispatchEvent; + + beforeAll(() => { + document.dispatchEvent = function (event) { + if (event.type !== "selectionchange") { + return dispatchDocumentEvent.call(this, event); + } + setTimeout(() => dispatchDocumentEvent.call(this, event)); + return true; + }; + }); + + afterAll(() => { + document.dispatchEvent = dispatchDocumentEvent; + }); +} diff --git a/tests/unit/actionStack.test.js b/tests/unit/actionStack.test.js index d467632..ec2469d 100644 --- a/tests/unit/actionStack.test.js +++ b/tests/unit/actionStack.test.js @@ -84,7 +84,7 @@ describe("actionStack app exit", () => { await actionStack.pop(); - expect(mocks.confirm).toHaveBeenCalledWith("WARNING", "Close Acode?"); + expect(mocks.confirm).toHaveBeenCalledWith("Warning", "Close Acode?"); expect(mocks.showInterstitialIfReady).not.toHaveBeenCalled(); expect(mocks.exitApp).toHaveBeenCalledOnce(); }); diff --git a/tests/unit/canvasOptions.test.ts b/tests/unit/canvasOptions.test.ts new file mode 100644 index 0000000..0b949f7 --- /dev/null +++ b/tests/unit/canvasOptions.test.ts @@ -0,0 +1,165 @@ +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { Compartment, Prec } from "@codemirror/state"; +import { tags as t } from "@lezer/highlight"; +import { + findHighlightStyle, + fontSizeToPx, + getCanvasDisplayOptions, + getCanvasFont, + getCanvasTheme, +} from "cm/canvasOptions"; +import { getThemeConfig, getThemes } from "cm/themes"; +import { describe, expect, it } from "vitest"; + +describe("canvas renderer theme bridge", () => { + it("finds the HighlightStyle inside wrapped theme extensions", () => { + const style = HighlightStyle.define([{ tag: t.keyword, color: "#f00" }]); + const compartment = new Compartment(); + const extension = [ + [], + compartment.of(Prec.high([syntaxHighlighting(style)])), + ]; + expect(findHighlightStyle(extension)).toBe(style); + expect(findHighlightStyle([])).toBeNull(); + }); + + it("gives every built-in theme syntax colors and its config colors", () => { + for (const theme of getThemes()) { + const spec = getCanvasTheme(theme.id); + const config = getThemeConfig(theme.id); + expect(spec.background, theme.id).toBe(config.background); + expect(spec.foreground, theme.id).toBe(config.foreground); + expect(spec.dark, theme.id).toBe(config.dark !== false); + expect(spec.highlight, theme.id).toBeInstanceOf(HighlightStyle); + // Acode's own scrollbars are used instead of the canvas thumbs. + expect(spec.scrollbar).toBe("transparent"); + } + }); + + it("maps the theme's line number and bracket colors", () => { + const config = getThemeConfig("dracula"); + expect(getCanvasTheme("dracula")).toMatchObject({ + selection: config.selection, + cursor: config.cursor, + gutterForeground: config.lineNumber, + gutterActiveForeground: config.lineNumberActive, + classes: { + "cm-matchingBracket": { background: config.matchingBracket }, + "cm-selectionMatch": { background: config.selection }, + }, + }); + }); +}); + +describe("canvas renderer settings", () => { + const defaults = { + linenumbers: true, + relativeLineNumbers: false, + fadeFoldWidgets: false, + textWrap: false, + wrappingIndent: "same", + leftMargin: 50, + scrollPastEnd: "medium", + scrollSpeed: "NORMAL", + indentGuides: false, + rainbowBrackets: true, + showSpaces: false, + highlightActiveLine: true, + cursorWidth: 2, + rtlText: false, + }; + + it("maps Acode's editor settings to canvas options", () => { + const options = getCanvasDisplayOptions(defaults, { themeId: "one_dark" }); + expect(options).toMatchObject({ + lineNumbers: true, + foldMarkers: "always", + lineWrapping: false, + wrapIndent: "same", + horizontalScrollPastEnd: 50, + scrollPastEnd: 0.5, + scrollSpeed: "normal", + indentGuides: false, + showWhitespace: false, + highlightActiveLine: true, + cursorWidth: 2, + cursorBlinkRate: 1200, + gutterMarkers: "shared", + stickyScroll: false, + textDirection: "ltr", + }); + expect(options.rainbowBrackets).toMatchObject({ + colors: expect.any(Array), + }); + }); + + it("follows changed settings", () => { + const options = getCanvasDisplayOptions( + { + ...defaults, + relativeLineNumbers: true, + fadeFoldWidgets: true, + textWrap: true, + wrappingIndent: "deepIndent", + scrollPastEnd: "none", + scrollSpeed: "FAST_X2", + indentGuides: true, + rainbowBrackets: false, + showSpaces: true, + cursorWidth: 40, + cursorBlinkRate: "0", + canvasGutterMarkers: "overlay", + stickyScroll: true, + stickyScrollMaxLines: 30, + rtlText: true, + }, + { themeId: "one_dark" }, + ); + expect(options).toMatchObject({ + lineNumbers: "relative", + foldMarkers: "fade", + lineWrapping: true, + wrapIndent: "deepIndent", + scrollPastEnd: 0, + scrollSpeed: "fastX2", + indentGuides: { active: true }, + rainbowBrackets: false, + showWhitespace: { spaces: true, trailing: true, lineBreaks: true }, + cursorWidth: 10, + cursorBlinkRate: 0, + gutterMarkers: "overlay", + stickyScroll: { maxLines: 10 }, + textDirection: "auto", + }); + expect( + getCanvasDisplayOptions( + { ...defaults, linenumbers: false }, + { themeId: "one_dark" }, + ).lineNumbers, + ).toBe(false); + expect( + getCanvasDisplayOptions( + { + ...defaults, + stickyScroll: true, + canvasGutterMarkers: "bad", + cursorBlinkRate: "bad", + }, + { themeId: "one_dark" }, + ), + ).toMatchObject({ + stickyScroll: { maxLines: 5 }, + gutterMarkers: "shared", + cursorBlinkRate: 1200, + }); + }); + + it("converts the editor font settings", () => { + expect(fontSizeToPx("14px")).toBe(14); + expect(fontSizeToPx("12pt")).toBe(16); + expect(fontSizeToPx("bad")).toBe(12); + expect( + getCanvasFont({ fontSize: "13px", lineHeight: 2 }, "Roboto Mono"), + ).toEqual({ family: "Roboto Mono", size: 13, lineHeight: 2 }); + }); +}); diff --git a/tests/unit/colorChip.test.ts b/tests/unit/colorChip.test.ts new file mode 100644 index 0000000..836b71a --- /dev/null +++ b/tests/unit/colorChip.test.ts @@ -0,0 +1,46 @@ +// @vitest-environment happy-dom + +import type { EditorView } from "@codemirror/view"; +import { ColorChipWidget, getColorChipPayload } from "cm/colorChip"; +import { describe, expect, it, vi } from "vitest"; + +// The picker dialog and color utilities (JSX) and the LSP client loader +// aren't needed here. +vi.mock("../../src/dialogs/color.js", () => ({ default: vi.fn() })); +vi.mock("../../src/utils/color/index.js", () => ({ default: vi.fn() })); +vi.mock("cm/lsp/clientLoader", () => ({ getAllLspPlugins: () => [] })); + +const payload = { + from: 4, + to: 11, + css: "#ff8800", + source: "regex" as const, +}; + +describe("color chip widget", () => { + it("tells a canvas view the box toDOM styles, in cells of its font", () => { + const view = { + defaultCharacterWidth: 8, + fontSizePx: 16, + } as unknown as EditorView; + const spec = new ColorChipWidget(payload).toCanvas(view); + // 0.9em of a 16px font is 14.4px: 1.8 cells, rounded to quarters. + expect(spec).toEqual({ + text: "", + cells: 1.75, + padLeft: 0.25, + padRight: 0.25, + style: { + background: "#ff8800", + border: "rgba(0,0,0,0.2)", + rounded: true, + }, + }); + }); + + it("still makes a DOM element carrying its payload (taps on canvas views)", () => { + const el = new ColorChipWidget(payload).toDOM(); + expect(el.style.backgroundColor).toBeTruthy(); + expect(getColorChipPayload(el)).toBe(payload); + }); +}); diff --git a/tests/unit/editorReadOnly.test.ts b/tests/unit/editorReadOnly.test.ts index 0ae77e8..8893b20 100644 --- a/tests/unit/editorReadOnly.test.ts +++ b/tests/unit/editorReadOnly.test.ts @@ -11,10 +11,30 @@ import { resolveReadOnlyContextSelection, shouldCommitReadOnlyTap, } from "cm/editorReadOnly"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; const views: EditorView[] = []; +// happy-dom dispatches selectionchange synchronously from Selection methods, +// while browsers queue it as a task. CodeMirror's view flushes on every +// selectionchange, so a synchronous one fired while the view syncs its own +// DOM selection re-enters EditorView.update. Queue it like a browser does. +const dispatchDocumentEvent = document.dispatchEvent; + +beforeAll(() => { + document.dispatchEvent = function (event) { + if (event.type !== "selectionchange") { + return dispatchDocumentEvent.call(this, event); + } + setTimeout(() => dispatchDocumentEvent.call(this, event)); + return true; + }; +}); + +afterAll(() => { + document.dispatchEvent = dispatchDocumentEvent; +}); + afterEach(() => { while (views.length) views.pop()?.destroy(); document.body.replaceChildren(); @@ -53,6 +73,23 @@ function expectReadOnly(view: EditorView) { } describe("editor read-only configuration", () => { + it("leaves focus and input to a canvas view, keeping the edit filter", () => { + const state = EditorState.create({ + doc: "abc", + extensions: createEditorReadOnlyExtension(true, { canvas: true }), + }); + expect(state.readOnly).toBe(true); + // Canvas views take focus without a keyboard while read-only. + expect(state.facet(EditorView.editable)).toBe(true); + const typed = state.update({ + changes: { from: 0, insert: "x" }, + userEvent: "input.type", + }); + expect(typed.state.doc.toString()).toBe("abc"); + const synced = state.update({ changes: { from: 0, insert: "x" } }); + expect(synced.state.doc.toString()).toBe("xabc"); + }); + it("keeps an editable editor focusable for text input", () => { const { view } = createEditor(); diff --git a/tests/unit/fileIconSidebar.test.ts b/tests/unit/fileIconSidebar.test.ts index 2a6eef2..bfd4e36 100644 --- a/tests/unit/fileIconSidebar.test.ts +++ b/tests/unit/fileIconSidebar.test.ts @@ -6,6 +6,10 @@ import { afterEach, beforeEach, expect, it, vi } from "vitest"; const sidebar = vi.hoisted(() => ({ on: vi.fn() })); vi.mock("components/sidebar", () => ({ default: sidebar })); vi.mock("lib/settings", () => ({ default: {} })); +vi.mock("sidebarApps/files/dragMove", () => ({ default: { init: vi.fn() } })); +vi.mock("sidebarApps/files/selection", () => ({ + default: { init: vi.fn(), syncBar: vi.fn() }, +})); beforeEach(() => { vi.stubGlobal("strings", { files: "Files", "open folder": "Open folder" }); diff --git a/tests/unit/fileTreeRecycling.test.ts b/tests/unit/fileTreeRecycling.test.ts index 74a3e48..a9ee43c 100644 --- a/tests/unit/fileTreeRecycling.test.ts +++ b/tests/unit/fileTreeRecycling.test.ts @@ -71,3 +71,29 @@ it("uses recycled file identity for open and context actions", () => { ); tree.destroy(); }); + +it("renders a folder once when it is toggled while still loading", async () => { + const pending: Array<() => void> = []; + const getEntries = vi.fn( + () => + new Promise((resolve) => { + pending.push(() => + resolve([{ name: "a.js", url: "file:///src/a.js", isFile: true }]), + ); + }), + ); + const tree = new FileTree(document.createElement("div"), { getEntries }); + const row = tree.createFolderElement("src", "file:///src"); + + const first = row.expand(); + await row.collapse(); + const second = row.expand(); + for (const resolve of pending) resolve(); + await Promise.all([first, second]); + + expect(getEntries).toHaveBeenCalledTimes(2); + expect( + row.$ul.querySelectorAll('[data-url="file:///src/a.js"]'), + ).toHaveLength(1); + tree.destroy(); +}); diff --git a/tests/unit/modelist.test.js b/tests/unit/modelist.test.js index ada2cb3..612dacf 100644 --- a/tests/unit/modelist.test.js +++ b/tests/unit/modelist.test.js @@ -1,6 +1,12 @@ import { afterEach, describe, expect, it } from "vitest"; import "cm/supportedModes"; -import { addMode, getModeForPath, getModes, removeMode } from "cm/modelist"; +import { + addMode, + getFormatAsMode, + getModeForPath, + getModes, + removeMode, +} from "cm/modelist"; function getModeSpecificityScore(modeInstance) { if (modeInstance.name.toLowerCase() === "text") { @@ -235,6 +241,43 @@ describe("getModeForPath", () => { expect(getModeForPath("acodepluginfile").name).toBe("acode-regex-foo"); }); + it("stores the normalized formatAs mode", () => { + registerTestMode("acode-json-like", "acodejsonlike", { + formatAs: " JSON ", + }); + registerTestMode("acode-plain", "acodeplain"); + + expect(getModeForPath("a.acodejsonlike").formatAs).toBe("json"); + expect(getModeForPath("a.acodeplain").formatAs).toBeNull(); + }); + + it("resolves formatAs aliases to the base mode name", () => { + registerTestMode("acode-json5-like", "acodejson5like", { + formatAs: "json5", + }); + registerTestMode("acode-unknown-base", "acodeunknownbase", { + formatAs: "acode-missing", + }); + + expect(getFormatAsMode("acode-json5-like")).toBe("json"); + expect(getFormatAsMode("acode-unknown-base")).toBe("acode-missing"); + expect(getFormatAsMode("json")).toBeNull(); + }); + + it("ignores malformed mode options from plugins", () => { + expect(() => + registerTestMode("acode-null-options", "acodenullopts", null), + ).not.toThrow(); + registerTestMode("acode-bad-options", "acodebadopts", { + aliases: "abc", + formatAs: 42, + }); + + const mode = getModeForPath("a.acodebadopts"); + expect(mode.aliases).toEqual([]); + expect(mode.formatAs).toBeNull(); + }); + it("invalidates plugin registrations immediately", () => { registerTestMode("acodepluginmode", "acodepluginmode"); diff --git a/tests/unit/openFolderExpand.test.ts b/tests/unit/openFolderExpand.test.ts new file mode 100644 index 0000000..4c1451e --- /dev/null +++ b/tests/unit/openFolderExpand.test.ts @@ -0,0 +1,176 @@ +// @vitest-environment happy-dom + +import tag from "html-tag-js"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; + +type Read = { + resolve: (entries: unknown[]) => void; + reject: (error: unknown) => void; +}; + +const fs = vi.hoisted(() => ({ reads: [] as Read[] })); +const files = vi.hoisted(() => ({ container: null as HTMLElement | null })); +const helpersError = vi.hoisted(() => vi.fn()); + +vi.mock("fileSystem", () => ({ + default: () => ({ + lsDir: () => + new Promise((resolve, reject) => fs.reads.push({ resolve, reject })), + }), +})); +vi.mock("sidebarApps", () => ({ default: { get: () => files.container } })); +vi.mock("sidebarApps/files", () => ({ fixHeight: vi.fn() })); +vi.mock("sidebarApps/files/selection", () => ({ + default: { onAction: vi.fn(), start: vi.fn() }, +})); +vi.mock("components/sidebar", () => ({ default: { hide: vi.fn() } })); +vi.mock("components/toast", () => ({ default: vi.fn() })); +vi.mock("dialogs/alert", () => ({ default: vi.fn() })); +vi.mock("dialogs/confirm", () => ({ default: vi.fn() })); +vi.mock("dialogs/prompt", () => ({ default: vi.fn() })); +vi.mock("dialogs/select", () => ({ default: vi.fn() })); +vi.mock("lib/fileIcons", () => ({ default: { onChange: vi.fn() } })); +vi.mock("lib/fileList", () => ({ + addRoot: vi.fn(async () => {}), + append: vi.fn(), + remove: vi.fn(), + rename: vi.fn(), +})); +vi.mock("lib/lazyImports", () => ({ loadFileBrowser: vi.fn() })); +vi.mock("lib/openFile", () => ({ default: vi.fn() })); +vi.mock("lib/recents", () => ({ + default: { addFolder: vi.fn(), removeFile: vi.fn(), removeFolder: vi.fn() }, +})); +vi.mock("lib/settings", () => ({ + default: { value: { fileBrowser: { listFiles: false } } }, +})); +vi.mock("utils/copyEntry", () => ({ default: vi.fn() })); +vi.mock("utils/helpers", () => ({ + default: { + getIconForFile: () => "icon file", + getIconForFolder: () => "icon folder", + sortDir: (entries: unknown[]) => entries, + error: helpersError, + isFile: (type: string) => type === "file", + isDir: (type: string) => type === "dir", + }, +})); + +const ROOT = "file:///project"; + +beforeEach(() => { + vi.resetModules(); + fs.reads = []; + files.container = document.createElement("div"); + helpersError.mockClear(); + // tile() and other components use the app's global `tag`. + vi.stubGlobal("tag", tag); + vi.stubGlobal("strings", {}); + vi.stubGlobal("editorManager", { emit: vi.fn(), onupdate: vi.fn() }); + // Helpers the app gets from the html-tag-js polyfill and utils/polyfill. + for (const [name, value] of [ + [ + "get", + function (this: HTMLElement, selector: string) { + return this.querySelector(selector); + }, + ], + [ + "getAll", + function (this: HTMLElement, selector: string) { + return this.querySelectorAll(selector); + }, + ], + ] as const) { + if (!(name in HTMLElement.prototype)) { + Object.defineProperty(HTMLElement.prototype, name, { + configurable: true, + value, + }); + } + } + if (!("hashCode" in String.prototype)) { + Object.defineProperty(String.prototype, "hashCode", { + configurable: true, + value: () => 1, + }); + } +}); +afterEach(() => vi.unstubAllGlobals()); + +/** + * Open a root folder and toggle it expand → collapse → expand, leaving two + * directory reads pending: the stale one and the latest one. + */ +async function openAndToggle() { + const { default: openFolder } = await import("lib/openFolder"); + openFolder(ROOT, { name: "project", saveState: false, listFiles: false }); + const { $node: $root } = openFolder.find(ROOT); + + $root.expand(); + $root.collapse(); + $root.expand(); + expect(fs.reads).toHaveLength(2); + + const [stale, latest] = fs.reads; + return { $root, stale, latest }; +} + +const entry = (name: string) => ({ + name, + url: `${ROOT}/${name}`, + isFile: true, +}); + +const rendered = ($root: HTMLElement & { $ul: HTMLElement }) => + Array.from($root.$ul.querySelectorAll('[data-type="file"]')).map( + ($el) => $el.dataset.name, + ); + +const isLoading = ($root: HTMLElement & { $title: HTMLElement }) => + $root.$title.classList.contains("loading"); + +const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); + +it("renders only the latest tree when the stale read finishes first", async () => { + const { $root, stale, latest } = await openAndToggle(); + + stale.resolve([entry("stale.txt")]); + await flush(); + expect(rendered($root)).toEqual([]); + expect(isLoading($root)).toBe(true); + + latest.resolve([entry("latest.txt")]); + await flush(); + expect(rendered($root)).toEqual(["latest.txt"]); + expect(isLoading($root)).toBe(false); +}); + +it("renders only the latest tree when the stale read finishes last", async () => { + const { $root, stale, latest } = await openAndToggle(); + + latest.resolve([entry("latest.txt")]); + await flush(); + expect(rendered($root)).toEqual(["latest.txt"]); + expect(isLoading($root)).toBe(false); + + stale.resolve([entry("stale.txt")]); + await flush(); + expect(rendered($root)).toEqual(["latest.txt"]); + expect(isLoading($root)).toBe(false); +}); + +it("ignores a failed stale read instead of collapsing the folder", async () => { + const { $root, stale, latest } = await openAndToggle(); + + stale.reject(new Error("connection lost")); + await flush(); + expect($root.collapsed).toBe(false); + expect(helpersError).not.toHaveBeenCalled(); + expect(isLoading($root)).toBe(true); + + latest.resolve([entry("latest.txt")]); + await flush(); + expect(rendered($root)).toEqual(["latest.txt"]); + expect(isLoading($root)).toBe(false); +}); diff --git a/tests/unit/pluginFileRestoration.test.js b/tests/unit/pluginFileRestoration.test.js index 6b310c0..04268d0 100644 --- a/tests/unit/pluginFileRestoration.test.js +++ b/tests/unit/pluginFileRestoration.test.js @@ -629,6 +629,7 @@ it.each([ placeholder, createEditorReadOnlyExtension, blurEditorIfReadOnly, + canvasRenderer: false, loadingPreviews, manager: { activeFile: file }, touchSelectionController: null, diff --git a/tests/unit/quickToolsNavigation.test.ts b/tests/unit/quickToolsNavigation.test.ts index 2c9a786..cee72d8 100644 --- a/tests/unit/quickToolsNavigation.test.ts +++ b/tests/unit/quickToolsNavigation.test.ts @@ -7,6 +7,7 @@ import { runQuickToolKey, } from "cm/quickToolsNavigation"; import { afterEach, expect, test, vi } from "vitest"; +import { queueSelectionChange } from "../helpers/queueSelectionChange"; vi.hoisted(() => { Object.defineProperty(navigator, "platform", { @@ -23,6 +24,8 @@ vi.mock("utils/keyboardEvent", () => ({ const views: EditorView[] = []; +queueSelectionChange(); + afterEach(() => { views.splice(0).forEach((view) => view.destroy()); document.body.replaceChildren();