diff --git a/vscode-extension-ashes/DYNAMIC_COMMANDS.md b/vscode-extension-ashes/DYNAMIC_COMMANDS.md new file mode 100644 index 00000000..0092f23d --- /dev/null +++ b/vscode-extension-ashes/DYNAMIC_COMMANDS.md @@ -0,0 +1,76 @@ +# Dynamic Command Generation + +The ASHES VSCode extension now supports dynamic command generation by parsing the `project.godot` file and extracting command information from the specified command directories. + +## How it Works + +1. **Project.godot Parsing**: The extension reads the `project.godot` file and looks for the `main/command_directories` setting in the `[escoria]` section. + +2. **Command Discovery**: It scans each directory specified in `command_directories` for `.gd` files (excluding `.gd.uid` files). + +3. **Command Parsing**: For each command file, it extracts: + - Command name (from filename) + - Description (from comment blocks) + - Parameters (from comment documentation and `configure()` method) + - Examples (if available) + +4. **Caching**: Commands are cached for 5 minutes to improve performance. + +## Configuration + +The extension automatically detects command directories from your `project.godot` file: + +```ini +[escoria] +main/command_directories=["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"] +``` + +## Features + +- **Auto-completion**: Commands are automatically available in IntelliSense +- **Hover Documentation**: Hover over commands to see descriptions and parameters +- **Command Reference**: Use `Ctrl+Shift+P` → "ASHES: Show ASHES Command Reference" to see all commands +- **Cache Refresh**: Use `Ctrl+Shift+P` → "ASHES: Refresh ASHES Commands" to reload commands + +## Command File Format + +Commands should follow the standard Escoria format: + +```gdscript +# `command_name param1 param2 [optional_param]` +# +# Description of what the command does. +# +# **Parameters** +# +# - *param1*: Description of parameter 1 +# - *param2*: Description of parameter 2 +# - *optional_param*: Description of optional parameter (default: default_value) +# +# Example: `command_name("value1", "value2")` +# +# @ESC +extends ESCBaseCommand +class_name CommandNameCommand + +func configure() -> ESCCommandArgumentDescriptor: + return ESCCommandArgumentDescriptor.new( + 2, # Number of required parameters + [TYPE_STRING, TYPE_STRING, TYPE_STRING], # Parameter types + [null, null, "default_value"], # Default values + [true, true, false] # Required flags + ) +``` + +## Fallback Behavior + +If the extension cannot find or parse the `project.godot` file, it falls back to the default command directories: +- `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` + +## Troubleshooting + +- **Commands not appearing**: Make sure your `project.godot` file has the correct `main/command_directories` setting +- **Outdated commands**: Use the "Refresh ASHES Commands" command to reload the cache +- **Missing descriptions**: Ensure your command files have proper comment documentation diff --git a/vscode-extension-ashes/HOWTO.md b/vscode-extension-ashes/HOWTO.md new file mode 100644 index 00000000..2b5509f1 --- /dev/null +++ b/vscode-extension-ashes/HOWTO.md @@ -0,0 +1,355 @@ +# ASHES Language Support - HowTo Guide + +This guide provides comprehensive instructions for configuring and using the ASHES Language Support extension for Visual Studio Code. + +## Table of Contents + +1. [Installation & Configuration](#installation--configuration) +2. [Available Commands & Functionality](#available-commands--functionality) +3. [Language Features](#language-features) +4. [Code Snippets](#code-snippets) +5. [Auto-completion & IntelliSense](#auto-completion--intellisense) +6. [Syntax Highlighting](#syntax-highlighting) +7. [Troubleshooting](#troubleshooting) + +## Installation & Configuration + +### Quick Installation + +1. **Copy the extension folder** to your VS Code extensions directory: + - **Linux**: `~/.vscode/extensions/` + - **macOS**: `~/.vscode/extensions/` + - **Windows**: `%USERPROFILE%\.vscode\extensions\` + +2. **Rename the folder** to `ashes-language-support-0.1.0` + +3. **Reload VS Code** or restart the application + +4. **Verify installation** by opening any `.esc` file - you should see "ASHES" in the language selector (bottom-right corner) + +### Development Mode Installation + +1. **Open VS Code** in the extension directory: + ```bash + cd vscode-extension-ashes + code . + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Compile TypeScript**: + ```bash + npm run compile + ``` + +4. **Press F5** to run the extension in a new Extension Development Host window + +### Configuration Options + +The extension automatically configures itself when you open `.esc` files. No additional configuration is required, but you can customize: + +- **File associations**: Ensure `.esc` files are associated with the ASHES language +- **Editor settings**: Configure indentation, word wrap, etc. in VS Code settings +- **Theme compatibility**: The extension works with all VS Code themes + +## Available Commands & Functionality + +### VSCode Commands + +#### `ASHES: Show Command Reference` +- **Access**: `Ctrl+Shift+P` → Type "ASHES: Show Command Reference" +- **Description**: Opens a webview panel with a complete reference of all ASHES commands +- **Features**: + - Searchable table of commands + - Parameter information + - Command descriptions + +### ASHES Language Commands + +The extension supports **60+ ASHES commands** organized by category: + +#### Animation Commands +- `anim(object_id, animation_name)` - Play animation on object +- `anim_block(object_id, animation_name)` - Play animation and wait for completion +- `set_animations(object_id, animations)` - Set object animations + +#### Camera Commands +- `camera_push(x, y)` - Push camera to new position +- `camera_set_pos(x, y)` - Set camera position +- `camera_set_target(object_id)` - Set camera target +- `camera_set_zoom(zoom_level)` - Set camera zoom level +- `camera_shift(x, y)` - Shift camera position + +#### Dialog & Text Commands +- `say(speaker, text, translation_key, type)` - Display dialog text +- `say_random(speaker, list_id, length)` - Say random text from list +- `say_sequence(speaker, list_id, length, loop)` - Say text sequence +- `block_say()` - Start a block of say commands +- `end_block_say()` - End a block of say commands + +#### Game State Commands +- `change_scene(scene_path, enable_transition, run_events)` - Change to a different scene +- `save_game(save_name)` - Save game state +- `set_global(variable_name, value, force)` - Set global variable +- `set_globals(variables_dict)` - Set multiple global variables +- `inc_global(variable_name)` - Increment global variable +- `dec_global(variable_name)` - Decrement global variable + +#### Inventory Commands +- `inventory_add(item_id)` - Add item to inventory +- `inventory_remove(item_id)` - Remove item from inventory +- `item_count_add(item_id, count)` - Add to item count +- `set_item_custom_data(item_id, key, value)` - Set item custom data + +#### Object Manipulation Commands +- `set_active(object_id, active)` - Set object active/inactive +- `set_interactive(object_id, interactive)` - Set object interactive state +- `set_state(object_id, state)` - Set object state +- `set_tooltip(object_id, action, text)` - Set object tooltip +- `teleport(object_id, target_id)` - Teleport object to target +- `teleport_pos(object_id, x, y)` - Teleport object to position +- `walk(object_id, target_id)` - Walk object to target +- `walk_to_pos(object_id, x, y)` - Walk object to position + +#### Audio/Video Commands +- `play_snd(sound_path, type)` - Play sound file +- `play_lib_snd(filename, namespace)` - Play library sound +- `play_video(video_path)` - Play video file +- `stop_snd(sound_type)` - Stop sound + +#### Utility Commands +- `print(message)` - Print debug message +- `wait(duration)` - Wait for specified time +- `transition(transition_type, duration)` - Play transition effect +- `custom(command_name, ...args)` - Execute custom command + +### Built-in Variables + +The extension provides auto-completion for these built-in variables: +- `CURRENT_PLAYER` - Current player object +- `ESC_LAST_SCENE` - Last scene identifier +- `ESC_CURRENT_SCENE` - Current scene identifier +- `FORCE_LAST_SCENE_NULL` - Force last scene to null +- `ANIMATION_RESOURCES` - Animation resources + +### Keywords + +Auto-completion for ASHES keywords: +- `var` - Local variable declaration +- `global` - Global variable declaration +- `if`, `elif`, `else` - Conditional statements +- `while` - Loop statement +- `break`, `done` - Loop control +- `stop`, `pass` - Flow control +- `true`, `false`, `nil` - Boolean values +- `and`, `or`, `not` - Logical operators +- `in`, `is`, `active` - Special operators + +## Language Features + +### Events +```ashes +:event_name + # Event code here + +:event_with_target "target_id" + # Event code here + +:event_with_flags | NO_UI | NO_TT + # Event code here +``` + +### Variables +```ashes +# Local variables +var my_variable = "value" + +# Global variables +global game_state = "playing" + +# Global IDs (with $ prefix) +set_active($player, true) +``` + +### Control Flow +```ashes +if condition: + # Code here +elif other_condition: + # Code here +else: + # Code here + +while condition: + # Code here + break # Exit loop + # or + done # Continue to next iteration +``` + +### Dialog System +```ashes +?! + - "Choice 1" + # Code for choice 1 + - "Choice 2" [condition] + # Code for choice 2 (only if condition is true) +``` + +### Comments +```ashes +# This is a line comment +``` + +## Code Snippets + +The extension provides **20+ code snippets** for common ASHES patterns: + +### Event Snippets +- `event` → Create new event +- `eventtarget` → Create event with target +- `eventflags` → Create event with flags +- `eventflagstarget` → Create event with flags and target + +### Command Snippets +- `say` → Say command +- `setglobal` → Set global variable +- `changescene` → Change scene +- `setactive` → Set object active +- `teleport` → Teleport object +- `walk` → Walk object +- `playsnd` → Play sound +- `playvideo` → Play video +- `inventoryadd` → Add to inventory +- `inventoryremove` → Remove from inventory + +### Control Flow Snippets +- `if` → If statement +- `ifelse` → If-else statement +- `while` → While loop + +### Dialog Snippets +- `dialog` → Dialog block with choice +- `dialogif` → Dialog choice with condition + +### Utility Snippets +- `var` → Local variable declaration +- `global` → Global variable declaration +- `comment` → Add comment +- `print` → Print debug message + +### Using Snippets +1. Type the snippet prefix (e.g., `event`) +2. Press `Tab` to expand +3. Use `Tab` to navigate between placeholders +4. Press `Enter` or `Escape` to finish + +## Auto-completion & IntelliSense + +### Trigger Characters +Auto-completion is triggered by: +- Space character (` `) +- Opening parenthesis (`(`) +- Dollar sign (`$`) for global IDs + +### Command Completion +- Type any ASHES command and press `Ctrl+Space` +- Commands show with descriptions and parameter hints +- Parameters are automatically inserted with placeholders + +### Variable Completion +- Built-in variables are automatically suggested +- Global IDs with `$` prefix are suggested +- Keywords are suggested in appropriate contexts + +### Hover Information +- Hover over any command to see detailed information +- Shows command description and parameter list +- Works for built-in variables and keywords + +## Syntax Highlighting + +The extension provides comprehensive syntax highlighting: + +### Color Scheme +- **Events** (`:event_name`) - Blue +- **Commands** (`say`, `set_global`, etc.) - Green +- **Variables** (`var`, `global`) - Orange +- **Strings** (`"text"`) - Yellow +- **Comments** (`# comment`) - Gray +- **Dialog blocks** (`?!`) - Special highlighting +- **Global IDs** (`$object_id`) - Distinct color + +### Smart Indentation +- Automatic indentation for events, control flow, and dialog blocks +- Proper outdenting for `break`, `done`, `else`, etc. +- Configurable through VS Code settings + +### Code Folding +- Events can be folded for better organization +- Dialog blocks can be folded +- Folding markers: `:event_name` (start), empty line (end) + +## Troubleshooting + +### Extension Not Loading +1. Check that the folder is in the correct extensions directory +2. Restart VS Code completely +3. Check the Developer Console for errors (`Help > Toggle Developer Tools`) + +### Syntax Highlighting Not Working +1. Ensure the file has a `.esc` extension +2. Check that the language is set to "ASHES" in the bottom-right corner +3. Reload the window (`Ctrl+Shift+P` → "Developer: Reload Window") + +### Auto-completion Not Working +1. Press `Ctrl+Space` to manually trigger completion +2. Check that the extension is activated (should show in the Extensions panel) +3. Ensure you're typing in a `.esc` file + +### Snippets Not Working +1. Make sure you're in a `.esc` file +2. Type the snippet prefix and press `Tab` +3. Check that the language is set to "ASHES" + +### Performance Issues +1. Large files may cause slower auto-completion +2. Consider splitting large `.esc` files into smaller ones +3. Disable other extensions if needed + +## Advanced Usage + +### Custom Commands +The extension supports custom commands through the `custom()` function: +```ashes +custom("my_custom_command", "param1", "param2") +``` + +### File Organization +- Use multiple `.esc` files for different scenes/characters +- Organize events logically within files +- Use comments to document complex logic + +### Integration with Escoria +- The extension is designed specifically for the Escoria framework +- Commands correspond to Escoria's ASHES implementation +- Global IDs should match your Escoria project structure + +## Contributing + +To contribute to the extension: +1. Fork the repository +2. Make your changes +3. Test thoroughly with `.esc` files +4. Submit a pull request + +## Support + +For issues or questions: +1. Check this HowTo guide first +2. Review the main README.md +3. Check the installation guide (INSTALL.md) +4. Report issues through the project's issue tracker diff --git a/vscode-extension-ashes/README.md b/vscode-extension-ashes/README.md index 33fe6a7a..80c205e9 100644 --- a/vscode-extension-ashes/README.md +++ b/vscode-extension-ashes/README.md @@ -10,6 +10,7 @@ A Visual Studio Code extension that provides syntax highlighting and IntelliSens - **Code Snippets**: Pre-built snippets for common ASHES patterns - **Command Reference**: Built-in command reference panel - **Smart Indentation**: Proper indentation rules for ASHES code structure +- **Dynamic Command Generation**: Automatically discovers and loads commands from your project's `project.godot` configuration - [Learn more about dynamic commands](DYNAMIC_COMMANDS.md) ## ASHES Language Features Supported diff --git a/vscode-extension-ashes/package.json b/vscode-extension-ashes/package.json index 0699ecd9..c83cd5a8 100644 --- a/vscode-extension-ashes/package.json +++ b/vscode-extension-ashes/package.json @@ -54,12 +54,20 @@ "command": "ashes.showCommandReference", "title": "Show ASHES Command Reference", "category": "ASHES" + }, + { + "command": "ashes.refreshCommands", + "title": "Refresh ASHES Commands", + "category": "ASHES" } ], "menus": { "commandPalette": [ { "command": "ashes.showCommandReference" + }, + { + "command": "ashes.refreshCommands" } ] } diff --git a/vscode-extension-ashes/src/commandParser.ts b/vscode-extension-ashes/src/commandParser.ts new file mode 100644 index 00000000..c55e2793 --- /dev/null +++ b/vscode-extension-ashes/src/commandParser.ts @@ -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 + })); + } +} diff --git a/vscode-extension-ashes/src/extension.ts b/vscode-extension-ashes/src/extension.ts index 3e15de7b..f576315d 100644 --- a/vscode-extension-ashes/src/extension.ts +++ b/vscode-extension-ashes/src/extension.ts @@ -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 => - `
No workspace found. Please open a workspace to view commands.
+ + + `; + 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 ? '' : ''; + const requiredEnd = param.required ? '' : ''; + 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('${command.name}(${exampleParams.join(', ')})`;
+ }
+
+ return `${command.name}${command.parameters.join(', ')}