UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

281 lines (280 loc) 10.9 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. Object.defineProperty(exports, "__esModule", { value: true }); exports.ToolCommandParser = void 0; /** * Parser for ToolCommand strings. */ class ToolCommandParser { /** * Parse a command string into structured parts. * @param text The command string to parse * @returns Parsed command, or undefined if invalid */ static parse(text) { const trimmed = text.trim(); if (!trimmed) return undefined; // Remove leading / if present const commandText = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed; if (!commandText) return undefined; const tokens = this.tokenize(commandText); if (tokens.length === 0) return undefined; const commandName = tokens[0].value; const args = []; const flags = {}; let i = 1; while (i < tokens.length) { const token = tokens[i]; if (token.type === "flag") { const flagName = token.value.replace(/^-+/, ""); // Check if next token is a value or another flag if (i + 1 < tokens.length && tokens[i + 1].type === "flagValue") { const valueToken = tokens[i + 1]; // Check for comma-separated values if (valueToken.value.includes(",")) { flags[flagName] = valueToken.value.split(",").map((v) => v.trim()); } else { flags[flagName] = valueToken.value; } i += 2; } else { // Boolean flag flags[flagName] = true; i++; } } else if (token.type === "arg") { args.push(token.value); i++; } else { i++; } } return { commandName, args, flags, originalText: text, }; } /** * Tokenize a command string. */ static tokenize(text) { const tokens = []; let i = 0; let isFirstToken = true; let expectingFlagValue = false; while (i < text.length) { // Skip whitespace while (i < text.length && /\s/.test(text[i])) { i++; } if (i >= text.length) break; const start = i; // Check for quoted string if (text[i] === '"' || text[i] === "'") { const quote = text[i]; i++; while (i < text.length && text[i] !== quote) { if (text[i] === "\\" && i + 1 < text.length) { i += 2; } else { i++; } } if (i < text.length) i++; // Skip closing quote const value = text.slice(start + 1, i - 1).replace(/\\(.)/g, "$1"); tokens.push({ type: expectingFlagValue ? "flagValue" : isFirstToken ? "command" : "arg", value, start, end: i, }); expectingFlagValue = false; isFirstToken = false; continue; } // Check for flag if (text[i] === "-") { let flagEnd = i; // Skip -- or - while (flagEnd < text.length && text[flagEnd] === "-") { flagEnd++; } // Read flag name while (flagEnd < text.length && /[a-zA-Z0-9_-]/.test(text[flagEnd])) { flagEnd++; } tokens.push({ type: "flag", value: text.slice(i, flagEnd), start, end: flagEnd, }); i = flagEnd; expectingFlagValue = true; isFirstToken = false; continue; } // Regular token (command name, arg, or flag value) while (i < text.length && !/\s/.test(text[i])) { i++; } const value = text.slice(start, i); tokens.push({ type: expectingFlagValue ? "flagValue" : isFirstToken ? "command" : "arg", value, start, end: i, }); expectingFlagValue = false; isFirstToken = false; } return tokens; } /** * Get autocomplete suggestions for a partial command. * @param text The partial command text * @param cursorPos Cursor position in the text * @param registry The command registry to search * @param context Execution context */ static async getCompletions(text, cursorPos, registry, context) { const beforeCursor = text.slice(0, cursorPos); const trimmed = beforeCursor.trim(); // Get available command names, filtering by scope and project availability const getAvailableNames = () => { const scope = context.scope; const all = registry.getAll(scope); return all .filter((c) => !c.metadata.requiresProject || context.project) .map((c) => c.metadata.name); }; // Handle empty or just / if (!trimmed || trimmed === "/") { return getAvailableNames().map((n) => "/" + n); } // Remove leading / const commandText = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed; const tokens = this.tokenize(commandText); if (tokens.length === 0) { return getAvailableNames().map((n) => "/" + n); } // Get current token at cursor const lastToken = tokens[tokens.length - 1]; const isCompletingLastToken = cursorPos >= (trimmed.startsWith("/") ? 1 : 0) + lastToken.start; // If ending with a space, we're completing a new token (not the last one) const isNewToken = beforeCursor.endsWith(" "); // If completing the command name (only one token AND not followed by a space) if (tokens.length === 1 && isCompletingLastToken && !isNewToken) { const partial = lastToken.value.toLowerCase(); return getAvailableNames() .filter((n) => n.toLowerCase().startsWith(partial)) .map((n) => "/" + n); } // Get the command const command = registry.get(tokens[0].value); if (!command) { return []; } // If completing a flag name if (lastToken.type === "flag" && isCompletingLastToken && !isNewToken) { const flagDefs = command.metadata.flags || []; const partial = lastToken.value.replace(/^-+/, "").toLowerCase(); return flagDefs.filter((f) => f.name.toLowerCase().startsWith(partial)).map((f) => "--" + f.name); } // If we just typed a flag, suggest its values if (lastToken.type === "flag" && isNewToken) { const flagName = lastToken.value.replace(/^-+/, ""); const flagDef = command.metadata.flags?.find((f) => f.name.toLowerCase() === flagName.toLowerCase() || f.shortName === flagName); if (flagDef) { if (flagDef.choices) { return flagDef.choices; } if (flagDef.autocompleteProvider) { return flagDef.autocompleteProvider("", context); } } return []; } // If completing a flag value if (lastToken.type === "flagValue" && isCompletingLastToken) { // Find the flag this value belongs to const flagToken = tokens[tokens.length - 2]; if (flagToken?.type === "flag") { const flagName = flagToken.value.replace(/^-+/, ""); const flagDef = command.metadata.flags?.find((f) => f.name.toLowerCase() === flagName.toLowerCase() || f.shortName === flagName); if (flagDef) { if (flagDef.choices) { return flagDef.choices.filter((c) => c.toLowerCase().startsWith(lastToken.value.toLowerCase())); } if (flagDef.autocompleteProvider) { return flagDef.autocompleteProvider(lastToken.value, context); } } } return []; } // Completing a positional argument const argTokens = tokens.filter((t) => t.type === "arg"); const argIndex = isNewToken ? argTokens.length : argTokens.length - 1; const partial = isNewToken ? "" : lastToken.value; // Use command's getCompletions if available if (command.getCompletions) { const argValues = argTokens.map((t) => t.value); const completions = await command.getCompletions(context, argValues, partial, argIndex); if (completions.length > 0) { return completions; } // Fall through to argument metadata for hints } // Fall back to argument metadata const argDef = command.metadata.arguments?.[argIndex]; if (argDef) { if (argDef.choices) { return argDef.choices.filter((c) => c.toLowerCase().startsWith(partial.toLowerCase())); } if (argDef.autocompleteProvider) { return argDef.autocompleteProvider(partial, context); } // No specific completions — return a usage hint so the UI can show what's expected if (!partial) { return [`<${argDef.name}>`]; } } return []; } /** * Format a command with arguments for display/execution. */ static format(commandName, args, flags) { const parts = ["/" + commandName]; for (const arg of args) { parts.push(arg.includes(" ") ? `"${arg}"` : arg); } for (const [key, value] of Object.entries(flags)) { if (value === true) { parts.push(`--${key}`); } else if (Array.isArray(value)) { parts.push(`--${key}`, value.join(",")); } else if (value !== false) { parts.push(`--${key}`, value.includes(" ") ? `"${value}"` : value); } } return parts.join(" "); } } exports.ToolCommandParser = ToolCommandParser;