/** * Interactive overlay layer (z-20) for inline code chips. * * Positioned above the textarea, provides: * - Hover tooltip with code preview (first ~12 lines, CodeMirror syntax highlighting) * - X button to remove a chip * * Uses mirror div technique (calculateChipPositions) to position elements * exactly over the corresponding chip tokens in the textarea. */ import * as React from 'react'; import { syntaxHighlighting } from '@codemirror/language'; import { EditorState } from '@codemirror/state'; import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'; import { EditorView } from '@codemirror/view'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { chipDisplayLabel } from '@renderer/types/inlineChip'; import { calculateChipPositions } from '@renderer/utils/chipUtils'; import { getSyncLanguageExtension } from '@renderer/utils/codemirrorLanguages'; import { ExternalLink, X } from 'lucide-react'; import type { InlineChip } from '@renderer/types/inlineChip'; import type { ChipPosition } from '@renderer/utils/chipUtils'; // ============================================================================= // Compact read-only CodeMirror theme for tooltip preview // ============================================================================= const chipPreviewTheme = EditorView.theme({ '&': { fontSize: '11px', backgroundColor: 'var(--code-bg, #1e1e2e)', color: 'var(--color-text)', }, '.cm-content': { padding: '8px 10px', fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace', lineHeight: '1.6', }, '&.cm-focused': { outline: 'none' }, '.cm-scroller': { overflow: 'auto' }, '.cm-gutters': { display: 'none' }, '.cm-activeLine': { backgroundColor: 'transparent' }, '.cm-selectionBackground': { backgroundColor: 'transparent' }, '.cm-cursor': { display: 'none' }, }); // ============================================================================= // Code preview subcomponent (CodeMirror read-only) // ============================================================================= const MAX_PREVIEW_LINES = 12; /** Simple tooltip for file-level mention chips (no code preview). */ const ChipFilePreview = ({ chip, onOpenInEditor, onRevealFolder, }: { chip: InlineChip; onOpenInEditor?: (filePath: string) => void; onRevealFolder?: (folderPath: string) => void; }): React.JSX.Element => { const displayPath = chip.displayPath ?? chip.filePath; const isFolder = chip.isFolder === true; return (
{chip.fileName} {displayPath} {isFolder && onRevealFolder ? ( ) : !isFolder && onOpenInEditor ? ( ) : null}
); }; const ChipCodePreview = ({ chip }: { chip: InlineChip }): React.JSX.Element => { const containerRef = React.useRef(null); const allLines = chip.codeText.split('\n'); const truncated = allLines.length > MAX_PREVIEW_LINES; const visibleCode = truncated ? allLines.slice(0, MAX_PREVIEW_LINES).join('\n') : chip.codeText; const label = chipDisplayLabel(chip); const lineRef = chip.fromLine === chip.toLine ? `line ${String(chip.fromLine)}` : `lines ${String(chip.fromLine)}-${String(chip.toLine)}`; React.useEffect(() => { const container = containerRef.current; if (!container) return; const langExt = getSyncLanguageExtension(chip.fileName); const state = EditorState.create({ doc: visibleCode, extensions: [ chipPreviewTheme, syntaxHighlighting(oneDarkHighlightStyle), EditorView.editable.of(false), EditorState.readOnly.of(true), ...(langExt ? [langExt] : []), ], }); const view = new EditorView({ state, parent: container }); return () => { view.destroy(); }; }, [visibleCode, chip.fileName]); return (
{label} {lineRef}
{truncated ? (
({allLines.length - MAX_PREVIEW_LINES} more lines...)
) : null}
); }; // ============================================================================= // Interaction layer // ============================================================================= interface ChipInteractionLayerProps { chips: InlineChip[]; value: string; textareaRef: React.RefObject; scrollTop: number; onRemove: (chipId: string) => void; } export const ChipInteractionLayer = ({ chips, value, textareaRef, scrollTop, onRemove, }: ChipInteractionLayerProps): React.JSX.Element | null => { const [positions, setPositions] = React.useState([]); const revealFileInEditor = useStore((s) => s.revealFileInEditor); const revealFolderInEditor = useStore((s) => s.revealFolderInEditor); React.useLayoutEffect(() => { if (chips.length === 0) { setPositions([]); return; } const textarea = textareaRef.current; if (!textarea) return; setPositions(calculateChipPositions(textarea, value, chips)); }, [chips, value, textareaRef]); if (positions.length === 0) return null; return (
{positions.map((pos) => { const isFileChip = pos.chip.fromLine == null; const isFolderChip = pos.chip.isFolder === true; return (
{ e.preventDefault(); e.stopPropagation(); if (isFolderChip) { revealFolderInEditor(pos.chip.filePath); } else { revealFileInEditor(pos.chip.filePath); } } : undefined } >
{isFileChip ? ( ) : ( )}
); })}
); };