Dynamic commands

This commit is contained in:
2025-09-07 02:18:11 +02:00
parent 7479320aab
commit 11a7724c58
7 changed files with 1039 additions and 365 deletions

View File

@@ -0,0 +1,407 @@
import * as fs from 'fs';
import * as path from 'path';
export interface CommandParameter {
name: string;
type: string;
required: boolean;
defaultValue?: string;
}
export interface CommandInfo {
name: string;
description: string;
parameters: CommandParameter[];
example?: string;
}
export class CommandParser {
private projectRoot: string;
constructor(projectRoot: string) {
this.projectRoot = projectRoot;
}
/**
* Parse project.godot file to extract command directories
*/
private parseProjectGodot(): string[] {
const projectGodotPath = path.join(this.projectRoot, 'project.godot');
if (!fs.existsSync(projectGodotPath)) {
console.warn('project.godot not found, using default command directories');
return [
'res://addons/escoria-core/game/core-scripts/esc/commands',
'res://addons/escoria-ui-return-monkey-island/esc/commands',
'res://addons/escoria-ui-return-monkey-island-dialog-simple/commands'
];
}
const content = fs.readFileSync(projectGodotPath, 'utf8');
const lines = content.split('\n');
let inEscoriaSection = false;
let commandDirectories: string[] = [];
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '[escoria]') {
inEscoriaSection = true;
continue;
}
if (inEscoriaSection && trimmedLine.startsWith('[')) {
break; // End of escoria section
}
if (inEscoriaSection && trimmedLine.startsWith('main/command_directories=')) {
// Parse the array format: ["path1", "path2", "path3"]
const arrayMatch = trimmedLine.match(/main\/command_directories=\[(.*)\]/);
if (arrayMatch) {
const pathsString = arrayMatch[1];
// Extract quoted paths
const pathMatches = pathsString.match(/"([^"]+)"/g);
if (pathMatches) {
commandDirectories = pathMatches.map(match => match.slice(1, -1)); // Remove quotes
}
}
break;
}
}
return commandDirectories.length > 0 ? commandDirectories : [
'res://addons/escoria-core/game/core-scripts/esc/commands',
'res://addons/escoria-ui-return-monkey-island/esc/commands',
'res://addons/escoria-ui-return-monkey-island-dialog-simple/commands'
];
}
/**
* Convert res:// path to actual filesystem path
*/
private resPathToFsPath(resPath: string): string {
if (resPath.startsWith('res://')) {
return path.join(this.projectRoot, resPath.substring(6));
}
return path.join(this.projectRoot, resPath);
}
/**
* Parse a single command file to extract command information
*/
private parseCommandFile(filePath: string): CommandInfo | null {
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
// Extract command name from filename
const fileName = path.basename(filePath, '.gd');
const commandName = fileName;
// Parse description from comments
let description = '';
let parameters: CommandParameter[] = [];
let example = '';
let inParametersSection = false;
let inExampleSection = false;
let foundFirstDescription = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip empty lines
if (!line) {
continue;
}
// Extract main description (first comment block)
if (line.startsWith('# `') && line.includes('`')) {
const match = line.match(/# `([^`]+)`/);
if (match) {
description = match[1];
foundFirstDescription = true;
}
continue;
}
// Look for description in subsequent comment lines (before parameters section)
if (line.startsWith('#') && !inParametersSection && !inExampleSection) {
const cleanLine = line.replace(/^#\s*/, '');
// Skip lines that are clearly not part of the main description
if (cleanLine.startsWith('**') || cleanLine.startsWith('@') ||
cleanLine.startsWith('Example:') || cleanLine.startsWith('e.g.') ||
cleanLine.includes('*') && cleanLine.includes(':') ||
cleanLine.includes('Constructor') || cleanLine.includes('Use look-ahead') ||
cleanLine.includes('Return the descriptor') || cleanLine.includes('Validate whether') ||
cleanLine.includes('Run the command') || cleanLine.includes('Function called when')) {
continue;
}
// If we haven't found the first description yet, this might be it
if (!foundFirstDescription && cleanLine) {
description = cleanLine;
foundFirstDescription = true;
continue;
}
// If we have a description and this line looks like continuation
if (foundFirstDescription && cleanLine && !cleanLine.includes('*')) {
// Stop if we hit code-related content
if (cleanLine.includes('Constructor') || cleanLine.includes('Use look-ahead') ||
cleanLine.includes('Return the descriptor') || cleanLine.includes('Validate whether') ||
cleanLine.includes('Run the command') || cleanLine.includes('Function called when')) {
break;
}
description += ' ' + cleanLine;
}
continue;
}
// Check for parameters section
if (line.includes('**Parameters**')) {
inParametersSection = true;
inExampleSection = false;
continue;
}
// Check for example section
if (line.includes('Example:')) {
inParametersSection = false;
inExampleSection = true;
const exampleMatch = line.match(/Example:\s*(.+)/);
if (exampleMatch) {
example = exampleMatch[1];
}
continue;
}
// Parse parameters (support both # - *param*: and # * *param*: formats)
if (inParametersSection && (line.includes('# - *') || line.includes('# * *'))) {
const paramMatch = line.match(/#\s*[-*]\s*\*([^*]+)\*:\s*(.+)/);
if (paramMatch) {
const paramName = paramMatch[1].trim();
const paramDesc = paramMatch[2].trim();
// Try to determine if parameter is required
const isRequired = !paramDesc.includes('default:') && !paramDesc.includes('(default:');
// Extract default value if present
let defaultValue: string | undefined;
const defaultMatch = paramDesc.match(/\(default:\s*([^)]+)\)/);
if (defaultMatch) {
defaultValue = defaultMatch[1].trim();
}
// Determine parameter type from description
let paramType = 'string';
if (paramDesc.includes('boolean') || paramDesc.includes('true') || paramDesc.includes('false')) {
paramType = 'boolean';
} else if (paramDesc.includes('number') || paramDesc.includes('int') || paramDesc.includes('float')) {
paramType = 'number';
}
parameters.push({
name: paramName,
type: paramType,
required: isRequired,
defaultValue: defaultValue
});
}
continue;
}
// Parse configure() method to get more accurate parameter info
if (line.includes('func configure()') && i + 5 < lines.length) {
// Look for ESCCommandArgumentDescriptor in the next few lines
for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) {
const configLine = lines[j].trim();
if (configLine.includes('ESCCommandArgumentDescriptor.new(')) {
// Parse the descriptor parameters
const descriptorMatch = configLine.match(/ESCCommandArgumentDescriptor\.new\(\s*(\d+)/);
if (descriptorMatch) {
const minArgs = parseInt(descriptorMatch[1]);
// Look for type arrays, defaults, and required flags in subsequent lines
let types: string[] = [];
let defaults: string[] = [];
let requiredFlags: boolean[] = [];
for (let k = j + 1; k < Math.min(j + 10, lines.length); k++) {
const typeLine = lines[k].trim();
// Extract types array
if (typeLine.includes('TYPE_') && typeLine.includes('[')) {
const typeMatch = typeLine.match(/\[([^\]]+)\]/);
if (typeMatch) {
types = typeMatch[1].split(',').map(t => t.trim());
}
}
// Extract defaults array
if (typeLine.includes('null') && typeLine.includes('[') && !typeLine.includes('TYPE_')) {
const defaultMatch = typeLine.match(/\[([^\]]+)\]/);
if (defaultMatch) {
defaults = defaultMatch[1].split(',').map(d => d.trim());
}
}
// Extract required flags array (look for true/false pattern)
if (typeLine.includes('true') && typeLine.includes('false') && typeLine.includes('[') && !typeLine.includes('TYPE_')) {
const requiredMatch = typeLine.match(/\[([^\]]+)\]/);
if (requiredMatch) {
requiredFlags = requiredMatch[1].split(',').map(f => f.trim() === 'true');
}
}
}
// Update or create parameters with accurate information
const maxParams = Math.max(types.length, defaults.length);
const newParameters: CommandParameter[] = [];
// Store original parameters from comments for name preservation
const originalParams = [...parameters];
for (let p = 0; p < maxParams; p++) {
const type = types[p] || 'TYPE_STRING';
const defaultValue = defaults[p] || 'null';
// If requiredFlags array is not provided, use minArgs to determine required parameters
const isRequired = p < minArgs || (requiredFlags[p] !== undefined ? requiredFlags[p] : p < minArgs);
// Convert Godot types to readable types
let paramType = 'string';
if (type.includes('TYPE_BOOL')) {
paramType = 'boolean';
} else if (type.includes('TYPE_INT') || type.includes('TYPE_FLOAT')) {
paramType = 'number';
} else if (type.includes('TYPE_STRING')) {
paramType = 'string';
}
// Try to get parameter name from comments if available, otherwise generate one
let paramName = `param${p + 1}`;
if (p < originalParams.length && originalParams[p] && originalParams[p].name !== `param${p + 1}`) {
paramName = originalParams[p].name;
}
newParameters.push({
name: paramName,
type: paramType,
required: isRequired,
defaultValue: defaultValue !== 'null' ? defaultValue : undefined
});
}
// Replace parameters with the new ones from configure method
parameters = newParameters;
}
break;
}
}
break;
}
}
// If we couldn't parse parameters from comments, try to infer from configure method
if (parameters.length === 0) {
// Look for configure method and try to extract parameter count
const configureMatch = content.match(/func configure\(\)[^}]*ESCCommandArgumentDescriptor\.new\(\s*(\d+)/);
if (configureMatch) {
const paramCount = parseInt(configureMatch[1]);
for (let i = 0; i < paramCount; i++) {
parameters.push({
name: `param${i + 1}`,
type: 'string',
required: true
});
}
}
}
// Clean up description - remove extra whitespace and fix common issues
if (description) {
description = description.trim();
// Remove extra whitespace
description = description.replace(/\s+/g, ' ');
// Truncate if too long (keep first sentence or first 200 chars)
const firstSentence = description.split('.')[0];
if (firstSentence.length < 200 && firstSentence.length > 20) {
description = firstSentence + '.';
} else if (description.length > 200) {
description = description.substring(0, 200).trim() + '...';
}
// If description is just the command name, try to get a better one
if (description === commandName || description === `${commandName} command`) {
description = `${commandName} command`;
}
}
return {
name: commandName,
description: description || `${commandName} command`,
parameters: parameters,
example: example
};
} catch (error) {
console.error(`Error parsing command file ${filePath}:`, error);
return null;
}
}
/**
* Parse all command files from the specified directories
*/
public parseCommands(): CommandInfo[] {
const commandDirectories = this.parseProjectGodot();
const commands: CommandInfo[] = [];
for (const dir of commandDirectories) {
const fsPath = this.resPathToFsPath(dir);
if (!fs.existsSync(fsPath)) {
console.warn(`Command directory not found: ${fsPath}`);
continue;
}
try {
const files = fs.readdirSync(fsPath);
const gdFiles = files.filter(file => file.endsWith('.gd') && !file.endsWith('.gd.uid'));
for (const file of gdFiles) {
const filePath = path.join(fsPath, file);
const commandInfo = this.parseCommandFile(filePath);
if (commandInfo) {
commands.push(commandInfo);
}
}
} catch (error) {
console.error(`Error reading directory ${fsPath}:`, error);
}
}
// Sort commands alphabetically
commands.sort((a, b) => a.name.localeCompare(b.name));
return commands;
}
/**
* Get commands in the format expected by the VSCode extension
*/
public getCommandsForExtension(): Array<{name: string, description: string, parameters: CommandParameter[]}> {
const commands = this.parseCommands();
return commands.map(cmd => ({
name: cmd.name,
description: cmd.description,
parameters: cmd.parameters
}));
}
}

View File

@@ -1,358 +1,10 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { CommandParser, CommandInfo, CommandParameter } from './commandParser';
// ASHES commands with their descriptions and parameters
const ASHES_COMMANDS = [
{
name: 'accept_input',
description: 'Accept specific input types',
parameters: ['input_type']
},
{
name: 'anim',
description: 'Play animation on object',
parameters: ['object_id', 'animation_name']
},
{
name: 'anim_block',
description: 'Play animation and wait for completion',
parameters: ['object_id', 'animation_name']
},
{
name: 'block_say',
description: 'Start a block of say commands',
parameters: []
},
{
name: 'camera_push',
description: 'Push camera to new position',
parameters: ['x', 'y']
},
{
name: 'camera_push_block',
description: 'Push camera and wait for completion',
parameters: ['x', 'y']
},
{
name: 'camera_set_limits',
description: 'Set camera movement limits',
parameters: ['left', 'top', 'right', 'bottom']
},
{
name: 'camera_set_pos',
description: 'Set camera position',
parameters: ['x', 'y']
},
{
name: 'camera_set_pos_block',
description: 'Set camera position and wait',
parameters: ['x', 'y']
},
{
name: 'camera_set_target',
description: 'Set camera target',
parameters: ['object_id']
},
{
name: 'camera_set_target_block',
description: 'Set camera target and wait',
parameters: ['object_id']
},
{
name: 'camera_set_zoom',
description: 'Set camera zoom level',
parameters: ['zoom_level']
},
{
name: 'camera_set_zoom_block',
description: 'Set camera zoom and wait',
parameters: ['zoom_level']
},
{
name: 'camera_set_zoom_height',
description: 'Set camera zoom height',
parameters: ['height']
},
{
name: 'camera_set_zoom_height_block',
description: 'Set camera zoom height and wait',
parameters: ['height']
},
{
name: 'camera_shift',
description: 'Shift camera position',
parameters: ['x', 'y']
},
{
name: 'camera_shift_block',
description: 'Shift camera and wait',
parameters: ['x', 'y']
},
{
name: 'change_scene',
description: 'Change to a different scene',
parameters: ['scene_path', 'enable_transition', 'run_events']
},
{
name: 'custom',
description: 'Execute custom command',
parameters: ['command_name', '...args']
},
{
name: 'dec_global',
description: 'Decrement global variable',
parameters: ['variable_name']
},
{
name: 'enable_terrain',
description: 'Enable/disable terrain',
parameters: ['terrain_name', 'enabled']
},
{
name: 'end_block_say',
description: 'End a block of say commands',
parameters: []
},
{
name: 'hide_menu',
description: 'Hide menu',
parameters: ['menu_name']
},
{
name: 'inc_global',
description: 'Increment global variable',
parameters: ['variable_name']
},
{
name: 'inventory_add',
description: 'Add item to inventory',
parameters: ['item_id']
},
{
name: 'inventory_remove',
description: 'Remove item from inventory',
parameters: ['item_id']
},
{
name: 'item_count_add',
description: 'Add to item count',
parameters: ['item_id', 'count']
},
{
name: 'play_lib_snd',
description: 'Play library sound',
parameters: ['filename', 'namespace']
},
{
name: 'play_snd',
description: 'Play sound file',
parameters: ['sound_path', 'type']
},
{
name: 'play_video',
description: 'Play video file',
parameters: ['video_path']
},
{
name: 'print',
description: 'Print debug message',
parameters: ['message']
},
{
name: 'print_internal',
description: 'Print internal message',
parameters: ['message']
},
{
name: 'queue_event',
description: 'Queue event for later execution',
parameters: ['object_id', 'event_name']
},
{
name: 'queue_resource',
description: 'Queue resource for loading',
parameters: ['resource_path']
},
{
name: 'rand_global',
description: 'Set random value to global',
parameters: ['variable_name', 'min', 'max']
},
{
name: 'repeat',
description: 'Repeat command',
parameters: ['count', 'command']
},
{
name: 'save_game',
description: 'Save game state',
parameters: ['save_name']
},
{
name: 'say',
description: 'Display dialog text',
parameters: ['speaker', 'text', 'translation_key', 'type']
},
{
name: 'say_last_dialog_option',
description: 'Say the last dialog option',
parameters: []
},
{
name: 'say_random',
description: 'Say random text from list',
parameters: ['speaker', 'list_id', 'length']
},
{
name: 'say_sequence',
description: 'Say text sequence',
parameters: ['speaker', 'list_id', 'length', 'loop']
},
{
name: 'sched_event',
description: 'Schedule event for later',
parameters: ['delay', 'object_id', 'event_name']
},
{
name: 'set_active',
description: 'Set object active/inactive',
parameters: ['object_id', 'active']
},
{
name: 'set_active_if_exists',
description: 'Set object active if it exists',
parameters: ['object_id', 'active']
},
{
name: 'set_angle',
description: 'Set object angle',
parameters: ['object_id', 'angle']
},
{
name: 'set_animations',
description: 'Set object animations',
parameters: ['object_id', 'animations']
},
{
name: 'set_direction',
description: 'Set object direction',
parameters: ['object_id', 'direction']
},
{
name: 'set_global',
description: 'Set global variable',
parameters: ['variable_name', 'value', 'force']
},
{
name: 'set_globals',
description: 'Set multiple global variables',
parameters: ['variables_dict']
},
{
name: 'set_gui_visible',
description: 'Set GUI visibility',
parameters: ['visible']
},
{
name: 'set_interactive',
description: 'Set object interactive state',
parameters: ['object_id', 'interactive']
},
{
name: 'set_item_custom_data',
description: 'Set item custom data',
parameters: ['item_id', 'key', 'value']
},
{
name: 'set_speed',
description: 'Set object speed',
parameters: ['object_id', 'speed']
},
{
name: 'set_state',
description: 'Set object state',
parameters: ['object_id', 'state']
},
{
name: 'set_tooltip',
description: 'Set object tooltip',
parameters: ['object_id', 'action', 'text']
},
{
name: 'show_menu',
description: 'Show menu',
parameters: ['menu_name']
},
{
name: 'slide',
description: 'Slide object to position',
parameters: ['object_id', 'x', 'y', 'duration']
},
{
name: 'slide_block',
description: 'Slide object and wait',
parameters: ['object_id', 'x', 'y', 'duration']
},
{
name: 'spawn',
description: 'Spawn object',
parameters: ['object_id', 'x', 'y']
},
{
name: 'stop',
description: 'Stop current event',
parameters: []
},
{
name: 'stop_snd',
description: 'Stop sound',
parameters: ['sound_type']
},
{
name: 'teleport',
description: 'Teleport object to target',
parameters: ['object_id', 'target_id']
},
{
name: 'teleport_pos',
description: 'Teleport object to position',
parameters: ['object_id', 'x', 'y']
},
{
name: 'transition',
description: 'Play transition effect',
parameters: ['transition_type', 'duration']
},
{
name: 'turn_to',
description: 'Turn object to face target',
parameters: ['object_id', 'target_id']
},
{
name: 'wait',
description: 'Wait for specified time',
parameters: ['duration']
},
{
name: 'walk',
description: 'Walk object to target',
parameters: ['object_id', 'target_id']
},
{
name: 'walk_block',
description: 'Walk object and wait',
parameters: ['object_id', 'target_id']
},
{
name: 'walk_to_pos',
description: 'Walk object to position',
parameters: ['object_id', 'x', 'y']
},
{
name: 'walk_to_pos_block',
description: 'Walk object to position and wait',
parameters: ['object_id', 'x', 'y']
}
];
// Cache for dynamically loaded commands
let ASHES_COMMANDS: Array<{name: string, description: string, parameters: CommandParameter[]}> = [];
let COMMAND_CACHE_TIMESTAMP = 0;
// Built-in variables
const BUILTIN_VARIABLES = [
@@ -369,6 +21,41 @@ const KEYWORDS = [
'true', 'false', 'nil', 'and', 'or', 'not', 'in', 'is', 'active'
];
/**
* Load commands dynamically from the project
*/
function loadCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[]}> {
try {
const parser = new CommandParser(workspaceRoot);
const commands = parser.getCommandsForExtension();
// Update cache timestamp
COMMAND_CACHE_TIMESTAMP = Date.now();
return commands;
} catch (error) {
console.error('Error loading commands:', error);
// Return empty array if loading fails
return [];
}
}
/**
* Get commands, using cache if available and not too old
*/
function getCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[]}> {
const now = Date.now();
const cacheAge = now - COMMAND_CACHE_TIMESTAMP;
const maxCacheAge = 5 * 60 * 1000; // 5 minutes
// Reload commands if cache is empty or too old
if (ASHES_COMMANDS.length === 0 || cacheAge > maxCacheAge) {
ASHES_COMMANDS = loadCommands(workspaceRoot);
}
return ASHES_COMMANDS;
}
export function activate(context: vscode.ExtensionContext) {
console.log('ASHES Language Support extension is now active!');
@@ -379,14 +66,52 @@ export function activate(context: vscode.ExtensionContext) {
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
const completions: vscode.CompletionItem[] = [];
// Get workspace root
const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath;
if (!workspaceRoot) {
return completions;
}
// Get dynamic commands
const commands = getCommands(workspaceRoot);
// Add command completions
ASHES_COMMANDS.forEach(command => {
commands.forEach(command => {
const completion = new vscode.CompletionItem(command.name, vscode.CompletionItemKind.Function);
completion.detail = command.description;
// Create detailed parameter documentation
let paramDocs = '';
let exampleUsage = '';
if (command.parameters && command.parameters.length > 0) {
paramDocs = '\n\n**Parameters:**\n';
const exampleParams: string[] = [];
command.parameters.forEach((param, index) => {
const required = param.required ? '**' : '';
const optional = param.required ? '' : ' (optional)';
const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : '';
paramDocs += `- ${required}${param.name}${required} (${param.type})${optional}${defaultValue}\n`;
// Create example parameter
const exampleParam = param.required ?
`${param.name}: ${param.type}` :
`[${param.name}: ${param.type}]`;
exampleParams.push(exampleParam);
});
// Create example usage
exampleUsage = `\n\n**Example:**\n\`${command.name}(${exampleParams.join(', ')})\``;
}
completion.documentation = new vscode.MarkdownString(
`**${command.name}**\n\n${command.description}\n\n**Parameters:** ${command.parameters.join(', ')}`
`## ${command.name}\n\n---\n\n${command.description}${paramDocs}${exampleUsage}`
);
completion.insertText = new vscode.SnippetString(`${command.name}($1)`);
// Create snippet with parameter placeholders
const snippetParams = command.parameters.map((param, index) => `\${${index + 1}:${param.name}}`).join(', ');
completion.insertText = new vscode.SnippetString(`${command.name}(${snippetParams})`);
completions.push(completion);
});
@@ -417,12 +142,46 @@ export function activate(context: vscode.ExtensionContext) {
{
provideHover(document: vscode.TextDocument, position: vscode.Position) {
const word = document.getText(document.getWordRangeAtPosition(position));
const command = ASHES_COMMANDS.find(cmd => cmd.name === word);
// Get workspace root
const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath;
if (!workspaceRoot) {
return null;
}
// Get dynamic commands
const commands = getCommands(workspaceRoot);
const command = commands.find(cmd => cmd.name === word);
if (command) {
// Create detailed parameter documentation
let paramDocs = '';
let exampleUsage = '';
if (command.parameters && command.parameters.length > 0) {
paramDocs = '\n\n**Parameters:**\n';
const exampleParams: string[] = [];
command.parameters.forEach((param, index) => {
const required = param.required ? '**' : '';
const optional = param.required ? '' : ' (optional)';
const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : '';
paramDocs += `- ${required}${param.name}${required} (${param.type})${optional}${defaultValue}\n`;
// Create example parameter
const exampleParam = param.required ?
`${param.name}: ${param.type}` :
`[${param.name}: ${param.type}]`;
exampleParams.push(exampleParam);
});
// Create example usage
exampleUsage = `\n\n**Example:**\n\`${command.name}(${exampleParams.join(', ')})\``;
}
const hover = new vscode.Hover(
new vscode.MarkdownString(
`**${command.name}**\n\n${command.description}\n\n**Parameters:** ${command.parameters.join(', ')}`
`## ${command.name}\n\n---\n\n${command.description}${paramDocs}${exampleUsage}`
)
);
return hover;
@@ -450,13 +209,65 @@ export function activate(context: vscode.ExtensionContext) {
{}
);
const commandsHtml = ASHES_COMMANDS.map(command =>
`<tr>
// Get workspace root from active editor
const activeEditor = vscode.window.activeTextEditor;
const workspaceRoot = activeEditor ?
vscode.workspace.getWorkspaceFolder(activeEditor.document.uri)?.uri.fsPath :
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) {
panel.webview.html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: var(--vscode-font-family); }
</style>
</head>
<body>
<h1>ASHES Command Reference</h1>
<p>No workspace found. Please open a workspace to view commands.</p>
</body>
</html>
`;
return;
}
// Get dynamic commands
const commands = getCommands(workspaceRoot);
const commandsHtml = commands.map(command => {
let paramInfo = '';
let exampleUsage = '';
if (command.parameters && command.parameters.length > 0) {
const exampleParams: string[] = [];
paramInfo = command.parameters.map(param => {
const required = param.required ? '<strong>' : '';
const requiredEnd = param.required ? '</strong>' : '';
const optional = param.required ? '' : ' (optional)';
const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : '';
// Create example parameter
const exampleParam = param.required ?
`${param.name}: ${param.type}` :
`[${param.name}: ${param.type}]`;
exampleParams.push(exampleParam);
return `${required}${param.name}${requiredEnd} (${param.type})${optional}${defaultValue}`;
}).join('<br>');
// Create example usage
exampleUsage = `<br><br><strong>Example:</strong><br><code>${command.name}(${exampleParams.join(', ')})</code>`;
}
return `<tr>
<td><code>${command.name}</code></td>
<td>${command.description}</td>
<td><code>${command.parameters.join(', ')}</code></td>
</tr>`
).join('');
<td>${command.description}${exampleUsage}</td>
<td>${paramInfo}</td>
</tr>`;
}).join('');
panel.webview.html = `
<!DOCTYPE html>
@@ -489,7 +300,16 @@ export function activate(context: vscode.ExtensionContext) {
`;
});
context.subscriptions.push(completionProvider, hoverProvider, showCommandReference);
// Register command for refreshing command cache
const refreshCommands = vscode.commands.registerCommand('ashes.refreshCommands', () => {
// Clear cache to force reload
ASHES_COMMANDS = [];
COMMAND_CACHE_TIMESTAMP = 0;
vscode.window.showInformationMessage('ASHES commands cache refreshed!');
});
context.subscriptions.push(completionProvider, hoverProvider, showCommandReference, refreshCommands);
}
export function deactivate() {}