Files
NetMesh/components/notes/noteCodeTooltips.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

129 lines
4.9 KiB
TypeScript

import { Prec } from "@codemirror/state";
import { EditorView, getTooltip, showTooltip, tooltips } from "@codemirror/view";
export interface NoteTooltipSpace {
top: number;
left: number;
right: number;
bottom: number;
}
/** True when the element clips horizontally overflowing descendants. */
const clipsHorizontally = (style: CSSStyleDeclaration): boolean =>
style.overflowX !== "visible" ||
/(^|\s)(strict|content|paint)(\s|$)/.test(style.contain || "");
/** True when the element clips vertically overflowing descendants. Paint
* containment (strict/content/paint) also clips and captures fixed
* descendants even when overflow is visible. */
const clipsVertically = (style: CSSStyleDeclaration): boolean =>
style.overflowY !== "visible" ||
/(^|\s)(strict|content|paint)(\s|$)/.test(style.contain || "");
/**
* Viewport-space bounds CodeMirror may place note tooltips in: the owning
* notes region (`boundsRoot`) shrunk by every clipping ancestor (terminal
* side-panel pane hosts, ScrollArea viewports). Tooltips are mounted on
* `document.body` so they escape clipped code blocks; bounding their space
* to the notes pane keeps a completion from rendering over an adjacent
* split pane. Without a mounted `boundsRoot`, falls back to the window
* bounds CodeMirror would use by default.
*/
export const getNoteTooltipSpace = (
boundsRoot: HTMLElement | null | undefined,
doc: Document,
): NoteTooltipSpace => {
if (!boundsRoot || !boundsRoot.isConnected) {
const docElt = doc.documentElement;
return { top: 0, left: 0, right: docElt.clientWidth, bottom: docElt.clientHeight };
}
const rootRect = boundsRoot.getBoundingClientRect();
let space: NoteTooltipSpace = {
top: rootRect.top,
left: rootRect.left,
right: rootRect.right,
bottom: rootRect.bottom,
};
const defaultView = boundsRoot.ownerDocument.defaultView;
if (!defaultView) return space;
for (let node: Element | null = boundsRoot; node; node = node.parentElement) {
const style = defaultView.getComputedStyle(node);
const rect = node.getBoundingClientRect();
if (clipsHorizontally(style)) {
space = {
...space,
left: Math.max(space.left, rect.left),
right: Math.min(space.right, rect.right),
};
}
if (clipsVertically(style)) {
space = {
...space,
top: Math.max(space.top, rect.top),
bottom: Math.min(space.bottom, rect.bottom),
};
}
}
return space;
};
export const syncNoteCodeTooltipStyle = (view: EditorView, space: NoteTooltipSpace): void => {
const width = `${Math.max(0, space.right - space.left)}px`;
const paneStyle = view.dom.ownerDocument.defaultView?.getComputedStyle(view.dom);
const tokens = ["--popover", "--popover-foreground", "--border", "--accent", "--accent-foreground"];
for (const tooltip of view.state.facet(showTooltip)) {
if (!tooltip) continue;
const dom = getTooltip(view, tooltip)?.dom;
if (!dom) continue;
dom.style.setProperty("--note-tooltip-width", width);
// Detached tooltips must retain terminal-side-panel theme overrides.
for (const token of tokens) {
const value = paneStyle?.getPropertyValue(token).trim();
if (value) dom.style.setProperty(token, value);
else dom.style.removeProperty(token);
}
}
};
export const createNoteCodeTooltipExtensions = (
parent: HTMLElement | undefined,
getBoundsRoot?: () => HTMLElement | null,
) => [
// Code blocks and their enclosing note panes can clip editor descendants.
Prec.highest(
tooltips({
parent,
// Tooltips escape those clips via the global parent, so constrain their
// placement to the owning notes pane instead of the whole window.
tooltipSpace: (view: EditorView) => {
const space = getNoteTooltipSpace(getBoundsRoot?.(), view.dom.ownerDocument);
// CodeMirror constrains coordinates and height, but not width. Set
// the available width; its ResizeObserver remeasures after resizing.
syncNoteCodeTooltipStyle(view, space);
return space;
},
}),
),
// CodeMirror carries this theme's scope onto its detached tooltip container.
// Keep these rules local to Notes and use the application's existing colors.
Prec.highest(EditorView.theme({
".cm-tooltip": {
backgroundColor: "hsl(var(--popover))",
color: "hsl(var(--popover-foreground))",
border: "1px solid hsl(var(--border))",
},
".cm-tooltip-autocomplete": {
maxWidth: "var(--note-tooltip-width, 95vw)",
boxSizing: "border-box",
},
".cm-tooltip.cm-tooltip-autocomplete > ul": {
minWidth: "min(250px, max(0px, calc(var(--note-tooltip-width, 95vw) - 2px)))",
maxWidth: "min(700px, max(0px, calc(var(--note-tooltip-width, 95vw) - 2px)))",
},
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
backgroundColor: "hsl(var(--accent))",
color: "hsl(var(--accent-foreground))",
},
})),
];