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",