/**
* 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 ? (
) : (
)}
);
})}
);
};