All files / languages/triplate triplate-prompt.ts

65.3% Statements 32/49
45.65% Branches 21/46
66.66% Functions 4/6
65.3% Lines 32/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160                                                13x   13x   3x     10x 10x 3x         7x   7x   1x 1x                       13x 6x   6x   6x                 6x         6x 2x     4x 3x       8x   8x                         8x   8x 6x 6x   6x             6x                             6x             6x 1x     5x     7x                                      
import * as vscode from 'vscode';
import type { ParamDecl, compile } from 'triplate';
 
type CompiledTemplate = ReturnType<typeof compile>;
 
/** The user's choice when running a template: a named example, or manually entered values. */
type TemplateInputChoice =
	| { mode: 'example'; id: string }
	| { mode: 'manual'; inputs: Map<string, string | undefined> };
 
interface ExampleQuickPickItem extends vscode.QuickPickItem {
	exampleId?: string;
}
 
/**
 * Renders a template interactively: lets the user pick a declared example or enter parameter
 * values manually, then renders the result. Reports render errors itself.
 *
 * Shared by the Execute Template command and the notebook controller's native "Run" handling.
 *
 * @param compiled A compiled triplate template.
 * @returns The rendered output, or `undefined` if the user cancelled or rendering failed.
 */
export async function renderTemplateInteractively(compiled: CompiledTemplate): Promise<string | undefined> {
	const choice = await selectTemplateInputs(compiled);
 
	if (!choice) {
		// The user cancelled a prompt.
		return undefined;
	}
 
	try {
		if (choice.mode === 'example') {
			return compiled.previewExample(choice.id);
		}
 
		// `triplate` coerces and validates the raw inputs against the schema,
		// throwing TriplateTypeError on bad input instead of rendering NaN.
		const context = compiled.contextFromStrings(Object.fromEntries(choice.inputs));
 
		return compiled.render(context);
	} catch (error) {
		vscode.window.showErrorMessage(`Failed to render template: ${(error as Error).message}`);
		return undefined;
	}
}
 
/**
 * Asks how the template should be run. When the template declares examples, offers a QuickPick of
 * those examples plus a "enter values manually" option; otherwise prompts for parameters directly.
 *
 * @param compiled A compiled triplate template.
 * @returns The chosen example or the manually entered values, or `undefined` if cancelled.
 */
async function selectTemplateInputs(compiled: CompiledTemplate): Promise<TemplateInputChoice | undefined> {
	if (compiled.examples.length > 0) {
		const manualItem: ExampleQuickPickItem = { label: 'Enter values manually…' };
 
		const items: ExampleQuickPickItem[] = [
			{ label: 'Example', kind: vscode.QuickPickItemKind.Separator },
			...compiled.examples.map(example => ({
				label: example.id,
				description: example.description,
				exampleId: example.id,
			})),
			{ label: 'Manual', kind: vscode.QuickPickItemKind.Separator },
			manualItem,
		];
 
		const pick = await vscode.window.showQuickPick(items, {
			title: 'Execute Template',
			placeHolder: 'Select an example or enter values manually',
		});
 
		if (!pick) {
			return undefined;
		}
 
		if (pick.exampleId !== undefined) {
			return { mode: 'example', id: pick.exampleId };
		}
	}
 
	const inputs = await promptForParameters(compiled.schema.params);
 
	return inputs ? { mode: 'manual', inputs } : undefined;
}
 
/**
 * Prompts sequentially for each declared triplate parameter value. Returns a map of
 * raw string inputs, or `undefined` if the user cancelled any prompt.
 *
 * Shared by the triplate execute commands and the notebook controller's native "Run"
 * handling so prompting behaves identically wherever a template is executed.
 *
 * @param params The parameter declarations from a compiled template's schema.
 */
async function promptForParameters(params: ParamDecl[]): Promise<Map<string, string | undefined> | undefined> {
	const inputs = new Map<string, string | undefined>();
 
	for (const param of params) {
		const { base, array, optional } = param.type;
		const typeLabel = `${base.kind}${array ? '[]' : ''}`;
 
		Iif (base.kind === 'record') {
			vscode.window.showInformationMessage(
				`Parameter "${param.name}" is a record and cannot be entered interactively. Use an example block instead.`
			);
			continue;
		}
 
		Iif (base.kind === 'bool' && !array) {
			const items = optional ? ['true', 'false', '(skip)'] : ['true', 'false'];
			const pick = await vscode.window.showQuickPick(items, {
				title: 'Execute Template',
				placeHolder: `${param.name}: ${typeLabel}${optional ? ' (optional)' : ''}`,
			});
 
			if (pick === undefined) {
				return undefined;
			}
 
			inputs.set(param.name, pick === '(skip)' ? undefined : pick);
			continue;
		}
 
		const value = await vscode.window.showInputBox({
			title: 'Execute Template',
			prompt: `${param.name}: ${typeLabel}${optional ? ' (optional)' : ''}`,
			placeHolder: array ? 'comma-separated values' : undefined,
			validateInput: (input: string) => validateInput(param, input),
		});
 
		if (value === undefined) {
			return undefined;
		}
 
		inputs.set(param.name, value);
	}
 
	return inputs;
}
 
function validateInput(param: ParamDecl, input: string): string | null {
	const { base, array, optional } = param.type;
	const value = input.trim();
 
	if (value === '') {
		return optional ? null : `${param.name} is required`;
	}
 
	const numeric = base.kind === 'int' || base.kind === 'decimal' || base.kind === 'double';
 
	if (numeric && !array && isNaN(Number(value))) {
		return `${param.name} must be a number`;
	} else {
		return null;
	}
}