Файли
openidea/src/editor/core/cm/scrollThrottle.ts
T
arookieofcтаl0ng-ai c938d712b5 fix(editor): pause expensive decorations during fast scroll bursts (#401)
* fix(editor): pause expensive decorations during fast scroll bursts

Add a scrollThrottle plugin that flags the editor with a .cm-scrolling
class while the wheel is moving. While the flag is up the sticky-scroll
header and indent guides are hidden via theme CSS, and their view
plugins skip per-viewport re-measures/rebuilds; a trailing scrollSettled
effect re-runs them once the wheel rests for 140ms. This keeps fast
mouse-wheel scrolling from painting rows of stale decorations over
freshly rendered code.

Fixes l0ng-ai/openidea#375

* fix(editor): keep decorations painted during scroll bursts

The scroll throttle hid the sticky header and indent guides entirely while
scrolling, which flickered on every burst - and the sticky header is exactly
what you want to see while scrolling. Drop the display/background hiding from
the theme and keep the previous paint instead: the plugins skip re-measuring
during a burst and catch up once on the trailing scrollSettled update.

* fix(editor): throttle the first scroll event and queued sticky measurements

---------

Co-authored-by: l0ng-ai <[email protected]>
2026-11-01 10:09:47 +08:00

53 рядки
1.9 KiB
TypeScript

/**
* Marks the editor with a `cm-scrolling` class while the user is actively
* scrolling; expensive decorations (sticky header, indent guides) keep
* their previous paint and defer re-measuring until the wheel / trackpad
* rests for a moment. Keeps fast mouse-wheel scrolling from spending every
* frame on layout work that will be stale by the next scroll event anyway.
*/
import { StateEffect, type Extension } from "@codemirror/state";
import { ViewPlugin, type EditorView, type ViewUpdate } from "@codemirror/view";
const IDLE_MS = 140;
/** Dispatched once a scroll burst ends so plugins that skipped work can catch up. */
export const scrollSettled = StateEffect.define<null>();
class ScrollThrottle {
private timer = 0;
private active = false;
constructor(private view: EditorView) {
// Run before bubble listeners, including stickyScroll when it was
// installed first, so the very first event is throttled too.
view.scrollDOM.addEventListener("scroll", this.onScroll, { passive: true, capture: true });
}
private onScroll = () => {
if (!this.active) {
this.active = true;
this.view.dom.classList.add("cm-scrolling");
}
clearTimeout(this.timer);
this.timer = window.setTimeout(() => {
this.active = false;
this.view.dom.classList.remove("cm-scrolling");
// Wake plugins that suppressed viewport work during the burst.
this.view.dispatch({ effects: scrollSettled.of(null) });
}, IDLE_MS);
};
destroy() {
clearTimeout(this.timer);
this.view.scrollDOM.removeEventListener("scroll", this.onScroll, true);
this.view.dom.classList.remove("cm-scrolling");
}
}
/** Check whether an update marks the end of a scroll burst. */
export function isScrollSettled(u: ViewUpdate): boolean {
return u.transactions.some((tr) => tr.effects.some((e) => e.is(scrollSettled)));
}
export const scrollThrottle: Extension = ViewPlugin.fromClass(ScrollThrottle);