From 8f77d7077cba8c59a0db1d5431d9c288a4be13b4 Mon Sep 17 00:00:00 2001 From: ntbang0901 <121003585+ntbang0901@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:10:51 +0700 Subject: [PATCH] fix(editor): keep tooltips full width at the editor's right edge (#1203) On WebKit, CodeMirror positions tooltips absolutely inside the editor, but the tooltip space still reported the whole window as the right bound. CodeMirror never shifted a tooltip near the editor's right edge to the left, the box was squeezed into the space left of that edge, and the next measurement read the squeezed width, so it could end as a one-character column. The tooltip space is now the editor's own horizontal extent (clamped to the window) and tooltips size to their content, which fixes hover, inline blame, debugger values, signature help and hints together. Co-authored-by: ntbang0901 <121003585+ntbang0901@users.noreply.github.com> --- src/editor/core/cm/setup.ts | 24 ++++++++++++++++-------- src/editor/core/cm/theme.ts | 3 +++ src/editor/core/cm/tooltipBounds.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/editor/core/cm/tooltipBounds.test.ts 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 }); + }); +});