import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useI18n } from '../application/i18n/I18nProvider'; import type { Snippet } from '../domain/models'; import { applySnippetVariables, parseSnippetVariables, previewSnippetCommand, snippetHasVariables, type SnippetVariableDef, } from '../domain/snippetVariables'; import { readSnippetVariableValuesForSnippet, saveSnippetVariableValues, } from '../application/state/snippetVariableValues'; import { Button } from './ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from './ui/dialog'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { ScrollArea } from './ui/scroll-area'; interface PendingPrompt { snippet: Snippet; variables: SnippetVariableDef[]; resolve: (values: Record | null) => void; } function buildInitialValues( snippet: Snippet, variables: SnippetVariableDef[], ): Record { const cached = readSnippetVariableValuesForSnippet(snippet.id); const values: Record = {}; for (const def of variables) { if (cached[def.name] !== undefined) { values[def.name] = cached[def.name]; } else if (def.defaultValue !== undefined) { values[def.name] = def.defaultValue; } else { values[def.name] = ''; } } return values; } function isFormValid( variables: SnippetVariableDef[], values: Record, ): boolean { for (const def of variables) { const raw = values[def.name] ?? ''; if (raw.trim() === '' && def.defaultValue === undefined) { return false; } } return true; } export const SnippetExecutionProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const { t } = useI18n(); const [pending, setPending] = useState(null); const [values, setValues] = useState>({}); const pendingRef = useRef(null); pendingRef.current = pending; const prompt = useCallback( (snippet: Snippet) => new Promise | null>((resolve) => { const prior = pendingRef.current; if (prior) prior.resolve(null); const variables = parseSnippetVariables(snippet.command); if (variables.length === 0) { resolve({}); return; } setValues(buildInitialValues(snippet, variables)); setPending({ snippet, variables, resolve }); }), [], ); useEffect(() => { promptSnippetVariablesSingleton = prompt; return () => { promptSnippetVariablesSingleton = null; }; }, [prompt]); useEffect(() => () => { const prior = pendingRef.current; if (prior) { prior.resolve(null); pendingRef.current = null; } }, []); const preview = useMemo(() => { if (!pending) return ''; return previewSnippetCommand(pending.snippet.command, values); }, [pending, values]); const canSubmit = pending ? isFormValid(pending.variables, values) : false; const closeWith = useCallback((result: Record | null) => { if (!pending) return; pending.resolve(result); setPending(null); }, [pending]); const handleSubmit = useCallback(() => { if (!pending || !canSubmit) return; const result = applySnippetVariables(pending.snippet.command, values); if (!result.ok) return; saveSnippetVariableValues(pending.snippet.id, values); closeWith(values); }, [pending, canSubmit, values, closeWith]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey && canSubmit) { e.preventDefault(); handleSubmit(); } }, [canSubmit, handleSubmit], ); return ( <> {children} { if (!open) closeWith(null); }}> {t('snippets.variables.dialogTitle')} {t('snippets.variables.dialogDesc', { label: pending?.snippet.label ?? '' })} {pending && (

{t('snippets.variables.hint')}

{pending.variables.map((def) => { const raw = values[def.name] ?? ''; const invalid = raw.trim() === '' && def.defaultValue === undefined; return (
{ const next = e.target.value; setValues((prev) => ({ ...prev, [def.name]: next })); }} className={invalid ? 'border-destructive' : undefined} autoFocus={def === pending.variables[0]} /> {invalid && (

{t('snippets.variables.required')}

)}
); })}

{t('snippets.variables.preview')}

                    {preview}
                  
)}
); }; let promptSnippetVariablesSingleton: | ((snippet: Snippet) => Promise | null>) | null = null; export async function resolveSnippetCommand(snippet: Snippet): Promise { if (!snippetHasVariables(snippet.command)) { return snippet.command; } const promptFn = promptSnippetVariablesSingleton; if (!promptFn) { return snippet.command; } const values = await promptFn(snippet); if (values === null) { return null; } const result = applySnippetVariables(snippet.command, values); if (!result.ok) { return null; } return result.command; }