diff --git a/bun.lock b/bun.lock index 723e01f48..9eca137c8 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "com.foxdebug.acode", "dependencies": { + "@acode.app/canvas-view": "^0.0.1", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.0", "@codemirror/lang-angular": "^0.1.4", @@ -28,15 +29,15 @@ "@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.4", @@ -149,16 +150,18 @@ "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", + "@codemirror/state": "^6.7.6", + "@codemirror/view": "^6.43.14", "@lezer/common": "^1.5.4", "@lezer/lr": "^1.4.10", "tar": "^7.5.22", }, "packages": { + "@acode.app/canvas-view": ["@acode.app/canvas-view@0.0.1", "", { "dependencies": { "@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" } }, "sha512-brS4L24KCMJUOGXgzwSl2AVv2kpStleD85fLtEXaxPpn0jroRrXmyid2aSqZv4bitralRVu8huzhosDSl9HSww=="], + "@babel/code-frame": ["@babel/code-frame@8.0.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^8.0.0", "js-tokens": "^10.0.0" } }, "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw=="], "@babel/compat-data": ["@babel/compat-data@8.0.0", "", {}, "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw=="], @@ -411,7 +414,7 @@ "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.3", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ=="], - "@codemirror/language": ["@codemirror/language@6.12.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A=="], + "@codemirror/language": ["@codemirror/language@6.13.1", "", { "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", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw=="], "@codemirror/language-data": ["@codemirror/language-data@6.5.2", "", { "dependencies": { "@codemirror/lang-angular": "^0.1.0", "@codemirror/lang-cpp": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-go": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-less": "^6.0.0", "@codemirror/lang-liquid": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-php": "^6.0.0", "@codemirror/lang-python": "^6.0.0", "@codemirror/lang-rust": "^6.0.0", "@codemirror/lang-sass": "^6.0.0", "@codemirror/lang-sql": "^6.0.0", "@codemirror/lang-vue": "^0.1.1", "@codemirror/lang-wast": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.4.0" } }, "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg=="], @@ -423,11 +426,13 @@ "@codemirror/search": ["@codemirror/search@6.7.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA=="], - "@codemirror/state": ["@codemirror/state@6.7.1", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A=="], + "@codemirror/state": ["@codemirror/state@6.7.6", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w=="], + + "@codemirror/streamparser": ["@codemirror/streamparser@6.0.0", "", {}, "sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w=="], "@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="], - "@codemirror/view": ["@codemirror/view@6.43.9", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw=="], + "@codemirror/view": ["@codemirror/view@6.43.15", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw=="], "@deadlyjack/ajax": ["@deadlyjack/ajax@1.2.6", "", {}, "sha512-VwZU8YUflO2/V/dl3dluu+3jg8Ghz/W5fwxD5Z21OZXKeV73d+vStKVBe4wi+Av2KbTR35K7Z+5Q3iIpjB41MA=="], diff --git a/package-lock.json b/package-lock.json index e555bd51b..b2c1a39e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.13.5", "license": "MIT", "dependencies": { + "@acode.app/canvas-view": "^0.0.1", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.0", "@codemirror/lang-angular": "^0.1.4", @@ -32,15 +33,15 @@ "@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.4", @@ -170,6 +171,20 @@ "typescript-language-server": "^4.3.4" } }, + "node_modules/@acode.app/canvas-view": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@acode.app/canvas-view/-/canvas-view-0.0.1.tgz", + "integrity": "sha512-brS4L24KCMJUOGXgzwSl2AVv2kpStleD85fLtEXaxPpn0jroRrXmyid2aSqZv4bitralRVu8huzhosDSl9HSww==", + "license": "MIT", + "dependencies": { + "@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": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -3946,12 +3961,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", @@ -4026,14 +4042,20 @@ } }, "node_modules/@codemirror/state": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", - "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz", + "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==", "license": "MIT", "dependencies": { "@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", @@ -4047,9 +4069,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.43.9", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.9.tgz", - "integrity": "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==", + "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", diff --git a/package.json b/package.json index ba2e7d80e..1105b1ea2 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "ws": "^8.21.3" }, "dependencies": { + "@acode.app/canvas-view": "^0.0.1", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.0", "@codemirror/lang-angular": "^0.1.4", @@ -148,15 +149,15 @@ "@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.4", @@ -207,11 +208,11 @@ "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", + "@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/src/cm/baseExtensions.ts b/src/cm/baseExtensions.ts index d55a6c7bf..c429b2063 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 000000000..fdaaac54d --- /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 5d5188918..c31970947 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 { LSPPlugin } from "@codemirror/lsp-client"; import type { Range } from "@codemirror/state"; @@ -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 000000000..bf3a2b1e2 --- /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 767b740c1..07f5e1570 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 36b4e243d..6768b27b2 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/touchSelectionMenu.js b/src/cm/touchSelectionMenu.js index b017acb1f..7f45f0afa 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/lang/ar-ye.json b/src/lang/ar-ye.json index 67b21b3d7..4e79917b5 100644 --- a/src/lang/ar-ye.json +++ b/src/lang/ar-ye.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/az-az.json b/src/lang/az-az.json index 6285bb588..b7ee2ee9b 100644 --- a/src/lang/az-az.json +++ b/src/lang/az-az.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/be-by.json b/src/lang/be-by.json index 98601603c..4740aac8e 100644 --- a/src/lang/be-by.json +++ b/src/lang/be-by.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/bn-bd.json b/src/lang/bn-bd.json index c439ccecd..081131579 100644 --- a/src/lang/bn-bd.json +++ b/src/lang/bn-bd.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/cs-cz.json b/src/lang/cs-cz.json index f33b46844..002613c29 100644 --- a/src/lang/cs-cz.json +++ b/src/lang/cs-cz.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/de-de.json b/src/lang/de-de.json index c19f4e6f5..4b0ec3251 100644 --- a/src/lang/de-de.json +++ b/src/lang/de-de.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/en-us.json b/src/lang/en-us.json index 2869c6c2c..569c59521 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/es-sv.json b/src/lang/es-sv.json index a65e2b862..3ad4fd47e 100644 --- a/src/lang/es-sv.json +++ b/src/lang/es-sv.json @@ -887,5 +887,24 @@ "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" + "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." } diff --git a/src/lang/fr-fr.json b/src/lang/fr-fr.json index 5fc53c6f1..5542fc28d 100644 --- a/src/lang/fr-fr.json +++ b/src/lang/fr-fr.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/he-il.json b/src/lang/he-il.json index 358b04abc..8743df929 100644 --- a/src/lang/he-il.json +++ b/src/lang/he-il.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/hi-in.json b/src/lang/hi-in.json index 6aa97f71a..8cf91745b 100644 --- a/src/lang/hi-in.json +++ b/src/lang/hi-in.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/hu-hu.json b/src/lang/hu-hu.json index f2865688a..d6cab64f8 100644 --- a/src/lang/hu-hu.json +++ b/src/lang/hu-hu.json @@ -887,5 +887,24 @@ "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": "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." } diff --git a/src/lang/id-id.json b/src/lang/id-id.json index e21a31590..5e48e7d93 100644 --- a/src/lang/id-id.json +++ b/src/lang/id-id.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/index.d.ts b/src/lang/index.d.ts index 843418993..64eed6cb1 100644 --- a/src/lang/index.d.ts +++ b/src/lang/index.d.ts @@ -891,6 +891,25 @@ 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; }; declare var strings: LangStrings; diff --git a/src/lang/ir-fa.json b/src/lang/ir-fa.json index 07f6c056b..77126e16b 100644 --- a/src/lang/ir-fa.json +++ b/src/lang/ir-fa.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/it-it.json b/src/lang/it-it.json index efe7c90e3..1a4912f01 100644 --- a/src/lang/it-it.json +++ b/src/lang/it-it.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/ja-jp.json b/src/lang/ja-jp.json index 27fd7b603..b35bd225a 100644 --- a/src/lang/ja-jp.json +++ b/src/lang/ja-jp.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/ko-kr.json b/src/lang/ko-kr.json index f331beb33..5413bbd4b 100644 --- a/src/lang/ko-kr.json +++ b/src/lang/ko-kr.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/ln-ln.json b/src/lang/ln-ln.json index 63d4e9ce7..002ad7598 100644 --- a/src/lang/ln-ln.json +++ b/src/lang/ln-ln.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/ml-in.json b/src/lang/ml-in.json index 04db79163..7982934ab 100644 --- a/src/lang/ml-in.json +++ b/src/lang/ml-in.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/mm-unicode.json b/src/lang/mm-unicode.json index 6411d5b9b..5cf1d2e41 100644 --- a/src/lang/mm-unicode.json +++ b/src/lang/mm-unicode.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/mm-zawgyi.json b/src/lang/mm-zawgyi.json index dd8f456bf..b6ae8a405 100644 --- a/src/lang/mm-zawgyi.json +++ b/src/lang/mm-zawgyi.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/pl-pl.json b/src/lang/pl-pl.json index d5b418a9e..7bd662321 100644 --- a/src/lang/pl-pl.json +++ b/src/lang/pl-pl.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/pt-br.json b/src/lang/pt-br.json index aa6de4b0e..f8b6eba9d 100644 --- a/src/lang/pt-br.json +++ b/src/lang/pt-br.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/pu-in.json b/src/lang/pu-in.json index 05b389b8c..fa5d2b46b 100644 --- a/src/lang/pu-in.json +++ b/src/lang/pu-in.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/ru-ru.json b/src/lang/ru-ru.json index 79e83dddb..79b0196c5 100644 --- a/src/lang/ru-ru.json +++ b/src/lang/ru-ru.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/tl-ph.json b/src/lang/tl-ph.json index b5a5424fc..0e95b2f41 100644 --- a/src/lang/tl-ph.json +++ b/src/lang/tl-ph.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/tr-tr.json b/src/lang/tr-tr.json index 0d723b7ff..2dd36d406 100644 --- a/src/lang/tr-tr.json +++ b/src/lang/tr-tr.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/uk-ua.json b/src/lang/uk-ua.json index ab5da7210..fa2161245 100644 --- a/src/lang/uk-ua.json +++ b/src/lang/uk-ua.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/uz-uz.json b/src/lang/uz-uz.json index 986916b7e..c03876491 100644 --- a/src/lang/uz-uz.json +++ b/src/lang/uz-uz.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/vi-vn.json b/src/lang/vi-vn.json index 64441eb46..fdc61e41a 100644 --- a/src/lang/vi-vn.json +++ b/src/lang/vi-vn.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index d6c1737b6..39e9c7907 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/zh-hant.json b/src/lang/zh-hant.json index 6ad71367e..9bba93728 100644 --- a/src/lang/zh-hant.json +++ b/src/lang/zh-hant.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index c814718a2..54d8e9c22 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -887,5 +887,24 @@ "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." } diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index ea2b57329..d56f521b6 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -29,6 +29,14 @@ import { wrapWithAbbreviation, } from "@emmetio/codemirror6-plugin"; import createBaseExtensions from "cm/baseExtensions"; +import { + getCanvasBackend, + getCanvasDisplayOptions, + getCanvasFont, + getCanvasTheme, + markCanvasViewSynced, + syncCanvasView, +} from "cm/canvasOptions"; import { setKeyBindings as applyKeyBindings, executeCommand, @@ -38,6 +46,10 @@ import { registerExternalCommand, removeExternalCommand, } from "cm/commandRegistry"; +import createEditorView, { + isCanvasRendererSupported, + isCanvasView, +} from "cm/createEditorView"; import { blurEditorIfReadOnly, createEditorReadOnlyExtension, @@ -66,7 +78,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"; @@ -960,9 +974,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() { @@ -1010,10 +1035,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 }, @@ -1100,12 +1128,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() { @@ -1114,9 +1151,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() { @@ -1161,6 +1201,7 @@ async function EditorManager($header, $body) { keys: ["linenumbers", "relativeLineNumbers"], compartments: [lineNumberCompartment], build() { + if (canvasRenderer) return []; return makeLineNumberExtension(); }, }, @@ -1168,6 +1209,7 @@ async function EditorManager($header, $body) { keys: ["rainbowBrackets"], compartments: [rainbowCompartment], build() { + if (canvasRenderer) return []; return makeRainbowBracketExtension(); }, }, @@ -1176,7 +1218,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, @@ -1201,6 +1243,7 @@ async function EditorManager($header, $body) { keys: ["textWrap", "wrappingIndent"], compartments: [wrapCompartment], build() { + if (canvasRenderer) return []; return makeWrapExtension(); }, }, @@ -1216,6 +1259,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" }, @@ -1227,7 +1271,7 @@ async function EditorManager($header, $body) { compartments: [whitespaceCompartment], build() { const show = !!appSettings?.value?.showSpaces; - return show + return show && !canvasRenderer ? [ highlightWhitespace(), highlightTrailingWhitespace(), @@ -1241,7 +1285,7 @@ async function EditorManager($header, $body) { compartments: [lineBreakMarkerCompartment], build() { const showSpaces = !!appSettings?.value?.showSpaces; - return showSpaces ? lineBreakMarker : []; + return showSpaces && !canvasRenderer ? lineBreakMarker : []; }, }, { @@ -1249,7 +1293,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, @@ -1293,7 +1337,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 = { @@ -1377,9 +1421,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) { @@ -1675,7 +1819,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(), @@ -1685,10 +1829,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; @@ -1734,7 +1875,7 @@ async function EditorManager($header, $body) { }, }); - touchSelectionController = createTouchSelectionMenu(editor, { + touchSelectionController = createPaneTouchSelectionMenu(editor, { container: $container, getActiveFile: () => manager?.activeFile || null, isShiftSelectionActive, @@ -1772,6 +1913,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; @@ -2407,15 +2551,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, @@ -2836,7 +2981,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"}...`)] : []), @@ -2978,7 +3125,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 } @@ -3608,7 +3759,9 @@ async function EditorManager($header, $body) { appSettings.on("update:lintGutter", function (value) { lspClientManager.setOptions({ - diagnosticsUiExtension: lspDiagnosticsUiExtension(value !== false), + diagnosticsUiExtension: lspDiagnosticsUiExtension(value !== false, { + canvas: canvasRenderer, + }), }); const active = manager.activeFile; if (active?.type === "editor") { @@ -3656,6 +3809,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/settings.js b/src/lib/settings.js index de8d3b18b..1fbf774c0 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/settings/editorSettings.js b/src/settings/editorSettings.js index ec4c34b00..0ba895e3e 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/scrollSettings.js b/src/settings/scrollSettings.js index dbb72f4d3..cb5eb49f7 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/tests/unit/canvasOptions.test.ts b/tests/unit/canvasOptions.test.ts new file mode 100644 index 000000000..0b949f7aa --- /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 000000000..836b71a3c --- /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 0ae77e840..8893b2042 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/pluginFileRestoration.test.js b/tests/unit/pluginFileRestoration.test.js index 4589c1812..ed12bdd3c 100644 --- a/tests/unit/pluginFileRestoration.test.js +++ b/tests/unit/pluginFileRestoration.test.js @@ -627,6 +627,7 @@ it.each([ placeholder, createEditorReadOnlyExtension, blurEditorIfReadOnly, + canvasRenderer: false, loadingPreviews, manager: { activeFile: file }, touchSelectionController: null,