perf(editor): debounce markdown doc link decorations (#1830)

Debounce Monaco markdown doc-link decoration refreshes so rapid content changes coalesce into one full-model scan, while preserving immediate initial render and cleanup behavior.
This commit is contained in:
Neil
2026-05-14 01:18:46 -07:00
committed by GitHub
parent dca8206c32
commit bfb62e2bcf
2 changed files with 122 additions and 4 deletions
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest'
import { getMarkdownDocLinkDecorationRanges } from './monaco-markdown-doc-link-decorations'
import type { editor } from 'monaco-editor'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
createMarkdownDocLinkDecorationController,
getMarkdownDocLinkDecorationRanges,
MARKDOWN_DOC_LINK_DECORATION_REFRESH_DELAY_MS
} from './monaco-markdown-doc-link-decorations'
describe('getMarkdownDocLinkDecorationRanges', () => {
it('returns Monaco ranges for valid doc links', () => {
@@ -30,3 +35,92 @@ describe('getMarkdownDocLinkDecorationRanges', () => {
])
})
})
describe('createMarkdownDocLinkDecorationController', () => {
afterEach(() => {
vi.useRealTimers()
})
it('debounces full-model decoration rebuilds during rapid content changes', () => {
vi.useFakeTimers()
let modelValue = '[[initial.md]]'
let contentListener = (): void => {}
const set = vi.fn()
const clear = vi.fn()
const dispose = vi.fn()
const editorInstance = {
createDecorationsCollection: () => ({ set, clear }),
getModel: () => ({ getValue: () => modelValue }),
onDidChangeModelContent: (listener: () => void) => {
contentListener = listener
return { dispose }
}
} as unknown as editor.IStandaloneCodeEditor
const controller = createMarkdownDocLinkDecorationController(editorInstance, () => 'markdown')
expect(set).toHaveBeenCalledTimes(1)
set.mockClear()
modelValue = '[[first.md]]'
contentListener?.()
vi.advanceTimersByTime(MARKDOWN_DOC_LINK_DECORATION_REFRESH_DELAY_MS - 1)
modelValue = '[[second.md]]'
contentListener?.()
modelValue = '[[final.md]]'
contentListener?.()
expect(set).not.toHaveBeenCalled()
vi.advanceTimersByTime(MARKDOWN_DOC_LINK_DECORATION_REFRESH_DELAY_MS - 1)
expect(set).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(set).toHaveBeenCalledTimes(1)
expect(set.mock.calls[0]?.[0]).toEqual([
{
range: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 13
},
options: {
inlineClassName: 'monaco-markdown-doc-link',
stickiness: 1
}
}
])
controller.dispose()
expect(dispose).toHaveBeenCalledTimes(1)
expect(clear).toHaveBeenCalledTimes(1)
})
it('cancels pending decoration rebuilds on dispose', () => {
vi.useFakeTimers()
let contentListener = (): void => {}
const set = vi.fn()
const clear = vi.fn()
const editorInstance = {
createDecorationsCollection: () => ({ set, clear }),
getModel: () => ({ getValue: () => '[[initial.md]]' }),
onDidChangeModelContent: (listener: () => void) => {
contentListener = listener
return { dispose: vi.fn() }
}
} as unknown as editor.IStandaloneCodeEditor
const controller = createMarkdownDocLinkDecorationController(editorInstance, () => 'markdown')
set.mockClear()
contentListener?.()
controller.dispose()
vi.advanceTimersByTime(MARKDOWN_DOC_LINK_DECORATION_REFRESH_DELAY_MS)
expect(set).not.toHaveBeenCalled()
expect(clear).toHaveBeenCalledTimes(1)
})
})
@@ -71,13 +71,25 @@ export type MarkdownDocLinkDecorationController = {
dispose: () => void
}
export const MARKDOWN_DOC_LINK_DECORATION_REFRESH_DELAY_MS = 120
export function createMarkdownDocLinkDecorationController(
editorInstance: editor.IStandaloneCodeEditor,
getLanguage: () => string
): MarkdownDocLinkDecorationController {
const collection = editorInstance.createDecorationsCollection()
let refreshTimer: ReturnType<typeof setTimeout> | null = null
const refresh = (): void => {
const cancelPendingRefresh = (): void => {
if (refreshTimer === null) {
return
}
clearTimeout(refreshTimer)
refreshTimer = null
}
const refreshNow = (): void => {
cancelPendingRefresh()
const model = editorInstance.getModel()
if (!model || getLanguage() !== 'markdown') {
collection.clear()
@@ -94,12 +106,24 @@ export function createMarkdownDocLinkDecorationController(
)
}
const refresh = (): void => {
if (getLanguage() !== 'markdown') {
refreshNow()
return
}
cancelPendingRefresh()
// Why: wiki-link decoration scans read the full Monaco model. During typing
// the exact highlight can lag briefly; coalescing avoids one full scan per key.
refreshTimer = setTimeout(refreshNow, MARKDOWN_DOC_LINK_DECORATION_REFRESH_DELAY_MS)
}
const listener: IDisposable = editorInstance.onDidChangeModelContent(refresh)
refresh()
refreshNow()
return {
refresh,
dispose: () => {
cancelPendingRefresh()
listener.dispose()
collection.clear()
}