Skip to content

fix: harden multi-pane editor state handling#2444

Merged
deadlyjack merged 6 commits into
feat/multi-pane-viewfrom
fix/pr-2416-greptile-p1s
Jul 4, 2026
Merged

fix: harden multi-pane editor state handling#2444
deadlyjack merged 6 commits into
feat/multi-pane-viewfrom
fix/pr-2416-greptile-p1s

Conversation

@deadlyjack

Copy link
Copy Markdown
Member

Addresses the current Greptile P1 concerns on PR #2416: target-editor async LSP dispatch, pane-scoped editor option dispatch, incremental open-file mirror sync, and pane/split listener cleanup. Validation: git diff --check; biome check src/lib/editorManager.js; git merge-tree origin/main HEAD. tsc --noEmit --pretty false still reports pre-existing LangStrings errors in src/cm/lsp.

@deadlyjack

Copy link
Copy Markdown
Member Author

@greptile

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens multi-pane editor state handling by migrating LSP request tokens, last-URI tracking, and session saves from module-level globals to per-pane state, and by adding proper cleanup of pane pointer-down listeners and split-handle drag listeners.

  • Per-pane LSP state: lspRequestToken and lastLspUri move from module globals to pane.* fields; all LSP functions (configureLspForFile, detachLspForFile, detachActiveLsp, scheduleLspForFile) now resolve and operate on the correct pane editor, fixing cross-pane token cancellation and stale dispatch bugs from PR feat(editor): implement multi-pane split layout #2416.
  • Session persistence: file.session = targetEditor.state is now saved after every LSP compartment change (empty detach, extensions applied, no-extensions, and error paths), closing a stale-state gap that existed in the "no extensions" and early-return paths.
  • Listener cleanup: pane.cleanupPaneListeners and __cleanupPaneSplitHandle are introduced so pointer-down and split-resize handlers are properly removed when panes close or layouts re-render, preventing listener leaks.

Confidence Score: 4/5

Safe to merge with awareness that this is a large, interconnected state change across the LSP, session, and pane-lifecycle paths — all previously flagged correctness issues are resolved, but the multi-pane editor remains an active development surface.

All four inline comment concerns and all three outside-diff concerns from the prior review are addressed: per-pane lspRequestToken/lastLspUri, file.session saved after every LSP compartment change, dispatchLanguageExtension wired to an explicit targetEditor, pane pointer-down and split-handle listeners properly removed, detachActiveLsp called before editor destruction in closePane, scheduleLspForFile guarded against the pane-local active file rather than the global one, and detachLspForFile dispatching to the correct pane editor. No new defects were identified in this diff.

src/lib/editorManager.js — particularly the getFileLspPane fallback chain (active-pane and primary-pane heuristics) and the detachActiveLsp / detachLspForFile interaction when a file has been moved between panes mid-flight.

Important Files Changed

Filename Overview
src/lib/editorManager.js Core multi-pane hardening: per-pane LSP tokens, session saves in all LSP paths, pane listener cleanup, explicit targetEditor threading — all previously-flagged concerns are addressed; no new critical defects found, but the fallback chain in getFileLspPane and the detachActiveLsp/detachLspForFile interaction when files move between panes mid-flight merit continued attention.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as User Action
    participant SM as scheduleLspForFile
    participant CF as configureLspForFile
    participant DA as detachActiveLsp
    participant LC as lspClientManager
    participant PE as pane.editor (EditorView)

    UI->>SM: file opened in pane P
    SM->>SM: setTimeout 80ms
    SM->>SM: "check pane.activeFile.id === file.id"
    alt file no longer active in pane
        SM-->>UI: return (skip)
    else file still active
        SM->>CF: configureLspForFile(file)
        CF->>CF: getFileLspPane(file) → pane P
        CF->>CF: "pane.lspRequestToken++ (token=N)"
        alt URI changed
            CF->>DA: "detachActiveLsp(pane, invalidate=false)"
            DA->>LC: detach(oldUri, pane.editor)
            DA->>DA: "pane.lastLspUri = null"
        end
        CF->>LC: await getExtensionsForFile(metadata)
        LC-->>CF: extensions[]
        alt "token !== pane.lspRequestToken (stale)"
            CF-->>UI: return (cancelled)
        else file no longer active
            CF-->>UI: "return (isFileActiveInEditor=false)"
        else extensions empty
            CF->>PE: dispatch lspCompartment.reconfigure([])
            CF->>CF: "file.session = pane.editor.state"
        else extensions present
            CF->>PE: dispatch lspCompartment.reconfigure(extensions)
            CF->>CF: "pane.lastLspUri = metadata.uri"
            CF->>CF: "file.session = pane.editor.state"
        end
    end

    UI->>DA: closePane(pane P)
    DA->>DA: "detachActiveLsp(pane, invalidate=true)"
    DA->>DA: pane.lspRequestToken++ (cancels pending)
    DA->>LC: detach(pane.lastLspUri, pane.editor)
    DA->>PE: pane.editor.destroy()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as User Action
    participant SM as scheduleLspForFile
    participant CF as configureLspForFile
    participant DA as detachActiveLsp
    participant LC as lspClientManager
    participant PE as pane.editor (EditorView)

    UI->>SM: file opened in pane P
    SM->>SM: setTimeout 80ms
    SM->>SM: "check pane.activeFile.id === file.id"
    alt file no longer active in pane
        SM-->>UI: return (skip)
    else file still active
        SM->>CF: configureLspForFile(file)
        CF->>CF: getFileLspPane(file) → pane P
        CF->>CF: "pane.lspRequestToken++ (token=N)"
        alt URI changed
            CF->>DA: "detachActiveLsp(pane, invalidate=false)"
            DA->>LC: detach(oldUri, pane.editor)
            DA->>DA: "pane.lastLspUri = null"
        end
        CF->>LC: await getExtensionsForFile(metadata)
        LC-->>CF: extensions[]
        alt "token !== pane.lspRequestToken (stale)"
            CF-->>UI: return (cancelled)
        else file no longer active
            CF-->>UI: "return (isFileActiveInEditor=false)"
        else extensions empty
            CF->>PE: dispatch lspCompartment.reconfigure([])
            CF->>CF: "file.session = pane.editor.state"
        else extensions present
            CF->>PE: dispatch lspCompartment.reconfigure(extensions)
            CF->>CF: "pane.lastLspUri = metadata.uri"
            CF->>CF: "file.session = pane.editor.state"
        end
    end

    UI->>DA: closePane(pane P)
    DA->>DA: "detachActiveLsp(pane, invalidate=true)"
    DA->>DA: pane.lspRequestToken++ (cancels pending)
    DA->>LC: detach(pane.lastLspUri, pane.editor)
    DA->>PE: pane.editor.destroy()
Loading

Reviews (7): Last reviewed commit: "fix: detach lsp before closing panes" | Re-trigger Greptile

Comment thread src/lib/editorManager.js
@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens the multi-pane editor state management in editorManager.js by addressing four classes of bugs raised in the prior multi-pane PR: LSP dispatch targeting the correct pane editor, pane-scoped editor option dispatch, incremental open-file mirror sync via an order-signature string instead of reference equality, and cleanup of pointerdown listeners on pane and split-handle elements.

  • LSP pane-targeting: configureLspForFile now resolves targetEditor from the file's owning pane, and a new isFileActiveInEditor guard prevents stale async LSP dispatches from landing on the wrong pane after the user switches files or panes.
  • Options/language scoping: applyCurrentEditorOptions and dispatchLanguageExtension now accept explicit targetEditor parameters, replacing the implicit global editor dependency so that background-pane files receive correct read-only and theme state.
  • Listener cleanup: cleanupPaneListeners and cleanupPaneSplitHandles are wired into both normal pane teardown and the error recovery path in createPane, fixing the listener leak that existed when pane creation failed or split handles were re-rendered.

Confidence Score: 4/5

The PR is safe to merge; all four targeted concerns from the prior review are correctly addressed and the core listener-cleanup and pane-scoped dispatch logic is sound.

The LSP async-guard (isFileActiveInEditor), pane-scoped targetEditor threading, split-handle cleanup, and order-signature mirror sync are all correctly implemented. Two minor gaps remain: file.session is not updated after the LSP-clear dispatch in the no-extensions and error paths (inconsistent with the success path which now updates it), and the withPaneEditorContext wrapper around dispatchLanguageExtension is now a no-op since the function uses its explicit targetEditor parameter directly.

The single changed file src/lib/editorManager.js — specifically the configureLspForFile no-extensions and error-catch return paths, and the async language-extension resolution callback.

Important Files Changed

Filename Overview
src/lib/editorManager.js Multi-pane hardening: LSP dispatch now targets the file's owning pane editor; applyCurrentEditorOptions and dispatchLanguageExtension are pane-scoped; split-handle and pane pointerdown listeners are correctly cleaned up; open-file mirror uses a stable order-signature string. Two inconsistencies remain: file.session is not updated after the LSP-clear dispatch in the no-extensions and error paths, and the withPaneEditorContext wrapper around dispatchLanguageExtension is now redundant.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant PaneA as Pane A (active)
    participant PaneB as Pane B (background)
    participant EM as EditorManager
    participant LSP as lspClientManager

    User->>PaneA: switch active file
    EM->>EM: scheduleLspForFile(file)
    EM->>EM: configureLspForFile(file)
    note over EM: getFilePane(file) → PaneA<br/>targetEditor = PaneA.editor<br/>token = ++lspRequestToken

    EM->>LSP: getExtensionsForFile(metadata) [async]
    LSP-->>EM: extensions[]

    EM->>EM: isFileActiveInEditor(file, PaneA.editor)
    note over EM: pane.editor === targetEditor &&<br/>pane.activeFile.id === file.id

    alt file still active in PaneA
        EM->>PaneA: dispatch(lspCompartment.reconfigure(ext))
        EM->>EM: "file.session = PaneA.editor.state"
    else file switched or pane changed
        EM->>EM: return (stale dispatch suppressed)
    end

    User->>PaneB: open file in background pane
    EM->>EM: applyFileToPaneEditor(file, PaneB)
    note over EM: scheduleLsp = false (non-active pane)
    EM->>PaneB: "setState + applyCurrentEditorOptions(targetEditor=PaneB.editor)"
    EM->>PaneB: dispatchLanguageExtension(..., PaneB.editor)
    EM->>EM: "file.session = PaneB.editor.state"

    User->>EM: closePane(PaneA)
    EM->>EM: pane.cleanupPaneListeners()
    EM->>EM: cleanupPaneSplitHandles(parent.element)
    EM->>PaneA: editor.destroy()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant PaneA as Pane A (active)
    participant PaneB as Pane B (background)
    participant EM as EditorManager
    participant LSP as lspClientManager

    User->>PaneA: switch active file
    EM->>EM: scheduleLspForFile(file)
    EM->>EM: configureLspForFile(file)
    note over EM: getFilePane(file) → PaneA<br/>targetEditor = PaneA.editor<br/>token = ++lspRequestToken

    EM->>LSP: getExtensionsForFile(metadata) [async]
    LSP-->>EM: extensions[]

    EM->>EM: isFileActiveInEditor(file, PaneA.editor)
    note over EM: pane.editor === targetEditor &&<br/>pane.activeFile.id === file.id

    alt file still active in PaneA
        EM->>PaneA: dispatch(lspCompartment.reconfigure(ext))
        EM->>EM: "file.session = PaneA.editor.state"
    else file switched or pane changed
        EM->>EM: return (stale dispatch suppressed)
    end

    User->>PaneB: open file in background pane
    EM->>EM: applyFileToPaneEditor(file, PaneB)
    note over EM: scheduleLsp = false (non-active pane)
    EM->>PaneB: "setState + applyCurrentEditorOptions(targetEditor=PaneB.editor)"
    EM->>PaneB: dispatchLanguageExtension(..., PaneB.editor)
    EM->>EM: "file.session = PaneB.editor.state"

    User->>EM: closePane(PaneA)
    EM->>EM: pane.cleanupPaneListeners()
    EM->>EM: cleanupPaneSplitHandles(parent.element)
    EM->>PaneA: editor.destroy()
Loading

Reviews (2): Last reviewed commit: "fix: harden multi-pane editor state hand..." | Re-trigger Greptile

Comment thread src/lib/editorManager.js Outdated
Comment thread src/lib/editorManager.js Outdated
@deadlyjack

Copy link
Copy Markdown
Member Author

@greptile

Comment thread src/lib/editorManager.js
@deadlyjack

Copy link
Copy Markdown
Member Author

@greptile

@deadlyjack

Copy link
Copy Markdown
Member Author

@greptile

@deadlyjack

Copy link
Copy Markdown
Member Author

@greptile

@deadlyjack

Copy link
Copy Markdown
Member Author

@greptile

@deadlyjack deadlyjack merged commit 9fc7d83 into feat/multi-pane-view Jul 4, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in The Code Board - Acode Jul 4, 2026
@deadlyjack deadlyjack deleted the fix/pr-2416-greptile-p1s branch July 4, 2026 11:00
deadlyjack added a commit that referenced this pull request Jul 4, 2026
* feat: add multi pane view

* add ability to drag one tab from one pane to other

* add vertical pane support and keybinds and bug fixes

- added vertical pane creation too
- vscode style keybinds
- resizing of pane
- few bug fixes

* fix resize pane empty space

* fix: issues and bugs

* fixed pane focus lifecycle

* fix

* fix debounce timer and load related issue

* fix

* fix the drag snap-back

* fix

* fix tab cleanup

* added cleanup

* fix

* fixed bunch of issues introduced accidently

* remove double doc sync and add Ctrl-touch for multi cursor too

* correctly drop at carret

* fix and use doc cache instead of direct doc.toString

* cleanup

* fix untiled.txt close the pane and remove ctrl-touch things

* fix: harden multi-pane editor state handling (#2444)

* fix: address multi-pane review concerns (#2443)

---------

Co-authored-by: Ajit Kumar <me@ajitkumar.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants