Skip to content

fix(editor): prevent syntax highlight color flicker on state recreation (#2304)#2457

Open
Elitex07 wants to merge 3 commits into
Acode-Foundation:mainfrom
Elitex07:fix/issue-2304
Open

fix(editor): prevent syntax highlight color flicker on state recreation (#2304)#2457
Elitex07 wants to merge 3 commits into
Acode-Foundation:mainfrom
Elitex07:fix/issue-2304

Conversation

@Elitex07

@Elitex07 Elitex07 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Cached resolved language extensions (file.__cmCachedLanguageExtension and file.__cmCachedLanguageSignature) on the EditorFile instance. When resolveLanguageExtension runs for a file whose language signature matches the cache, it synchronously returns the cached extension immediately.

@Elitex07

Elitex07 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@greptile

@Elitex07 Elitex07 moved this from Backlog to Ready in The Code Board - Acode Jul 8, 2026
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes syntax highlight color flickering (issue #2304) that occurred when the CodeMirror editor state was recreated. The flicker happened because the async language extension loader would return [] (plain text) on first call, then re-apply the highlight extension once the promise resolved — causing a brief unstyled flash on every state recreation.

  • editorFile.js: Adds __cmCachedLanguageExtension and __cmCachedLanguageSignature initialization to null inside setMode, ensuring the cache is invalidated whenever the language mode changes.
  • editorManager.js: Populates the two new cache fields in three code paths — successful synchronous resolution, successful async resolution, and successful dispatchLanguageExtension. Also adds an early-return cache hit in resolveLanguageExtension so subsequent state recreations get the extension synchronously, without async flicker.

Confidence Score: 5/5

Safe to merge — the cache logic is well-scoped, invalidation on mode change is correct, and the hasLanguageSupport fallback in shouldApplyLanguage provides a safety net if a dispatch ever fails after the cache is populated.

The fix is minimal and targeted: it caches the resolved language extension keyed to a language signature, clears the cache when the mode changes, and the three write sites in editorManager.js are each placed after the relevant resolution succeeds. The previously flagged concern about cache ordering was already addressed in this diff — the writes inside dispatchLanguageExtension now follow a successful targetEditor.dispatch. The async path intentionally writes the cache before the pane-active guard so future activations can return synchronously, and __cmLanguageReady remains false until dispatch actually completes.

No files require special attention; both changed files have clear, well-bounded modifications.

Important Files Changed

Filename Overview
src/lib/editorFile.js Adds cache field resets to setMode — correctly invalidates cache on every language change.
src/lib/editorManager.js Adds cache hit early-return path and cache writes in all three resolution paths; dispatch-path cache writes are correctly placed after a successful dispatch.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[applyFileToEditor called] --> B{isReusableEditorState?}
    B -- Yes --> C[reuse existing state]
    B -- No --> D[recreate state]
    C --> E{shouldApplyLanguage?}
    E -- No --> Z[done]
    E -- Yes --> F[resolveLanguageExtension]
    D --> F
    F --> G{cache hit?}
    G -- Yes --> H[return cached ext synchronously]
    G -- No --> K[call langExtFn]
    K --> L{result is Promise?}
    L -- No --> M[write cache and return result]
    L -- Yes --> N[return empty immediately]
    N --> O[async: promise resolves]
    O --> P{sig still matches?}
    P -- No --> Q[abandon]
    P -- Yes --> R[write cache]
    R --> S{pane active?}
    S -- No --> T[cache stored for future activation]
    S -- Yes --> U[dispatchLanguageExtension]
    U --> V{dispatch succeeds?}
    V -- No --> W[warnRecoverable]
    V -- Yes --> X[markLanguageReady true]
    D --> AA[push ext into initial EditorState - no flicker]
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"}}}%%
flowchart TD
    A[applyFileToEditor called] --> B{isReusableEditorState?}
    B -- Yes --> C[reuse existing state]
    B -- No --> D[recreate state]
    C --> E{shouldApplyLanguage?}
    E -- No --> Z[done]
    E -- Yes --> F[resolveLanguageExtension]
    D --> F
    F --> G{cache hit?}
    G -- Yes --> H[return cached ext synchronously]
    G -- No --> K[call langExtFn]
    K --> L{result is Promise?}
    L -- No --> M[write cache and return result]
    L -- Yes --> N[return empty immediately]
    N --> O[async: promise resolves]
    O --> P{sig still matches?}
    P -- No --> Q[abandon]
    P -- Yes --> R[write cache]
    R --> S{pane active?}
    S -- No --> T[cache stored for future activation]
    S -- Yes --> U[dispatchLanguageExtension]
    U --> V{dispatch succeeds?}
    V -- No --> W[warnRecoverable]
    V -- Yes --> X[markLanguageReady true]
    D --> AA[push ext into initial EditorState - no flicker]
Loading

Reviews (3): Last reviewed commit: "fix(editor): move language extension cac..." | Re-trigger Greptile

Comment thread src/lib/editorManager.js
@Elitex07 Elitex07 mentioned this pull request Jul 8, 2026
1 task
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR caches the resolved CodeMirror language extension on each EditorFile instance (__cmCachedLanguageExtension / __cmCachedLanguageSignature) so that when the editor state is recreated for a file whose language signature has not changed, the extension is returned synchronously and applied at EditorState.create time, eliminating the async-load flicker.

  • resolveLanguageExtension now checks the per-file cache before invoking langExtFn(); on a hit it calls markLanguageReady(true) and returns immediately.
  • dispatchLanguageExtension (and the async .then() path) populates the cache after resolving the extension; the async path also moves the stale-signature guard to the top of the callback, which is a correctness improvement over the original.
  • setMode explicitly nulls both cache fields, ensuring a mode change always triggers a fresh load.

Confidence Score: 4/5

Safe to merge; the caching logic is sound and cache invalidation is handled correctly on mode changes and signature mismatches.

The fix correctly addresses the flicker by pre-populating the language extension cache. The only notable concern is that dispatchLanguageExtension writes both cache fields before calling targetEditor.dispatch(), so a dispatch failure leaves the cache populated while markLanguageReady was never called. The existing hasLanguageSupport check in shouldApplyLanguage mitigates this in practice, but the ordering is a subtle invariant worth tightening.

The cache-write ordering inside dispatchLanguageExtension in src/lib/editorManager.js is the one spot worth a second look.

Important Files Changed

Filename Overview
src/lib/editorManager.js Adds per-file caching of resolved language extensions in resolveLanguageExtension and dispatchLanguageExtension; cache is set before dispatch succeeds in dispatchLanguageExtension, which is a minor invariant concern handled by the existing hasLanguageSupport fallback
src/lib/editorFile.js Clears the two new cache fields (__cmCachedLanguageExtension and __cmCachedLanguageSignature) on every setMode call, correctly invalidating the cache when the mode changes

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant F as EditorFile
    participant EM as EditorManager
    participant CM as CodeMirror

    Note over F,CM: First activation (cold)
    EM->>EM: resolveLanguageExtension(file, sig)
    EM->>EM: cache miss → langExtFn()
    alt Async language load
        EM-->>CM: return [] (placeholder)
        CM->>CM: "EditorState.create(exts=[])"
        Note over CM: Renders with no syntax highlighting
        EM->>EM: promise.then(ext)
        EM->>F: "__cmCachedLanguageExtension = ext"
        EM->>F: "__cmCachedLanguageSignature = sig"
        EM->>CM: dispatchLanguageExtension(ext)
        Note over CM: Syntax highlights applied (flicker)
    else Sync language load
        EM-->>CM: return ext
        EM->>F: "__cmCachedLanguageExtension = ext"
        EM->>F: "__cmCachedLanguageSignature = sig"
        CM->>CM: "EditorState.create(exts=[ext])"
    end

    Note over F,CM: Subsequent activation (warm, same sig)
    EM->>EM: resolveLanguageExtension(file, sig)
    EM->>F: cache check → HIT
    EM->>EM: markLanguageReady(true)
    EM-->>CM: return __cmCachedLanguageExtension
    CM->>CM: "EditorState.create(exts=[cachedExt])"
    Note over CM: Syntax highlights applied immediately, no flicker

    Note over F,CM: setMode() invalidation
    F->>F: "__cmCachedLanguageExtension = null"
    F->>F: "__cmCachedLanguageSignature = null"
    Note over F: Forces fresh load on next activation
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 F as EditorFile
    participant EM as EditorManager
    participant CM as CodeMirror

    Note over F,CM: First activation (cold)
    EM->>EM: resolveLanguageExtension(file, sig)
    EM->>EM: cache miss → langExtFn()
    alt Async language load
        EM-->>CM: return [] (placeholder)
        CM->>CM: "EditorState.create(exts=[])"
        Note over CM: Renders with no syntax highlighting
        EM->>EM: promise.then(ext)
        EM->>F: "__cmCachedLanguageExtension = ext"
        EM->>F: "__cmCachedLanguageSignature = sig"
        EM->>CM: dispatchLanguageExtension(ext)
        Note over CM: Syntax highlights applied (flicker)
    else Sync language load
        EM-->>CM: return ext
        EM->>F: "__cmCachedLanguageExtension = ext"
        EM->>F: "__cmCachedLanguageSignature = sig"
        CM->>CM: "EditorState.create(exts=[ext])"
    end

    Note over F,CM: Subsequent activation (warm, same sig)
    EM->>EM: resolveLanguageExtension(file, sig)
    EM->>F: cache check → HIT
    EM->>EM: markLanguageReady(true)
    EM-->>CM: return __cmCachedLanguageExtension
    CM->>CM: "EditorState.create(exts=[cachedExt])"
    Note over CM: Syntax highlights applied immediately, no flicker

    Note over F,CM: setMode() invalidation
    F->>F: "__cmCachedLanguageExtension = null"
    F->>F: "__cmCachedLanguageSignature = null"
    Note over F: Forces fresh load on next activation
Loading

Comments Outside Diff (1)

  1. src/lib/editorManager.js, line 2700-2718 (link)

    P2 Cache set before dispatch may survive a failed dispatch. Both __cmCachedLanguageExtension and __cmCachedLanguageSignature are written before targetEditor.dispatch() is called. If dispatch throws, the cache records the extension as resolved for this signature, but markLanguageReady (inside the try block) is never called, so __cmLanguageSignature remains stale. On the very next activation the cache hit in resolveLanguageExtension will call markLanguageReady(true), marking the language as ready even though the extension was never applied. The existing hasLanguageSupport fallback in shouldApplyLanguage rescues most cases, but consider moving the cache writes to after a successful dispatch to keep the invariant clean.

Reviews (2): Last reviewed commit: "style: normalize line endings with biome..." | Re-trigger Greptile

@Elitex07

Elitex07 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@greptile

@Elitex07

Elitex07 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@bajrangCoder :)

@Palloxin

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

2 participants