[tab] src/preview/index.ts (typescript)
import * as vscode from 'vscode';
import { Action, toVscodeRange, toVscodePosition, truncate } from './types';
import { DecorationPool, COLORS } from './decorations';
import { CrowdPilotInlineProvider } from './inlineProvider';
import { MetaActionHoverProvider } from './hoverProvider';
import { showPendingActionQuickPick, QuickPickResult } from './quickPick';
import { computeDeletionRanges } from '../utils/diff';

// Re-export types
export { Action, toVscodeRange, toVscodePosition, truncate } from './types';
export { QuickPickResult } from './quickPick';

/**
 * Manages the preview UI for suggested actions.
 * Single entry point for all preview rendering.
 */
export class PreviewManager {
    private decorationPool: DecorationPool;
    private inlineProvider: CrowdPilotInlineProvider;
    private hoverProvider: MetaActionHoverProvider;
    private hoverProviderDisposable: vscode.Disposable | null = null;
    private currentAction: Action | null = null;
    private visible: boolean = false;

    constructor() {
        this.decorationPool = new DecorationPool();
        this.inlineProvider = new CrowdPilotInlineProvider();
        this.hoverProvider = new MetaActionHoverProvider();
    }

    /**
     * Register all providers with VS Code.
     * Call this during extension activation.
     */
    register(context: vscode.ExtensionContext): void {
        // Register inline completion provider for all files
        context.subscriptions.push(
            vscode.languages.registerInlineCompletionItemProvider(
                { pattern: '**' },
                this.inlineProvider
            )
        );

        // Register hover provider for all files
        this.hoverProviderDisposable = vscode.languages.registerHoverProvider(
            { pattern: '**' },
            this.hoverProvider
        );
        context.subscriptions.push(this.hoverProviderDisposable);
    }

    /**
     * Show a preview for the given action.
     */
    show(action: Action): void {
        const editor = vscode.window.activeTextEditor;
        
        // Clear previous preview
        this.clear();
        
        this.currentAction = action;
        this.visible = true;

        // Route to appropriate renderer based on action type
        switch (action.kind) {
            case 'editInsert':
                this.showInsertPreview(action, editor);
                break;

            case 'editReplace':
                this.showReplacePreview(action, editor);
                break;

            case 'editDelete':
                this.showDeletePreview(action, editor);
                break;

            case 'terminalSendText':
                this.showTerminalCommandPreview(action, editor);
                break;

            case 'setSelections':
                this.showCursorMovePreview(action, editor);
                break;

            case 'openFile':
                this.showFileSwitchPreview(action, editor);
                break;

            case 'terminalShow':
            case 'showTextDocument':
                // These don't need previews
                break;
        }
    }

    /**
     * Clear all preview UI.
     */
    clear(): void {
        this.decorationPool.clearAll();
        this.inlineProvider.clearAction();
        this.hoverProvider.clearAction();
        this.currentAction = null;
        this.visible = false;
    }

    /**
     * Check if a preview is currently visible.
     */
    isVisible(): boolean {
        return this.visible;
    }

    /**
     * Get the current action being previewed.
     */
    getCurrentAction(): Action | null {
        return this.currentAction;
    }

    /**
     * Show the pending action in a quick pick (for terminal focus scenario).
     */
    async showQuickPick(): Promise<QuickPickResult> {
        if (!this.currentAction) {
            return null;
        }
        return showPendingActionQuickPick(this.currentAction);
    }

    /**
     * Dispose all resources.
     */
    dispose(): void {
        this.decorationPool.dispose();
        this.hoverProviderDisposable?.dispose();
    }

    // -------------------- Preview Renderers --------------------

    /**
     * Show preview for text insertion using inline completion.
     * Ghost text will appear at the insert position.
     * Shows a fallback indicator if the insert position is far from cursor.
     */
    private showInsertPreview(action: { kind: 'editInsert'; position: [number, number]; text: string }, editor?: vscode.TextEditor): void {
        // Use inline completion provider for ghost text
        this.inlineProvider.setAction(action);
        
        if (editor) {
            const anchorLine = Math.min(action.position[0], editor.document.lineCount - 1);
            const cursorLine = editor.selection.active.line;
            
            // If cursor is far from the insert position, VS Code may not show the ghost text
            // In that case, show a fallback indicator pointing to where the insert will happen
            if (!this.inlineProvider.isActionNearCursor(cursorLine)) {
                const lineNum = action.position[0] + 1; // 1-based for display
                this.showMetaIndicator(editor, cursorLine, '$(arrow-down)', `Insert at line ${lineNum}`, COLORS.cursorMove);
            }
            
            // Set up hover provider for detailed view
            this.hoverProvider.setAction(action, anchorLine);
        }
    }

    /**
     * Show preview for text replacement.
     * Always shows deletion decorations because VS Code's inline completion
     * only reliably shows ghost text when the range starts at/after the cursor.
     */
    private showReplacePreview(action: { kind: 'editReplace'; range: { start: [number, number]; end: [number, number] }; text: string }, editor?: vscode.TextEditor): void {
        // Try inline completion - it may or may not show depending on cursor position
        this.inlineProvider.setAction(action);

        if (editor) {
            const range = toVscodeRange(action.range);
            
            // Always show deletion decorations for replacements
            // VS Code's inline completion doesn't reliably show ghost text
            // when the replacement range doesn't start at the cursor position
            const deletionRanges = computeDeletionRanges(editor.document, range, action.text);
            
            if (deletionRanges.length > 0) {
                // Character-level deletion highlights
                const decorationOptions: vscode.DecorationOptions[] = deletionRanges.map(r => ({
                    range: r
                }));
                this.decorationPool.setDecorations(editor, 'deletion-char', decorationOptions);
            } else {
                // Fall back to highlighting the entire range
                this.decorationPool.setDecorations(editor, 'deletion', [{ range }]);
            }
            
            // Also show what will replace it as a meta indicator at end of line
            const preview = truncate(action.text, 50);
            this.showMetaIndicator(editor, range.start.line, '$(replace)', `→ ${preview}`, COLORS.cursorMove);

            // Set hover provider for full details
            this.hoverProvider.setAction(action, range.start.line);
        }
    }

    /**
     * Show preview for text deletion with strikethrough decoration.
     */
    private showDeletePreview(action: { kind: 'editDelete'; range: { start: [number, number]; end: [number, number] } }, editor?: vscode.TextEditor): void {
        if (!editor) return;

        const range = toVscodeRange(action.range);
        
        // Highlight the deletion range
        this.decorationPool.setDecorations(editor, 'deletion', [{ range }]);

        // Set hover provider
        this.hoverProvider.setAction(action, range.start.line);
    }

    /**
     * Show preview for terminal command with indicator decoration.
     */
    private showTerminalCommandPreview(action: { kind: 'terminalSendText'; text: string }, editor?: vscode.TextEditor): void {
        if (!editor) return;

        const anchorLine = this.getVisibleAnchorLine(editor);
        const cmdPreview = truncate(action.text, 60);
        
        this.showMetaIndicator(editor, anchorLine, '$(terminal)', `Run: ${cmdPreview}`, COLORS.terminal);
        this.hoverProvider.setAction(action, anchorLine);
    }

    /**
     * Show preview for cursor movement with indicator decoration.
     */
    private showCursorMovePreview(action: { kind: 'setSelections'; selections: Array<{ start: [number, number]; end: [number, number] }> }, editor?: vscode.TextEditor): void {
        if (!editor) return;

        const targetLine = action.selections[0].start[0];
        const targetPos = new vscode.Position(targetLine, action.selections[0].start[1]);
        const isTargetVisible = editor.visibleRanges.some(r => r.contains(targetPos));

        let anchorLine: number;
        let icon: string;
        let label: string;

        if (isTargetVisible) {
            // Target is visible, show indicator at target
            anchorLine = targetLine;
            icon = '$(arrow-right)';
            label = 'Move cursor here';
        } else {
            // Target is off-screen, show indicator at edge of visible area
            anchorLine = this.getVisibleAnchorLine(editor);
            const direction = targetLine < anchorLine ? '↑' : '↓';
            icon = `$(arrow-${targetLine < anchorLine ? 'up' : 'down'})`;
            label = `Go to line ${targetLine + 1}`;
        }

        this.showMetaIndicator(editor, anchorLine, icon, label, COLORS.cursorMove);
        this.hoverProvider.setAction(action, anchorLine);
    }

    /**
     * Show preview for file switch with indicator decoration.
     */
    private showFileSwitchPreview(action: { kind: 'openFile'; filePath: string; selections?: Array<{ start: [number, number]; end: [number, number] }> }, editor?: vscode.TextEditor): void {
        if (!editor) return;

        const anchorLine = this.getVisibleAnchorLine(editor);
        const fileName = action.filePath.split(/[/\\]/).pop() || action.filePath;
        const targetLine = action.selections?.[0]?.start[0];
        
        const label = targetLine !== undefined
            ? `Open: ${fileName}:${targetLine + 1}`
            : `Open: ${fileName}`;

        this.showMetaIndicator(editor, anchorLine, '$(file)', label, COLORS.fileSwitch);
        this.hoverProvider.setAction(action, anchorLine);
    }

    // -------------------- Helper Methods --------------------

    /**
     * Show a meta-action indicator decoration at the specified line.
     */
    private showMetaIndicator(
        editor: vscode.TextEditor,
        line: number,
        icon: string,
        label: string,
        color: vscode.ThemeColor
    ): void {
        const anchorPos = new vscode.Position(line, Number.MAX_SAFE_INTEGER);
        const range = new vscode.Range(anchorPos, anchorPos);

        const decorationOptions: vscode.DecorationOptions[] = [{
            range,
            renderOptions: {
                after: {
                    contentText: `  ${icon} ${label}`,
                    color: color,
                    fontStyle: 'italic',
                    margin: '0 0 0 2ch',
                }
            }
        }];

        this.decorationPool.setDecorations(editor, 'meta-indicator', decorationOptions);
    }

    /**
     * Get a visible anchor line for decorations.
     * Returns the line of the cursor if visible, or a line at the edge of the visible area.
     */
    private getVisibleAnchorLine(editor: vscode.TextEditor): number {
        const cursor = editor.selection.active;
        const isVisible = editor.visibleRanges.some(r => r.contains(cursor));

        if (isVisible) {
            return cursor.line;
        }

        if (editor.visibleRanges.length > 0) {
            const firstVisible = editor.visibleRanges[0];
            const lastVisible = editor.visibleRanges[editor.visibleRanges.length - 1];

            if (cursor.isBefore(firstVisible.start)) {
                return firstVisible.start.line;
            } else {
                return lastVisible.end.line;
            }
        }

        return 0;
    }
}