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 <noreply@anthropic.com>
This commit is contained in:
Jaw
2026-11-03 20:31:57 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 659afe66f5
commit 07456b2562
3 changed files with 64 additions and 1 deletions
+19
View File
@@ -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);
});
+34 -1
View File
@@ -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" }),
];
+11
View File
@@ -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",