From 6cd119b23d88be8cf5d74e0daa7b487b848ec004 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:20:00 +0530 Subject: [PATCH 01/14] update deps --- bun.lock | 32 ++++++++++++++++++++++++++++++-- package-lock.json | 16 ++++++++-------- package.json | 2 +- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index d8d8a80b5..dd13a85f2 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.38.8", + "@codemirror/view": "^6.40.0", "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.0", "@lezer/highlight": "^1.2.3", @@ -409,7 +409,7 @@ "@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.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + "@codemirror/view": ["@codemirror/view@6.40.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg=="], "@deadlyjack/ajax": ["@deadlyjack/ajax@1.2.6", "", {}, "sha512-VwZU8YUflO2/V/dl3dluu+3jg8Ghz/W5fwxD5Z21OZXKeV73d+vStKVBe4wi+Av2KbTR35K7Z+5Q3iIpjB41MA=="], @@ -2057,6 +2057,32 @@ "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@codemirror/autocomplete/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/commands/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/lang-html/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/lang-javascript/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/lang-liquid/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/lang-markdown/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/lang-xml/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/language/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/lint/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/lsp-client/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/search/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/theme-one-dark/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + + "@codemirror/view/@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.65.0", "", { "dependencies": { "@jsonjoy.com/base64": "17.65.0", "@jsonjoy.com/buffers": "17.65.0", "@jsonjoy.com/codegen": "17.65.0", "@jsonjoy.com/json-pointer": "17.65.0", "@jsonjoy.com/util": "17.65.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ=="], "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util": ["@jsonjoy.com/util@17.65.0", "", { "dependencies": { "@jsonjoy.com/buffers": "17.65.0", "@jsonjoy.com/codegen": "17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w=="], @@ -2125,6 +2151,8 @@ "chevrotain-allstar/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "codemirror/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "compression/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], diff --git a/package-lock.json b/package-lock.json index 70506ef61..749412e9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.38.8", + "@codemirror/view": "^6.40.0", "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.0", "@lezer/highlight": "^1.2.3", @@ -2385,9 +2385,9 @@ } }, "node_modules/@codemirror/state": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", - "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" @@ -2406,12 +2406,12 @@ } }, "node_modules/@codemirror/view": { - "version": "6.39.15", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz", - "integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==", + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", "license": "MIT", "dependencies": { - "@codemirror/state": "^6.5.0", + "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" diff --git a/package.json b/package.json index 2b76ad418..2f59aa596 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.38.8", + "@codemirror/view": "^6.40.0", "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.0", "@lezer/highlight": "^1.2.3", From 7a57f5b9e1cf1de9c0285b1cd72df7bf1207ea39 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:25:33 +0530 Subject: [PATCH 02/14] fix: native teardrop thing --- src/cm/touchSelectionMenu.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cm/touchSelectionMenu.js b/src/cm/touchSelectionMenu.js index 6c7e0cfb8..7c186acbb 100644 --- a/src/cm/touchSelectionMenu.js +++ b/src/cm/touchSelectionMenu.js @@ -621,7 +621,12 @@ class TouchSelectionMenuController { return; } - this.#moveCursorToCoords(clientX, clientY); + const moved = this.#moveCursorToCoords(clientX, clientY); + if (moved != null) { + event.preventDefault(); + event.stopPropagation(); + this.#view.focus(); + } this.#selectionActive = false; this.#hideMenu(); this.#removeSelectionHandles(); From 614204077453c149792e598d05c950c14396b83c Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:37:15 +0530 Subject: [PATCH 03/14] fix: initialisation setting issue --- src/cm/lsp/clientManager.ts | 86 ++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src/cm/lsp/clientManager.ts b/src/cm/lsp/clientManager.ts index 409272097..8a0e70734 100644 --- a/src/cm/lsp/clientManager.ts +++ b/src/cm/lsp/clientManager.ts @@ -36,6 +36,7 @@ import type { ParsedUri, RootUriContext, TextEdit, + Transport, TransportHandle, } from "./types"; import AcodeWorkspace from "./workspace"; @@ -61,6 +62,79 @@ function safeString(value: unknown): string { return value != null ? String(value) : ""; } +function isPlainObject(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +function resolveInitializationOptions( + server: LspServerDefinition, + clientConfig: Record, +): Record | undefined { + const serverOptions = isPlainObject(server.initializationOptions) + ? server.initializationOptions + : null; + const clientOptions = isPlainObject(clientConfig.initializationOptions) + ? clientConfig.initializationOptions + : null; + + if (serverOptions && clientOptions) { + return { + ...serverOptions, + ...clientOptions, + }; + } + + return serverOptions || clientOptions || undefined; +} + +interface InternalLSPRequest { + promise: Promise; +} + +type RequestInnerFn = ( + method: string, + params: Params, + mapped?: boolean, +) => InternalLSPRequest; + +function connectClient( + client: ExtendedLSPClient, + transport: Transport, + initializationOptions?: Record, +): void { + if (!initializationOptions || !Object.keys(initializationOptions).length) { + client.connect(transport); + return; + } + + const patchedClient = client as unknown as { + requestInner: RequestInnerFn; + }; + const originalRequestInner = patchedClient.requestInner.bind( + patchedClient, + ) as RequestInnerFn; + + patchedClient.requestInner = function patchedRequestInner( + method: string, + params: Params, + mapped?: boolean, + ): InternalLSPRequest { + if (method === "initialize" && isPlainObject(params)) { + params = { + ...params, + initializationOptions, + } as Params; + } + return originalRequestInner(method, params, mapped); + }; + + try { + client.connect(transport); + } finally { + patchedClient.requestInner = originalRequestInner; + } +} + interface BuiltinExtensionsResult { extensions: Extension[]; diagnosticsExtension: Extension | LSPClientExtension | null; @@ -431,6 +505,10 @@ export class LspClientManager { }; const clientConfig = { ...(server.clientConfig ?? {}) }; + const initializationOptions = resolveInitializationOptions( + server, + clientConfig as Record, + ); const builtinConfig = clientConfig.builtinExtensions ?? {}; const useDefaultExtensions = clientConfig.useDefaultExtensions !== false; const { extensions: defaultExtensions, diagnosticsExtension } = @@ -695,7 +773,7 @@ export class LspClientManager { }); await transportHandle.ready; client = new LSPClient(clientConfig) as ExtendedLSPClient; - client.connect(transportHandle.transport); + connectClient(client, transportHandle.transport, initializationOptions); await client.initializing; if (!client.__acodeLoggedInfo) { // Log root URI info to console @@ -710,6 +788,12 @@ export class LspClientManager { } else if (originalRootUri) { console.info(`[LSP:${server.id}] root ignored`, originalRootUri); } + if (initializationOptions) { + console.info( + `[LSP:${server.id}] initializationOptions keys`, + Object.keys(initializationOptions), + ); + } console.info(`[LSP:${server.id}] initialized`); client.__acodeLoggedInfo = true; } From c8e14fe60a2bb59bca1961317460b57479f7b292 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:17:49 +0530 Subject: [PATCH 04/14] revert --- src/cm/touchSelectionMenu.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/cm/touchSelectionMenu.js b/src/cm/touchSelectionMenu.js index 7c186acbb..a5f45f8ba 100644 --- a/src/cm/touchSelectionMenu.js +++ b/src/cm/touchSelectionMenu.js @@ -621,12 +621,6 @@ class TouchSelectionMenuController { return; } - const moved = this.#moveCursorToCoords(clientX, clientY); - if (moved != null) { - event.preventDefault(); - event.stopPropagation(); - this.#view.focus(); - } this.#selectionActive = false; this.#hideMenu(); this.#removeSelectionHandles(); From c09dc06b9ee4a5c0773b80df3a5f165604a7ccd2 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:21:50 +0530 Subject: [PATCH 05/14] fix --- src/cm/touchSelectionMenu.js | 24 ++++++++++++++++++++---- src/lib/editorManager.js | 13 ++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/cm/touchSelectionMenu.js b/src/cm/touchSelectionMenu.js index a5f45f8ba..18e7efdc6 100644 --- a/src/cm/touchSelectionMenu.js +++ b/src/cm/touchSelectionMenu.js @@ -149,6 +149,8 @@ class TouchSelectionMenuController { #selectionActive = false; #menuActive = false; #enabled = true; + #touchListenersAttached = false; + #touchMovePassive = true; #handlingMenuAction = false; #pendingPointerTriggered = false; #pendingSelectionChanged = false; @@ -494,7 +496,7 @@ class TouchSelectionMenuController { longPressFired: false, }; - this.#addTouchListeners(); + this.#addTouchListeners({ passiveMove: true }); this.#clearLongPress(); this.#longPressTimer = setTimeout(() => { if (!this.#touchSession || this.#touchSession.moved) return; @@ -627,18 +629,32 @@ class TouchSelectionMenuController { this.#showCursorHandle(); }; - #addTouchListeners() { + #addTouchListeners({ passiveMove = true } = {}) { + if ( + this.#touchListenersAttached && + this.#touchMovePassive === passiveMove + ) { + return; + } + if (this.#touchListenersAttached) { + this.#removeTouchListeners(); + } + this.#touchMovePassive = passiveMove; document.addEventListener("touchmove", this.#onTouchMove, { - passive: false, + passive: passiveMove, }); document.addEventListener("touchend", this.#onTouchEnd, { passive: false, }); + this.#touchListenersAttached = true; } #removeTouchListeners() { + if (!this.#touchListenersAttached) return; document.removeEventListener("touchmove", this.#onTouchMove); document.removeEventListener("touchend", this.#onTouchEnd); + this.#touchListenersAttached = false; + this.#touchMovePassive = true; } #clearLongPress() { @@ -1047,7 +1063,7 @@ class TouchSelectionMenuController { }; this.#pointer.x = x; this.#pointer.y = y; - this.#addTouchListeners(); + this.#addTouchListeners({ passiveMove: false }); } #dragTo(x, y) { diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 2170f6a10..bc034d438 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -233,7 +233,6 @@ async function EditorManager($header, $body) { update.selectionSet || update.docChanged || update.geometryChanged || - update.viewportChanged || pointerTriggered ) { cancelAnimationFrame(touchSelectionSyncRaf); @@ -1771,13 +1770,21 @@ async function EditorManager($header, $body) { let checkTimeout = null; let autosaveTimeout; let scrollTimeout; + let scrollSyncRaf = 0; const scroller = editor.scrollDOM; - function handleEditorScroll() { - if (!scroller) return; + function syncScrollUi() { + scrollSyncRaf = 0; onscrolltop(); onscrollleft(); touchSelectionController?.onScroll(); + } + + function handleEditorScroll() { + if (!scroller) return; + if (!scrollSyncRaf) { + scrollSyncRaf = requestAnimationFrame(syncScrollUi); + } clearTimeout(scrollTimeout); isScrolling = true; scrollTimeout = setTimeout(() => { From 33d784ed8955783cde29eb8da29b624e42a2d7d6 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:38:43 +0530 Subject: [PATCH 06/14] fix indent guide setting issue --- src/cm/mainEditorExtensions.ts | 56 ++++++++++++++++++++++++++++++++++ src/lib/editorManager.js | 51 +++++++++++++------------------ src/lib/settings.js | 1 + 3 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 src/cm/mainEditorExtensions.ts diff --git a/src/cm/mainEditorExtensions.ts b/src/cm/mainEditorExtensions.ts new file mode 100644 index 000000000..8ddf9c68d --- /dev/null +++ b/src/cm/mainEditorExtensions.ts @@ -0,0 +1,56 @@ +import type { Extension } from "@codemirror/state"; +import { EditorView, scrollPastEnd } from "@codemirror/view"; + +interface MainEditorExtensionOptions { + emmetExtensions?: Extension[]; + baseExtensions?: Extension[]; + commandKeymapExtension?: Extension; + themeExtension?: Extension; + pointerCursorVisibilityExtension?: Extension; + shiftClickSelectionExtension?: Extension; + touchSelectionUpdateExtension?: Extension; + searchExtension?: Extension; + readOnlyExtension?: Extension; + optionExtensions?: Extension[]; +} + +function pushExtension(target: Extension[], extension?: Extension): void { + if (extension == null) return; + target.push(extension); +} + +export const fixedHeightTheme = EditorView.theme({ + "&": { height: "100%" }, + ".cm-scroller": { height: "100%", overflow: "auto" }, +}); + +export function createMainEditorExtensions( + options: MainEditorExtensionOptions = {}, +): Extension[] { + const extensions: Extension[] = []; + + if (options.emmetExtensions?.length) { + extensions.push(...options.emmetExtensions); + } + if (options.baseExtensions?.length) { + extensions.push(...options.baseExtensions); + } + + pushExtension(extensions, options.commandKeymapExtension); + pushExtension(extensions, options.themeExtension); + extensions.push(fixedHeightTheme); + extensions.push(scrollPastEnd()); + pushExtension(extensions, options.pointerCursorVisibilityExtension); + pushExtension(extensions, options.shiftClickSelectionExtension); + pushExtension(extensions, options.touchSelectionUpdateExtension); + pushExtension(extensions, options.searchExtension); + pushExtension(extensions, options.readOnlyExtension); + + if (options.optionExtensions?.length) { + extensions.push(...options.optionExtensions); + } + + return extensions; +} + +export default createMainEditorExtensions; diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index bc034d438..f2360b344 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -16,7 +16,6 @@ import { highlightWhitespace, keymap, lineNumbers, - scrollPastEnd, } from "@codemirror/view"; import { abbreviationTracker, @@ -27,6 +26,7 @@ import { wrapWithAbbreviation, } from "@emmetio/codemirror6-plugin"; import createBaseExtensions from "cm/baseExtensions"; +import createMainEditorExtensions from "cm/mainEditorExtensions"; import { setKeyBindings as applyKeyBindings, executeCommand, @@ -163,12 +163,6 @@ async function EditorManager($header, $body) { }, }); - // Make CodeMirror fill the container height and manage scrolling internally - const fixedHeightTheme = EditorView.theme({ - "&": { height: "100%" }, - ".cm-scroller": { height: "100%", overflow: "auto" }, - }); - const pointerCursorVisibilityExtension = EditorView.updateListener.of( (update) => { if (!update.selectionSet) return; @@ -764,24 +758,25 @@ async function EditorManager($header, $body) { // Create minimal CodeMirror editor const editorState = EditorState.create({ doc: "", - extensions: [ + extensions: createMainEditorExtensions({ // Emmet needs highest precedence so place before default keymaps - ...createEmmetExtensionSet({ syntax: EmmetKnownSyntax.html }), - ...createBaseExtensions(), - getCommandKeymapExtension(), - // Default theme - themeCompartment.of(oneDark), - fixedHeightTheme, - scrollPastEnd(), + emmetExtensions: createEmmetExtensionSet({ + syntax: EmmetKnownSyntax.html, + }), + baseExtensions: createBaseExtensions(), + commandKeymapExtension: getCommandKeymapExtension(), + themeExtension: themeCompartment.of(oneDark), pointerCursorVisibilityExtension, shiftClickSelectionExtension, touchSelectionUpdateExtension, - search(), + searchExtension: search(), // Ensure read-only can be toggled later via compartment - readOnlyCompartment.of(EditorState.readOnly.of(false)), + readOnlyExtension: readOnlyCompartment.of( + EditorState.readOnly.of(false), + ), // Editor options driven by settings via compartments - ...getBaseExtensionsFromOptions(), - ], + optionExtensions: getBaseExtensionsFromOptions(), + }), }); const editor = new EditorView({ @@ -1128,22 +1123,20 @@ async function EditorManager($header, $body) { function applyFileToEditor(file) { if (!file || file.type !== "editor") return; const syntax = getEmmetSyntaxForFile(file); - const baseExtensions = [ + const baseExtensions = createMainEditorExtensions({ // Emmet needs to precede default keymaps so tracker Tab wins over indent - ...createEmmetExtensionSet({ syntax }), - ...createBaseExtensions(), - getCommandKeymapExtension(), + emmetExtensions: createEmmetExtensionSet({ syntax }), + baseExtensions: createBaseExtensions(), + commandKeymapExtension: getCommandKeymapExtension(), // keep compartment in the state to allow dynamic theme changes later - themeCompartment.of(oneDark), - fixedHeightTheme, - scrollPastEnd(), + themeExtension: themeCompartment.of(oneDark), pointerCursorVisibilityExtension, shiftClickSelectionExtension, touchSelectionUpdateExtension, - search(), + searchExtension: search(), // Keep dynamic compartments across state swaps - ...getBaseExtensionsFromOptions(), - ]; + optionExtensions: getBaseExtensionsFromOptions(), + }); const exts = [...baseExtensions]; maybeAttachEmmetCompletions(exts, syntax); try { diff --git a/src/lib/settings.js b/src/lib/settings.js index 3ef5adeb4..d25859742 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -179,6 +179,7 @@ class Settings { showSponsorSidebarApp: true, showAnnotations: false, lintGutter: true, + indentGuides: true, rainbowBrackets: true, pluginsDisabled: {}, // pluginId: true/false lsp: { From be461a7bd9fab4d40acf43324ef7c6f1f7b214f4 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:38:56 +0530 Subject: [PATCH 07/14] fix --- src/lib/editorManager.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index f2360b344..221843d2e 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -26,7 +26,6 @@ import { wrapWithAbbreviation, } from "@emmetio/codemirror6-plugin"; import createBaseExtensions from "cm/baseExtensions"; -import createMainEditorExtensions from "cm/mainEditorExtensions"; import { setKeyBindings as applyKeyBindings, executeCommand, @@ -45,6 +44,7 @@ import { lspDiagnosticsUiExtension, } from "cm/lsp/diagnostics"; import { stopManagedServer } from "cm/lsp/serverLauncher"; +import createMainEditorExtensions from "cm/mainEditorExtensions"; // CodeMirror mode management import { getModeForPath, @@ -771,9 +771,7 @@ async function EditorManager($header, $body) { touchSelectionUpdateExtension, searchExtension: search(), // Ensure read-only can be toggled later via compartment - readOnlyExtension: readOnlyCompartment.of( - EditorState.readOnly.of(false), - ), + readOnlyExtension: readOnlyCompartment.of(EditorState.readOnly.of(false)), // Editor options driven by settings via compartments optionExtensions: getBaseExtensionsFromOptions(), }), From a3232296a46b4f57cf26d3a415619d951e4a7a29 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:55:09 +0530 Subject: [PATCH 08/14] rework the indent guides --- src/cm/indentGuides.ts | 199 ++++++++++++++++----------------------- src/test/editor.tests.js | 14 +++ 2 files changed, 95 insertions(+), 118 deletions(-) diff --git a/src/cm/indentGuides.ts b/src/cm/indentGuides.ts index fb3324bfc..6f84533b7 100644 --- a/src/cm/indentGuides.ts +++ b/src/cm/indentGuides.ts @@ -1,4 +1,4 @@ -import { syntaxTree } from "@codemirror/language"; +import { getIndentUnit, syntaxTree } from "@codemirror/language"; import type { Extension } from "@codemirror/state"; import { EditorState, RangeSetBuilder } from "@codemirror/state"; import { @@ -7,7 +7,6 @@ import { EditorView, ViewPlugin, type ViewUpdate, - WidgetType, } from "@codemirror/view"; import type { SyntaxNode } from "@lezer/common"; @@ -26,11 +25,23 @@ const defaultConfig: Required = { hideOnBlankLines: false, }; +const GUIDE_MARK_CLASS = "cm-indent-guides"; + /** * Get the tab size from editor state */ function getTabSize(state: EditorState): number { - return state.facet(EditorState.tabSize); + const tabSize = state.facet(EditorState.tabSize); + return Number.isFinite(tabSize) && tabSize > 0 ? tabSize : 4; +} + +/** + * Resolve the indentation width used for guide spacing. + */ +function getIndentUnitColumns(state: EditorState): number { + const width = getIndentUnit(state); + if (Number.isFinite(width) && width > 0) return width; + return getTabSize(state); } /** @@ -57,6 +68,21 @@ function isBlankLine(line: string): boolean { return /^\s*$/.test(line); } +/** + * Count the leading indentation characters of a line. + */ +function getLeadingWhitespaceLength(line: string): number { + let count = 0; + for (const ch of line) { + if (ch === " " || ch === "\t") { + count++; + continue; + } + break; + } + return count; +} + /** * Node types that represent scope blocks in various languages */ @@ -83,7 +109,6 @@ const SCOPE_NODE_TYPES = new Set([ "Element", "SelfClosingTag", "RuleSet", - "Block", "DeclarationList", "Body", "Suite", @@ -114,15 +139,12 @@ function getActiveScope( const tree = syntaxTree(state); if (!tree || tree.length === 0) { - // No syntax tree available, fall back to indentation-based return getActiveScopeByIndentation(state, indentUnit); } - // Find the innermost scope node containing the cursor let scopeNode: SyntaxNode | null = null; let node: SyntaxNode | null = tree.resolveInner(cursorPos, 0); - // Walk up the tree to find a scope-defining node while (node) { if (SCOPE_NODE_TYPES.has(node.name)) { scopeNode = node; @@ -135,12 +157,8 @@ function getActiveScope( return null; } - // Get the line range of this scope const startLine = state.doc.lineAt(scopeNode.from); const endLine = state.doc.lineAt(scopeNode.to); - - // Calculate indent level from the first line of the scope's content - // (usually the line after the opening bracket) let contentStartLine = startLine.number; if (startLine.number < endLine.number) { contentStartLine = startLine.number + 1; @@ -149,7 +167,6 @@ function getActiveScope( const tabSize = getTabSize(state); let level = 0; - // Find the first non-blank line inside the scope to determine indent level for (let ln = contentStartLine; ln <= endLine.number; ln++) { const line = state.doc.line(ln); if (!isBlankLine(line.text)) { @@ -228,56 +245,31 @@ function getActiveScopeByIndentation( return { level: cursorLevel, startLine, endLine }; } -/** - * Widget that renders indent guide lines - */ -class IndentGuidesWidget extends WidgetType { - constructor( - readonly levels: number, - readonly indentUnit: number, - readonly activeGuideIndex: number, - readonly lineHeight: number, - ) { - super(); - } - - eq(other: IndentGuidesWidget): boolean { - return ( - other.levels === this.levels && - other.indentUnit === this.indentUnit && - other.activeGuideIndex === this.activeGuideIndex && - other.lineHeight === this.lineHeight - ); - } - - toDOM(): HTMLElement { - const container = document.createElement("span"); - container.className = "cm-indent-guides-wrapper"; - container.setAttribute("aria-hidden", "true"); - - const guidesContainer = document.createElement("span"); - guidesContainer.className = "cm-indent-guides"; - - for (let i = 0; i < this.levels; i++) { - const guide = document.createElement("span"); - guide.className = "cm-indent-guide"; - guide.style.left = `${i * this.indentUnit}ch`; - guide.style.height = `${this.lineHeight}px`; - - if (i === this.activeGuideIndex) { - guide.classList.add("cm-indent-guide-active"); - } - - guidesContainer.appendChild(guide); - } - - container.appendChild(guidesContainer); - return container; +function buildGuideStyle( + levels: number, + guideStepPx: number, + activeGuideIndex: number, +): string { + const images = []; + const positions = []; + const sizes = []; + + for (let i = 0; i < levels; i++) { + const color = + i === activeGuideIndex + ? "var(--indent-guide-active-color)" + : "var(--indent-guide-color)"; + images.push(`linear-gradient(${color}, ${color})`); + positions.push(`${i * guideStepPx}px 0`); + sizes.push("1px 100%"); } - ignoreEvent(): boolean { - return true; - } + return [ + `background-image:${images.join(",")}`, + "background-repeat:no-repeat", + `background-position:${positions.join(",")}`, + `background-size:${sizes.join(",")}`, + ].join(";"); } /** @@ -290,16 +282,13 @@ function buildDecorations( const builder = new RangeSetBuilder(); const { state } = view; const tabSize = getTabSize(state); - const indentUnit = tabSize; + const indentUnit = getIndentUnitColumns(state); + const guideStepPx = Math.max(view.defaultCharacterWidth * indentUnit, 1); - // Get active scope using syntax tree (or fallback to indentation) const activeScope = config.highlightActiveGuide ? getActiveScope(view, indentUnit) : null; - const lineHeight = view.defaultLineHeight; - - // Only process visible lines for performance for (const { from: blockFrom, to: blockTo } of view.visibleRanges) { const startLine = state.doc.lineAt(blockFrom); const endLine = state.doc.lineAt(blockTo); @@ -314,34 +303,30 @@ function buildDecorations( const indentColumns = getLineIndentation(lineText, tabSize); const levels = Math.floor(indentColumns / indentUnit); - - if (levels > 0) { - let activeGuideIndex = -1; - - // Check if this line is in the active scope - if ( - activeScope && - lineNum >= activeScope.startLine && - lineNum <= activeScope.endLine && - levels >= activeScope.level - ) { - activeGuideIndex = activeScope.level - 1; - } - - const widget = new IndentGuidesWidget( - levels, - indentUnit, - activeGuideIndex, - lineHeight, - ); - - const deco = Decoration.widget({ - widget, - side: -1, - }); - - builder.add(line.from, line.from, deco); + if (levels <= 0) continue; + const leadingWhitespaceLength = getLeadingWhitespaceLength(lineText); + if (leadingWhitespaceLength <= 0) continue; + + let activeGuideIndex = -1; + if ( + activeScope && + lineNum >= activeScope.startLine && + lineNum <= activeScope.endLine && + levels >= activeScope.level + ) { + activeGuideIndex = activeScope.level - 1; } + + builder.add( + line.from, + line.from + leadingWhitespaceLength, + Decoration.mark({ + attributes: { + class: GUIDE_MARK_CLASS, + style: buildGuideStyle(levels, guideStepPx, activeGuideIndex), + }, + }), + ); } } @@ -366,7 +351,6 @@ function createIndentGuidesPlugin( } update(update: ViewUpdate): void { - // Only rebuild when necessary if ( update.docChanged || update.viewportChanged || @@ -384,34 +368,13 @@ function createIndentGuidesPlugin( } /** - * Theme for indent guides with subtle animation + * Theme for indent guides. + * Uses a single span around leading indentation instead of per-guide widgets. */ const indentGuidesTheme = EditorView.baseTheme({ - ".cm-indent-guides-wrapper": { - display: "inline", - position: "relative", - width: "0", - height: "0", - overflow: "visible", - verticalAlign: "top", - }, ".cm-indent-guides": { - position: "absolute", - top: "0", - left: "0", - height: "100%", - pointerEvents: "none", - zIndex: "0", - }, - ".cm-indent-guide": { - position: "absolute", - top: "0", - width: "1px", - background: "var(--indent-guide-color)", - transition: "background 0.15s ease, opacity 0.15s ease", - }, - ".cm-indent-guide-active": { - background: "var(--indent-guide-active-color)", + display: "inline-block", + verticalAlign: "top", }, "&": { "--indent-guide-color": "rgba(128, 128, 128, 0.25)", diff --git a/src/test/editor.tests.js b/src/test/editor.tests.js index a1e687692..d382add49 100644 --- a/src/test/editor.tests.js +++ b/src/test/editor.tests.js @@ -9,6 +9,7 @@ import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; import { EditorSelection, EditorState } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import createBaseExtensions from "cm/baseExtensions"; +import indentGuides from "cm/indentGuides"; import { getEdgeScrollDirections } from "cm/touchSelectionMenu"; import { TestRunner } from "./tester"; @@ -417,6 +418,19 @@ export async function runCodeMirrorTests(writeOutput) { }); }); + runner.test("Indent guides render as indentation spans", async (test) => { + const doc = "function x() {\n if (true) {\n return 1;\n }\n}"; + await withEditor(test, async (view) => { + const guideLine = view.dom.querySelector(".cm-indent-guides"); + const legacyWidget = view.dom.querySelector(".cm-indent-guides-wrapper"); + test.assert(guideLine != null, "Indent guide span should exist"); + test.assert( + legacyWidget == null, + "Indent guides should not create widget wrapper DOM", + ); + }, doc, [indentGuides()]); + }); + runner.test("Focus and blur", async (test) => { await withEditor(test, async (view) => { view.focus(); From a92ffff94ecc956b7c2568b66f823d2df6a22a97 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:57:11 +0530 Subject: [PATCH 09/14] fix: formatting --- src/test/editor.tests.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/test/editor.tests.js b/src/test/editor.tests.js index d382add49..b039524be 100644 --- a/src/test/editor.tests.js +++ b/src/test/editor.tests.js @@ -420,15 +420,22 @@ export async function runCodeMirrorTests(writeOutput) { runner.test("Indent guides render as indentation spans", async (test) => { const doc = "function x() {\n if (true) {\n return 1;\n }\n}"; - await withEditor(test, async (view) => { - const guideLine = view.dom.querySelector(".cm-indent-guides"); - const legacyWidget = view.dom.querySelector(".cm-indent-guides-wrapper"); - test.assert(guideLine != null, "Indent guide span should exist"); - test.assert( - legacyWidget == null, - "Indent guides should not create widget wrapper DOM", - ); - }, doc, [indentGuides()]); + await withEditor( + test, + async (view) => { + const guideLine = view.dom.querySelector(".cm-indent-guides"); + const legacyWidget = view.dom.querySelector( + ".cm-indent-guides-wrapper", + ); + test.assert(guideLine != null, "Indent guide span should exist"); + test.assert( + legacyWidget == null, + "Indent guides should not create widget wrapper DOM", + ); + }, + doc, + [indentGuides()], + ); }); runner.test("Focus and blur", async (test) => { From 173a2eb0cf512b78f5d82c05232299b511e8dd8e Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:31:25 +0530 Subject: [PATCH 10/14] update deps --- bun.lock | 69 ++++++++++++++++++++--------------------------- package-lock.json | 54 ++++++++++++++++++------------------- package.json | 25 +++++++++++------ 3 files changed, 73 insertions(+), 75 deletions(-) diff --git a/bun.lock b/bun.lock index dd13a85f2..b2682259c 100644 --- a/bun.lock +++ b/bun.lock @@ -4,14 +4,14 @@ "": { "name": "com.foxdebug.acode", "dependencies": { - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.0", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", @@ -21,13 +21,13 @@ "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.11.3", + "@codemirror/language": "^6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/lsp-client": "^6.2.1", - "@codemirror/search": "^6.5.11", - "@codemirror/state": "^6.5.2", + "@codemirror/lint": "^6.9.5", + "@codemirror/lsp-client": "^6.2.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", "@deadlyjack/ajax": "^1.2.6", @@ -122,6 +122,15 @@ }, }, }, + "overrides": { + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", + "@codemirror/language": "^6.12.2", + "@codemirror/lint": "^6.9.5", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.40.0", + }, "packages": { "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], @@ -347,9 +356,9 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], - "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="], - "@codemirror/commands": ["@codemirror/commands@6.10.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q=="], + "@codemirror/commands": ["@codemirror/commands@6.10.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q=="], "@codemirror/lang-angular": ["@codemirror/lang-angular@0.1.4", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.3" } }, "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g=="], @@ -363,7 +372,7 @@ "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="], - "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], "@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="], @@ -393,19 +402,19 @@ "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "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-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="], - "@codemirror/language": ["@codemirror/language@6.12.1", "", { "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-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ=="], + "@codemirror/language": ["@codemirror/language@6.12.2", "", { "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-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="], "@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=="], "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="], - "@codemirror/lint": ["@codemirror/lint@6.9.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ=="], + "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="], - "@codemirror/lsp-client": ["@codemirror/lsp-client@6.2.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.20.0", "@codemirror/language": "^6.11.0", "@codemirror/lint": "^6.8.5", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.37.0", "@lezer/highlight": "^1.2.1", "marked": "^15.0.12", "vscode-languageserver-protocol": "^3.17.5" } }, "sha512-fjEkEc+H0kG60thaybj5+UpSnt49yAaTzOLSYZC2wlhwNAtDsWO2uZnE2AXiRGQxBVDQBvVj01MsX3F/0Vivjg=="], + "@codemirror/lsp-client": ["@codemirror/lsp-client@6.2.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.20.0", "@codemirror/language": "^6.11.0", "@codemirror/lint": "^6.8.5", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.37.0", "@lezer/highlight": "^1.2.1", "marked": "^15.0.12", "vscode-languageserver-protocol": "^3.17.5" } }, "sha512-swTF98necGzfwswEIc9hAFWpNKa/Qe1PzTi8rqJ/5pKnvCwN8nbhXsROtjz2kwgkC+MBoL+OEcSKILa60xUaZw=="], - "@codemirror/search": ["@codemirror/search@6.5.11", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "crelt": "^1.0.5" } }, "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA=="], + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], - "@codemirror/state": ["@codemirror/state@6.5.3", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A=="], + "@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="], "@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=="], @@ -2057,31 +2066,13 @@ "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - "@codemirror/autocomplete/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/commands/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/lang-html/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/lang-javascript/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/lang-liquid/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/lang-markdown/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + "@codemirror/lang-angular/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], - "@codemirror/lang-xml/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + "@codemirror/lang-html/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], - "@codemirror/language/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], + "@codemirror/lang-vue/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], - "@codemirror/lint/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/lsp-client/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/search/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/theme-one-dark/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - - "@codemirror/view/@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="], + "@codemirror/language-data/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.65.0", "", { "dependencies": { "@jsonjoy.com/base64": "17.65.0", "@jsonjoy.com/buffers": "17.65.0", "@jsonjoy.com/codegen": "17.65.0", "@jsonjoy.com/json-pointer": "17.65.0", "@jsonjoy.com/util": "17.65.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ=="], @@ -2151,8 +2142,6 @@ "chevrotain-allstar/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - "codemirror/@codemirror/view": ["@codemirror/view@6.39.9", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA=="], - "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "compression/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], diff --git a/package-lock.json b/package-lock.json index c1b1e03bc..106795b82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "1.11.8", "license": "MIT", "dependencies": { - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.0", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", @@ -26,13 +26,13 @@ "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.11.3", + "@codemirror/language": "^6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/lsp-client": "^6.2.1", - "@codemirror/search": "^6.5.11", - "@codemirror/state": "^6.5.2", + "@codemirror/lint": "^6.9.5", + "@codemirror/lsp-client": "^6.2.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", "@deadlyjack/ajax": "^1.2.6", @@ -1992,9 +1992,9 @@ "license": "Apache-2.0" }, "node_modules/@codemirror/autocomplete": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", - "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2004,13 +2004,13 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", - "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } @@ -2093,9 +2093,9 @@ } }, "node_modules/@codemirror/lang-javascript": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", - "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -2293,9 +2293,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", - "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -2347,9 +2347,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.4.tgz", - "integrity": "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==", + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -2358,9 +2358,9 @@ } }, "node_modules/@codemirror/lsp-client": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/lsp-client/-/lsp-client-6.2.1.tgz", - "integrity": "sha512-fjEkEc+H0kG60thaybj5+UpSnt49yAaTzOLSYZC2wlhwNAtDsWO2uZnE2AXiRGQxBVDQBvVj01MsX3F/0Vivjg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lsp-client/-/lsp-client-6.2.2.tgz", + "integrity": "sha512-swTF98necGzfwswEIc9hAFWpNKa/Qe1PzTi8rqJ/5pKnvCwN8nbhXsROtjz2kwgkC+MBoL+OEcSKILa60xUaZw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.20.0", diff --git a/package.json b/package.json index 2f59aa596..d5fb12de1 100644 --- a/package.json +++ b/package.json @@ -107,14 +107,14 @@ "webpack-cli": "^6.0.1" }, "dependencies": { - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.0", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", @@ -124,13 +124,13 @@ "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.11.3", + "@codemirror/language": "^6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/lsp-client": "^6.2.1", - "@codemirror/search": "^6.5.11", - "@codemirror/state": "^6.5.2", + "@codemirror/lint": "^6.9.5", + "@codemirror/lsp-client": "^6.2.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", "@deadlyjack/ajax": "^1.2.6", @@ -173,5 +173,14 @@ "vanilla-picker": "^2.12.3", "yargs": "^18.0.0" }, + "overrides": { + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", + "@codemirror/language": "^6.12.2", + "@codemirror/lint": "^6.9.5", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.40.0" + }, "browserslist": "cover 100%,not android < 5" } From 84a3ea8ffc8e431e94d4a5571abfc3fdf7e00dbc Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:41:54 +0530 Subject: [PATCH 11/14] fix tooltip, inlayhints things --- src/cm/commandRegistry.js | 6 +- src/cm/lsp/clientManager.ts | 7 +- src/cm/lsp/index.ts | 5 + src/cm/lsp/tooltipExtensions.ts | 562 ++++++++++++++++++++++++++++++++ src/settings/lspServerDetail.js | 11 +- 5 files changed, 582 insertions(+), 9 deletions(-) create mode 100644 src/cm/lsp/tooltipExtensions.ts diff --git a/src/cm/commandRegistry.js b/src/cm/commandRegistry.js index f0ceba6e8..c647c8be1 100644 --- a/src/cm/commandRegistry.js +++ b/src/cm/commandRegistry.js @@ -70,9 +70,6 @@ import { jumpToDefinition as lspJumpToDefinition, jumpToImplementation as lspJumpToImplementation, jumpToTypeDefinition as lspJumpToTypeDefinition, - nextSignature as lspNextSignature, - prevSignature as lspPrevSignature, - showSignatureHelp as lspShowSignatureHelp, } from "@codemirror/lsp-client"; import { Compartment, EditorSelection } from "@codemirror/state"; import { keymap } from "@codemirror/view"; @@ -80,6 +77,9 @@ import { renameSymbol as acodeRenameSymbol, clearDiagnosticsEffect, clientManager, + nextSignature as lspNextSignature, + prevSignature as lspPrevSignature, + showSignatureHelp as lspShowSignatureHelp, } from "cm/lsp"; import { closeReferencesPanel as acodeCloseReferencesPanel, diff --git a/src/cm/lsp/clientManager.ts b/src/cm/lsp/clientManager.ts index 8a0e70734..29678f65c 100644 --- a/src/cm/lsp/clientManager.ts +++ b/src/cm/lsp/clientManager.ts @@ -3,13 +3,11 @@ import type { LSPClientExtension } from "@codemirror/lsp-client"; import { findReferencesKeymap, formatKeymap, - hoverTooltips, jumpToDefinitionKeymap, LSPClient, LSPPlugin, serverCompletion, serverDiagnostics, - signatureHelp, } from "@codemirror/lsp-client"; import { EditorState, Extension, MapMode } from "@codemirror/state"; import { EditorView, keymap } from "@codemirror/view"; @@ -23,6 +21,7 @@ import { inlayHintsExtension } from "./inlayHints"; import { acodeRenameKeymap } from "./rename"; import { ensureServerRunning } from "./serverLauncher"; import serverRegistry from "./serverRegistry"; +import { hoverTooltips, signatureHelp } from "./tooltipExtensions"; import { createTransport } from "./transport"; import type { BuiltinExtensionsConfig, @@ -149,7 +148,7 @@ function buildBuiltinExtensions( signature: includeSignature = true, keymaps: includeKeymaps = true, diagnostics: includeDiagnostics = true, - inlayHints: includeInlayHints = true, + inlayHints: includeInlayHints = false, documentHighlights: includeDocumentHighlights = true, formatting: includeFormatting = true, } = config; @@ -519,7 +518,7 @@ export class LspClientManager { signature: builtinConfig.signature !== false, keymaps: builtinConfig.keymaps !== false, diagnostics: builtinConfig.diagnostics !== false, - inlayHints: builtinConfig.inlayHints !== false, + inlayHints: builtinConfig.inlayHints === true, documentHighlights: builtinConfig.documentHighlights !== false, formatting: builtinConfig.formatting !== false, }) diff --git a/src/cm/lsp/index.ts b/src/cm/lsp/index.ts index 232905c33..55a02acfb 100644 --- a/src/cm/lsp/index.ts +++ b/src/cm/lsp/index.ts @@ -72,6 +72,11 @@ export { stopManagedServer, } from "./serverLauncher"; export { default as serverRegistry } from "./serverRegistry"; +export { + nextSignature, + prevSignature, + showSignatureHelp, +} from "./tooltipExtensions"; export { createTransport } from "./transport"; export type { diff --git a/src/cm/lsp/tooltipExtensions.ts b/src/cm/lsp/tooltipExtensions.ts new file mode 100644 index 000000000..5145e8d34 --- /dev/null +++ b/src/cm/lsp/tooltipExtensions.ts @@ -0,0 +1,562 @@ +import { + highlightingFor, + type Language, + language as languageFacet, +} from "@codemirror/language"; +import { LSPPlugin } from "@codemirror/lsp-client"; +import { + type Extension, + Prec, + StateEffect, + StateField, +} from "@codemirror/state"; +import { + type Command, + closeHoverTooltips, + EditorView, + hasHoverTooltips, + hoverTooltip, + type KeyBinding, + keymap, + showTooltip, + type Tooltip, + ViewPlugin, + type ViewUpdate, +} from "@codemirror/view"; +import { highlightCode } from "@lezer/highlight"; +import type { + HoverParams, + SignatureHelpContext, + SignatureHelpParams, +} from "vscode-languageserver-protocol"; +import type { + Hover, + SignatureHelp as LspSignatureHelp, + MarkedString, + MarkupContent, +} from "vscode-languageserver-types"; + +interface LspClientInternals { + config?: { + highlightLanguage?: (language: string) => Language | null | undefined; + }; + hasCapability?: (name: string) => boolean; +} + +function fromPosition( + doc: EditorView["state"]["doc"], + position: { line: number; character: number }, +): number { + const line = doc.line(position.line + 1); + return Math.min(line.to, line.from + position.character); +} + +function escapeHtml(value: string): string { + return value.replace(/[&<>"']/g, (match) => { + switch (match) { + case "&": + return "&"; + case "<": + return "<"; + case ">": + return ">"; + case '"': + return """; + default: + return "'"; + } + }); +} + +function renderCode(plugin: LSPPlugin, code: MarkedString): string { + const client = plugin.client as typeof plugin.client & LspClientInternals; + + if (typeof code === "string") { + return plugin.docToHTML(code, "markdown"); + } + + const { language, value } = code; + let lang = client.config?.highlightLanguage?.(language || "") ?? undefined; + + if (!lang) { + const viewLang = plugin.view.state.facet(languageFacet); + if (viewLang && (!language || viewLang.name === language)) { + lang = viewLang; + } + } + + if (!lang) return escapeHtml(value); + + let result = ""; + highlightCode( + value, + lang.parser.parse(value), + { style: (tags) => highlightingFor(plugin.view.state, tags) }, + (text, cls) => { + result += cls + ? `${escapeHtml(text)}` + : escapeHtml(text); + }, + () => { + result += "
"; + }, + ); + return result; +} + +function renderTooltipContent( + plugin: LSPPlugin, + value: string | MarkupContent | MarkedString | MarkedString[], +): string { + if (Array.isArray(value)) { + return value.map((item) => renderCode(plugin, item)).join("
"); + } + + if ( + typeof value === "string" || + (typeof value === "object" && value != null && "language" in value) + ) { + return renderCode(plugin, value); + } + + return plugin.docToHTML(value); +} + +function hoverRequest(plugin: LSPPlugin, pos: number) { + const client = plugin.client as typeof plugin.client & LspClientInternals; + if (client.hasCapability?.("hoverProvider") === false) { + return Promise.resolve(null); + } + + plugin.client.sync(); + return plugin.client.request( + "textDocument/hover", + { + position: plugin.toPosition(pos), + textDocument: { uri: plugin.uri }, + }, + ); +} + +function lspTooltipSource( + view: EditorView, + pos: number, +): Promise { + const plugin = LSPPlugin.get(view); + if (!plugin) return Promise.resolve(null); + + return hoverRequest(plugin, pos).then((result) => { + if (!result) return null; + + return { + pos: result.range + ? fromPosition(view.state.doc, result.range.start) + : pos, + end: result.range ? fromPosition(view.state.doc, result.range.end) : pos, + create() { + const dom = document.createElement("div"); + dom.className = "cm-lsp-hover-tooltip cm-lsp-documentation"; + dom.innerHTML = renderTooltipContent(plugin, result.contents); + return { dom }; + }, + above: true, + }; + }); +} + +const closeHoverOnScroll = ViewPlugin.fromClass( + class { + constructor(readonly view: EditorView) {} + }, + { + eventObservers: { + scroll() { + if (hasHoverTooltips(this.view.state)) { + this.view.dispatch({ effects: closeHoverTooltips }); + } + }, + }, + }, +); + +function getSignatureHelp( + plugin: LSPPlugin, + pos: number, + context: SignatureHelpContext, +) { + const client = plugin.client as typeof plugin.client & LspClientInternals; + if (client.hasCapability?.("signatureHelpProvider") === false) { + return Promise.resolve(null); + } + + plugin.client.sync(); + return plugin.client.request( + "textDocument/signatureHelp", + { + context, + position: plugin.toPosition(pos), + textDocument: { uri: plugin.uri }, + }, + ); +} + +function sameSignatures(a: LspSignatureHelp, b: LspSignatureHelp): boolean { + if (a.signatures.length !== b.signatures.length) return false; + return a.signatures.every((signature, index) => { + return signature.label === b.signatures[index]?.label; + }); +} + +function sameActiveParam( + a: LspSignatureHelp, + b: LspSignatureHelp, + active: number, +): boolean { + const current = a.signatures[active]; + const next = b.signatures[active]; + if (!current || !next) return false; + + return ( + (current.activeParameter ?? a.activeParameter) === + (next.activeParameter ?? b.activeParameter) + ); +} + +class SignatureState { + constructor( + readonly data: LspSignatureHelp, + readonly active: number, + readonly tooltip: Tooltip, + ) {} +} + +const signatureEffect = StateEffect.define<{ + data: LspSignatureHelp; + active: number; + pos: number; +} | null>(); + +function signatureTooltip( + data: LspSignatureHelp, + active: number, + pos: number, +): Tooltip { + return { + pos, + above: true, + create: (view) => drawSignatureTooltip(view, data, active), + }; +} + +const signatureState = StateField.define({ + create() { + return null; + }, + update(value, tr) { + for (const effect of tr.effects) { + if (effect.is(signatureEffect)) { + if (effect.value) { + return new SignatureState( + effect.value.data, + effect.value.active, + signatureTooltip( + effect.value.data, + effect.value.active, + effect.value.pos, + ), + ); + } + return null; + } + } + + if (value && tr.docChanged) { + return new SignatureState(value.data, value.active, { + ...value.tooltip, + pos: tr.changes.mapPos(value.tooltip.pos), + }); + } + + return value; + }, + provide: (field) => + showTooltip.from(field, (value) => value?.tooltip ?? null), +}); + +function drawSignatureTooltip( + view: EditorView, + data: LspSignatureHelp, + active: number, +) { + const dom = document.createElement("div"); + dom.className = "cm-lsp-signature-tooltip"; + + if (data.signatures.length > 1) { + dom.classList.add("cm-lsp-signature-multiple"); + const num = dom.appendChild(document.createElement("div")); + num.className = "cm-lsp-signature-num"; + num.textContent = `${active + 1}/${data.signatures.length}`; + } + + const signature = data.signatures[active]; + if (!signature) { + return { dom }; + } + + const sig = dom.appendChild(document.createElement("div")); + sig.className = "cm-lsp-signature"; + let activeFrom = 0; + let activeTo = 0; + const activeParamIndex = signature.activeParameter ?? data.activeParameter; + const activeParam = + activeParamIndex != null && signature.parameters + ? signature.parameters[activeParamIndex] + : null; + + if (activeParam && Array.isArray(activeParam.label)) { + [activeFrom, activeTo] = activeParam.label; + } else if (activeParam) { + const found = signature.label.indexOf(activeParam.label as string); + if (found > -1) { + activeFrom = found; + activeTo = found + activeParam.label.length; + } + } + + if (activeTo) { + sig.appendChild( + document.createTextNode(signature.label.slice(0, activeFrom)), + ); + const activeElt = sig.appendChild(document.createElement("span")); + activeElt.className = "cm-lsp-active-parameter"; + activeElt.textContent = signature.label.slice(activeFrom, activeTo); + sig.appendChild(document.createTextNode(signature.label.slice(activeTo))); + } else { + sig.textContent = signature.label; + } + + if (signature.documentation) { + const plugin = LSPPlugin.get(view); + if (plugin) { + const docs = dom.appendChild(document.createElement("div")); + docs.className = "cm-lsp-signature-documentation cm-lsp-documentation"; + docs.innerHTML = plugin.docToHTML(signature.documentation); + } + } + + return { dom }; +} + +const signaturePlugin = ViewPlugin.fromClass( + class { + activeRequest: { pos: number; drop: boolean } | null = null; + delayedRequest = 0; + + constructor(readonly view: EditorView) {} + + update(update: ViewUpdate) { + if (this.activeRequest) { + if (update.selectionSet) { + this.activeRequest.drop = true; + this.activeRequest = null; + } else if (update.docChanged) { + this.activeRequest.pos = update.changes.mapPos( + this.activeRequest.pos, + ); + } + } + + const plugin = LSPPlugin.get(update.view); + if (!plugin) return; + + const sigState = update.view.state.field(signatureState); + let triggerCharacter = ""; + + if ( + update.docChanged && + update.transactions.some((tr) => tr.isUserEvent("input.type")) + ) { + const serverConf = + plugin.client.serverCapabilities?.signatureHelpProvider; + const triggers = (serverConf?.triggerCharacters || []).concat( + (sigState && serverConf?.retriggerCharacters) || [], + ); + + if (triggers.length) { + update.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => { + const insertedText = inserted.toString(); + if (!insertedText) return; + for (const trigger of triggers) { + if (insertedText.includes(trigger)) { + triggerCharacter = trigger; + } + } + }); + } + } + + if (triggerCharacter) { + this.startRequest(plugin, { + triggerKind: 2, + isRetrigger: !!sigState, + triggerCharacter, + activeSignatureHelp: sigState?.data, + }); + } else if (sigState && update.selectionSet) { + if (this.delayedRequest) clearTimeout(this.delayedRequest); + this.delayedRequest = window.setTimeout(() => { + this.startRequest(plugin, { + triggerKind: 3, + isRetrigger: true, + activeSignatureHelp: sigState.data, + }); + }, 250); + } + } + + startRequest(plugin: LSPPlugin, context: SignatureHelpContext) { + if (this.delayedRequest) { + clearTimeout(this.delayedRequest); + this.delayedRequest = 0; + } + + const { view } = plugin; + const pos = view.state.selection.main.head; + + if (this.activeRequest) this.activeRequest.drop = true; + const request = (this.activeRequest = { pos, drop: false }); + + getSignatureHelp(plugin, pos, context).then( + (result) => { + if (request.drop) return; + + if (result && result.signatures.length) { + const current = view.state.field(signatureState); + const same = current && sameSignatures(current.data, result); + const active = + same && context.triggerKind === 3 + ? current!.active + : (result.activeSignature ?? 0); + + if (same && sameActiveParam(current!.data, result, active)) return; + + view.dispatch({ + effects: signatureEffect.of({ + data: result, + active, + pos: same ? current!.tooltip.pos : request.pos, + }), + }); + } else if (view.state.field(signatureState, false)) { + view.dispatch({ effects: signatureEffect.of(null) }); + } + }, + context.triggerKind === 1 + ? (error) => plugin.reportError("Signature request failed", error) + : undefined, + ); + } + + close() { + if (this.delayedRequest) { + clearTimeout(this.delayedRequest); + this.delayedRequest = 0; + } + if (this.activeRequest) { + this.activeRequest.drop = true; + this.activeRequest = null; + } + if (this.view.state.field(signatureState, false)) { + this.view.dispatch({ effects: signatureEffect.of(null) }); + } + } + + destroy() { + this.close(); + } + }, + { + eventObservers: { + scroll() { + this.close(); + }, + }, + }, +); + +export const showSignatureHelp: Command = (view) => { + let plugin = view.plugin(signaturePlugin); + if (!plugin) { + view.dispatch({ + effects: StateEffect.appendConfig.of([signatureState, signaturePlugin]), + }); + plugin = view.plugin(signaturePlugin); + } + + const field = view.state.field(signatureState); + if (!plugin || field === undefined) return false; + + const lspPlugin = LSPPlugin.get(view); + if (!lspPlugin) return false; + + plugin.startRequest(lspPlugin, { + triggerKind: 1, + activeSignatureHelp: field ? field.data : undefined, + isRetrigger: !!field, + }); + return true; +}; + +export const nextSignature: Command = (view) => { + const field = view.state.field(signatureState, false); + if (!field) return false; + if (field.active < field.data.signatures.length - 1) { + view.dispatch({ + effects: signatureEffect.of({ + data: field.data, + active: field.active + 1, + pos: field.tooltip.pos, + }), + }); + } + return true; +}; + +export const prevSignature: Command = (view) => { + const field = view.state.field(signatureState, false); + if (!field) return false; + if (field.active > 0) { + view.dispatch({ + effects: signatureEffect.of({ + data: field.data, + active: field.active - 1, + pos: field.tooltip.pos, + }), + }); + } + return true; +}; + +export const signatureKeymap: readonly KeyBinding[] = [ + { key: "Mod-Shift-Space", run: showSignatureHelp }, + { key: "Mod-Shift-ArrowUp", run: prevSignature }, + { key: "Mod-Shift-ArrowDown", run: nextSignature }, +]; + +export function hoverTooltips(config: { hoverTime?: number } = {}): Extension { + return [ + hoverTooltip(lspTooltipSource, { + hideOn: (tr) => tr.docChanged, + hoverTime: config.hoverTime, + }), + closeHoverOnScroll, + ]; +} + +export function signatureHelp(config: { keymap?: boolean } = {}): Extension { + return [ + signatureState, + signaturePlugin, + config.keymap === false ? [] : Prec.high(keymap.of(signatureKeymap)), + ]; +} diff --git a/src/settings/lspServerDetail.js b/src/settings/lspServerDetail.js index 22e918860..24fd9ff3d 100644 --- a/src/settings/lspServerDetail.js +++ b/src/settings/lspServerDetail.js @@ -330,7 +330,7 @@ function createItems(snapshot) { items.push({ key, text, - checkbox: snapshot.builtinExts[extKey] !== false, + checkbox: isBuiltinFeatureEnabled(snapshot.builtinExts, extKey), info, category: categories.features, }); @@ -377,11 +377,18 @@ async function refreshVisibleState($list, itemsByKey, serverId) { getFeatureItems().forEach(([key, extKey]) => { updateItemDisplay($list, itemsByKey, key, undefined, { - checkbox: snapshot.builtinExts[extKey] !== false, + checkbox: isBuiltinFeatureEnabled(snapshot.builtinExts, extKey), }); }); } +function isBuiltinFeatureEnabled(builtinExts, extKey) { + if (extKey === "inlayHints") { + return builtinExts?.[extKey] === true; + } + return builtinExts?.[extKey] !== false; +} + async function persistEnabled(serverId, value) { await updateServerConfig(serverId, { enabled: value }); lspApi.servers.update(serverId, (current) => ({ From 649bbcff9b07d41d67c8aaa37cfbb825ea5bb73b Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:19:32 +0530 Subject: [PATCH 12/14] fix --- bun.lock | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index b2682259c..334a23938 100644 --- a/bun.lock +++ b/bun.lock @@ -6,19 +6,25 @@ "dependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", + "@codemirror/lang-angular": "^0.1.4", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-less": "^6.0.2", + "@codemirror/lang-liquid": "^6.3.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sass": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-vue": "^0.1.3", + "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.2", @@ -380,7 +386,7 @@ "@codemirror/lang-less": ["@codemirror/lang-less@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ=="], - "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw=="], + "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw=="], "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="], @@ -2074,6 +2080,8 @@ "@codemirror/language-data/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + "@codemirror/language-data/@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.65.0", "", { "dependencies": { "@jsonjoy.com/base64": "17.65.0", "@jsonjoy.com/buffers": "17.65.0", "@jsonjoy.com/codegen": "17.65.0", "@jsonjoy.com/json-pointer": "17.65.0", "@jsonjoy.com/util": "17.65.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ=="], "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util": ["@jsonjoy.com/util@17.65.0", "", { "dependencies": { "@jsonjoy.com/buffers": "17.65.0", "@jsonjoy.com/codegen": "17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w=="], From c8bb5c7c816d12534df139019353830067ec966e Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:52:27 +0530 Subject: [PATCH 13/14] refactor a lot --- hooks/modify-java-files.js | 252 +++- package-lock.json | 12 +- src/cm/colorView.ts | 24 +- src/cm/indentGuides.ts | 33 +- src/cm/lsp/clientManager.ts | 34 +- src/cm/lsp/diagnostics.ts | 93 +- src/cm/lsp/documentHighlights.ts | 34 +- src/cm/lsp/tooltipExtensions.ts | 95 +- src/cm/rainbowBrackets.ts | 27 +- src/cm/touchSelectionMenu.js | 1114 +++-------------- src/lib/editorManager.js | 59 +- src/lib/settings.js | 2 - src/main.scss | 43 - .../android/com/foxdebug/system/System.java | 25 +- src/settings/editorSettings.js | 16 - src/styles/codemirror.scss | 11 - 16 files changed, 761 insertions(+), 1113 deletions(-) diff --git a/hooks/modify-java-files.js b/hooks/modify-java-files.js index 7cf9e06b3..f4ed73449 100644 --- a/hooks/modify-java-files.js +++ b/hooks/modify-java-files.js @@ -5,9 +5,13 @@ const prettier = require('prettier'); main(); async function main() { + const patchVersion = '2'; const flagFile = path.resolve(__dirname, '../platforms/android/.flag_done'); if (fs.existsSync(flagFile)) { - return; + const appliedVersion = fs.readFileSync(flagFile, 'utf8').trim(); + if (appliedVersion === patchVersion) { + return; + } } const base = path.resolve(__dirname, `../platforms/android/CordovaLib/src/org/apache/cordova`); @@ -31,6 +35,18 @@ async function main() { ], }; + const nativeContextMenuInterfaceMethod = { + name: 'setNativeContextMenuDisabled', + modifier: 'public', + returnType: 'void', + params: [ + { + type: 'boolean', + name: 'disabled', + } + ], + }; + const setInputTypeMethod = { name: 'setInputType', modifier: 'public', @@ -44,12 +60,31 @@ async function main() { body: ['webView.setInputType(type);'], }; + const setNativeContextMenuDisabledMethod = { + name: 'setNativeContextMenuDisabled', + modifier: 'public', + returnType: 'void', + params: [ + { + type: 'boolean', + name: 'disabled', + } + ], + body: ['webView.setNativeContextMenuDisabled(disabled);'], + }; + const contentToAdd = { 'SystemWebView.java': { 'import': [ + 'android.graphics.Rect', + 'android.os.Build', 'android.text.InputType', + 'android.view.ActionMode', 'android.view.inputmethod.InputConnection', 'android.view.inputmethod.EditorInfo', + 'android.view.Menu', + 'android.view.MenuItem', + 'android.view.View', ], 'fields': [ { @@ -70,12 +105,30 @@ async function main() { modifier: 'private', value: '1', }, + { + type: 'boolean', + name: 'nativeContextMenuDisabled', + modifier: 'private', + value: 'false', + }, ], methods: [ { ...setInputTypeMethod, body: [`this.type = type;`] }, + { + name: 'setNativeContextMenuDisabled', + modifier: 'public', + returnType: 'void', + params: [ + { + type: 'boolean', + name: 'disabled', + } + ], + body: [`this.nativeContextMenuDisabled = disabled;`], + }, { name: 'onCreateInputConnection', modifier: 'public', @@ -102,21 +155,204 @@ async function main() { ], notation: '@Override', }, + { + name: 'startActionMode', + modifier: 'public', + returnType: 'ActionMode', + params: [ + { + type: 'ActionMode.Callback', + name: 'callback', + } + ], + body: [ + `return suppressActionMode(super.startActionMode(wrapActionModeCallback(callback)));`, + ], + notation: '@Override', + }, + { + name: 'startActionMode', + modifier: 'public', + returnType: 'ActionMode', + params: [ + { + type: 'ActionMode.Callback', + name: 'callback', + }, + { + type: 'int', + name: 'type', + } + ], + body: [ + `return suppressActionMode(super.startActionMode(wrapActionModeCallback(callback), type));`, + ], + notation: '@Override', + }, + { + name: 'startActionModeForChild', + modifier: 'public', + returnType: 'ActionMode', + params: [ + { + type: 'View', + name: 'originalView', + }, + { + type: 'ActionMode.Callback', + name: 'callback', + } + ], + body: [ + `return suppressActionMode(super.startActionModeForChild(originalView, wrapActionModeCallback(callback)));`, + ], + notation: '@Override', + }, + { + name: 'startActionModeForChild', + modifier: 'public', + returnType: 'ActionMode', + params: [ + { + type: 'View', + name: 'originalView', + }, + { + type: 'ActionMode.Callback', + name: 'callback', + }, + { + type: 'int', + name: 'type', + } + ], + body: [ + `return suppressActionMode(super.startActionModeForChild(originalView, wrapActionModeCallback(callback), type));`, + ], + notation: '@Override', + }, + { + name: 'wrapActionModeCallback', + modifier: 'private', + returnType: 'ActionMode.Callback', + params: [ + { + type: 'ActionMode.Callback', + name: 'callback', + } + ], + body: [ + `if (!nativeContextMenuDisabled || callback == null) { + return callback; + } + return new ActionMode.Callback2() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + boolean created = callback.onCreateActionMode(mode, menu); + if (created) { + suppressActionModeUi(mode, menu); + } + return created; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + boolean prepared = callback.onPrepareActionMode(mode, menu); + suppressActionModeUi(mode, menu); + return prepared; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return callback.onActionItemClicked(mode, item); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + callback.onDestroyActionMode(mode); + } + + @Override + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { + if (callback instanceof ActionMode.Callback2) { + ((ActionMode.Callback2) callback).onGetContentRect(mode, view, outRect); + return; + } + super.onGetContentRect(mode, view, outRect); + } + };`, + ], + }, + { + name: 'suppressActionMode', + modifier: 'private', + returnType: 'ActionMode', + params: [ + { + type: 'ActionMode', + name: 'mode', + } + ], + body: [ + `if (mode == null || !nativeContextMenuDisabled) { + return mode; + } + suppressActionModeUi(mode, mode.getMenu()); + return mode;`, + ], + }, + { + name: 'suppressActionModeUi', + modifier: 'private', + returnType: 'void', + params: [ + { + type: 'ActionMode', + name: 'mode', + }, + { + type: 'Menu', + name: 'menu', + } + ], + body: [ + `if (mode == null || !nativeContextMenuDisabled || menu == null) { + return; + } + menu.clear(); + mode.setTitle(null); + mode.setSubtitle(null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + post(() -> { + if (!nativeContextMenuDisabled) { + return; + } + try { + mode.hide(0); + } catch (Throwable ignored) { + } + }); + }`, + ], + }, ] }, 'SystemWebViewEngine.java': { methods: [ - setInputTypeMethod + setInputTypeMethod, + setNativeContextMenuDisabledMethod, ] }, 'CordovaWebViewEngine.java': { methods: [ - interfaceMethod + interfaceMethod, + nativeContextMenuInterfaceMethod, ] }, 'CordovaWebView.java': { methods: [ - interfaceMethod + interfaceMethod, + nativeContextMenuInterfaceMethod, ] }, 'CordovaWebViewImpl.java': { @@ -124,6 +360,10 @@ async function main() { { ...setInputTypeMethod, body: [`engine.setInputType(type);`] + }, + { + ...setNativeContextMenuDisabledMethod, + body: [`engine.setNativeContextMenuDisabled(disabled);`] } ] } @@ -198,7 +438,7 @@ async function main() { }); } - fs.writeFile(flagFile, '', err => { + fs.writeFile(flagFile, patchVersion, err => { if (err) { console.log(err); process.exit(1); @@ -254,4 +494,4 @@ async function main() { function removeComments(content) { return content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, ''); } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 106795b82..7a483ec6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,19 +11,25 @@ "dependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", + "@codemirror/lang-angular": "^0.1.4", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-less": "^6.0.2", + "@codemirror/lang-liquid": "^6.3.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sass": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-vue": "^0.1.3", + "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.2", @@ -2144,9 +2150,9 @@ } }, "node_modules/@codemirror/lang-liquid": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.1.tgz", - "integrity": "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.2.tgz", + "integrity": "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", diff --git a/src/cm/colorView.ts b/src/cm/colorView.ts index 070a1d68b..731b72a5d 100644 --- a/src/cm/colorView.ts +++ b/src/cm/colorView.ts @@ -193,6 +193,8 @@ function colorDecorations(view: EditorView): DecorationSet { class ColorViewPlugin { decorations: DecorationSet; + raf = 0; + pendingView: EditorView | null = null; constructor(view: EditorView) { this.decorations = colorDecorations(view); @@ -200,7 +202,7 @@ class ColorViewPlugin { update(update: ViewUpdate): void { if (update.docChanged || update.viewportChanged) { - this.decorations = colorDecorations(update.view); + this.scheduleDecorations(update.view); } const readOnly = update.view.contentDOM.ariaReadOnly === "true"; const editable = update.view.contentDOM.contentEditable === "true"; @@ -208,6 +210,18 @@ class ColorViewPlugin { this.changePicker(update.view, canBeEdited); } + scheduleDecorations(view: EditorView): void { + this.pendingView = view; + if (this.raf) return; + this.raf = requestAnimationFrame(() => { + this.raf = 0; + const pendingView = this.pendingView; + this.pendingView = null; + if (!pendingView) return; + this.decorations = colorDecorations(pendingView); + }); + } + changePicker(view: EditorView, canBeEdited: boolean): void { const doms = view.contentDOM.querySelectorAll("input[type=color]"); doms.forEach((inp) => { @@ -219,6 +233,14 @@ class ColorViewPlugin { } }); } + + destroy(): void { + if (this.raf) { + cancelAnimationFrame(this.raf); + this.raf = 0; + } + this.pendingView = null; + } } export const colorView = (showPicker = true) => diff --git a/src/cm/indentGuides.ts b/src/cm/indentGuides.ts index 6f84533b7..1806c978a 100644 --- a/src/cm/indentGuides.ts +++ b/src/cm/indentGuides.ts @@ -345,6 +345,8 @@ function createIndentGuidesPlugin( return ViewPlugin.fromClass( class { decorations: DecorationSet; + raf = 0; + pendingView: EditorView | null = null; constructor(view: EditorView) { this.decorations = buildDecorations(view, config); @@ -352,13 +354,34 @@ function createIndentGuidesPlugin( update(update: ViewUpdate): void { if ( - update.docChanged || - update.viewportChanged || - update.geometryChanged || - (config.highlightActiveGuide && update.selectionSet) + !update.docChanged && + !update.viewportChanged && + !update.geometryChanged && + !(config.highlightActiveGuide && update.selectionSet) ) { - this.decorations = buildDecorations(update.view, config); + return; } + this.scheduleBuild(update.view); + } + + scheduleBuild(view: EditorView): void { + this.pendingView = view; + if (this.raf) return; + this.raf = requestAnimationFrame(() => { + this.raf = 0; + const pendingView = this.pendingView; + this.pendingView = null; + if (!pendingView) return; + this.decorations = buildDecorations(pendingView, config); + }); + } + + destroy(): void { + if (this.raf) { + cancelAnimationFrame(this.raf); + this.raf = 0; + } + this.pendingView = null; } }, { diff --git a/src/cm/lsp/clientManager.ts b/src/cm/lsp/clientManager.ts index 29678f65c..2ccb84254 100644 --- a/src/cm/lsp/clientManager.ts +++ b/src/cm/lsp/clientManager.ts @@ -61,6 +61,17 @@ function safeString(value: unknown): string { return value != null ? String(value) : ""; } +function isVerboseLspLoggingEnabled(): boolean { + const buildInfo = (globalThis as { BuildInfo?: { debug?: boolean } }) + .BuildInfo; + return !!buildInfo?.debug; +} + +function logLspInfo(...args: unknown[]): void { + if (!isVerboseLspLoggingEnabled()) return; + console.info(...args); +} + function isPlainObject(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } @@ -626,8 +637,7 @@ export class LspClientManager { icon: type === 1 ? "error" : "warningreport_problem", type: type === 1 ? "error" : "warning", }); - // Log full message to console for debugging - console.info(`[LSP:${server.id}] ${message}`); + logLspInfo(`[LSP:${server.id}] ${message}`); return true; } @@ -639,7 +649,7 @@ export class LspClientManager { icon: type === 4 ? "autorenew" : "info", duration: 5000, }); - console.info(`[LSP:${server.id}] ${message}`); + logLspInfo(`[LSP:${server.id}] ${message}`); return true; }, "$/progress": (_client: LSPClient, params: unknown): boolean => { @@ -684,7 +694,7 @@ export class LspClientManager { lspStatusBar.hideById(statusId); } - console.info( + logLspInfo( `[LSP:${server.id}] Progress: ${kind} - ${message || title || ""} ${typeof percentage === "number" ? `(${percentage}%)` : ""}`, ); return true; @@ -699,7 +709,7 @@ export class LspClientManager { const serverLabel = server.label || server.id; const source = versionParams.source || "bundled"; - console.info( + logLspInfo( `[LSP:${server.id}] TypeScript ${versionParams.version} (${source})`, ); @@ -729,7 +739,7 @@ export class LspClientManager { method: string, params: unknown, ) => { - console.info( + logLspInfo( `[LSP:${server.id}] Unhandled notification: ${method}`, params, ); @@ -778,22 +788,22 @@ export class LspClientManager { // Log root URI info to console if (normalizedRootUri) { if (originalRootUri && originalRootUri !== normalizedRootUri) { - console.info( + logLspInfo( `[LSP:${server.id}] root ${normalizedRootUri} (from ${originalRootUri})`, ); } else { - console.info(`[LSP:${server.id}] root`, normalizedRootUri); + logLspInfo(`[LSP:${server.id}] root`, normalizedRootUri); } } else if (originalRootUri) { - console.info(`[LSP:${server.id}] root ignored`, originalRootUri); + logLspInfo(`[LSP:${server.id}] root ignored`, originalRootUri); } if (initializationOptions) { - console.info( + logLspInfo( `[LSP:${server.id}] initializationOptions keys`, Object.keys(initializationOptions), ); } - console.info(`[LSP:${server.id}] initialized`); + logLspInfo(`[LSP:${server.id}] initialized`); client.__acodeLoggedInfo = true; } } catch (error) { @@ -838,7 +848,7 @@ export class LspClientManager { existing.add(view); fileRefs.set(uri, existing); const suffix = effectiveRoot ? ` (root ${effectiveRoot})` : ""; - console.info(`[LSP:${server.id}] attached to ${uri}${suffix}`); + logLspInfo(`[LSP:${server.id}] attached to ${uri}${suffix}`); }; const detach = (uri: string, view?: EditorView): void => { diff --git a/src/cm/lsp/diagnostics.ts b/src/cm/lsp/diagnostics.ts index c442bec3a..274bcb93e 100644 --- a/src/cm/lsp/diagnostics.ts +++ b/src/cm/lsp/diagnostics.ts @@ -1,4 +1,4 @@ -import { Diagnostic, forceLinting, linter, lintGutter } from "@codemirror/lint"; +import { Diagnostic, linter, lintGutter } from "@codemirror/lint"; import type { LSPClient } from "@codemirror/lsp-client"; import { LSPPlugin } from "@codemirror/lsp-client"; import type { Extension } from "@codemirror/state"; @@ -18,9 +18,27 @@ import type { } from "./types"; const setPublishedDiagnostics = StateEffect.define(); +let diagnosticsEventTimer = 0; export const LSP_DIAGNOSTICS_EVENT = "acode:lsp-diagnostics-updated"; +function isCoarsePointerDevice(): boolean { + if (typeof window !== "undefined") { + try { + if (window.matchMedia?.("(pointer: coarse)").matches) { + return true; + } + } catch (_) { + // Ignore matchMedia failures and fall back to maxTouchPoints. + } + } + + return ( + typeof navigator !== "undefined" && + Number(navigator.maxTouchPoints || 0) > 0 + ); +} + function emitDiagnosticsUpdated(): void { if ( typeof document === "undefined" || @@ -72,10 +90,10 @@ const severities: DiagnosticSeverity[] = [ "hint", ]; -function storeLspDiagnostics( +function collectLspDiagnostics( plugin: LSPPluginAPI, diagnostics: RawDiagnostic[], -): StateEffect { +): LspDiagnostic[] { const items: LspDiagnostic[] = []; const { syncedDoc } = plugin; @@ -115,14 +133,48 @@ function storeLspDiagnostics( }); } + return items; +} + +function storeLspDiagnostics( + items: LspDiagnostic[], +): StateEffect { return setPublishedDiagnostics.of(items); } +function sameDiagnostics( + current: readonly LspDiagnostic[], + next: readonly LspDiagnostic[], +): boolean { + if (current.length !== next.length) return false; + for (let index = 0; index < current.length; index++) { + const left = current[index]; + const right = next[index]; + if ( + left.from !== right.from || + left.to !== right.to || + left.severity !== right.severity || + left.message !== right.message || + left.source !== right.source + ) { + return false; + } + } + return true; +} + +function scheduleDiagnosticsUpdated(): void { + if (diagnosticsEventTimer) return; + diagnosticsEventTimer = window.setTimeout(() => { + diagnosticsEventTimer = 0; + emitDiagnosticsUpdated(); + }, 32); +} + function mapDiagnostics( plugin: LSPPluginAPI, state: EditorState, ): Diagnostic[] { - plugin.client.sync(); const stored = state.field(lspPublishedDiagnostics); const changes = plugin.unsyncedChanges; const mapped: Diagnostic[] = []; @@ -179,18 +231,23 @@ export function lspDiagnosticsClientExtension(): { !file || (params.version != null && params.version !== file.version) ) { - return false; + return true; } const view = file.getView(); - if (!view) return false; + if (!view) return true; const plugin = LSPPlugin.get(view) as LSPPluginAPI | null; - if (!plugin) return false; + if (!plugin) return true; + + const diagnostics = collectLspDiagnostics(plugin, params.diagnostics); + const current = view.state.field(lspPublishedDiagnostics, false) ?? []; + if (sameDiagnostics(current, diagnostics)) { + return true; + } view.dispatch({ - effects: storeLspDiagnostics(plugin, params.diagnostics), + effects: storeLspDiagnostics(diagnostics), }); - forceLinting(view); - emitDiagnosticsUpdated(); + scheduleDiagnosticsUpdated(); return true; }, }, @@ -198,6 +255,12 @@ export function lspDiagnosticsClientExtension(): { } export function lspDiagnosticsUiExtension(includeGutter = true): Extension[] { + const diagnosticsMarkerFilter = isCoarsePointerDevice() + ? () => [] + : undefined; + const diagnosticsTooltipFilter = isCoarsePointerDevice() + ? () => [] + : undefined; const extensions: Extension[] = [ lspPublishedDiagnostics, linter(lspLinterSource, { @@ -206,12 +269,20 @@ export function lspDiagnosticsUiExtension(includeGutter = true): Extension[] { tr.effects.some((effect) => effect.is(setPublishedDiagnostics)), ); }, + markerFilter: diagnosticsMarkerFilter, + tooltipFilter: diagnosticsTooltipFilter, // keep panel closed by default autoPanel: false, }), ]; if (includeGutter) { - extensions.splice(1, 0, lintGutter()); + extensions.splice( + 1, + 0, + lintGutter({ + tooltipFilter: diagnosticsTooltipFilter, + }), + ); } return extensions; } diff --git a/src/cm/lsp/documentHighlights.ts b/src/cm/lsp/documentHighlights.ts index 141b3ac76..e2019006c 100644 --- a/src/cm/lsp/documentHighlights.ts +++ b/src/cm/lsp/documentHighlights.ts @@ -93,22 +93,34 @@ function getMarkForKind( } function buildDecos( + view: EditorView, highlights: ProcessedHighlight[], - docLen: number, distinguishReadWrite: boolean, ): DecorationSet { if (!highlights.length) return Decoration.none; + const docLen = view.state.doc.length; + const visibleRanges = view.visibleRanges.length + ? view.visibleRanges + : [view.viewport]; const decos: Range[] = []; + let rangeIndex = 0; for (const h of highlights) { if (h.from < 0 || h.to > docLen || h.from >= h.to) continue; + while ( + rangeIndex < visibleRanges.length && + visibleRanges[rangeIndex].to <= h.from + ) { + rangeIndex++; + } + if (rangeIndex >= visibleRanges.length) break; + const visible = visibleRanges[rangeIndex]; + if (h.to <= visible.from) continue; decos.push( getMarkForKind(h.kind, distinguishReadWrite).range(h.from, h.to), ); } - // Sort by position for RangeSet - decos.sort((a, b) => a.from - b.from || a.to - b.to); - return RangeSet.of(decos); + return decos.length ? RangeSet.of(decos) : Decoration.none; } function createPlugin(config: DocumentHighlightsConfig) { @@ -122,18 +134,26 @@ function createPlugin(config: DocumentHighlightsConfig) { reqId = 0; lastPos = -1; - constructor(private view: EditorView) {} + constructor(private view: EditorView) { + this.decorations = buildDecos( + view, + view.state.field(highlightsField, false) ?? [], + distinguishReadWrite, + ); + } update(update: ViewUpdate): void { // Rebuild decorations if highlights changed if ( update.transactions.some((t) => t.effects.some((e) => e.is(setHighlights)), - ) + ) || + update.viewportChanged || + update.geometryChanged ) { this.decorations = buildDecos( + update.view, update.state.field(highlightsField, false) ?? [], - update.state.doc.length, distinguishReadWrite, ); } diff --git a/src/cm/lsp/tooltipExtensions.ts b/src/cm/lsp/tooltipExtensions.ts index 5145e8d34..db09cc53c 100644 --- a/src/cm/lsp/tooltipExtensions.ts +++ b/src/cm/lsp/tooltipExtensions.ts @@ -43,6 +43,9 @@ interface LspClientInternals { hasCapability?: (name: string) => boolean; } +const SIGNATURE_TRIGGER_DELAY = 120; +const SIGNATURE_RETRIGGER_DELAY = 250; + function fromPosition( doc: EditorView["state"]["doc"], position: { line: number; character: number }, @@ -122,6 +125,25 @@ function renderTooltipContent( return plugin.docToHTML(value); } +function isPointerOrTouchSelection(update: ViewUpdate): boolean { + return ( + update.selectionSet && + update.transactions.some( + (tr) => + tr.isUserEvent("pointer") || + tr.isUserEvent("select.pointer") || + tr.isUserEvent("touch") || + tr.isUserEvent("select.touch"), + ) + ); +} + +function closeHoverIfNeeded(view: EditorView): void { + if (hasHoverTooltips(view.state)) { + view.dispatch({ effects: closeHoverTooltips }); + } +} + function hoverRequest(plugin: LSPPlugin, pos: number) { const client = plugin.client as typeof plugin.client & LspClientInternals; if (client.hasCapability?.("hoverProvider") === false) { @@ -164,16 +186,23 @@ function lspTooltipSource( }); } -const closeHoverOnScroll = ViewPlugin.fromClass( +const closeHoverOnInteraction = ViewPlugin.fromClass( class { constructor(readonly view: EditorView) {} }, { eventObservers: { + pointerdown() { + closeHoverIfNeeded(this.view); + }, + touchstart() { + closeHoverIfNeeded(this.view); + }, + wheel() { + closeHoverIfNeeded(this.view); + }, scroll() { - if (hasHoverTooltips(this.view.state)) { - this.view.dispatch({ effects: closeHoverTooltips }); - } + closeHoverIfNeeded(this.view); }, }, }, @@ -355,6 +384,8 @@ const signaturePlugin = ViewPlugin.fromClass( constructor(readonly view: EditorView) {} update(update: ViewUpdate) { + const pointerOrTouchSelection = isPointerOrTouchSelection(update); + if (this.activeRequest) { if (update.selectionSet) { this.activeRequest.drop = true; @@ -396,22 +427,41 @@ const signaturePlugin = ViewPlugin.fromClass( } if (triggerCharacter) { - this.startRequest(plugin, { - triggerKind: 2, - isRetrigger: !!sigState, - triggerCharacter, - activeSignatureHelp: sigState?.data, - }); - } else if (sigState && update.selectionSet) { - if (this.delayedRequest) clearTimeout(this.delayedRequest); - this.delayedRequest = window.setTimeout(() => { - this.startRequest(plugin, { + this.scheduleRequest( + plugin, + { + triggerKind: 2, + isRetrigger: !!sigState, + triggerCharacter, + activeSignatureHelp: sigState?.data, + }, + SIGNATURE_TRIGGER_DELAY, + ); + } else if (sigState && update.selectionSet && !pointerOrTouchSelection) { + this.scheduleRequest( + plugin, + { triggerKind: 3, isRetrigger: true, activeSignatureHelp: sigState.data, - }); - }, 250); + }, + SIGNATURE_RETRIGGER_DELAY, + ); + } + } + + scheduleRequest( + plugin: LSPPlugin, + context: SignatureHelpContext, + delay: number, + ) { + if (this.delayedRequest) { + clearTimeout(this.delayedRequest); } + this.delayedRequest = window.setTimeout(() => { + this.delayedRequest = 0; + this.startRequest(plugin, context); + }, delay); } startRequest(plugin: LSPPlugin, context: SignatureHelpContext) { @@ -477,6 +527,15 @@ const signaturePlugin = ViewPlugin.fromClass( }, { eventObservers: { + pointerdown() { + this.close(); + }, + touchstart() { + this.close(); + }, + wheel() { + this.close(); + }, scroll() { this.close(); }, @@ -546,10 +605,10 @@ export const signatureKeymap: readonly KeyBinding[] = [ export function hoverTooltips(config: { hoverTime?: number } = {}): Extension { return [ hoverTooltip(lspTooltipSource, { - hideOn: (tr) => tr.docChanged, + hideOnChange: true, hoverTime: config.hoverTime, }), - closeHoverOnScroll, + closeHoverOnInteraction, ]; } diff --git a/src/cm/rainbowBrackets.ts b/src/cm/rainbowBrackets.ts index b1929c9c9..0df78dd8d 100644 --- a/src/cm/rainbowBrackets.ts +++ b/src/cm/rainbowBrackets.ts @@ -25,15 +25,28 @@ interface BracketInfo { const rainbowBracketsPlugin = ViewPlugin.fromClass( class { decorations: DecorationSet; + raf = 0; + pendingView: EditorView | null = null; constructor(view: EditorView) { this.decorations = this.buildDecorations(view); } update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged) { - this.decorations = this.buildDecorations(update.view); - } + if (!update.docChanged && !update.viewportChanged) return; + this.scheduleBuild(update.view); + } + + scheduleBuild(view: EditorView): void { + this.pendingView = view; + if (this.raf) return; + this.raf = requestAnimationFrame(() => { + this.raf = 0; + const pendingView = this.pendingView; + this.pendingView = null; + if (!pendingView) return; + this.decorations = this.buildDecorations(pendingView); + }); } buildDecorations(view: EditorView): DecorationSet { @@ -154,6 +167,14 @@ const rainbowBracketsPlugin = ViewPlugin.fromClass( return null; } } + + destroy(): void { + if (this.raf) { + cancelAnimationFrame(this.raf); + this.raf = 0; + } + this.pendingView = null; + } }, { decorations: (v) => v.decorations, diff --git a/src/cm/touchSelectionMenu.js b/src/cm/touchSelectionMenu.js index 18e7efdc6..602a4e4ba 100644 --- a/src/cm/touchSelectionMenu.js +++ b/src/cm/touchSelectionMenu.js @@ -1,19 +1,13 @@ -import { EditorSelection } from "@codemirror/state"; -import constants from "lib/constants"; import selectionMenu from "lib/selectionMenu"; -import appSettings from "lib/settings"; -import { getColorRange } from "utils/color/regex"; const TAP_MAX_DELAY = 500; const TAP_MAX_DISTANCE = 20; -const LONG_PRESS_DELAY = 450; const EDGE_SCROLL_GAP = 40; -const EDGE_SCROLL_STEP = 16; const MENU_MARGIN = 10; -const DRAG_FINGER_OFFSET_FACTOR = 1.8; -const HANDLE_HIT_SLOP = 8; -const CURSOR_HANDLE_HIT_SLOP = 2; -const CURSOR_HANDLE_GUARD_MS = 320; +const MENU_SHOW_DELAY = 120; +const MENU_CARET_GAP = 10; +const MENU_SELECTION_GAP = 12; +const MENU_HANDLE_CLEARANCE = 28; const TAP_MAX_COLUMN_DELTA = 2; const TAP_MAX_POS_DELTA = 2; @@ -124,11 +118,6 @@ function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } -function getElementRect($el) { - if (!$el?.isConnected) return null; - return $el.getBoundingClientRect(); -} - export default function createTouchSelectionMenu(view, options = {}) { return new TouchSelectionMenuController(view, options); } @@ -137,571 +126,196 @@ class TouchSelectionMenuController { #view; #container; #getActiveFile; - #tap = null; - #touchSession = null; - #dragState = null; - #longPressTimer = null; - #cursorHideTimer = null; - #scrollTimeout = null; - #autoScrollRaf = 0; #stateSyncRaf = 0; #isScrolling = false; - #selectionActive = false; + #isPointerInteracting = false; #menuActive = false; + #menuRequested = false; #enabled = true; - #touchListenersAttached = false; - #touchMovePassive = true; #handlingMenuAction = false; - #pendingPointerTriggered = false; - #pendingSelectionChanged = false; - #cursorHandleGuardUntil = 0; - #pointer = { x: 0, y: 0 }; - #mouseSelecting = false; + #menuShowTimer = null; constructor(view, options = {}) { this.#view = view; this.#container = options.container || view.dom.closest(".editor-container") || view.dom; this.#getActiveFile = options.getActiveFile || (() => null); - - this.config = { - teardropSize: appSettings.value.teardropSize, - teardropTimeout: appSettings.value.teardropTimeout, - touchMoveThreshold: appSettings.value.touchMoveThreshold, - }; - - this.$start = this.#createHandle("start"); - this.$end = this.#createHandle("end"); - this.$cursor = this.#createHandle("single"); this.$menu = document.createElement("menu"); this.$menu.className = "cursor-menu"; - this.$start.addEventListener("touchstart", this.#onStartHandleTouchStart, { - passive: false, - }); - this.$end.addEventListener("touchstart", this.#onEndHandleTouchStart, { - passive: false, - }); - this.$cursor.addEventListener( - "touchstart", - this.#onCursorHandleTouchStart, - { - passive: false, - }, - ); - this.#bindEvents(); - this.#syncHandleSize(); - } - - #createHandle(type) { - const $handle = document.createElement("span"); - $handle.className = `cursor ${type}`; - $handle.dataset.size = String(this.config.teardropSize); - return $handle; } #bindEvents() { const root = this.#view.dom; - root.addEventListener("touchstart", this.#onTouchStart, { - passive: false, - capture: true, - }); - root.addEventListener("mousedown", this.#onMouseDown, true); root.addEventListener("contextmenu", this.#onContextMenu, true); - document.addEventListener("mouseup", this.#onMouseUp, true); - document.addEventListener("mousedown", this.#onGlobalPointerDown, true); - document.addEventListener("touchstart", this.#onGlobalPointerDown, true); - - appSettings.on("update:teardropSize", this.#onTeardropSizeUpdate); - appSettings.on("update:teardropTimeout", this.#onTeardropTimeoutUpdate); - appSettings.on( - "update:touchMoveThreshold", - this.#onTouchMoveThresholdUpdate, - ); + document.addEventListener("pointerdown", this.#onGlobalPointerDown, true); + document.addEventListener("pointerup", this.#onGlobalPointerUp, true); + document.addEventListener("pointercancel", this.#onGlobalPointerUp, true); } destroy() { const root = this.#view.dom; - root.removeEventListener("touchstart", this.#onTouchStart, true); - root.removeEventListener("mousedown", this.#onMouseDown, true); root.removeEventListener("contextmenu", this.#onContextMenu, true); - document.removeEventListener("mouseup", this.#onMouseUp, true); - document.removeEventListener("mousedown", this.#onGlobalPointerDown, true); - document.removeEventListener("touchstart", this.#onGlobalPointerDown, true); - this.#removeTouchListeners(); - this.#stopAutoScroll(); - this.#clearScrollTimeout(); + document.removeEventListener( + "pointerdown", + this.#onGlobalPointerDown, + true, + ); + document.removeEventListener("pointerup", this.#onGlobalPointerUp, true); + document.removeEventListener( + "pointercancel", + this.#onGlobalPointerUp, + true, + ); + this.#clearMenuShowTimer(); cancelAnimationFrame(this.#stateSyncRaf); this.#stateSyncRaf = 0; - this.#pendingPointerTriggered = false; - this.#pendingSelectionChanged = false; - this.#clearLongPress(); - this.#clearCursorHideTimer(); - this.#clearSelectionUi(); this.#hideMenu(true); - this.$start.removeEventListener( - "touchstart", - this.#onStartHandleTouchStart, - ); - this.$end.removeEventListener("touchstart", this.#onEndHandleTouchStart); - this.$cursor.removeEventListener( - "touchstart", - this.#onCursorHandleTouchStart, - ); - appSettings.off("update:teardropSize", this.#onTeardropSizeUpdate); - appSettings.off("update:teardropTimeout", this.#onTeardropTimeoutUpdate); - appSettings.off( - "update:touchMoveThreshold", - this.#onTouchMoveThresholdUpdate, - ); } setEnabled(enabled) { this.#enabled = !!enabled; - if (!this.#enabled) { - this.#touchSession = null; - this.#dragState = null; - this.#removeTouchListeners(); - this.#stopAutoScroll(); - this.#clearScrollTimeout(); - cancelAnimationFrame(this.#stateSyncRaf); - this.#stateSyncRaf = 0; - this.#pendingPointerTriggered = false; - this.#pendingSelectionChanged = false; - this.#clearLongPress(); - this.#clearCursorHideTimer(); - this.#clearSelectionUi(); - this.#hideMenu(true); - } + if (this.#enabled) return; + this.#menuRequested = false; + this.#isPointerInteracting = false; + this.#isScrolling = false; + this.#clearMenuShowTimer(); + cancelAnimationFrame(this.#stateSyncRaf); + this.#stateSyncRaf = 0; + this.#hideMenu(true); } setSelection(value) { - this.#selectionActive = !!value; if (!this.#enabled) return; - if (value && !this.#hasSelection()) { - this.#selectWordAtCursor(); + if (value) { + this.#menuRequested = true; } - this.onStateChanged({ selectionChanged: true, pointerTriggered: !!value }); + this.onStateChanged({ + pointerTriggered: !!value, + selectionChanged: true, + }); } setMenu(value) { - this.#menuActive = !!value; + this.#menuRequested = !!value; if (!this.#enabled) return; if (!value) { + this.#clearMenuShowTimer(); this.#hideMenu(); return; } - const triggerType = this.#hasSelection() ? "end" : "cursor"; - if (triggerType === "end") { - this.#selectionActive = true; - this.#showSelectionHandles(); - } else { - this.#showCursorHandle(); - } - this.#showMenuDeferred(triggerType); + this.#scheduleMenuShow(MENU_SHOW_DELAY); } - onScroll() { + isMenuVisible() { + return this.#menuActive && this.$menu.isConnected; + } + + onScrollStart() { if (!this.#enabled) return; - if (this.#dragState) return; - this.#clearScrollTimeout(); + if (this.#isScrolling) return; + this.#clearMenuShowTimer(); this.#isScrolling = true; - cancelAnimationFrame(this.#stateSyncRaf); - this.#stateSyncRaf = 0; - this.#clearSelectionUi(); - this.#hideMenu(false, false); - this.#scrollTimeout = setTimeout(() => { - this.#onScrollEnd(); - }, 100); + this.#hideMenu(); } - #onScrollEnd() { - this.#scrollTimeout = null; - if (!this.#enabled) return; - if (!this.#isScrolling) return; + onScrollEnd() { + if (!this.#enabled || !this.#isScrolling) return; this.#isScrolling = false; - if (this.#dragState) return; - - if (this.#selectionActive && this.#hasSelection()) { - this.#showSelectionHandles(); - } else { - this.#showCursorHandle(); - } - - if (this.#menuActive) { - const triggerType = - this.#selectionActive && this.#hasSelection() ? "end" : "cursor"; - this.#showMenuDeferred(triggerType); - } - - if (this.#pendingPointerTriggered || this.#pendingSelectionChanged) { - this.onStateChanged(); - } + if (this.#shouldShowMenu()) this.#scheduleMenuShow(MENU_SHOW_DELAY); } onStateChanged(meta = {}) { if (!this.#enabled) return; if (this.#handlingMenuAction) return; - if (meta.pointerTriggered) this.#pendingPointerTriggered = true; - if (meta.selectionChanged) this.#pendingSelectionChanged = true; - if (this.#isScrolling) return; - cancelAnimationFrame(this.#stateSyncRaf); - this.#stateSyncRaf = requestAnimationFrame(() => { - this.#stateSyncRaf = 0; - this.#applyStateChange(); - }); + if (meta.selectionChanged && this.#menuActive) { + this.#hideMenu(); + } + if (!this.#shouldShowMenu()) { + if (!this.#hasSelection()) { + this.#menuRequested = false; + } + this.#clearMenuShowTimer(); + this.#hideMenu(); + return; + } + const delay = + meta.pointerTriggered || meta.selectionChanged ? MENU_SHOW_DELAY : 0; + this.#scheduleMenuShow(delay); } onSessionChanged() { if (!this.#enabled) return; - this.#clearSelectionUi(); + this.#menuRequested = false; + this.#isPointerInteracting = false; + this.#isScrolling = false; + this.#clearMenuShowTimer(); this.#hideMenu(true); - this.#selectionActive = this.#hasSelection(); - this.onStateChanged({ - selectionChanged: true, - pointerTriggered: this.#selectionActive, - }); } - #onTeardropSizeUpdate = (value) => { - this.config.teardropSize = value; - this.#syncHandleSize(); - if (!this.#enabled) return; - this.onStateChanged({ selectionChanged: true }); - }; - - #onTeardropTimeoutUpdate = (value) => { - this.config.teardropTimeout = value; - }; - - #onTouchMoveThresholdUpdate = (value) => { - this.config.touchMoveThreshold = value; - }; - - #onGlobalPointerDown = (event) => { - if (!this.#menuActive || !this.$menu.isConnected) return; - const target = event.target; - if ( - this.$menu.contains(target) || - this.$start.contains(target) || - this.$end.contains(target) || - this.$cursor.contains(target) - ) { - return; - } - if (this.#isIgnoredPointerTarget(target)) { - return; - } - if ( - event.type === "touchstart" && - target instanceof Node && - this.#view.dom.contains(target) - ) { - this.#hideMenu(false, false); - return; - } - this.#hideMenu(); - }; - #onContextMenu = (event) => { if (!this.#enabled) return; if (this.#isIgnoredPointerTarget(event.target)) return; event.preventDefault(); event.stopPropagation(); - - const { clientX, clientY } = event; - const moved = this.#moveCursorToCoords(clientX, clientY); - if (moved == null) return; - - if (!this.#hasSelection()) { - this.#selectWordAtCursor(); - } - - this.#selectionActive = this.#hasSelection(); - if (this.#selectionActive) { - this.#showSelectionHandles(); - this.#showMenuDeferred("end"); - return; - } - this.#showCursorHandle(); - this.#showMenuDeferred("cursor"); - }; - - #onMouseDown = (event) => { - if (!this.#enabled) return; - if (event.button !== 0) return; - if (this.#isIgnoredPointerTarget(event.target)) return; - this.#mouseSelecting = true; - }; - - #onMouseUp = (event) => { - if (!this.#enabled) return; - if (event.button !== 0) return; - if (!this.#mouseSelecting) return; - this.#mouseSelecting = false; - requestAnimationFrame(() => { - if (!this.#enabled || !this.#hasSelection()) return; - this.#selectionActive = true; - this.onStateChanged({ - pointerTriggered: true, - selectionChanged: true, - }); - }); - }; - - #onTouchStart = (event) => { - if (!this.#enabled || event.touches.length !== 1) return; - if (this.#isIgnoredPointerTarget(event.target)) { - this.#touchSession = null; - this.#clearLongPress(); - return; - } - const touch = event.touches[0]; - const { clientX, clientY } = touch; - const now = performance.now(); - this.#pointer.x = clientX; - this.#pointer.y = clientY; - - if (this.#isInHandle(this.$start, clientX, clientY)) { - event.preventDefault(); - this.#startDrag("start", clientX, clientY); - return; - } - - if (this.#isInHandle(this.$end, clientX, clientY)) { - event.preventDefault(); - this.#startDrag("end", clientX, clientY); - return; - } - - if ( - now >= this.#cursorHandleGuardUntil && - this.#isInHandle(this.$cursor, clientX, clientY, CURSOR_HANDLE_HIT_SLOP) - ) { - event.preventDefault(); - this.#startDrag("cursor", clientX, clientY); - return; - } - - if (this.#isEdgeGestureStart(clientX)) { - event.stopPropagation(); - event.stopImmediatePropagation?.(); - return; - } - - this.#touchSession = { - startX: clientX, - startY: clientY, - moved: false, - longPressFired: false, - }; - - this.#addTouchListeners({ passiveMove: true }); - this.#clearLongPress(); - this.#longPressTimer = setTimeout(() => { - if (!this.#touchSession || this.#touchSession.moved) return; - this.#touchSession.longPressFired = true; - this.#moveCursorToCoords(clientX, clientY); - this.#selectWordAtCursor(); - this.#selectionActive = true; - this.#showSelectionHandles(); - this.#showMenuDeferred("end"); - this.#vibrate(); - }, LONG_PRESS_DELAY); - }; - - #onStartHandleTouchStart = (event) => { - if (!this.#enabled || event.touches.length !== 1) return; - const touch = event.touches[0]; - event.preventDefault(); - event.stopPropagation(); - this.#startDrag("start", touch.clientX, touch.clientY); - }; - - #onEndHandleTouchStart = (event) => { - if (!this.#enabled || event.touches.length !== 1) return; - const touch = event.touches[0]; - event.preventDefault(); - event.stopPropagation(); - this.#startDrag("end", touch.clientX, touch.clientY); - }; - - #onCursorHandleTouchStart = (event) => { - if (!this.#enabled || event.touches.length !== 1) return; - const touch = event.touches[0]; - event.preventDefault(); - event.stopPropagation(); - this.#startDrag("cursor", touch.clientX, touch.clientY); + this.#menuRequested = true; + this.#scheduleMenuShow(MENU_SHOW_DELAY); }; - #onTouchMove = (event) => { - if (event.touches.length !== 1) return; - const touch = event.touches[0]; - const { clientX, clientY } = touch; - this.#pointer.x = clientX; - this.#pointer.y = clientY; - - if (this.#dragState) { - event.preventDefault(); - this.#dragTo(clientX, clientY); + #onGlobalPointerDown = (event) => { + const target = event.target; + if (this.$menu.contains(target)) return; + if (this.#isIgnoredPointerTarget(target)) return; + if (target instanceof Node && this.#view.dom.contains(target)) { + this.#isPointerInteracting = true; + this.#clearMenuShowTimer(); + this.#hideMenu(); return; } - - if (!this.#touchSession) return; - const dx = Math.abs(clientX - this.#touchSession.startX); - const dy = Math.abs(clientY - this.#touchSession.startY); - if ( - dx >= this.config.touchMoveThreshold || - dy >= this.config.touchMoveThreshold - ) { - this.#clearLongPress(); - } - if (dx >= TAP_MAX_DISTANCE || dy >= TAP_MAX_DISTANCE) { - this.#touchSession.moved = true; - } + this.#isPointerInteracting = false; + this.#menuRequested = false; + this.#hideMenu(); }; - #onTouchEnd = (event) => { - if (this.#dragState) { - event.preventDefault(); - this.#finishDrag(); - return; - } - - const session = this.#touchSession; - this.#touchSession = null; - this.#removeTouchListeners(); - this.#clearLongPress(); - if (!session) return; - if (session.longPressFired || session.moved) return; - - const changedTouch = event.changedTouches?.[0]; - if (!changedTouch) return; - const { clientX, clientY } = changedTouch; - const tapMeta = this.#getTapMeta(clientX, clientY); - const previousTap = this.#tap; - - let tap = classifyTap(previousTap, { - x: clientX, - y: clientY, - time: performance.now(), - pos: tapMeta.pos, - line: tapMeta.line, - column: tapMeta.column, - }); - if ( - tap.count > 1 && - previousTap?.line != null && - tapMeta.line != null && - (tapMeta.line !== previousTap.line || - Math.abs(tapMeta.column - previousTap.column) > TAP_MAX_COLUMN_DELTA) - ) { - tap = { ...tap, count: 1 }; - } - this.#tap = tap; - - const tapPos = tapMeta.pos ?? this.#coordsToPos(clientX, clientY); - if (tapPos == null) return; - - if (tap.count >= 3) { - event.preventDefault(); - this.#selectLineAtPos(tapPos); - this.#selectionActive = true; - this.#showSelectionHandles(); - this.#showMenuDeferred("end"); - this.#vibrate(); + #onGlobalPointerUp = () => { + if (!this.#isPointerInteracting) return; + this.#isPointerInteracting = false; + if (!this.#enabled) return; + if (this.#shouldShowMenu()) { + this.#scheduleMenuShow(MENU_SHOW_DELAY); return; } - - if (tap.count === 2) { - event.preventDefault(); - this.#selectWordAtPos(tapPos); - this.#selectionActive = true; - this.#showSelectionHandles(); - this.#showMenuDeferred("end"); - this.#vibrate(); - return; + if (!this.#hasSelection()) { + this.#menuRequested = false; } - - this.#selectionActive = false; this.#hideMenu(); - this.#removeSelectionHandles(); - this.#showCursorHandle(); }; - #addTouchListeners({ passiveMove = true } = {}) { - if ( - this.#touchListenersAttached && - this.#touchMovePassive === passiveMove - ) { - return; - } - if (this.#touchListenersAttached) { - this.#removeTouchListeners(); - } - this.#touchMovePassive = passiveMove; - document.addEventListener("touchmove", this.#onTouchMove, { - passive: passiveMove, - }); - document.addEventListener("touchend", this.#onTouchEnd, { - passive: false, - }); - this.#touchListenersAttached = true; - } - - #removeTouchListeners() { - if (!this.#touchListenersAttached) return; - document.removeEventListener("touchmove", this.#onTouchMove); - document.removeEventListener("touchend", this.#onTouchEnd); - this.#touchListenersAttached = false; - this.#touchMovePassive = true; - } - - #clearLongPress() { - clearTimeout(this.#longPressTimer); - this.#longPressTimer = null; - } - - #getTapMeta(x, y) { - const pos = this.#coordsToPos(x, y); - if (pos == null) { - return { pos: null, line: null, column: null }; - } - const lineInfo = this.#view.state.doc.lineAt(pos); - return { - pos, - line: lineInfo.number, - column: pos - lineInfo.from, - }; - } - - #vibrate() { - if (!appSettings.value.vibrateOnTap) return; - navigator.vibrate?.(constants.VIBRATION_TIME); - } - - #syncHandleSize() { - const size = this.config.teardropSize; - this.$start.dataset.size = size; - this.$end.dataset.size = size; - this.$cursor.dataset.size = size; - } - - #isEdgeGestureStart(x) { - const edge = constants.SIDEBAR_SLIDE_START_THRESHOLD_PX; - const width = window.innerWidth || 0; - return x <= edge || x >= width - edge; - } - - #isInHandle($el, x, y, hitSlop = HANDLE_HIT_SLOP) { - const rect = getElementRect($el); - if (!rect) return false; - return ( - x >= rect.left - hitSlop && - x <= rect.right + hitSlop && - y >= rect.top - hitSlop && - y <= rect.bottom + hitSlop - ); + #shouldShowMenu() { + if (this.#isScrolling || this.#isPointerInteracting || !this.#view.hasFocus) + return false; + return this.#hasSelection() || this.#menuRequested; + } + + #scheduleMenuShow(delay = 0) { + this.#clearMenuShowTimer(); + if (!this.#enabled || this.#isScrolling) return; + this.#menuShowTimer = setTimeout(() => { + this.#menuShowTimer = null; + if (!this.#enabled || this.#isScrolling) return; + if (!this.#shouldShowMenu()) { + if (!this.#hasSelection()) { + this.#menuRequested = false; + } + this.#hideMenu(); + return; + } + cancelAnimationFrame(this.#stateSyncRaf); + this.#stateSyncRaf = requestAnimationFrame(() => { + this.#stateSyncRaf = 0; + this.#showMenuDeferred(); + }); + }, delay); } #safeCoordsAtPos(view, pos) { @@ -712,130 +326,33 @@ class TouchSelectionMenuController { } } - #applyStateChange() { - const pointerTriggered = this.#pendingPointerTriggered; - const selectionChanged = this.#pendingSelectionChanged; - this.#pendingPointerTriggered = false; - this.#pendingSelectionChanged = false; - - if (this.#hasSelection()) { - if (pointerTriggered || selectionChanged) { - this.#selectionActive = true; - } - if (this.#selectionActive) { - this.#showSelectionHandles(); - this.$cursor.remove(); - if (pointerTriggered && !this.#dragState && !this.#mouseSelecting) { - this.#showMenuDeferred("end"); - } - } - } else { - this.#removeSelectionHandles(); - this.#selectionActive = false; - this.#showCursorHandle(); - } - - if (this.#menuActive && !this.#dragState && !this.#hasSelection()) { - this.#hideMenu(); - } - } - - #showSelectionHandles() { - if (!this.config.teardropSize || !this.#hasSelection()) { - this.#removeSelectionHandles(); - return; - } - - this.#clearCursorHideTimer(); - this.$cursor.remove(); - this.#view.requestMeasure({ - read: (view) => { - const range = view.state.selection.main; - const startCoords = this.#safeCoordsAtPos(view, range.from); - const endCoords = this.#safeCoordsAtPos(view, range.to); - if (!startCoords || !endCoords) return null; - const containerRect = this.#container.getBoundingClientRect(); - return { - startLeft: - startCoords.left - containerRect.left - this.config.teardropSize, - startTop: startCoords.bottom - containerRect.top, - endLeft: endCoords.left - containerRect.left, - endTop: endCoords.bottom - containerRect.top, - }; - }, - write: (data) => { - if (!data || !this.#selectionActive || !this.#hasSelection()) { - this.#removeSelectionHandles(); - return; - } - - this.$start.style.left = `${data.startLeft}px`; - this.$start.style.top = `${data.startTop}px`; - this.$end.style.left = `${data.endLeft}px`; - this.$end.style.top = `${data.endTop}px`; - - if (!this.$start.isConnected) this.#container.append(this.$start); - if (!this.$end.isConnected) this.#container.append(this.$end); - }, - }); - } - - #removeSelectionHandles() { - this.$start.remove(); - this.$end.remove(); - } - - #showCursorHandle() { - if ( - !this.config.teardropSize || - !this.#view.hasFocus || - this.#selectionActive - ) { - this.$cursor.remove(); - return; + #getMenuAnchor(selection = this.#hasSelection()) { + const range = this.#view.state.selection.main; + if (!selection) { + const caret = this.#safeCoordsAtPos(this.#view, range.head); + if (!caret) return null; + return { + x: (caret.left + caret.right) / 2, + top: caret.top, + bottom: caret.bottom, + hasSelection: false, + }; } - this.#view.requestMeasure({ - read: (view) => { - const head = view.state.selection.main.head; - const caret = this.#safeCoordsAtPos(view, head); - if (!caret) return null; - const containerRect = this.#container.getBoundingClientRect(); - return { - left: caret.left - containerRect.left, - top: caret.bottom - containerRect.top, - }; - }, - write: (data) => { - if (!data || this.#selectionActive) { - this.$cursor.remove(); - return; - } - this.$cursor.style.left = `${data.left}px`; - this.$cursor.style.top = `${data.top}px`; - if (!this.$cursor.isConnected) this.#container.append(this.$cursor); - this.#cursorHandleGuardUntil = - performance.now() + CURSOR_HANDLE_GUARD_MS; - this.#clearCursorHideTimer(); - this.#cursorHideTimer = setTimeout(() => { - this.$cursor.remove(); - }, this.config.teardropTimeout); - }, - }); - } - - #clearCursorHideTimer() { - clearTimeout(this.#cursorHideTimer); - this.#cursorHideTimer = null; - } - - #clearScrollTimeout() { - clearTimeout(this.#scrollTimeout); - this.#scrollTimeout = null; - this.#isScrolling = false; + const start = this.#safeCoordsAtPos(this.#view, range.from); + const end = this.#safeCoordsAtPos(this.#view, range.to); + const primary = start || end; + if (!primary) return null; + const secondary = end || start || primary; + return { + x: ((start?.left ?? primary.left) + (end?.left ?? secondary.left)) / 2, + top: Math.min(primary.top, secondary.top), + bottom: Math.max(primary.bottom, secondary.bottom), + hasSelection: true, + }; } - #showMenu($trigger) { + #showMenu(anchor) { const hasSelection = this.#hasSelection(); const items = filterSelectionMenuItems(selectionMenu(), { readOnly: this.#isReadOnly(), @@ -844,6 +361,7 @@ class TouchSelectionMenuController { this.$menu.innerHTML = ""; if (!items.length) { + this.#menuRequested = false; this.#hideMenu(true); return; } @@ -866,6 +384,7 @@ class TouchSelectionMenuController { onclick?.(); } finally { this.#handlingMenuAction = false; + this.#menuRequested = false; this.#hideMenu(); this.#view.focus(); } @@ -879,23 +398,28 @@ class TouchSelectionMenuController { this.#container.append(this.$menu); } - const triggerRect = getElementRect($trigger); - if (!triggerRect) { - this.#hideMenu(true); - return; - } - const containerRect = this.#container.getBoundingClientRect(); - const initialLeft = triggerRect.left; - const initialTop = triggerRect.bottom; - this.$menu.style.left = `${initialLeft - containerRect.left}px`; - this.$menu.style.top = `${initialTop - containerRect.top}px`; + this.$menu.style.left = "0px"; + this.$menu.style.top = "0px"; + this.$menu.style.visibility = "hidden"; const menuRect = this.$menu.getBoundingClientRect(); + const preferredLeft = anchor.x - menuRect.width / 2; + const aboveGap = anchor.hasSelection ? MENU_SELECTION_GAP : MENU_CARET_GAP; + const belowGap = anchor.hasSelection + ? MENU_HANDLE_CLEARANCE + : MENU_CARET_GAP; + const topAbove = anchor.top - menuRect.height - aboveGap; + const topBelow = anchor.bottom + belowGap; + const minTop = containerRect.top + MENU_MARGIN; + const maxTop = + containerRect.top + containerRect.height - menuRect.height - MENU_MARGIN; + const fitsAbove = topAbove >= minTop; + const fitsBelow = topBelow <= maxTop; const clamped = clampMenuPosition( { - left: menuRect.left, - top: menuRect.top, + left: preferredLeft, + top: fitsAbove || !fitsBelow ? topAbove : topBelow, width: menuRect.width, height: menuRect.height, }, @@ -909,319 +433,41 @@ class TouchSelectionMenuController { this.$menu.style.left = `${clamped.left - containerRect.left}px`; this.$menu.style.top = `${clamped.top - containerRect.top}px`; + this.$menu.style.visibility = ""; this.#menuActive = true; + this.#menuRequested = false; } - #showMenuDeferred(triggerType = "auto") { - requestAnimationFrame(() => { - if (!this.#enabled) return; - let $trigger = null; - const normalized = - triggerType === "auto" - ? this.#hasSelection() - ? "end" - : "cursor" - : triggerType; - - if (normalized === "cursor") { - $trigger = this.$cursor; - if (!$trigger.isConnected) { - this.#showCursorHandle(); - requestAnimationFrame(() => { - if (!this.#enabled || !this.$cursor.isConnected) return; - this.#showMenu(this.$cursor); - }); + #showMenuDeferred() { + if (!this.#enabled || this.#isScrolling || !this.#shouldShowMenu()) return; + const useSelectionAnchor = this.#hasSelection(); + this.#view.requestMeasure({ + read: () => this.#getMenuAnchor(useSelectionAnchor), + write: (anchor) => { + if (!this.#enabled || this.#isScrolling || !this.#shouldShowMenu()) { + this.#hideMenu(); return; } - this.#showMenu($trigger); - return; - } - - $trigger = normalized === "start" ? this.$start : this.$end; - if (!$trigger.isConnected) { - this.#showSelectionHandles(); - requestAnimationFrame(() => { - if (!this.#enabled || !this.#hasSelection()) return; - const $retryTrigger = - normalized === "start" - ? this.$start - : this.$end.isConnected - ? this.$end - : this.$start; - if (!$retryTrigger?.isConnected) return; - this.#showMenu($retryTrigger); - }); - return; - } - this.#showMenu($trigger); + if (!anchor) { + this.#hideMenu(true); + return; + } + this.#showMenu(anchor); + }, }); } - #hideMenu(force = false, clearActive = true) { + #hideMenu(force = false) { if (!force && !this.#menuActive && !this.$menu.isConnected) return; if (this.$menu.isConnected) { this.$menu.remove(); } - if (clearActive) { - this.#menuActive = false; - } - } - - #moveCursorToCoords(x, y) { - const pos = this.#coordsToPos(x, y); - if (pos == null) return null; - this.#view.dispatch({ - selection: EditorSelection.cursor(pos), - scrollIntoView: true, - userEvent: "select.pointer", - }); - return pos; - } - - #coordsToPos(x, y) { - let pos; - try { - pos = this.#view.posAtCoords({ x, y }); - } catch { - return null; - } - if (pos != null) return pos; - - const rect = this.#view.scrollDOM.getBoundingClientRect(); - const cx = clamp(x, rect.left + 1, rect.right - 1); - const cy = clamp(y, rect.top + 1, rect.bottom - 1); - try { - return this.#view.posAtCoords({ x: cx, y: cy }); - } catch { - return null; - } - } - - #selectWordAtCursor() { - const state = this.#view.state; - const head = state.selection.main.head; - this.#selectWordAtPos(head); - } - - #selectWordAtPos(pos) { - const state = this.#view.state; - const colorRange = getColorRange(); - if (colorRange) { - this.#view.dispatch({ - selection: EditorSelection.range(colorRange.from, colorRange.to), - scrollIntoView: true, - userEvent: "select.pointer", - }); - return; - } - - const word = state.wordAt(pos); - if (word) { - this.#view.dispatch({ - selection: EditorSelection.range(word.from, word.to), - scrollIntoView: true, - userEvent: "select.pointer", - }); - return; - } - - const line = state.doc.lineAt(pos); - this.#view.dispatch({ - selection: EditorSelection.range(line.from, line.to), - scrollIntoView: true, - userEvent: "select.pointer", - }); - } - - #selectLineAtCursor() { - const head = this.#view.state.selection.main.head; - this.#selectLineAtPos(head); - } - - #selectLineAtPos(pos) { - const line = this.#view.state.doc.lineAt(pos); - this.#view.dispatch({ - selection: EditorSelection.range(line.from, line.to), - scrollIntoView: true, - userEvent: "select.pointer", - }); - } - - #startDrag(type, x, y) { - this.#clearCursorHideTimer(); - this.#hideMenu(); - const range = this.#view.state.selection.main; - this.#dragState = { - type, - startX: x, - startY: y, - moved: false, - scrollX: 0, - scrollY: 0, - fixedPos: - type === "start" ? range.to : type === "end" ? range.from : null, - }; - this.#pointer.x = x; - this.#pointer.y = y; - this.#addTouchListeners({ passiveMove: false }); - } - - #dragTo(x, y) { - const state = this.#view.state; - const range = state.selection.main; - const lineHeight = this.#view.defaultLineHeight || 20; - const offsetY = y - lineHeight * DRAG_FINGER_OFFSET_FACTOR; - let effectiveX = x; - if (this.#dragState.type === "start") { - effectiveX += this.config.teardropSize; - } - const pos = this.#coordsToPos(effectiveX, offsetY); - if (pos == null) return; - const dragDistance = Math.hypot( - x - this.#dragState.startX, - y - this.#dragState.startY, - ); - if ( - !this.#dragState.moved && - dragDistance < this.config.touchMoveThreshold - ) { - return; - } - if (!this.#dragState.moved) { - this.#dragState.moved = true; - } - - if (this.#dragState.type === "cursor") { - this.#view.dispatch({ - selection: EditorSelection.cursor(pos), - scrollIntoView: true, - userEvent: "select.pointer", - }); - this.#showCursorHandle(); - return; - } - - let from = range.from; - let to = range.to; - if (this.#dragState.type === "start") { - to = this.#dragState.fixedPos ?? to; - const maxFrom = Math.max(0, to - 1); - from = clamp(pos, 0, maxFrom); - } else { - from = this.#dragState.fixedPos ?? from; - const minTo = Math.min(state.doc.length, from + 1); - to = clamp(pos, minTo, state.doc.length); - } - - this.#view.dispatch({ - selection: EditorSelection.range(from, to), - scrollIntoView: true, - userEvent: "select.pointer", - }); - this.#selectionActive = true; - this.#showSelectionHandles(); - this.#startAutoScrollIfNeeded(x, y); - } - - #finishDrag() { - this.#removeTouchListeners(); - this.#stopAutoScroll(); - const dragType = this.#dragState?.type; - const moved = !!this.#dragState?.moved; - this.#dragState = null; - if (dragType === "cursor") { - this.#showCursorHandle(); - this.#showMenuDeferred("cursor"); - } else { - this.#showSelectionHandles(); - if (moved || this.#hasSelection()) { - this.#showMenuDeferred(dragType === "start" ? "start" : "end"); - } - } - this.#view.focus(); - } - - #getAutoScrollDelta(x, y) { - const scroller = this.#view.scrollDOM; - const rect = scroller.getBoundingClientRect(); - const { horizontal, vertical } = getEdgeScrollDirections({ - x, - y, - rect, - allowHorizontal: !this.#view.lineWrapping, - }); - const maxScrollLeft = Math.max( - 0, - scroller.scrollWidth - scroller.clientWidth, - ); - const maxScrollTop = Math.max( - 0, - scroller.scrollHeight - scroller.clientHeight, - ); - let scrollX = horizontal * EDGE_SCROLL_STEP; - let scrollY = vertical * EDGE_SCROLL_STEP; - - if ( - (scrollX < 0 && scroller.scrollLeft <= 0) || - (scrollX > 0 && scroller.scrollLeft >= maxScrollLeft) - ) { - scrollX = 0; - } - - if ( - (scrollY < 0 && scroller.scrollTop <= 0) || - (scrollY > 0 && scroller.scrollTop >= maxScrollTop) - ) { - scrollY = 0; - } - - return { scrollX, scrollY }; + this.#menuActive = false; } - #startAutoScrollIfNeeded(x, y) { - const { scrollX, scrollY } = this.#getAutoScrollDelta(x, y); - if (this.#dragState) { - this.#dragState.scrollX = scrollX; - this.#dragState.scrollY = scrollY; - } - - if (!scrollX && !scrollY) { - this.#stopAutoScroll(); - return; - } - - if (this.#autoScrollRaf) return; - - const tick = () => { - if (!this.#dragState) { - this.#autoScrollRaf = 0; - return; - } - - const delta = this.#getAutoScrollDelta(this.#pointer.x, this.#pointer.y); - this.#dragState.scrollX = delta.scrollX; - this.#dragState.scrollY = delta.scrollY; - if (!delta.scrollX && !delta.scrollY) { - this.#autoScrollRaf = 0; - return; - } - - this.#view.scrollDOM.scrollLeft += delta.scrollX; - this.#view.scrollDOM.scrollTop += delta.scrollY; - this.#dragTo(this.#pointer.x, this.#pointer.y); - this.#autoScrollRaf = requestAnimationFrame(tick); - }; - - this.#autoScrollRaf = requestAnimationFrame(tick); - } - - #stopAutoScroll() { - cancelAnimationFrame(this.#autoScrollRaf); - this.#autoScrollRaf = 0; - if (this.#dragState) { - this.#dragState.scrollX = 0; - this.#dragState.scrollY = 0; - } + #clearMenuShowTimer() { + clearTimeout(this.#menuShowTimer); + this.#menuShowTimer = null; } #isReadOnly() { @@ -1241,7 +487,6 @@ class TouchSelectionMenuController { } if (!element) return false; if (element.closest(".cm-tooltip, .cm-panel")) return true; - // CodeMirror editor surface is contenteditable; do not ignore it. const editorContent = element.closest(".cm-content"); if (editorContent && this.#view.dom.contains(editorContent)) { return false; @@ -1260,9 +505,4 @@ class TouchSelectionMenuController { const selection = this.#view.state.selection.main; return selection.from !== selection.to; } - - #clearSelectionUi() { - this.$cursor.remove(); - this.#removeSelectionHandles(); - } } diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index e211db42b..6fc6798ad 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -10,7 +10,9 @@ import { } from "@codemirror/state"; import { oneDark } from "@codemirror/theme-one-dark"; import { + closeHoverTooltips, EditorView, + hasHoverTooltips, highlightActiveLineGutter, highlightTrailingWhitespace, highlightWhitespace, @@ -117,6 +119,22 @@ async function EditorManager($header, $body) { console.warn(message, error); } + function isCoarsePointerDevice() { + if (typeof window !== "undefined") { + try { + if (window.matchMedia?.("(pointer: coarse)").matches) { + return true; + } + } catch (_) { + // Ignore matchMedia capability errors and fall through. + } + } + return ( + typeof navigator !== "undefined" && + Number(navigator.maxTouchPoints || 0) > 0 + ); + } + const setNativeContextMenuDisabled = (disabled) => { const value = !!disabled; if (nativeContextMenuDisabled === value) return; @@ -224,12 +242,7 @@ async function EditorManager($header, $body) { tr.isUserEvent("touch") || tr.isUserEvent("select.touch"), ); - if ( - update.selectionSet || - update.docChanged || - update.geometryChanged || - pointerTriggered - ) { + if (update.selectionSet || pointerTriggered) { cancelAnimationFrame(touchSelectionSyncRaf); touchSelectionSyncRaf = requestAnimationFrame(() => { touchSelectionController?.onStateChanged({ @@ -463,7 +476,10 @@ async function EditorManager($header, $body) { compartments: [completionCompartment], build() { const live = !!appSettings?.value?.liveAutoCompletion; - return autocompletion({ activateOnTyping: live }); + return autocompletion({ + activateOnTyping: live, + activateOnTypingDelay: isCoarsePointerDevice() ? 220 : 100, + }); }, }, ]; @@ -1383,12 +1399,17 @@ async function EditorManager($header, $body) { if (typeof existing === "function") { document.removeEventListener(LSP_DIAGNOSTICS_EVENT, existing); } + let diagnosticsButtonSyncRaf = 0; const listener = () => { - const active = manager.activeFile; - if (active?.type === "editor") { - active.session = editor.state; - } - toggleProblemButton(); + cancelAnimationFrame(diagnosticsButtonSyncRaf); + diagnosticsButtonSyncRaf = requestAnimationFrame(() => { + diagnosticsButtonSyncRaf = 0; + const active = manager.activeFile; + if (active?.type === "editor") { + active.session = editor.state; + } + toggleProblemButton(); + }); }; document.addEventListener(LSP_DIAGNOSTICS_EVENT, listener); if (globalTarget) { @@ -1773,23 +1794,29 @@ async function EditorManager($header, $body) { scrollSyncRaf = 0; onscrolltop(); onscrollleft(); - touchSelectionController?.onScroll(); } function handleEditorScroll() { if (!scroller) return; + if (!isScrolling) { + isScrolling = true; + if (hasHoverTooltips(editor.state)) { + editor.dispatch({ effects: closeHoverTooltips }); + } + touchSelectionController?.onScrollStart(); + } if (!scrollSyncRaf) { scrollSyncRaf = requestAnimationFrame(syncScrollUi); } clearTimeout(scrollTimeout); - isScrolling = true; scrollTimeout = setTimeout(() => { isScrolling = false; + touchSelectionController?.onScrollEnd(); }, 100); } scroller?.addEventListener("scroll", handleEditorScroll, { passive: true }); - handleEditorScroll(); + syncScrollUi(); keyboardHandler.on("keyboardShowStart", () => { requestAnimationFrame(() => { @@ -1870,7 +1897,7 @@ async function EditorManager($header, $body) { const relativeTop = caret.top - scrollerRect.top + scroller.scrollTop; const relativeBottom = caret.bottom - scrollerRect.top + scroller.scrollTop; const topMargin = 16; - const bottomMargin = (appSettings.value?.teardropSize || 24) + 12; + const bottomMargin = 24; const scrollTop = scroller.scrollTop; const visibleTop = scrollTop + topMargin; diff --git a/src/lib/settings.js b/src/lib/settings.js index d25859742..7ee887856 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -158,8 +158,6 @@ class Settings { rememberFolders: true, diagonalScrolling: false, reverseScrolling: false, - teardropTimeout: 3000, - teardropSize: 30, scrollSpeed: constants.SCROLL_SPEED_NORMAL, customTheme: this.#customTheme, relativeLineNumbers: false, diff --git a/src/main.scss b/src/main.scss index 1f21cfeb2..124db5738 100644 --- a/src/main.scss +++ b/src/main.scss @@ -455,49 +455,6 @@ textarea { } } -.cursor { - position: absolute; - top: 0; - left: 0; - display: block; - border-radius: 50%; - background-color: white; - background-color: var(--primary-text-color); - border: solid 1px #666; - box-sizing: border-box; - transform-origin: left top; - z-index: 4; - pointer-events: none; - - &[data-size="60"] { - width: 60px; - height: 60px; - } - - &[data-size="30"] { - width: 30px; - height: 30px; - } - - &[data-size="20"] { - width: 20px; - height: 20px; - } - - &.end { - border-radius: 0% 50% 50% 50%; - } - - &.start { - border-radius: 50% 0 50% 50%; - } - - &.single { - transform: rotate(45deg); - border-radius: 0 50% 50% 50%; - } -} - .cursor-menu { position: absolute; top: 0; diff --git a/src/plugins/system/android/com/foxdebug/system/System.java b/src/plugins/system/android/com/foxdebug/system/System.java index 1509a9f44..0c32cdfe2 100644 --- a/src/plugins/system/android/com/foxdebug/system/System.java +++ b/src/plugins/system/android/com/foxdebug/system/System.java @@ -137,7 +137,7 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) { new Runnable() { @Override public void run() { - setNativeContextMenuDisabled(true); + setNativeContextMenuDisabled(false); } } ); @@ -2051,28 +2051,9 @@ private void setInputType(String type) { } private void setNativeContextMenuDisabled(boolean disabled) { - View webViewView = webView == null ? null : webView.getView(); - if (webViewView == null) { + if (webView == null) { return; } - - webViewView.setLongClickable(!disabled); - webViewView.setHapticFeedbackEnabled(!disabled); - if (disabled) { - webViewView.setOnLongClickListener(v -> true); - } else { - webViewView.setOnLongClickListener(null); - } - - try { - Method method = webViewView - .getClass() - .getMethod("setNativeContextMenuDisabled", boolean.class); - method.invoke(webViewView, disabled); - } catch (NoSuchMethodException ignored) { - // Fallback above keeps long-press context disabled even without CordovaLib patch. - } catch (IllegalAccessException | InvocationTargetException e) { - Log.w("System", "Failed to toggle native context menu state", e); - } + webView.setNativeContextMenuDisabled(disabled); } } diff --git a/src/settings/editorSettings.js b/src/settings/editorSettings.js index 83d727f37..ee983fe09 100644 --- a/src/settings/editorSettings.js +++ b/src/settings/editorSettings.js @@ -182,22 +182,6 @@ export default function editorSettings() { info: strings["settings-info-editor-fade-fold-widgets"], category: categories.guidesIndicators, }, - { - key: "teardropSize", - text: strings["cursor controller size"], - value: values.teardropSize, - valueText(value) { - return this.select.find(([v]) => v === value)[1]; - }, - select: [ - [0, strings.none], - [20, strings.small], - [30, strings.medium], - [60, strings.large], - ], - info: strings["settings-info-editor-teardrop-size"], - category: categories.cursorSelection, - }, { key: "shiftClickSelection", text: strings["shift click selection"], diff --git a/src/styles/codemirror.scss b/src/styles/codemirror.scss index 68610d760..972b38cf9 100644 --- a/src/styles/codemirror.scss +++ b/src/styles/codemirror.scss @@ -2,17 +2,6 @@ position: relative; } -.cm-editor, -.cm-editor * { - -webkit-touch-callout: none; -} - -.editor-container > .cursor { - pointer-events: auto; - touch-action: none; - z-index: 600; -} - .editor-container > .cursor-menu { z-index: 600; } From cb0d9215cba52a4cb4ee35723492c9126ca5fd3d Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:25:04 +0530 Subject: [PATCH 14/14] fix --- src/cm/colorView.ts | 2 ++ src/cm/indentGuides.ts | 2 ++ src/cm/lsp/diagnostics.ts | 37 ++++++++++++++++++++++++++------ src/cm/lsp/documentHighlights.ts | 2 ++ src/cm/rainbowBrackets.ts | 2 ++ 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/cm/colorView.ts b/src/cm/colorView.ts index 731b72a5d..94029d7ad 100644 --- a/src/cm/colorView.ts +++ b/src/cm/colorView.ts @@ -213,6 +213,8 @@ class ColorViewPlugin { scheduleDecorations(view: EditorView): void { this.pendingView = view; if (this.raf) return; + // Color chips are decorative, so batch rapid viewport/doc changes into + // one animation frame instead of rebuilding on every intermediate update. this.raf = requestAnimationFrame(() => { this.raf = 0; const pendingView = this.pendingView; diff --git a/src/cm/indentGuides.ts b/src/cm/indentGuides.ts index 1806c978a..61ac48eed 100644 --- a/src/cm/indentGuides.ts +++ b/src/cm/indentGuides.ts @@ -367,6 +367,8 @@ function createIndentGuidesPlugin( scheduleBuild(view: EditorView): void { this.pendingView = view; if (this.raf) return; + // Guide rebuilding is cosmetic and can be expensive on large + // viewports, so we intentionally collapse bursts into one frame. this.raf = requestAnimationFrame(() => { this.raf = 0; const pendingView = this.pendingView; diff --git a/src/cm/lsp/diagnostics.ts b/src/cm/lsp/diagnostics.ts index 274bcb93e..ae671b374 100644 --- a/src/cm/lsp/diagnostics.ts +++ b/src/cm/lsp/diagnostics.ts @@ -8,7 +8,7 @@ import { StateEffect, StateField, } from "@codemirror/state"; -import type { EditorView } from "@codemirror/view"; +import { type EditorView, ViewPlugin } from "@codemirror/view"; import type { LSPClientWithWorkspace, LSPPluginAPI, @@ -18,7 +18,8 @@ import type { } from "./types"; const setPublishedDiagnostics = StateEffect.define(); -let diagnosticsEventTimer = 0; +let diagnosticsEventTimer: ReturnType | null = null; +let diagnosticsViewCount = 0; export const LSP_DIAGNOSTICS_EVENT = "acode:lsp-diagnostics-updated"; @@ -67,6 +68,12 @@ function emitDiagnosticsUpdated(): void { document.dispatchEvent(event); } +function clearScheduledDiagnosticsUpdated(): void { + if (diagnosticsEventTimer == null) return; + clearTimeout(diagnosticsEventTimer); + diagnosticsEventTimer = null; +} + const lspPublishedDiagnostics = StateField.define({ create(): LspDiagnostic[] { return []; @@ -164,13 +171,30 @@ function sameDiagnostics( } function scheduleDiagnosticsUpdated(): void { - if (diagnosticsEventTimer) return; - diagnosticsEventTimer = window.setTimeout(() => { - diagnosticsEventTimer = 0; - emitDiagnosticsUpdated(); + if (diagnosticsEventTimer != null) return; + diagnosticsEventTimer = setTimeout(() => { + diagnosticsEventTimer = null; + if (diagnosticsViewCount > 0) { + emitDiagnosticsUpdated(); + } }, 32); } +const diagnosticsLifecyclePlugin = ViewPlugin.fromClass( + class { + constructor() { + diagnosticsViewCount++; + } + + destroy(): void { + diagnosticsViewCount = Math.max(0, diagnosticsViewCount - 1); + if (!diagnosticsViewCount) { + clearScheduledDiagnosticsUpdated(); + } + } + }, +); + function mapDiagnostics( plugin: LSPPluginAPI, state: EditorState, @@ -262,6 +286,7 @@ export function lspDiagnosticsUiExtension(includeGutter = true): Extension[] { ? () => [] : undefined; const extensions: Extension[] = [ + diagnosticsLifecyclePlugin, lspPublishedDiagnostics, linter(lspLinterSource, { needsRefresh(update) { diff --git a/src/cm/lsp/documentHighlights.ts b/src/cm/lsp/documentHighlights.ts index e2019006c..1abc5362a 100644 --- a/src/cm/lsp/documentHighlights.ts +++ b/src/cm/lsp/documentHighlights.ts @@ -105,6 +105,8 @@ function buildDecos( : [view.viewport]; const decos: Range[] = []; let rangeIndex = 0; + // process() keeps highlights sorted so visible range culling can advance + // with a single cursor instead of rescanning visibleRanges for every mark. for (const h of highlights) { if (h.from < 0 || h.to > docLen || h.from >= h.to) continue; while ( diff --git a/src/cm/rainbowBrackets.ts b/src/cm/rainbowBrackets.ts index 0df78dd8d..de57ffcab 100644 --- a/src/cm/rainbowBrackets.ts +++ b/src/cm/rainbowBrackets.ts @@ -40,6 +40,8 @@ const rainbowBracketsPlugin = ViewPlugin.fromClass( scheduleBuild(view: EditorView): void { this.pendingView = view; if (this.raf) return; + // Rainbow bracket colors are purely visual, so batch rebuilds to the + // next frame instead of recomputing on every transient update. this.raf = requestAnimationFrame(() => { this.raf = 0; const pendingView = this.pendingView;