diff --git a/src/editor/core/cm/setup.ts b/src/editor/core/cm/setup.ts index 3197fb1f1..87e6df7d8 100644 --- a/src/editor/core/cm/setup.ts +++ b/src/editor/core/cm/setup.ts @@ -319,15 +319,23 @@ export interface SetupOptions { } /** - * Tooltips (hints, docs, completion) stay below the editor's top edge: one - * above the first lines flips below them instead of hiding under the tabs. + * Tooltips (hints, docs, completion) stay inside the editor horizontally and + * below its top edge. WebKit positions them `absolute` within `.cm-editor`, so + * a window-wide right bound makes CodeMirror skip the shift left and the + * browser squeezes the box into the space left of the editor's edge. */ -const tooltipSpace = tooltips({ - tooltipSpace: (view) => { - const top = view.scrollDOM.getBoundingClientRect().top; - return { top, left: 0, right: window.innerWidth, bottom: window.innerHeight }; - }, -}); +export function tooltipBounds(view: { scrollDOM: Element; dom: Element }) { + const { top } = view.scrollDOM.getBoundingClientRect(); + const { left, right } = view.dom.getBoundingClientRect(); + return { + top, + left: Math.max(0, left), + right: Math.min(window.innerWidth, right), + bottom: window.innerHeight, + }; +} + +const tooltipSpace = tooltips({ tooltipSpace: tooltipBounds }); export function baseExtensions(o: SetupOptions): Extension { return [ diff --git a/src/editor/core/cm/theme.ts b/src/editor/core/cm/theme.ts index bc4ad7dbe..49641a972 100644 --- a/src/editor/core/cm/theme.ts +++ b/src/editor/core/cm/theme.ts @@ -248,6 +248,9 @@ export const openideaTheme = EditorView.theme({ // Overlays ".cm-tooltip": { + // Natural width, so a measurement taken while squeezed is not fed back (#1188). + width: "max-content", + maxWidth: "100%", backgroundColor: "var(--bg-popup)", color: "var(--fg)", border: "1px solid var(--border-popup)", diff --git a/src/editor/core/cm/tooltipBounds.test.ts b/src/editor/core/cm/tooltipBounds.test.ts new file mode 100644 index 000000000..b24cf170b --- /dev/null +++ b/src/editor/core/cm/tooltipBounds.test.ts @@ -0,0 +1,24 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { tooltipBounds } from "./setup"; + +const el = (r: Partial) => ({ getBoundingClientRect: () => r as DOMRect }) as Element; +const original = window.innerWidth; + +beforeEach(() => { + Object.defineProperty(window, "innerWidth", { value: 1200, configurable: true }); +}); +afterEach(() => { + Object.defineProperty(window, "innerWidth", { value: original, configurable: true }); +}); + +describe("tooltipBounds", () => { + it.each([ + ["editor flush with the right edge", { left: 300, right: 1200 }, 300, 1200], + ["narrow split pane", { left: 0, right: 400 }, 0, 400], + ["editor in the middle", { left: 250, right: 750 }, 250, 750], + ["editor spilling outside the window", { left: -20, right: 1500 }, 0, 1200], + ])("%s", (_, rect, left, right) => { + const b = tooltipBounds({ scrollDOM: el({ top: 80 }), dom: el(rect) }); + expect(b).toMatchObject({ top: 80, left, right }); + }); +});