diff --git a/apps/desktop/src/lib/__tests__/editor/currentStatementFrameLayer.spec.ts b/apps/desktop/src/lib/__tests__/editor/currentStatementFrameLayer.spec.ts index f6675200dc..2808d3ea20 100644 --- a/apps/desktop/src/lib/__tests__/editor/currentStatementFrameLayer.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/currentStatementFrameLayer.spec.ts @@ -287,4 +287,54 @@ describe("currentStatementFrameLayer", () => { expect(layer).toHaveBeenCalledWith(expect.objectContaining({ above: true, class: "cm-db-currentStatementFrameLayer" })); }); + + it("does not rebuild the frame while the cursor stays in the same statement", () => { + const layer = vi.fn((config) => config); + const RectangleMarker = vi.fn(); + const view = buildView(["SELECT 1;", "SELECT 2;"]); + const resolver = vi.fn(() => ({ from: 0, to: 8 })); + const config = currentStatementFrameLayer({ layer, RectangleMarker } as never, resolver) as unknown as { + markers: (currentView: typeof view) => unknown; + update: (update: { docChanged: boolean; selectionSet: boolean; viewportChanged: boolean; geometryChanged: boolean; transactions: never[]; view: typeof view }) => boolean; + }; + + config.markers(view); + + expect( + config.update({ + docChanged: false, + selectionSet: true, + viewportChanged: false, + geometryChanged: false, + transactions: [], + view, + }), + ).toBe(false); + expect(resolver).toHaveBeenCalledTimes(2); + }); + + it("rebuilds the frame when the cursor enters another statement", () => { + const layer = vi.fn((config) => config); + const RectangleMarker = vi.fn(); + const view = buildView(["SELECT 1;", "SELECT 2;"]); + let range = { from: 0, to: 8 }; + const config = currentStatementFrameLayer({ layer, RectangleMarker } as never, () => range) as unknown as { + markers: (currentView: typeof view) => unknown; + update: (update: { docChanged: boolean; selectionSet: boolean; viewportChanged: boolean; geometryChanged: boolean; transactions: never[]; view: typeof view }) => boolean; + }; + + config.markers(view); + range = { from: 9, to: 17 }; + + expect( + config.update({ + docChanged: false, + selectionSet: true, + viewportChanged: false, + geometryChanged: false, + transactions: [], + view, + }), + ).toBe(true); + }); }); diff --git a/apps/desktop/src/lib/editor/codemirrorCurrentStatementFrameLayer.ts b/apps/desktop/src/lib/editor/codemirrorCurrentStatementFrameLayer.ts index 4d45080670..d9a6254f8a 100644 --- a/apps/desktop/src/lib/editor/codemirrorCurrentStatementFrameLayer.ts +++ b/apps/desktop/src/lib/editor/codemirrorCurrentStatementFrameLayer.ts @@ -151,6 +151,10 @@ interface CurrentStatementFrameModule { * returning null hides the frame. */ export function currentStatementFrameLayer(viewModule: CurrentStatementFrameModule, resolve: StatementFrameResolver): Extension { + let lastRequest: StatementFrameRequest | null | undefined; + + const sameRequest = (left: StatementFrameRequest | null | undefined, right: StatementFrameRequest | null): boolean => left === right || (!!left && !!right && left.from === right.from && left.to === right.to); + return viewModule.layer({ // Keep the outline above line decorations so opaque active-line colors // from editor themes cannot cover the frame border. @@ -158,13 +162,25 @@ export function currentStatementFrameLayer(viewModule: CurrentStatementFrameModu class: "cm-db-currentStatementFrameLayer", markers(view) { const request = resolve(view); + lastRequest = request; if (!request || request.to < request.from) return []; const rect = currentStatementFrameRect(view, request.from, request.to); if (!rect) return []; return [new viewModule.RectangleMarker("cm-db-currentStatementFrame", rect.left, rect.top, rect.width, rect.height)]; }, update(update) { - return update.docChanged || update.selectionSet || update.viewportChanged || update.geometryChanged || update.transactions.some((transaction) => transaction.reconfigured); + if (update.docChanged || update.viewportChanged || update.geometryChanged || update.transactions.some((transaction) => transaction.reconfigured)) { + lastRequest = undefined; + return true; + } + if (!update.selectionSet) return false; + + // Moving inside one statement does not change its frame. Avoid repeating + // the per-line DOM geometry probes for every auto-repeated key event. + const request = resolve(update.view); + if (sameRequest(lastRequest, request)) return false; + lastRequest = undefined; + return true; }, }); }