From 900e25dc021471164c5b1f44c1be62383dbd773e Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Sat, 4 Jul 2026 09:10:44 +0530 Subject: [PATCH 1/6] fix: harden multi-pane editor state handling --- src/lib/editorManager.js | 138 +++++++++++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 34 deletions(-) diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index d80d64c1d..889f4316e 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -231,7 +231,7 @@ async function EditorManager($header, $body) { replaceChildren: $globalOpenFileList.replaceChildren.bind($globalOpenFileList), }; - let globalOpenFileListMirrorFiles = null; + let globalOpenFileListMirrorOrderSignature = ""; let globalOpenFileListMirrorActiveFileId = ""; const globalOpenFileListMirrorTabs = new Map(); const globalOpenFileListMirrorTabsById = new Map(); @@ -339,6 +339,7 @@ async function EditorManager($header, $body) { activeFile: null, editor: null, cleanupEditorListeners: null, + cleanupPaneListeners: null, editorContainer, touchSelectionController: null, element:
, @@ -355,13 +356,17 @@ async function EditorManager($header, $body) { pane.editorContainer.__editorPane = pane; pane.content.append(pane.editorContainer); pane.element.append(pane.tabList, pane.content); - pane.element.addEventListener( - "pointerdown", - () => { - activatePane(pane, { focusEditor: false }); - }, - true, - ); + function handlePanePointerDown() { + activatePane(pane, { focusEditor: false }); + } + pane.element.addEventListener("pointerdown", handlePanePointerDown, true); + pane.cleanupPaneListeners = () => { + pane.element.removeEventListener( + "pointerdown", + handlePanePointerDown, + true, + ); + }; if (registerPane) panes.push(pane); return pane; } @@ -401,6 +406,7 @@ async function EditorManager($header, $body) { if (!node || node.type !== "split") return; node.element.dataset.direction = node.direction; + cleanupPaneSplitHandles(node.element); node.element.replaceChildren(); node.children.forEach((child, index) => { if (index > 0) { @@ -414,12 +420,22 @@ async function EditorManager($header, $body) { function createPaneSplitHandle(splitNode, childIndex) { const $handle =
; $handle.dataset.direction = splitNode.direction; - $handle.addEventListener("pointerdown", (event) => { + function handleSplitPointerDown(event) { startPaneResize(event, splitNode, childIndex, $handle); - }); + } + $handle.addEventListener("pointerdown", handleSplitPointerDown); + $handle.__cleanupPaneSplitHandle = () => { + $handle.removeEventListener("pointerdown", handleSplitPointerDown); + }; return $handle; } + function cleanupPaneSplitHandles(container) { + container + ?.querySelectorAll?.(".editor-pane-split-handle") + .forEach((handle) => handle.__cleanupPaneSplitHandle?.()); + } + function replacePaneLayoutNode(oldNode, nextNode) { const parent = oldNode?.parent || null; if (parent) { @@ -518,6 +534,7 @@ async function EditorManager($header, $body) { : "1 1 0"; replacePaneLayoutNode(parent, onlyChild); parent.children = []; + cleanupPaneSplitHandles(parent.element); parent.element.remove(); return; } @@ -1327,7 +1344,7 @@ async function EditorManager($header, $body) { }); } - function buildLspMetadata(file) { + function buildLspMetadata(file, targetEditor = editor) { if (!file || file.type !== "editor") return null; const uri = getFileLspUri(file); if (!uri) return null; @@ -1336,18 +1353,20 @@ async function EditorManager($header, $body) { uri, languageId, languageName: file.currentMode || file.mode || languageId, - view: editor, + view: targetEditor, file, rootUri: resolveRootUriForContext({ uri, file }), }; } async function configureLspForFile(file) { - const metadata = buildLspMetadata(file); + const pane = getFilePane(file); + const targetEditor = pane?.editor || editor; + const metadata = buildLspMetadata(file, targetEditor); const token = ++lspRequestToken; if (!metadata) { detachActiveLsp(); - editor.dispatch({ effects: lspCompartment.reconfigure([]) }); + targetEditor?.dispatch({ effects: lspCompartment.reconfigure([]) }); return; } if (metadata.uri !== lastLspUri) { @@ -1357,23 +1376,36 @@ async function EditorManager($header, $body) { const extensions = (await lspClientManager.getExtensionsForFile(metadata)) || []; if (token !== lspRequestToken) return; + if (!isFileActiveInEditor(file, targetEditor)) return; if (!extensions.length) { lastLspUri = null; - editor.dispatch({ effects: lspCompartment.reconfigure([]) }); + targetEditor.dispatch({ effects: lspCompartment.reconfigure([]) }); return; } lastLspUri = metadata.uri; - editor.dispatch({ + targetEditor.dispatch({ effects: lspCompartment.reconfigure(extensions), }); + file.session = targetEditor.state; } catch (error) { if (token !== lspRequestToken) return; + if (!isFileActiveInEditor(file, targetEditor)) return; console.error("Failed to configure LSP", error); lastLspUri = null; - editor.dispatch({ effects: lspCompartment.reconfigure([]) }); + targetEditor.dispatch({ effects: lspCompartment.reconfigure([]) }); } } + function isFileActiveInEditor(file, targetEditor) { + const pane = getFilePane(file); + return !!( + file && + targetEditor && + pane?.editor === targetEditor && + pane.activeFile?.id === file.id + ); + } + function detachLspForFile(file) { if (!file || file.type !== "editor") return; const uri = getFileLspUri(file); @@ -2354,6 +2386,8 @@ async function EditorManager($header, $body) { } catch (error) { pane.touchSelectionController?.destroy?.(); pane.touchSelectionController = null; + pane.cleanupPaneListeners?.(); + pane.cleanupPaneListeners = null; pane.editor?.destroy?.(); pane.editor = null; warnRecoverable( @@ -2432,6 +2466,8 @@ async function EditorManager($header, $body) { pane.touchSelectionController?.destroy?.(); pane.touchSelectionController = null; + pane.cleanupPaneListeners?.(); + pane.cleanupPaneListeners = null; pane.cleanupEditorListeners?.(); pane.cleanupEditorListeners = null; pane.editor?.destroy?.(); @@ -2620,12 +2656,18 @@ async function EditorManager($header, $body) { file.__cmLanguageReady = ready; } - function dispatchLanguageExtension(file, languageSignature, ext, warnKey) { + function dispatchLanguageExtension( + file, + languageSignature, + ext, + warnKey, + targetEditor = editor, + ) { try { - editor.dispatch({ + targetEditor.dispatch({ effects: languageCompartment.reconfigure(ext || []), }); - file.session = editor.state; + file.session = targetEditor.state; markLanguageReady(file, languageSignature, true); } catch (error) { warnRecoverable("Failed to apply language extensions.", error, warnKey); @@ -2664,12 +2706,24 @@ async function EditorManager($header, $body) { if (isPaneActive && pane !== getActivePane()) { withPaneEditorContext(pane, () => { - dispatchLanguageExtension(file, languageSignature, ext, warnKey); + dispatchLanguageExtension( + file, + languageSignature, + ext, + warnKey, + pane.editor, + ); }); return; } - dispatchLanguageExtension(file, languageSignature, ext, warnKey); + dispatchLanguageExtension( + file, + languageSignature, + ext, + warnKey, + pane?.editor || editor, + ); }) .catch(() => { markLanguageReady(file, languageSignature, true); @@ -2689,21 +2743,27 @@ async function EditorManager($header, $body) { }, 80); } - function applyCurrentEditorOptions(file, { forceOptions = false } = {}) { - touchSelectionController?.onSessionChanged(); + function applyCurrentEditorOptions( + file, + { forceOptions = false, targetEditor = editor } = {}, + ) { + const targetPane = getEditorCompatibilityPane(targetEditor); + const targetTouchSelectionController = + targetPane?.touchSelectionController || touchSelectionController; + targetTouchSelectionController?.onSessionChanged(); const optionsSignature = getEditorOptionsSignature(); if (forceOptions || file.__cmOptionsSignature !== optionsSignature) { const desiredTheme = appSettings?.value?.editorTheme; - if (desiredTheme) editor.setTheme(desiredTheme); - applyOptions(); + if (desiredTheme) targetEditor.setTheme(desiredTheme); + applyOptions(null, targetEditor); file.__cmOptionsSignature = optionsSignature; } try { const ro = !file.editable || !!file.loading; - editor.dispatch({ + targetEditor.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(ro)), }); - file.session = editor.state; + file.session = targetEditor.state; } catch (error) { warnRecoverable( "Failed to apply read-only compartment update.", @@ -2784,7 +2844,7 @@ async function EditorManager($header, $body) { if (!forceRecreate && isReusableEditorState(file, extensionSignature)) { const reusedState = getRawEditorState(file.session); editor.setState(reusedState); - applyCurrentEditorOptions(file); + applyCurrentEditorOptions(file, { targetEditor: editor }); if (shouldApplyLanguage(file, reusedState, languageSignature)) { const ext = resolveLanguageExtension( @@ -2798,6 +2858,7 @@ async function EditorManager($header, $body) { languageSignature, ext, "reused-language-reconfigure", + editor, ); } } @@ -2867,7 +2928,7 @@ async function EditorManager($header, $body) { markLanguageReady(file, languageSignature, true); } editor.setState(state); - applyCurrentEditorOptions(file); + applyCurrentEditorOptions(file, { targetEditor: editor }); // Restore selection from previous state if available try { @@ -3673,12 +3734,13 @@ async function EditorManager($header, $body) { } function syncGlobalOpenFileListMirror() { + const nextOrderSignature = getGlobalOpenFileListMirrorOrderSignature(); const shouldRebuild = - globalOpenFileListMirrorFiles !== manager.files || + globalOpenFileListMirrorOrderSignature !== nextOrderSignature || $globalOpenFileList.childElementCount !== manager.files.length; if (shouldRebuild) { - rebuildGlobalOpenFileListMirror(); + rebuildGlobalOpenFileListMirror(nextOrderSignature); return; } @@ -3720,7 +3782,9 @@ async function EditorManager($header, $body) { syncGlobalOpenFileListMirrorActiveState(); } - function rebuildGlobalOpenFileListMirror() { + function rebuildGlobalOpenFileListMirror( + orderSignature = getGlobalOpenFileListMirrorOrderSignature(), + ) { globalOpenFileListMirrorTabs.clear(); globalOpenFileListMirrorTabsById.clear(); globalOpenFileListMirrorTabSignatures.clear(); @@ -3738,10 +3802,16 @@ async function EditorManager($header, $body) { }), ); - globalOpenFileListMirrorFiles = manager.files; + globalOpenFileListMirrorOrderSignature = orderSignature; globalOpenFileListMirrorActiveFileId = manager.activeFile?.id || ""; } + function getGlobalOpenFileListMirrorOrderSignature() { + return manager.files + .map((file) => `${file.id}:${file.paneId || ""}`) + .join("|"); + } + function createGlobalOpenFileListMirrorTab(file) { const tab = file.tab.cloneNode(true); tab.dataset.fileId = file.id; From 68ba6474967d8af4c49059eb0c20c94b30ec2920 Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Sat, 4 Jul 2026 09:45:09 +0530 Subject: [PATCH 2/6] fix: scope multi-pane lsp state --- src/lib/editorManager.js | 91 ++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 889f4316e..439fab622 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -340,6 +340,8 @@ async function EditorManager($header, $body) { editor: null, cleanupEditorListeners: null, cleanupPaneListeners: null, + lspRequestToken: 0, + lastLspUri: null, editorContainer, touchSelectionController: null, element:
, @@ -921,8 +923,6 @@ async function EditorManager($header, $body) { const diagnosticsClientExt = lspDiagnosticsClientExtension(); const buildDiagnosticsUiExt = () => lspDiagnosticsUiExtension(appSettings?.value?.lintGutter !== false); - let lspRequestToken = 0; - let lastLspUri = null; const UNTITLED_URI_PREFIX = "untitled://acode/"; function getEditorFontFamily() { @@ -1360,39 +1360,45 @@ async function EditorManager($header, $body) { } async function configureLspForFile(file) { - const pane = getFilePane(file); + const pane = getFileLspPane(file); + if (!pane) return; const targetEditor = pane?.editor || editor; const metadata = buildLspMetadata(file, targetEditor); - const token = ++lspRequestToken; + const token = ++pane.lspRequestToken; if (!metadata) { - detachActiveLsp(); + detachActiveLsp(pane, { invalidate: false }); targetEditor?.dispatch({ effects: lspCompartment.reconfigure([]) }); + if (file?.type === "editor" && targetEditor) { + file.session = targetEditor.state; + } return; } - if (metadata.uri !== lastLspUri) { - detachActiveLsp(); + if (metadata.uri !== pane.lastLspUri) { + detachActiveLsp(pane, { invalidate: false }); } try { const extensions = (await lspClientManager.getExtensionsForFile(metadata)) || []; - if (token !== lspRequestToken) return; + if (token !== pane.lspRequestToken) return; if (!isFileActiveInEditor(file, targetEditor)) return; if (!extensions.length) { - lastLspUri = null; + pane.lastLspUri = null; targetEditor.dispatch({ effects: lspCompartment.reconfigure([]) }); + file.session = targetEditor.state; return; } - lastLspUri = metadata.uri; + pane.lastLspUri = metadata.uri; targetEditor.dispatch({ effects: lspCompartment.reconfigure(extensions), }); file.session = targetEditor.state; } catch (error) { - if (token !== lspRequestToken) return; + if (token !== pane.lspRequestToken) return; if (!isFileActiveInEditor(file, targetEditor)) return; console.error("Failed to configure LSP", error); - lastLspUri = null; + pane.lastLspUri = null; targetEditor.dispatch({ effects: lspCompartment.reconfigure([]) }); + file.session = targetEditor.state; } } @@ -1410,14 +1416,19 @@ async function EditorManager($header, $body) { if (!file || file.type !== "editor") return; const uri = getFileLspUri(file); if (!uri) return; + const pane = getFileLspPane(file); + if (!pane) return; + const targetEditor = pane?.editor || editor; try { - lspClientManager.detach(uri); + lspClientManager.detach(uri, targetEditor); } catch (error) { console.warn(`Failed to detach LSP client for ${uri}`, error); } - if (uri === lastLspUri && manager.activeFile?.id === file.id) { - lastLspUri = null; - editor.dispatch({ effects: lspCompartment.reconfigure([]) }); + if (uri === pane.lastLspUri && pane.activeFile?.id === file.id) { + pane.lspRequestToken++; + pane.lastLspUri = null; + targetEditor.dispatch({ effects: lspCompartment.reconfigure([]) }); + file.session = targetEditor.state; } } @@ -1480,14 +1491,20 @@ async function EditorManager($header, $body) { return uri; } - function detachActiveLsp() { - if (!lastLspUri) return; + function detachActiveLsp(pane = getActivePane(), { invalidate = true } = {}) { + if (!pane) return; + if (invalidate) pane.lspRequestToken++; + if (!pane.lastLspUri) return; + const targetEditor = pane.editor || editor; try { - lspClientManager.detach(lastLspUri, editor); + lspClientManager.detach(pane.lastLspUri, targetEditor); } catch (error) { - console.warn(`Failed to detach LSP session for ${lastLspUri}`, error); + console.warn( + `Failed to detach LSP session for ${pane.lastLspUri}`, + error, + ); } - lastLspUri = null; + pane.lastLspUri = null; } function applyLspSettings() { @@ -2705,15 +2722,13 @@ async function EditorManager($header, $body) { } if (isPaneActive && pane !== getActivePane()) { - withPaneEditorContext(pane, () => { - dispatchLanguageExtension( - file, - languageSignature, - ext, - warnKey, - pane.editor, - ); - }); + dispatchLanguageExtension( + file, + languageSignature, + ext, + warnKey, + pane.editor, + ); return; } @@ -2738,7 +2753,10 @@ async function EditorManager($header, $body) { function scheduleLspForFile(file) { const fileId = file?.id; window.setTimeout(() => { - if (!fileId || manager.activeFile?.id !== fileId) return; + const pane = getFileLspPane(file); + const isGlobalActive = manager.activeFile?.id === fileId; + const isPaneActive = pane?.activeFile?.id === fileId; + if (!fileId || (!isGlobalActive && !isPaneActive)) return; void configureLspForFile(file); }, 80); } @@ -3943,6 +3961,17 @@ async function EditorManager($header, $body) { ); } + function getFileLspPane(file, targetEditor = null) { + return ( + getFilePane(file) || + getPaneById(file?.paneId) || + targetEditor?.__editorPane || + getActivePane() || + panes[0] || + null + ); + } + function getPaneFiles(fileOrPane = getActivePane()) { const pane = fileOrPane?.files ? fileOrPane : getFilePane(fileOrPane); return pane?.files || manager.files; From 526cbe3f2a2afc07aa15402134d37a554798cb56 Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Sat, 4 Jul 2026 09:54:34 +0530 Subject: [PATCH 3/6] fix: harden pane split cleanup --- src/lib/editorManager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 439fab622..774e6bc5d 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -435,7 +435,7 @@ async function EditorManager($header, $body) { function cleanupPaneSplitHandles(container) { container ?.querySelectorAll?.(".editor-pane-split-handle") - .forEach((handle) => handle.__cleanupPaneSplitHandle?.()); + ?.forEach((handle) => handle.__cleanupPaneSplitHandle?.()); } function replacePaneLayoutNode(oldNode, nextNode) { @@ -3961,11 +3961,10 @@ async function EditorManager($header, $body) { ); } - function getFileLspPane(file, targetEditor = null) { + function getFileLspPane(file) { return ( getFilePane(file) || getPaneById(file?.paneId) || - targetEditor?.__editorPane || getActivePane() || panes[0] || null From c585af79ba0a9ee7b62f9ddcae4dd1b6f3b5a71d Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Sat, 4 Jul 2026 10:13:53 +0530 Subject: [PATCH 4/6] fix: avoid unrelated lsp pane fallback --- src/lib/editorManager.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 774e6bc5d..1aa84b756 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -3962,13 +3962,14 @@ async function EditorManager($header, $body) { } function getFileLspPane(file) { - return ( - getFilePane(file) || - getPaneById(file?.paneId) || - getActivePane() || - panes[0] || - null - ); + const pane = getFilePane(file) || getPaneById(file?.paneId); + if (pane) return pane; + const id = file?.id || null; + const active = getActivePane(); + if (id && active?.activeFile?.id === id) return active; + const primary = panes[0] || null; + if (id && primary?.activeFile?.id === id) return primary; + return null; } function getPaneFiles(fileOrPane = getActivePane()) { From 34e8404eeeb52be53ff76ca6ee1f3a43c430da67 Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Sat, 4 Jul 2026 10:25:46 +0530 Subject: [PATCH 5/6] fix: guard pane-targeted async editor updates --- src/lib/editorManager.js | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 1aa84b756..f7f60febf 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -1361,8 +1361,8 @@ async function EditorManager($header, $body) { async function configureLspForFile(file) { const pane = getFileLspPane(file); - if (!pane) return; - const targetEditor = pane?.editor || editor; + if (!pane?.editor || pane.activeFile?.id !== file?.id) return; + const targetEditor = pane.editor; const metadata = buildLspMetadata(file, targetEditor); const token = ++pane.lspRequestToken; if (!metadata) { @@ -1403,7 +1403,7 @@ async function EditorManager($header, $body) { } function isFileActiveInEditor(file, targetEditor) { - const pane = getFilePane(file); + const pane = targetEditor?.__editorPane || getFileLspPane(file); return !!( file && targetEditor && @@ -2711,33 +2711,21 @@ async function EditorManager($header, $body) { markLanguageReady(file, languageSignature, false); result .then((ext) => { - const pane = getFilePane(file); - const isGlobalActive = manager.activeFile?.id === fileId; - const isPaneActive = pane?.activeFile?.id === fileId; + const pane = getFileLspPane(file); if ( - (!isGlobalActive && !isPaneActive) || + !pane?.editor || + pane.activeFile?.id !== fileId || file.__cmLanguageSignature !== languageSignature ) { return; } - if (isPaneActive && pane !== getActivePane()) { - dispatchLanguageExtension( - file, - languageSignature, - ext, - warnKey, - pane.editor, - ); - return; - } - dispatchLanguageExtension( file, languageSignature, ext, warnKey, - pane?.editor || editor, + pane.editor, ); }) .catch(() => { @@ -2754,9 +2742,8 @@ async function EditorManager($header, $body) { const fileId = file?.id; window.setTimeout(() => { const pane = getFileLspPane(file); - const isGlobalActive = manager.activeFile?.id === fileId; const isPaneActive = pane?.activeFile?.id === fileId; - if (!fileId || (!isGlobalActive && !isPaneActive)) return; + if (!fileId || !isPaneActive) return; void configureLspForFile(file); }, 80); } From bb4e50b3ab9ac2d2744f4a3d7c782dd0201e58b8 Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Sat, 4 Jul 2026 10:37:35 +0530 Subject: [PATCH 6/6] fix: detach lsp before closing panes --- src/lib/editorManager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index f7f60febf..dcffbc860 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -2487,6 +2487,7 @@ async function EditorManager($header, $body) { pane.cleanupPaneListeners = null; pane.cleanupEditorListeners?.(); pane.cleanupEditorListeners = null; + detachActiveLsp(pane); pane.editor?.destroy?.(); pane.editor = null; removePaneFromLayout(pane);