From 07456b2562807a53edf70d854074176eecc33fa7 Mon Sep 17 00:00:00 2001 From: Jaw <53564781+jaw52@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:31:57 +0800 Subject: [PATCH] fix(editor): show the scrollbar thumb over overview marks (#1031) The overview lane lies over the vertical scrollbar, so a mark the height of the lane (a new file added to git is one green VCS mark) hid the native thumb completely. The lane now draws the thumb itself, above its marks and without taking the pointer, at the position of the native one (scrollbarThumb, inset by its 2px border); it switches to the hover color while the pointer is on the thumb. The native thumb is made transparent in editors that have the lane, so the two don't add up, and still takes the drag. Editors without the lane (notebook cells, previews) keep the native thumb. Fixes #1014 Co-authored-by: Claude Opus 5.5 --- e2e/overviewLane.spec.ts | 19 +++++++++++++++++++ src/editor/core/cm/lanes.ts | 35 ++++++++++++++++++++++++++++++++++- src/editor/core/cm/theme.ts | 11 +++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/e2e/overviewLane.spec.ts b/e2e/overviewLane.spec.ts index 6c51df7c..af634991 100644 --- a/e2e/overviewLane.spec.ts +++ b/e2e/overviewLane.spec.ts @@ -28,3 +28,22 @@ test("the scrollbar thumb drags even where overview marks cover it (#752)", asyn await page.mouse.move(x, box.y + box.height - 20); await expect(editor.locator(".cm-openidea-overview")).not.toHaveClass(/cm-openidea-overview-onThumb/); }); + +test("the scrollbar thumb shows over a mark the height of the lane (#1014)", async ({ workbench: page }) => { + const editor = await openFromTree(page, "build.gradle.kts"); + await editor.locator(".cm-content").click(); + await page.keyboard.press("Meta+a"); + await page.keyboard.insertText(Array.from({ length: 400 }, (_, i) => `val mark${i} = ${i}`).join("\n")); + await page.keyboard.press("Meta+f"); + await page.getByRole("textbox", { name: "Find" }).fill("mark"); + await expect(editor.locator(".cm-openidea-overviewMark").first()).toBeVisible(); + const thumb = editor.locator(".cm-openidea-overviewThumb"); + await expect(thumb).toBeVisible(); + // Drawn last in the lane, so it paints over every mark. + expect(await thumb.evaluate((t) => t.nextElementSibling)).toBeNull(); + const scroller = editor.locator(".cm-scroller"); + const top = async () => (await thumb.boundingBox())!.y; + const before = await top(); + await scroller.evaluate((s) => (s.scrollTop = s.scrollHeight)); + await expect.poll(top).toBeGreaterThan(before + 100); +}); diff --git a/src/editor/core/cm/lanes.ts b/src/editor/core/cm/lanes.ts index 9e6ac7c6..68a2e6c3 100644 --- a/src/editor/core/cm/lanes.ts +++ b/src/editor/core/cm/lanes.ts @@ -64,11 +64,18 @@ class OverviewLane { private lastKey = ""; /** The last pointer position over the editor, to re-check the thumb when it scrolls. */ private pointer: { x: number; y: number } | null = null; + /** + * The scrollbar thumb drawn over the marks: the native one sits under the lane, so a mark + * the height of the lane (a new file) would hide it (#1014). + */ + private thumb: HTMLElement; constructor(private view: EditorView) { this.dom = document.createElement("div"); this.dom.className = "cm-openidea-overview"; this.dom.setAttribute("aria-hidden", "true"); + this.thumb = document.createElement("div"); + this.thumb.className = "cm-openidea-overviewThumb"; this.dom.addEventListener("mousedown", this.onMouseDown); view.dom.addEventListener("mousemove", this.onPointerMove); view.dom.addEventListener("mouseleave", this.onPointerLeave); @@ -153,9 +160,31 @@ class OverviewLane { c.className = "cm-openidea-overviewCaret"; c.style.top = `${caret}${unit}`; frag.appendChild(c); + frag.appendChild(this.thumb); this.dom.replaceChildren(frag); }, }); + this.scheduleThumb(); + } + + private scheduleThumb() { + this.view.requestMeasure({ + key: this.thumb, + read: (view) => { + const s = view.scrollDOM; + const thumb = scrollbarThumb(s.scrollTop, s.scrollHeight, s.clientHeight); + // The lane starts at the top of the editor, the scrollbar at the top of the scroller. + const offset = s.getBoundingClientRect().top - this.dom.getBoundingClientRect().top; + // Inset by the thumb's 2px transparent border (global.css). + return thumb && { top: thumb.top + offset + 2, height: thumb.bottom - thumb.top - 4 }; + }, + write: (thumb) => { + this.thumb.style.display = thumb ? "" : "none"; + if (!thumb) return; + this.thumb.style.top = `${thumb.top}px`; + this.thumb.style.height = `${thumb.height}px`; + }, + }); } /** @@ -187,6 +216,7 @@ class OverviewLane { }; private onScroll = () => { + this.scheduleThumb(); if (this.pointer) this.updateThumbPassThrough(); }; @@ -218,4 +248,7 @@ class OverviewLane { } } -export const overviewLane: Extension = ViewPlugin.fromClass(OverviewLane); +export const overviewLane: Extension = [ + ViewPlugin.fromClass(OverviewLane), + EditorView.editorAttributes.of({ class: "cm-openidea-withOverview" }), +]; diff --git a/src/editor/core/cm/theme.ts b/src/editor/core/cm/theme.ts index 3bfd8427..82ce7acc 100644 --- a/src/editor/core/cm/theme.ts +++ b/src/editor/core/cm/theme.ts @@ -285,6 +285,17 @@ export const openideaTheme = EditorView.theme({ ".cm-openidea-overviewMark-thin": { left: "4px", width: "4px", minHeight: "2px" }, // The scrollbar thumb wins over the marks under it (cm/lanes.ts, #752). ".cm-openidea-overview-onThumb .cm-openidea-overviewMark": { pointerEvents: "none" }, + // The lane draws the thumb over its marks (#1014); the native one only takes the drag. + "&.cm-openidea-withOverview .cm-scroller::-webkit-scrollbar-thumb:vertical": { backgroundColor: "transparent" }, + ".cm-openidea-overviewThumb": { + position: "absolute", + right: "2px", + width: "6px", + borderRadius: "3px", + background: "var(--scrollbar-thumb)", + pointerEvents: "none", + }, + ".cm-openidea-overview-onThumb .cm-openidea-overviewThumb": { background: "var(--scrollbar-thumb-hover)" }, ".cm-openidea-overviewCaret": { position: "absolute", left: "2px",