mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(editor): let the scrollbar thumb drag where overview marks cover it (#807)
The overview lane lies over the vertical scrollbar and its marks take the pointer, so a mark under the thumb swallowed the press; a new or fully changed file is one lane-high mark, so its thumb could not be dragged at all. While the pointer is over the thumb (computed from the scroller's metrics and the 32px minimum thumb) the marks now let the pointer through; elsewhere clicking a mark still jumps to its line. Fixes #752 Co-authored-by: HEOJUNFO <100739943+HEOJUNFO@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { expect, openFromTree, test } from "./fixtures";
|
||||
|
||||
// Headless Chromium hides scrollbars by default; this test drags one.
|
||||
test.use({ launchOptions: { ignoreDefaultArgs: ["--hide-scrollbars"] } });
|
||||
|
||||
test("the scrollbar thumb drags even where overview marks cover it (#752)", 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"));
|
||||
// A match on every line covers the overview lane, as a new file's change mark does.
|
||||
await page.keyboard.press("Meta+f");
|
||||
await page.getByRole("textbox", { name: "Find" }).fill("mark");
|
||||
await expect(editor.locator(".cm-openidea-overviewMark").first()).toBeVisible();
|
||||
const scroller = editor.locator(".cm-scroller");
|
||||
await scroller.evaluate((s) => (s.scrollTop = 0));
|
||||
const box = (await scroller.boundingBox())!;
|
||||
// Over a mark and inside the thumb, which is at the top of the track.
|
||||
const x = box.x + box.width - 5;
|
||||
const y = box.y + 12;
|
||||
await page.mouse.move(x - 40, y);
|
||||
await page.mouse.move(x, y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(x, y + 150, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
expect(await scroller.evaluate((s) => s.scrollTop)).toBeGreaterThan(0);
|
||||
// Away from the thumb the marks take the pointer again.
|
||||
await page.mouse.move(x, box.y + box.height - 20);
|
||||
await expect(editor.locator(".cm-openidea-overview")).not.toHaveClass(/cm-openidea-overview-onThumb/);
|
||||
});
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
foldSelectionEffects,
|
||||
} from "./folding";
|
||||
import { columnModeExtension, extendBlock, toggleColumnMode } from "./columnMode";
|
||||
import { nearestMark, unfoldAround } from "./lanes";
|
||||
import { nearestMark, scrollbarThumb, unfoldAround } from "./lanes";
|
||||
import { lineRangeSelection } from "./gutterDrag";
|
||||
import { matchBraceTarget } from "./caretCommands";
|
||||
import { overwriteField, toggleOverwrite } from "./overwrite";
|
||||
@@ -291,6 +291,16 @@ describe("error stripe", () => {
|
||||
expect(nearestMark(marks, 21)).toBeNull();
|
||||
});
|
||||
|
||||
it("finds the scrollbar thumb the marks must let through", () => {
|
||||
// Nothing to scroll: no thumb.
|
||||
expect(scrollbarThumb(0, 400, 400)).toBeNull();
|
||||
// A quarter of the document visible: a 100px thumb, at the top, then at the bottom.
|
||||
expect(scrollbarThumb(0, 1600, 400)).toEqual({ top: 0, bottom: 100 });
|
||||
expect(scrollbarThumb(1200, 1600, 400)).toEqual({ top: 300, bottom: 400 });
|
||||
// A long document keeps the 32px minimum thumb; halfway down it sits mid-track.
|
||||
expect(scrollbarThumb(49800, 100000, 400)).toEqual({ top: 184, bottom: 216 });
|
||||
});
|
||||
|
||||
it("unfolds what hides the target line", () => {
|
||||
const s = EditorState.create({ doc: "f {\n x\n}", extensions: [codeFolding()] }).update({
|
||||
effects: foldEffect.of({ from: 3, to: 8 }),
|
||||
|
||||
@@ -27,6 +27,27 @@ export function nearestMark<M extends { top: number; bottom: number }>(marks: re
|
||||
return best;
|
||||
}
|
||||
|
||||
/** The smallest scrollbar thumb (`::-webkit-scrollbar-thumb` min-height in global.css). */
|
||||
const MIN_THUMB = 32;
|
||||
|
||||
/**
|
||||
* Where the vertical scrollbar thumb of a scroller is, in px from the top of its track, or null
|
||||
* when nothing scrolls. The overview lane lies over the scrollbar, so its marks must not take
|
||||
* the pointer where the thumb is (#752).
|
||||
*/
|
||||
export function scrollbarThumb(
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
clientHeight: number,
|
||||
minThumb = MIN_THUMB,
|
||||
): { top: number; bottom: number } | null {
|
||||
const max = scrollHeight - clientHeight;
|
||||
if (max <= 0 || clientHeight <= 0) return null;
|
||||
const length = Math.min(clientHeight, Math.max(minThumb, (clientHeight * clientHeight) / scrollHeight));
|
||||
const top = (Math.min(Math.max(scrollTop, 0), max) / max) * (clientHeight - length);
|
||||
return { top, bottom: top + length };
|
||||
}
|
||||
|
||||
/** Unfolds the folds hiding `pos` or the rest of its line (navigating to a mark shows it). */
|
||||
export function unfoldAround(state: EditorState, pos: number): StateEffect<unknown>[] {
|
||||
const lineEnd = state.doc.lineAt(pos).to;
|
||||
@@ -41,12 +62,17 @@ class OverviewLane {
|
||||
dom: HTMLElement;
|
||||
private scheduled = false;
|
||||
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;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
this.dom = document.createElement("div");
|
||||
this.dom.className = "cm-openidea-overview";
|
||||
this.dom.setAttribute("aria-hidden", "true");
|
||||
this.dom.addEventListener("mousedown", this.onMouseDown);
|
||||
view.dom.addEventListener("mousemove", this.onPointerMove);
|
||||
view.dom.addEventListener("mouseleave", this.onPointerLeave);
|
||||
view.scrollDOM.addEventListener("scroll", this.onScroll, { passive: true });
|
||||
view.dom.appendChild(this.dom);
|
||||
this.schedule();
|
||||
}
|
||||
@@ -132,6 +158,38 @@ class OverviewLane {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Over the scrollbar thumb the marks let the pointer through, so the thumb can be dragged
|
||||
* even where a mark covers it (a new file is one mark the height of the lane, #752).
|
||||
*/
|
||||
private updateThumbPassThrough() {
|
||||
const p = this.pointer;
|
||||
let over = false;
|
||||
if (p) {
|
||||
const lane = this.dom.getBoundingClientRect();
|
||||
const s = this.view.scrollDOM;
|
||||
const thumb = scrollbarThumb(s.scrollTop, s.scrollHeight, s.clientHeight);
|
||||
const y = p.y - s.getBoundingClientRect().top;
|
||||
over = !!thumb && p.x >= lane.left && p.x <= lane.right && y >= thumb.top && y <= thumb.bottom;
|
||||
}
|
||||
this.dom.classList.toggle("cm-openidea-overview-onThumb", over);
|
||||
}
|
||||
|
||||
private onPointerMove = (e: MouseEvent) => {
|
||||
this.pointer = { x: e.clientX, y: e.clientY };
|
||||
// A thumb drag in progress keeps the marks out of the way until the button is released.
|
||||
if (e.buttons === 0) this.updateThumbPassThrough();
|
||||
};
|
||||
|
||||
private onPointerLeave = () => {
|
||||
this.pointer = null;
|
||||
this.updateThumbPassThrough();
|
||||
};
|
||||
|
||||
private onScroll = () => {
|
||||
if (this.pointer) this.updateThumbPassThrough();
|
||||
};
|
||||
|
||||
private onMouseDown = (e: MouseEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
const marks = [...this.dom.querySelectorAll<HTMLElement>(".cm-openidea-overviewMark")].map((el) => {
|
||||
@@ -153,6 +211,9 @@ class OverviewLane {
|
||||
|
||||
destroy() {
|
||||
this.dom.removeEventListener("mousedown", this.onMouseDown);
|
||||
this.view.dom.removeEventListener("mousemove", this.onPointerMove);
|
||||
this.view.dom.removeEventListener("mouseleave", this.onPointerLeave);
|
||||
this.view.scrollDOM.removeEventListener("scroll", this.onScroll);
|
||||
this.dom.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,8 @@ export const openideaTheme = EditorView.theme({
|
||||
cursor: "pointer",
|
||||
},
|
||||
".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" },
|
||||
".cm-openidea-overviewCaret": {
|
||||
position: "absolute",
|
||||
left: "2px",
|
||||
|
||||
Reference in New Issue
Block a user