Fix races between Markdown extension loading and mid-edit renames

Why

The single file editor loads and unloads the Markdown extensions (live preview and formatting shortcuts) through a dynamic import as the file is renamed mid-edit. When the file name changes again before that import resolves, the rename races with the load: switching away calls unuse for extensions that were never loaded, and the load that resolves afterward installs the extensions anyway, leaving the live preview active on a non-Markdown file:

---
config:
#  theme: default
  fontSize: 14
---
sequenceDiagram
    autonumber
    actor User
    participant EditBlob
    participant Import as dynamic import

    User->>EditBlob: types filename → foo.md<br>(input event per keystroke)
    EditBlob->>+Import: start loading the Markdown extensions

    rect
        note right of User: race window — the load is still in flight
        User->>EditBlob: keeps typing → foo.mdx
        Note over EditBlob: unuse is called for extensions<br>that were never loaded
    end

    Import-->>-EditBlob: import resolves (too late)
    Note over EditBlob: the extensions are installed anyway —<br>live preview stays on a non-Markdown file

As an adjacent bug on the same code path, addEditorMarkdownListeners also ran on failed loads, registering Markdown shortcut listeners on an editor without the extensions.

What this MR does

🔴 RED → 🟢 GREEN cycles:

  1. Add a spec for switching away from Markdown while the extensions are still loading — 🔴 RED at this point due to the existing bug.
  2. Only unuse extensions that have actually been loaded, turning it 🟢 GREEN.
  3. Add a spec for listener registration after a failed load — 🔴 RED.
  4. Move addEditorMarkdownListeners to the success path only, turning it 🟢 GREEN.
  5. Add specs for duplicate extension loading — 🔴 RED.
  6. Cache the load promise so re-entrant calls don't start a second load, turning it 🟢 GREEN.
  7. Add a spec for stale extension loads — 🔴 RED.
  8. Skip installing the extensions once the imports resolve if the file was renamed away in the meantime, turning it 🟢 GREEN.
  9. Add a spec for two loads racing after a rename round-trip — 🔴 RED.
  10. Skip extensions that a racing load has already installed, turning it 🟢 GREEN.
---
title: Extension load lifecycle
config:
#  theme: default
#  look: handDrawn
#  layout: elk
#  elk:
#    cycleBreakingStrategy: GREEDY_MODEL_ORDER
#    nodePlacementStrategy: LINEAR_SEGMENTS
---

stateDiagram-v2
    direction LR
    state "Not loaded<br>load: null<br>extensions: null" as Idle
    state "Loading<br>load: Promise<br>extensions: null" as Loading
    state "Loaded<br>load: Promise<br>extensions: handle" as Loaded

    [*] --> Idle
    Idle --> Loading: switched to markdown<br>(fetch starts, cache set)
    # Idle --> Idle: stale import resolves<br>(guard skips install)
    Loading --> Loaded: import resolves<br>(guard passes → use + listeners)
    Loading --> Idle: switched away mid-load<br>(cache cleared — no unuse)
    Loading --> Idle: load fails<br>(alert + cache cleared)
    Loaded --> Idle: switched away<br>(unuse + cache cleared)

    note right of Idle
        A stale import settling here is skipped by the guard.
    end note
    note right of Loading
        Switching to markdown again hits the cache — no second load.
    end note
    note right of Loaded
        Switching to markdown again hits the cache.
        A racing load resolving here is skipped (already installed).
    end note

References

This MR is groundwork for:

How to set up and validate locally

The race requires acting before the load completes, so emulate a slow connection:

  1. In DevTools, set network throttling to "Slow 4G" or similar, and check "Disable cache".
  2. In a repository, open a non-Markdown file (e.g. foo.rst) in the single file editor.
  3. Rename it to foo.md, then rename it back before the extensions finish loading.
  4. Once the load completes, right-click inside the editor.

Before the fix, Preview Markdown shows up in the menu even though the file is no longer Markdown. After the fix, it does not.

Notes

  • I don't work on the frontend regularly — a careful look at this approach would be very helpful.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by skkzsh

Merge request reports

Loading
Loading