дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
This reverts commit c938d712b5.
Цей коміт міститься в:
1 джерело
2c3eabb786
коміт
16033c481b
5 files changed
+6
-197
No files matched your search
@@ -6,7 +6,6 @@
|
||||
import { RangeSetBuilder, type EditorState, type Extension } from "@codemirror/state";
|
||||
import { Decoration, ViewPlugin, type DecorationSet, type EditorView, type ViewUpdate } from "@codemirror/view";
|
||||
import { getIndentUnit } from "@codemirror/language";
|
||||
import { isScrollSettled } from "./scrollThrottle";
|
||||
|
||||
const cache = new Map<string, Decoration>();
|
||||
function guideDeco(levels: number, step: number) {
|
||||
@@ -77,14 +76,8 @@ export const indentGuides: Extension = ViewPlugin.fromClass(
|
||||
this.decorations = build(view);
|
||||
}
|
||||
update(u: ViewUpdate) {
|
||||
if (u.docChanged || u.startState.tabSize !== u.state.tabSize || isScrollSettled(u)) {
|
||||
if (u.docChanged || u.viewportChanged || u.startState.tabSize !== u.state.tabSize)
|
||||
this.decorations = build(u.view);
|
||||
return;
|
||||
}
|
||||
// During a scroll burst keep the previous decorations (stale ranges
|
||||
// for a moment) instead of rebuilding per frame; the scrollSettled
|
||||
// effect above triggers one rebuild once scrolling stops.
|
||||
if (u.viewportChanged && !u.view.dom.classList.contains("cm-scrolling")) this.decorations = build(u.view);
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { Compartment, EditorState } from "@codemirror/state";
|
||||
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||
import { isScrollSettled, scrollThrottle } from "./scrollThrottle";
|
||||
import { stickyScroll } from "./stickyScroll";
|
||||
|
||||
let view: EditorView | undefined;
|
||||
type MeasureRequest = NonNullable<Parameters<EditorView["requestMeasure"]>[0]>;
|
||||
|
||||
afterEach(() => {
|
||||
view?.destroy();
|
||||
view = undefined;
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function editor(extensions: Parameters<typeof EditorState.create>[0] = {}) {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
||||
view = new EditorView({ state: EditorState.create(extensions), parent: document.body });
|
||||
return view;
|
||||
}
|
||||
|
||||
describe("scroll throttle", () => {
|
||||
it("marks scrolling before previously installed bubble listeners run", () => {
|
||||
const seen: boolean[] = [];
|
||||
const listener = ViewPlugin.define((v) => {
|
||||
const onScroll = () => seen.push(v.dom.classList.contains("cm-scrolling"));
|
||||
v.scrollDOM.addEventListener("scroll", onScroll);
|
||||
return { destroy: () => v.scrollDOM.removeEventListener("scroll", onScroll) };
|
||||
});
|
||||
const v = editor({ extensions: [listener, scrollThrottle] });
|
||||
v.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
expect(seen).toEqual([true]);
|
||||
});
|
||||
|
||||
it("settles once after the last event in a burst", () => {
|
||||
const settled = vi.fn();
|
||||
const observer = EditorView.updateListener.of((u) => {
|
||||
if (isScrollSettled(u)) settled(u.view.dom.classList.contains("cm-scrolling"));
|
||||
});
|
||||
const v = editor({ extensions: [scrollThrottle, observer] });
|
||||
v.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
vi.advanceTimersByTime(100);
|
||||
v.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
vi.advanceTimersByTime(139);
|
||||
expect(v.dom.classList.contains("cm-scrolling")).toBe(true);
|
||||
expect(settled).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(settled).toHaveBeenCalledExactlyOnceWith(false);
|
||||
});
|
||||
|
||||
it("removes its capture listener and pending timer when disabled", () => {
|
||||
const compartment = new Compartment();
|
||||
const settled = vi.fn();
|
||||
const v = editor({
|
||||
extensions: [
|
||||
compartment.of(scrollThrottle),
|
||||
EditorView.updateListener.of((u) => {
|
||||
if (isScrollSettled(u)) settled();
|
||||
}),
|
||||
],
|
||||
});
|
||||
v.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
v.dispatch({ effects: compartment.reconfigure([]) });
|
||||
expect(v.dom.classList.contains("cm-scrolling")).toBe(false);
|
||||
v.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
expect(v.dom.classList.contains("cm-scrolling")).toBe(false);
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(settled).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defers sticky measurements from the first scroll event until settling", () => {
|
||||
const requests: MeasureRequest[] = [];
|
||||
const measure = vi.spyOn(EditorView.prototype, "requestMeasure").mockImplementation((request) => {
|
||||
if (request) requests.push(request);
|
||||
});
|
||||
// Match setup.ts: stickyScroll is installed before scrollThrottle.
|
||||
const v = editor({ extensions: [stickyScroll, scrollThrottle] });
|
||||
const initial = requests.shift()!;
|
||||
initial.write?.(initial.read(v), v);
|
||||
measure.mockClear();
|
||||
v.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
expect(measure).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(140);
|
||||
expect(requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps painted sticky rows when a queued measurement meets a new burst", () => {
|
||||
const requests: MeasureRequest[] = [];
|
||||
vi.spyOn(EditorView.prototype, "requestMeasure").mockImplementation((request) => {
|
||||
if (request) requests.push(request);
|
||||
});
|
||||
const v = editor({ extensions: [stickyScroll, scrollThrottle] });
|
||||
const sticky = v.dom.querySelector<HTMLElement>(".cm-openidea-sticky")!;
|
||||
sticky.style.display = "";
|
||||
sticky.textContent = "previous header";
|
||||
const initial = requests.shift()!;
|
||||
const geometry = vi.spyOn(v.scrollDOM, "getBoundingClientRect");
|
||||
v.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
initial.write?.(initial.read(v), v);
|
||||
expect(geometry).not.toHaveBeenCalled();
|
||||
expect(sticky.textContent).toBe("previous header");
|
||||
expect(sticky.style.display).toBe("");
|
||||
vi.advanceTimersByTime(140);
|
||||
expect(requests).toHaveLength(1);
|
||||
const settled = requests.shift()!;
|
||||
settled.write?.(settled.read(v), v);
|
||||
expect(geometry).toHaveBeenCalled();
|
||||
expect(sticky.style.display).toBe("none");
|
||||
});
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
@@ -39,7 +39,6 @@ import { syntaxHighlightField } from "./syntaxHighlight";
|
||||
import { indentGuides } from "./indentGuides";
|
||||
import { rainbowBrackets } from "./rainbowBrackets";
|
||||
import { stickyMaxLines, stickyScroll } from "./stickyScroll";
|
||||
import { scrollThrottle } from "./scrollThrottle";
|
||||
import { findExtension } from "./find";
|
||||
import { codeLensExtension } from "./codeLens";
|
||||
import { caretExtension, fontZoom, methodLinesField, prefsExtension, softWrapActive } from "./prefs";
|
||||
@@ -365,10 +364,6 @@ export function baseExtensions(o: SetupOptions): Extension {
|
||||
compartments.wrap.of(wrapExtension(o.settings, o.ctx.path, o.wrapOverride)),
|
||||
compartments.sticky.of(stickyExtension(o.settings)),
|
||||
compartments.heavy.of(heavyExtension(o.docLength, o.settings)),
|
||||
// Flag "cm-scrolling" while the wheel is moving; expensive decorations
|
||||
// (sticky header, indent guides, ...) keep their previous paint and
|
||||
// defer re-measuring until scrolling settles.
|
||||
scrollThrottle,
|
||||
compartments.prefs.of(
|
||||
prefsFor({
|
||||
settings: o.settings,
|
||||
|
||||
@@ -10,7 +10,6 @@ import { foldNodeProp, syntaxTree } from "@codemirror/language";
|
||||
import { highlightTree } from "@lezer/highlight";
|
||||
import { backendFolds } from "./folding";
|
||||
import { openideaHighlightStyle } from "./highlight";
|
||||
import { isScrollSettled } from "./scrollThrottle";
|
||||
|
||||
/** Maximum number of sticky lines (Settings › Editor › Code Editing). */
|
||||
export const stickyMaxLines = Facet.define<number, number>({
|
||||
@@ -121,36 +120,23 @@ class StickyScroll {
|
||||
update(u: ViewUpdate) {
|
||||
if (
|
||||
u.docChanged ||
|
||||
u.viewportChanged ||
|
||||
u.geometryChanged ||
|
||||
syntaxTree(u.startState) !== syntaxTree(u.state) ||
|
||||
u.startState.field(backendFolds, false) !== u.state.field(backendFolds, false) ||
|
||||
isScrollSettled(u)
|
||||
u.startState.field(backendFolds, false) !== u.state.field(backendFolds, false)
|
||||
)
|
||||
this.schedule();
|
||||
// Viewport moves during a scroll burst are wasteful: the previous rows
|
||||
// stay painted (just no longer aligned), so skip re-measuring until the
|
||||
// scrollSettled effect above re-runs one pass.
|
||||
if (u.viewportChanged && !u.view.dom.classList.contains("cm-scrolling")) this.schedule();
|
||||
}
|
||||
|
||||
private onScroll = () => {
|
||||
// The class is set by the scrollThrottle plugin on the same element;
|
||||
// during a burst the stale rows stay visible instead of re-measuring
|
||||
// every frame — catch up on the trailing update after scrolling settles.
|
||||
if (this.view.dom.classList.contains("cm-scrolling")) return;
|
||||
this.schedule();
|
||||
};
|
||||
private onScroll = () => this.schedule();
|
||||
|
||||
private schedule() {
|
||||
if (this.scheduled || this.view.dom.classList.contains("cm-scrolling")) return;
|
||||
if (this.scheduled) return;
|
||||
this.scheduled = true;
|
||||
this.view.requestMeasure({
|
||||
key: this,
|
||||
read: (view) => {
|
||||
this.scheduled = false;
|
||||
// A queued measurement may outlive the start of a scroll burst.
|
||||
// Preserve the painted rows until scrollSettled schedules a fresh pass.
|
||||
if (view.dom.classList.contains("cm-scrolling")) return null;
|
||||
const scroller = view.scrollDOM;
|
||||
const scrollTop = scroller.scrollTop;
|
||||
const docOffset = view.documentTop - scroller.getBoundingClientRect().top + scrollTop;
|
||||
@@ -180,9 +166,7 @@ class StickyScroll {
|
||||
}
|
||||
return { lines: headers, gutterWidth, numberWidth, numberLeft, scrollLeft: scroller.scrollLeft };
|
||||
},
|
||||
write: (measured) => {
|
||||
if (!measured) return;
|
||||
const { lines, gutterWidth, numberWidth, numberLeft, scrollLeft } = measured;
|
||||
write: ({ lines, gutterWidth, numberWidth, numberLeft, scrollLeft }) => {
|
||||
const state = this.view.state;
|
||||
const texts = lines.map((n) => (n <= state.doc.lines ? state.doc.line(n).text : ""));
|
||||
const tree = syntaxTree(state);
|
||||
|
||||
Reference in new issue
Заблокувати користувача