diff --git a/.vscode/settings.json b/.vscode/settings.json index d057c896..ccdf8c37 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "editor.tabSize": 4, "editor.insertSpaces": false, - "editor.detectIndentation": false +"editor.detectIndentation": false, +"godotTools.editorPath.godot4": "/home/oier/Descargas/Godot_v4.4.1-stable_linux.x86_64" } diff --git a/install-extension.sh b/install-extension.sh new file mode 100755 index 00000000..53bb2105 --- /dev/null +++ b/install-extension.sh @@ -0,0 +1,216 @@ +#!/bin/bash + +# ASHES Language Support Extension Installer +# This script installs the ASHES language support extension for VSCode/VSCodium + +set -e # Exit on any error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to detect VSCode/VSCodium extensions directory +detect_extensions_dir() { + local extensions_dir="" + + # Check for VSCodium first (more common on Linux) + if command -v codium &> /dev/null; then + extensions_dir="$HOME/.vscode-oss/extensions" + elif command -v code &> /dev/null; then + extensions_dir="$HOME/.vscode/extensions" + else + print_warning "Neither VSCode nor VSCodium found in PATH" + extensions_dir="$HOME/.vscode/extensions" + fi + + echo "$extensions_dir" +} + +# Function to check if directory exists and is writable +check_directory() { + local dir="$1" + + if [ ! -d "$dir" ]; then + print_info "Creating directory: $dir" + mkdir -p "$dir" || { + print_error "Failed to create directory: $dir" + return 1 + } + fi + + if [ ! -w "$dir" ]; then + print_error "Directory is not writable: $dir" + return 1 + fi + + return 0 +} + +# Main installation function +main() { + print_info "ASHES Language Support Extension Installer" + print_info "==========================================" + echo + + # Get the script directory (where the vscode-extension folder is located) + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + EXTENSION_SOURCE="$SCRIPT_DIR/vscode-extension-ashes" + + # Check if source extension directory exists + if [ ! -d "$EXTENSION_SOURCE" ]; then + print_error "Extension source directory not found: $EXTENSION_SOURCE" + print_error "Please run this script from the project root directory." + exit 1 + fi + + # Detect default extensions directory + DEFAULT_EXTENSIONS_DIR=$(detect_extensions_dir) + + print_info "Default VSCode/VSCodium extensions directory:" + print_info " $DEFAULT_EXTENSIONS_DIR" + echo + + # Ask for confirmation or custom path + read -p "Use default path? (y/n) [y]: " use_default + use_default=${use_default:-y} + + if [[ $use_default =~ ^[Yy]$ ]]; then + EXTENSIONS_DIR="$DEFAULT_EXTENSIONS_DIR" + else + echo + read -p "Enter custom extensions directory path: " custom_path + EXTENSIONS_DIR="$custom_path" + fi + + # Validate the extensions directory + if ! check_directory "$EXTENSIONS_DIR"; then + exit 1 + fi + + # Set the destination path for the symlink + EXTENSION_DEST="$EXTENSIONS_DIR/vscode-extension-ashes" + + echo + print_info "Installation Summary:" + print_info "====================" + print_info "Source: $EXTENSION_SOURCE" + print_info "Destination: $EXTENSION_DEST" + print_info "Link name: vscode-extension-ashes" + echo + + # Final confirmation + read -p "Proceed with installation? (y/n) [y]: " confirm + confirm=${confirm:-y} + + if [[ ! $confirm =~ ^[Yy]$ ]]; then + print_info "Installation cancelled." + exit 0 + fi + + echo + print_info "Installing extension..." + + # Compile the extension before creating symlink + print_info "Compiling extension..." + cd "$EXTENSION_SOURCE" || { + print_error "Failed to change to extension directory: $EXTENSION_SOURCE" + exit 1 + } + + # Check if nvm is available and use it if .nvmrc exists + if [ -f ".nvmrc" ] && command -v nvm &> /dev/null; then + print_info "Using nvm to set Node.js version from .nvmrc" + source ~/.nvm/nvm.sh + nvm use || { + print_warning "Failed to use nvm, continuing with system Node.js" + } + elif [ -f ".nvmrc" ] && [ -s "$NVM_DIR/nvm.sh" ]; then + print_info "Loading nvm and using Node.js version from .nvmrc" + source "$NVM_DIR/nvm.sh" + nvm use || { + print_warning "Failed to use nvm, continuing with system Node.js" + } + fi + + # Install dependencies if node_modules doesn't exist + if [ ! -d "node_modules" ]; then + print_info "Installing npm dependencies..." + npm install || { + print_error "Failed to install npm dependencies" + exit 1 + } + else + print_info "Dependencies already installed, skipping npm install" + fi + + # Compile TypeScript + print_info "Compiling TypeScript..." + npm run compile || { + print_error "Failed to compile TypeScript" + exit 1 + } + + print_success "Extension compiled successfully!" + + # Return to original directory + cd - > /dev/null + + # Check if destination already exists + if [ -e "$EXTENSION_DEST" ]; then + print_warning "Destination already exists: $EXTENSION_DEST" + read -p "Remove existing installation? (y/n) [y]: " remove_existing + remove_existing=${remove_existing:-y} + + if [[ $remove_existing =~ ^[Yy]$ ]]; then + print_info "Removing existing installation..." + rm -rf "$EXTENSION_DEST" || { + print_error "Failed to remove existing installation" + exit 1 + } + else + print_info "Installation cancelled." + exit 0 + fi + fi + + # Create the symlink + print_info "Creating symlink..." + if ln -s "$EXTENSION_SOURCE" "$EXTENSION_DEST"; then + print_success "Extension installed successfully!" + echo + print_info "Next steps:" + print_info "1. Restart VSCode/VSCodium" + print_info "2. Open any .esc file to test the extension" + print_info "3. The language should be automatically detected as 'ASHES'" + echo + print_info "To uninstall, simply remove the symlink:" + print_info " rm '$EXTENSION_DEST'" + else + print_error "Failed to create symlink" + print_error "Make sure you have write permissions to: $EXTENSIONS_DIR" + exit 1 + fi +} + +# Run main function +main "$@" diff --git a/vscode-extension-ashes/.gitignore b/vscode-extension-ashes/.gitignore new file mode 100644 index 00000000..154499d3 --- /dev/null +++ b/vscode-extension-ashes/.gitignore @@ -0,0 +1,4 @@ +out +node_modules +*.vsix +.DS_Store diff --git a/vscode-extension-ashes/.nvmrc b/vscode-extension-ashes/.nvmrc new file mode 100644 index 00000000..92f279e3 --- /dev/null +++ b/vscode-extension-ashes/.nvmrc @@ -0,0 +1 @@ +v22 \ No newline at end of file diff --git a/vscode-extension-ashes/.vscodeignore b/vscode-extension-ashes/.vscodeignore new file mode 100644 index 00000000..38999676 --- /dev/null +++ b/vscode-extension-ashes/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts 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/INSTALL.md b/vscode-extension-ashes/INSTALL.md new file mode 100644 index 00000000..38952889 --- /dev/null +++ b/vscode-extension-ashes/INSTALL.md @@ -0,0 +1,125 @@ +# Installation Guide for ASHES Language Support Extension + +## Quick Installation + +### Automated Installation (Recommended) + +Use the provided installation script for the easiest setup: + +```bash +./install-extension.sh +``` + +This script will: +1. **Compile the extension** using npm and the correct Node.js version (via nvm if available) +2. **Create a symlink** to your VS Code extensions directory +3. **Handle dependencies** automatically + +### Manual Installation + +1. **Compile the extension**: + ```bash + cd vscode-extension-ashes + + # Use nvm if available (recommended) + nvm use # Uses version from .nvmrc + + # Install dependencies + npm install + + # Compile TypeScript + npm run compile + ``` + +2. **Copy the extension folder** to your VS Code extensions directory: + - **Linux**: `~/.vscode/extensions/` + - **macOS**: `~/.vscode/extensions/` + - **Windows**: `%USERPROFILE%\.vscode\extensions\` + +3. **Rename the folder** to `ashes-language-support-0.1.0` (or similar) + +4. **Reload VS Code** or restart the application + +5. **Test the extension** by opening any `.esc` file + +## Alternative Installation (Development Mode) + +1. **Open VS Code** in the extension directory: + ```bash + cd vscode-extension + code . + ``` + +2. **Press F5** to run the extension in a new Extension Development Host window + +3. **Open a .esc file** in the new window to test the extension + +## Building from Source + +1. **Set up Node.js version** (recommended): + ```bash + # Use nvm if available to get the correct Node.js version + nvm use # Uses version from .nvmrc (v22) + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Compile TypeScript**: + ```bash + npm run compile + ``` + +4. **Package the extension** (optional): + ```bash + npx vsce package + ``` + +## Testing the Extension + +1. **Open the sample file**: `sample.esc` in the extension directory +2. **Check syntax highlighting**: Events, commands, and variables should be colorized +3. **Test auto-completion**: Type `say(` and press `Ctrl+Space` +4. **Test snippets**: Type `event` and press `Tab` +5. **Test hover**: Hover over commands like `say` or `set_global` +6. **Test command reference**: Press `Ctrl+Shift+P` and type "ASHES: Show Command Reference" + +## Features to Test + +- ✅ Syntax highlighting for events (`:event_name`) +- ✅ Syntax highlighting for commands (`say`, `set_global`, etc.) +- ✅ Syntax highlighting for variables (`var`, `global`) +- ✅ Syntax highlighting for dialog blocks (`?!`) +- ✅ Auto-completion for commands +- ✅ Auto-completion for built-in variables +- ✅ Code snippets for common patterns +- ✅ Hover information for commands +- ✅ Smart indentation +- ✅ Code folding for events + +## Troubleshooting + +### Extension not loading +- Check that the folder is in the correct extensions directory +- Restart VS Code completely +- Check the Developer Console for errors (`Help > Toggle Developer Tools`) + +### Syntax highlighting not working +- Make sure the file has a `.esc` extension +- Check that the language is set to "ASHES" in the bottom-right corner of VS Code + +### Auto-completion not working +- Press `Ctrl+Space` to manually trigger completion +- Check that the extension is activated (should show in the Extensions panel) + +### Compilation errors +- Make sure you're using the correct Node.js version: `nvm use` (uses v22 from .nvmrc) +- Clear node_modules and reinstall: `rm -rf node_modules && npm install` +- Check that TypeScript is installed: `npm list typescript` +- Ensure all dependencies are installed: `npm install` + +## Uninstalling + +Simply delete the extension folder from your VS Code extensions directory and restart VS Code. diff --git a/vscode-extension-ashes/README.md b/vscode-extension-ashes/README.md new file mode 100644 index 00000000..e91e5b7f --- /dev/null +++ b/vscode-extension-ashes/README.md @@ -0,0 +1,105 @@ +# ASHES Language Support + +A Visual Studio Code extension that provides syntax highlighting and IntelliSense for the ASHES (Adventure Scripting Helping Escoria) language used in Escoria adventure game framework. + +## Features + +- **Syntax Highlighting**: Full syntax highlighting for ASHES language files (.esc) +- **Auto-completion**: IntelliSense for ASHES commands, built-in variables, and keywords with enhanced parameter information +- **Hover Information**: Detailed information about commands and variables on hover with clear parameter indicators (required/optional, type, variable name) +- **Go to Definition**: Ctrl+click on any command to navigate to its source file +- **Code Snippets**: Pre-built snippets for common ASHES patterns +- **Command Reference**: Built-in command reference panel with clickable command names +- **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 + +### Events +- Event definitions with `:event_name` +- Event flags with `| FLAG_NAME` + +### Commands +- All standard Escoria commands (say, set_global, change_scene, etc.) +- Custom commands +- Enhanced command parameter hints with: + - **Required** / **Optional** indicators + - Parameter types (string, boolean, number, object, scene, animation, etc.) + - Variable names in bold + - Default values when available + +### Variables +- Local variables with `var` +- Global variables with `global` +- Built-in variables (CURRENT_PLAYER, ESC_LAST_SCENE, etc.) +- Global IDs with `$` prefix + +### Control Flow +- If/elif/else statements +- While loops +- Break and done keywords + +### Dialog System +- Dialog blocks with `?!` +- Dialog choices with `-` +- Conditional dialog choices with `[condition]` + +### Comments +- Line comments with `#` + +## Installation + +1. Copy this extension folder to your VS Code extensions directory +2. Reload VS Code +3. Open any `.esc` file to see syntax highlighting + +## Usage + +### Auto-completion +- Type any ASHES command and press `Ctrl+Space` for suggestions +- Use `$` prefix for global ID suggestions +- Built-in variables are automatically suggested + +### Go to Definition +- Ctrl+click on any command name in `.esc` files to navigate to its source file +- Works in both the editor and the command reference panel + +### Snippets +- Type snippet prefixes and press `Tab` to expand: + - `event` - Create new event + - `say` - Say command + - `dialog` - Dialog block + - `if` - If statement + - And many more... + +### Command Reference +- Press `Ctrl+Shift+P` and type "ASHES: Show Command Reference" +- View all available commands with descriptions and enhanced parameter information +- Parameters show clear indicators: **Required** / **Optional**, type, variable name, and default values +- Click on any command name to navigate to its source file + +## Language Features + +### Syntax Highlighting +- Events are highlighted in blue +- Commands are highlighted in green +- Variables are highlighted in orange +- Strings are highlighted in yellow +- Comments are highlighted in gray +- Dialog blocks have special highlighting + +### Smart Indentation +- Automatic indentation for events, control flow, and dialog blocks +- Proper outdenting for `break`, `done`, `else`, etc. + +### Folding +- Events can be folded for better code organization +- Dialog blocks can be folded + +## Contributing + +This extension is designed specifically for the Escoria framework and ASHES language. If you find issues or want to add features, please contribute to the project. + +## License + +This extension is part of the Gymkhana project and follows the same license terms. diff --git a/vscode-extension-ashes/Requirements.md b/vscode-extension-ashes/Requirements.md new file mode 100644 index 00000000..b990ed01 --- /dev/null +++ b/vscode-extension-ashes/Requirements.md @@ -0,0 +1,594 @@ +# ASHES Language Support Extension - Requirements + +## Project Overview + +The ASHES Language Support extension is a Visual Studio Code extension that provides comprehensive language support for the ASHES (Adventure Scripting Helping Escoria) language used in the Escoria adventure game framework. ASHES is a Python-like scripting language specifically designed for creating adventure games, borrowing heavily from Pythonic languages while being optimized for the Escoria framework. + +This extension enhances the development experience for game developers working with ASHES scripts by providing syntax highlighting, IntelliSense, error detection, and other modern IDE features. The extension dynamically discovers commands from the project's configuration and provides real-time validation based on the actual Escoria command implementations. + +## ASHES Language Specifications + +Based on the Escoria core documentation, ASHES supports the following language features: + +### Data Types +- **Integers**: Whole numbers +- **Floats/Reals**: Decimal numbers +- **Booleans**: `true` and `false` values +- **Strings**: Text enclosed in quotes, can be concatenated with `+` operator +- **Nil**: The keyword `nil` is used in place of "null" and/or where empty values are desired + +### Language Features +- **Events**: Defined with `:event_name` at the highest level of scope (no indentation) +- **Variables**: Local variables with `var`, global variables with `global` +- **Comments**: Line comments using `#` character (block comments not supported) +- **Branching**: `if`, `elif`, and `else` statements +- **Looping**: `while` loops with `break` keyword to exit +- **Dialog System**: Dialog blocks with `?!` and choices with `-` +- **Global IDs**: Can be prefixed with `$` to avoid quotation marks +- **Inventory Checking**: `if $item in inventory:` syntax +- **State Checking**: `if $object is active:` and `if $object is "state":` syntax + +### Operators +- **Boolean**: `not`, `and`, `or`, `!` (exclamation mark for not) +- **Arithmetic**: `+`, `-`, `*`, `/` +- **Comparison**: `>`, `>=`, `<`, `<=`, `==` + +## Functional Requirements + +### 1. Language Recognition and File Association + +**FR-1.1: File Extension Association** +- The extension MUST recognize `.esc` files as ASHES language files +- The extension MUST set the language ID to `ashes` for `.esc` files +- The extension MUST activate automatically when `.esc` files are opened + +**FR-1.2: Language Configuration** +- The extension MUST provide proper language configuration including: + - Line comments using `#` character + - Bracket matching for `{}`, `[]`, `()` + - Auto-closing pairs for brackets and quotes + - Word pattern recognition for identifiers + - Smart indentation rules for ASHES syntax + +### 2. Syntax Highlighting + +**FR-2.1: Comprehensive Syntax Highlighting** +- The extension MUST provide syntax highlighting for ASHES language features as specified in the Escoria core documentation: + - **Events**: `:event_name` (blue highlighting) - Events are denoted using `:event` where "event" is the name of the event + - **Commands**: Function calls like `say()`, `set_global()` (green highlighting) - Commands are called just as functions normally are + - **Variables**: `var`, `global` declarations (orange highlighting) - Local variables with `var`, global variables with `global` + - **Strings**: Quoted text `"text"` (yellow highlighting) - Strings can be concatenated using the `+` operator + - **Comments**: `# comment` (gray highlighting) - Line comments using hashtag/pound sign + - **Dialog blocks**: `?!` syntax (special highlighting) - Dialog blocks begin with `?!` + - **Dialog choices**: `- "choice text" [condition]` (special highlighting) - Each dialog choice is prefixed with `-` + - **Global IDs**: `$object_id` (distinct color) - Global IDs can be prefixed with `$` to avoid quotation marks + - **Keywords**: `if`, `elif`, `else`, `while`, `break`, `done`, `stop`, `pass`, `true`, `false`, `nil`, `and`, `or`, `not`, `in`, `is`, `active` + +**FR-2.2: Advanced Syntax Patterns** +- The extension MUST highlight event definitions with flags: `:event_name | FLAG_NAME` (e.g., `:look | NO_UI | NO_TT`) +- The extension MUST highlight event definitions with targets: `:event_name "target"` +- The extension MUST highlight dialog choices with conditions: `- "choice text" [condition]` +- The extension MUST highlight control flow statements with proper indentation +- The extension MUST highlight boolean operators: `not`, `and`, `or`, `!` (exclamation mark for not) +- The extension MUST highlight arithmetic operators: `+`, `-`, `*`, `/` +- The extension MUST highlight comparison operators: `>`, `>=`, `<`, `<=`, `==` +- The extension MUST highlight the `in inventory` feature: `if $gold_brick in inventory:` +- The extension MUST highlight the `is active` feature: `if $room_monster is active:` +- The extension MUST highlight the `is` keyword for state checking: `if $room_monster is "running_away":` + +### 3. IntelliSense and Auto-completion + +**FR-3.1: Command Auto-completion** +- The extension MUST provide auto-completion for all ASHES commands from the configured command directories: + - **Core Commands** (from `escoria-core`): `accept_input`, `anim`, `anim_block`, `block_say`, `camera_push`, `camera_set_pos`, `camera_set_target`, `camera_set_zoom`, `change_scene`, `custom`, `dec_global`, `enable_terrain`, `end_block_say`, `hide_menu`, `inc_global`, `inventory_add`, `inventory_remove`, `play_snd`, `print_internal`, `queue_event`, `queue_resource`, `rand_global`, `repeat`, `save_game`, `say`, `sched_event`, `set_active`, `set_angle`, `set_animations`, `set_direction`, `set_global`, `set_globals`, `set_gui_visible`, `set_interactive`, `set_item_custom_data`, `set_speed`, `set_state`, `show_menu`, `slide`, `spawn`, `stop`, `stop_snd`, `teleport`, `teleport_pos`, `transition`, `turn_to`, `wait`, `walk`, `walk_to_pos` + - **UI Commands** (from `escoria-ui-return-monkey-island`): `item_count_add`, `music_enable`, `play_lib_snd`, `play_video`, `set_tooltip` + - **Dialog Commands** (from `escoria-ui-return-monkey-island-dialog-simple`): `say_last_dialog_option`, `say_random`, `say_sequence` +- The extension MUST dynamically load commands from project configuration +- The extension MUST show command descriptions and parameter information +- The extension MUST support trigger characters: space, `(`, `$` + +**FR-3.2: Enhanced Parameter Information** +- The extension MUST display parameter information with: + - **Required** / **Optional** indicators + - Parameter types (string, boolean, number, object, scene, animation, etc.) + - Variable names in bold formatting + - Default values when available + - Example usage patterns + +**FR-3.3: Variable and Keyword Completion** +- The extension MUST provide auto-completion for built-in variables as specified in the Escoria core documentation: + - `CURRENT_PLAYER` - References the current player scene being used, irrespective of its global ID + - `ESC_LAST_SCENE` - Holds the global ID of the last visited room + - `ESC_CURRENT_SCENE` - Holds the global ID of the current room + - `FORCE_LAST_SCENE_NULL` - Internal use only + - `ANIMATION_RESOURCES` - Internal use only +- The extension MUST provide auto-completion for ASHES keywords: `var`, `global`, `if`, `elif`, `else`, `while`, `break`, `done`, `stop`, `pass`, `true`, `false`, `nil`, `and`, `or`, `not`, `in`, `is`, `active` +- The extension MUST provide auto-completion for global IDs with `$` prefix (e.g., `$object_1_id`) + +### 4. Dynamic Command Discovery + +**FR-4.1: Project Configuration Parsing** +- The extension MUST parse `project.godot` files to extract command directories from the `[escoria]` section +- The extension MUST support the `main/command_directories` setting with array format: `["path1", "path2", "path3"]` +- The extension MUST handle the actual project configuration format as seen in real projects: + ```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"] + ``` +- The extension MUST provide fallback to default command directories if parsing fails: + - `res://addons/escoria-core/game/core-scripts/esc/commands` (core commands) + - `res://addons/escoria-ui-return-monkey-island/esc/commands` (UI commands) + - `res://addons/escoria-ui-return-monkey-island-dialog-simple/commands` (dialog commands) + +**FR-4.2: Command File Parsing** +- The extension MUST scan specified directories for `.gd` files (excluding `.gd.uid`) +- The extension MUST extract command information from GDScript files following the Escoria command format: + - Command name from filename (e.g., `say.gd` → `say` command) + - Description from comment blocks starting with `# \`command_name param1 param2 [optional_param]\`` + - Parameters from comment documentation in `**Parameters**` section with `- *param*: description` format + - Parameter types and requirements from `configure()` method using `ESCCommandArgumentDescriptor` + - Examples from comment blocks with `Example: \`command_name("value1", "value2")\`` format +- The extension MUST handle the standard Escoria command documentation 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 + ``` + +**FR-4.3: Command Caching** +- The extension MUST cache discovered commands for 5 minutes to improve performance +- The extension MUST provide manual cache refresh functionality +- The extension MUST update cache when workspace changes + +### 5. Hover Information + +**FR-5.1: Command Documentation** +- The extension MUST provide detailed hover information for commands +- The extension MUST show command descriptions and parameter details +- The extension MUST format parameter information with clear indicators +- The extension MUST provide example usage in hover tooltips + +**FR-5.2: Variable Documentation** +- The extension MUST provide hover information for built-in variables +- The extension MUST explain the purpose of each built-in variable + +### 6. Go to Definition + +**FR-6.1: Command Navigation** +- The extension MUST support Ctrl+click navigation to command source files +- The extension MUST find the class definition line in command files +- The extension MUST open the correct file and position the cursor appropriately +- The extension MUST work in both the editor and command reference panel + +### 7. Code Snippets + +**FR-7.1: Event Snippets** +- The extension MUST provide snippets for: + - `event` → Create new event + - `eventtarget` → Create event with target + - `eventflags` → Create event with flags + - `eventflagstarget` → Create event with flags and target + +**FR-7.2: Command Snippets** +- The extension MUST provide snippets for common commands: + - `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 + +**FR-7.3: Control Flow Snippets** +- The extension MUST provide snippets for: + - `if` → If statement + - `ifelse` → If-else statement + - `while` → While loop + +**FR-7.4: Dialog Snippets** +- The extension MUST provide snippets for: + - `dialog` → Dialog block with choice + - `dialogif` → Dialog choice with condition + +**FR-7.5: Utility Snippets** +- The extension MUST provide snippets for: + - `var` → Local variable declaration + - `global` → Global variable declaration + - `comment` → Add comment + - `print` → Print debug message + +### 8. Command Reference Panel + +**FR-8.1: Interactive Command Reference** +- The extension MUST provide a webview panel showing all available commands +- The extension MUST display commands in a searchable table format +- The extension MUST show command descriptions and parameter information +- The extension MUST support clickable command names for navigation + +**FR-8.2: Command Reference Features** +- The extension MUST format parameter information with clear indicators +- The extension MUST provide example usage for each command +- The extension MUST support navigation to source files from the reference panel + +### 9. Syntax Validation and Error Detection + +**FR-9.1: Real-time Syntax Validation** +- The extension MUST validate ASHES syntax in real-time +- The extension MUST provide error diagnostics with appropriate severity levels +- The extension MUST debounce validation to avoid excessive processing (500ms) +- The extension MUST validate on document changes, open, and save + +**FR-9.2: Event Definition Validation** +- The extension MUST validate event definitions: `:event_name [| FLAGS] ["target"]` +- The extension MUST check event name format (letters, numbers, underscores) +- The extension MUST validate event flags against known valid flags (e.g., `TK`, `TR`, `TG`, `SKIP`, `GLOBAL`) +- The extension MUST validate event targets as quoted strings +- The extension MUST suggest common event names for better practices (e.g., `init`, `ready`, `setup`, `action1`, `action2`, `action3`, `use`, `look`, `talk`, `walk`, `interact`) +- The extension MUST validate that events are at the highest level of scope (no indentation) + +**FR-9.3: Dialog System Validation** +- The extension MUST validate dialog blocks (`?!`) syntax +- The extension MUST validate dialog choices (`- "text" [condition]`) +- The extension MUST check for proper dialog block closure +- The extension MUST validate choice text is not empty +- The extension MUST validate condition syntax in dialog choices +- The extension MUST support nested dialog blocks with proper indentation +- The extension MUST validate `break` and `done` keywords in dialog context +- The extension MUST validate `break` with level specification (e.g., `break 2`) + +**FR-9.4: Command and Control Flow Validation** +- The extension MUST validate function calls against known commands from all configured directories +- The extension MUST validate control flow statements (`if`, `elif`, `else`, `while`) +- The extension MUST check for missing colons in control flow statements +- The extension MUST validate condition expressions including: + - Boolean operators (`and`, `or`, `not`, `!`) + - Comparison operators (`>`, `>=`, `<`, `<=`, `==`) + - Inventory checks (`if $item in inventory:`) + - State checks (`if $object is active:`, `if $object is "state":`) + - Global ID comparisons (`if $object1 == $object2:`) +- The extension MUST check for proper indentation after control flow statements +- The extension MUST validate `while` loops with `break` keyword support + +**FR-9.5: String and Bracket Validation** +- The extension MUST validate string literals for proper quoting +- The extension MUST check for unclosed strings and brackets +- The extension MUST validate parentheses, brackets, and braces matching +- The extension MUST handle escaped characters in strings +- The extension MUST validate string concatenation with `+` operator +- The extension MUST validate global variable substitution in strings using `{variable_name}` syntax + +**FR-9.6: Indentation Validation** +- The extension MUST enforce tab-based indentation (not spaces) as required by ASHES +- The extension MUST detect mixed tabs and spaces +- The extension MUST allow deep indentation for complex nested structures +- The extension MUST validate proper indentation for control flow blocks +- The extension MUST validate that events are at the top level (no indentation) +- The extension MUST validate proper indentation for dialog blocks and choices + +**FR-9.7: Variable and Assignment Validation** +- The extension MUST validate variable declarations (`var`, `global`) +- The extension MUST check for reserved keyword usage in variable names +- The extension MUST validate assignment syntax +- The extension MUST validate global variable scoping rules +- The extension MUST warn about potential issues with global variable initialization order + +**FR-9.8: Specific Command Pattern Validation** +- The extension MUST validate `say()` command patterns with proper speaker, text, and optional parameters +- The extension MUST validate inventory command patterns (`inventory_add`, `inventory_remove`) +- The extension MUST validate global variable commands (`set_global`, `inc_global`, `dec_global`) +- The extension MUST validate camera commands (`camera_set_pos`, `camera_set_target`, etc.) +- The extension MUST validate animation commands (`anim`, `anim_block`, `set_animations`) +- The extension MUST validate movement commands (`walk`, `teleport`, `slide`) +- The extension MUST check for proper parameter formatting in common commands +- The extension MUST validate command-specific parameter requirements based on actual command implementations + +### 10. Smart Indentation + +**FR-10.1: Automatic Indentation** +- The extension MUST provide automatic indentation for: + - Events and their content + - Control flow statements (`if`, `elif`, `else`, `while`) + - Dialog blocks and choices +- The extension MUST properly outdent for `break`, `done`, `else`, etc. + +**FR-10.2: Indentation Rules** +- The extension MUST use tab-based indentation exclusively +- The extension MUST increase indentation after control flow statements ending with `:` +- The extension MUST decrease indentation for control flow keywords + +### 11. Code Folding + +**FR-11.1: Event Folding** +- The extension MUST support folding for events +- The extension MUST use `:event_name` as start markers +- The extension MUST use empty lines as end markers + +**FR-11.2: Dialog Block Folding** +- The extension MUST support folding for dialog blocks +- The extension MUST properly handle nested dialog structures + +### 12. VSCode Commands + +**FR-12.1: Command Palette Integration** +- The extension MUST provide the following commands: + - `ASHES: Show Command Reference` - Opens the command reference panel + - `ASHES: Refresh ASHES Commands` - Refreshes the command cache + - `ASHES: Refresh ASHES Diagnostics` - Refreshes syntax validation + +**FR-12.2: Command Functionality** +- The extension MUST make all commands available in the command palette +- The extension MUST provide appropriate command categories +- The extension MUST show success/error messages for command execution + +## Non-Functional Requirements + +### 1. Performance + +**NFR-1.1: Response Time** +- The extension MUST provide auto-completion suggestions within 200ms +- The extension MUST validate syntax with minimal impact on editor performance +- The extension MUST use debouncing for real-time validation (500ms) + +**NFR-1.2: Memory Usage** +- The extension MUST cache commands efficiently with 5-minute expiration +- The extension MUST not retain unnecessary data in memory +- The extension MUST handle large projects without excessive memory usage + +**NFR-1.3: Startup Time** +- The extension MUST activate quickly when `.esc` files are opened +- The extension MUST not significantly impact VSCode startup time + +### 2. Compatibility + +**NFR-2.1: VSCode Version** +- The extension MUST be compatible with VSCode version 1.74.0 and later +- The extension MUST use the VSCode API appropriately for the target version + +**NFR-2.2: Platform Support** +- The extension MUST work on Windows, macOS, and Linux +- The extension MUST handle different file system path formats correctly + +**NFR-2.3: Theme Compatibility** +- The extension MUST work with all VSCode themes +- The extension MUST use VSCode theme variables for consistent appearance + +### 3. Reliability + +**NFR-3.1: Error Handling** +- The extension MUST handle file system errors gracefully +- The extension MUST provide fallback behavior when command parsing fails +- The extension MUST not crash VSCode under any circumstances + +**NFR-3.2: Robustness** +- The extension MUST handle malformed `project.godot` files +- The extension MUST handle missing command directories +- The extension MUST handle corrupted or invalid command files + +### 4. Usability + +**NFR-4.1: User Experience** +- The extension MUST provide clear and helpful error messages +- The extension MUST offer suggestions for common mistakes +- The extension MUST be intuitive for users familiar with VSCode + +**NFR-4.2: Documentation** +- The extension MUST provide comprehensive hover documentation +- The extension MUST include helpful examples in tooltips +- The extension MUST offer clear parameter information + +### 5. Maintainability + +**NFR-5.1: Code Quality** +- The extension MUST be written in TypeScript for type safety +- The extension MUST follow VSCode extension best practices +- The extension MUST have clear separation of concerns + +**NFR-5.2: Extensibility** +- The extension MUST be designed to easily add new command types +- The extension MUST support custom command directories +- The extension MUST allow for future language feature additions + +## Technical Requirements + +### 1. Architecture + +**TR-1.1: Extension Structure** +- The extension MUST use the standard VSCode extension structure +- The extension MUST have a clear separation between language features and UI components +- The extension MUST use the VSCode Language Server Protocol where appropriate + +**TR-1.2: Module Organization** +- The extension MUST organize code into logical modules: + - `extension.ts` - Main extension entry point + - `commandParser.ts` - Command discovery and parsing + - `diagnosticProvider.ts` - Syntax validation and error reporting + - `syntaxValidator.ts` - ASHES syntax validation logic + +### 2. Dependencies + +**TR-2.1: External Dependencies** +- The extension MUST use minimal external dependencies +- The extension MUST use only VSCode-approved packages +- The extension MUST specify exact version requirements + +**TR-2.2: Built-in Dependencies** +- The extension MUST use Node.js built-in modules (`fs`, `path`) +- The extension MUST use VSCode API modules appropriately + +### 3. Configuration + +**TR-3.1: Project Configuration** +- The extension MUST read configuration from `project.godot` files +- The extension MUST support the Escoria framework configuration format +- The extension MUST provide sensible defaults when configuration is missing + +**TR-3.2: Extension Configuration** +- The extension MUST use VSCode's configuration system appropriately +- The extension MUST not require user configuration for basic functionality + +### 4. File Handling + +**TR-4.1: File System Operations** +- The extension MUST handle file system operations asynchronously where possible +- The extension MUST handle file permission errors gracefully +- The extension MUST support different file system encodings + +**TR-4.2: Path Handling** +- The extension MUST handle both relative and absolute paths correctly +- The extension MUST convert `res://` paths to filesystem paths appropriately +- The extension MUST handle different operating system path separators + +## Integration Requirements + +### 1. Escoria Framework Integration + +**IR-1.1: Command Compatibility** +- The extension MUST support all standard Escoria commands +- The extension MUST work with custom command implementations +- The extension MUST handle command parameter variations correctly + +**IR-1.2: Project Structure Support** +- The extension MUST work with standard Escoria project structures +- The extension MUST support multiple command directories +- The extension MUST handle addon-based command extensions + +### 2. VSCode Integration + +**IR-2.1: Language Features** +- The extension MUST integrate with VSCode's language features system +- The extension MUST provide proper language configuration +- The extension MUST support VSCode's IntelliSense system + +**IR-2.2: UI Integration** +- The extension MUST integrate with VSCode's command palette +- The extension MUST provide webview panels for command reference +- The extension MUST use VSCode's diagnostic system for error reporting + +## Quality Assurance Requirements + +### 1. Testing + +**QA-1.1: Unit Testing** +- The extension MUST have unit tests for core functionality +- The extension MUST test command parsing logic +- The extension MUST test syntax validation rules + +**QA-1.2: Integration Testing** +- The extension MUST be tested with real ASHES files +- The extension MUST be tested with various project configurations +- The extension MUST be tested with different VSCode versions + +### 2. Documentation + +**QA-2.1: User Documentation** +- The extension MUST provide comprehensive user documentation +- The extension MUST include installation and configuration instructions +- The extension MUST provide examples and tutorials + +**QA-2.2: Developer Documentation** +- The extension MUST have clear code documentation +- The extension MUST include API documentation +- The extension MUST provide contribution guidelines + +### 3. Error Reporting + +**QA-3.1: Error Messages** +- The extension MUST provide clear and actionable error messages +- The extension MUST include error codes for diagnostic purposes +- The extension MUST suggest solutions for common problems + +**QA-3.2: Logging** +- The extension MUST provide appropriate logging for debugging +- The extension MUST not log sensitive information +- The extension MUST use VSCode's logging system appropriately + +## Security Requirements + +### 1. File System Security + +**SR-1.1: Path Validation** +- The extension MUST validate file paths to prevent directory traversal +- The extension MUST handle malicious file names appropriately +- The extension MUST not execute arbitrary code from files + +**SR-1.2: Content Validation** +- The extension MUST validate file content before processing +- The extension MUST handle malformed files gracefully +- The extension MUST not expose sensitive file content + +### 2. User Data Protection + +**SR-2.1: Privacy** +- The extension MUST not collect or transmit user data +- The extension MUST not access files outside the workspace +- The extension MUST respect user privacy settings + +## Deployment Requirements + +### 1. Packaging + +**DR-1.1: Extension Package** +- The extension MUST be packaged as a standard VSCode extension +- The extension MUST include all necessary files and dependencies +- The extension MUST have proper version information + +**DR-1.2: Installation** +- The extension MUST be installable via VSCode's extension system +- The extension MUST not require additional system dependencies +- The extension MUST provide clear installation instructions + +### 2. Distribution + +**DR-2.1: Version Management** +- The extension MUST follow semantic versioning +- The extension MUST provide clear release notes +- The extension MUST support automatic updates + +**DR-2.2: Compatibility** +- The extension MUST maintain backward compatibility where possible +- The extension MUST provide migration guides for breaking changes +- The extension MUST support multiple VSCode versions + +## Success Criteria + +The ASHES Language Support extension will be considered successful when: + +1. **Functionality**: All functional requirements are implemented and working correctly +2. **Command Coverage**: The extension supports all commands from the three main directories: + - 60+ core commands from `escoria-core` + - 5 UI commands from `escoria-ui-return-monkey-island` + - 3 dialog commands from `escoria-ui-return-monkey-island-dialog-simple` +3. **Performance**: The extension provides responsive IntelliSense and validation without impacting VSCode performance +4. **Usability**: Users can effectively develop ASHES scripts with improved productivity +5. **Reliability**: The extension works consistently across different projects and configurations +6. **Integration**: The extension integrates seamlessly with the Escoria framework and VSCode +7. **Quality**: The extension provides accurate syntax validation and helpful error messages based on actual ASHES language specifications +8. **Documentation**: Users can easily understand and use all extension features +9. **Language Compliance**: The extension correctly implements all ASHES language features as specified in the Escoria core documentation + +## Future Enhancements + +While not part of the current requirements, potential future enhancements include: + +1. **Language Server Protocol**: Full LSP implementation for advanced language features +2. **Debugging Support**: Integration with ASHES debugging capabilities +3. **Refactoring Tools**: Code refactoring and restructuring features +4. **Project Templates**: ASHES project templates and scaffolding +5. **Advanced Validation**: More sophisticated semantic analysis and validation +6. **Performance Profiling**: Tools for analyzing ASHES script performance +7. **Collaboration Features**: Real-time collaboration support for ASHES development diff --git a/vscode-extension-ashes/language-configuration.json b/vscode-extension-ashes/language-configuration.json new file mode 100644 index 00000000..09cf6aca --- /dev/null +++ b/vscode-extension-ashes/language-configuration.json @@ -0,0 +1,35 @@ +{ + "comments": { + "lineComment": "#" + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "folding": { + "markers": { + "start": "^\\s*:", + "end": "^\\s*$" + } + }, + "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)", + "indentationRules": { + "increaseIndentPattern": "^\\s*(?!\\s*#).*:\\s*$|^\\s*if\\s+.*:|^\\s*elif\\s+.*:|^\\s*else\\s*:|^\\s*while\\s+.*:|^\\s*-\\s+.*:|^\\s*\\?!\\s*$", + "decreaseIndentPattern": "^\\s*(elif|else|done|break|stop)\\b" + } +} diff --git a/vscode-extension-ashes/package-lock.json b/vscode-extension-ashes/package-lock.json new file mode 100644 index 00000000..c19417d1 --- /dev/null +++ b/vscode-extension-ashes/package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "ashes-language-support", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ashes-language-support", + "version": "0.1.0", + "license": "ISC", + "devDependencies": { + "@types/node": "16.x", + "@types/vscode": "^1.74.0", + "typescript": "^4.9.4" + }, + "engines": { + "vscode": "^1.74.0" + } + }, + "node_modules/@types/node": { + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.103.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.103.0.tgz", + "integrity": "sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + } +} diff --git a/vscode-extension-ashes/package.json b/vscode-extension-ashes/package.json new file mode 100644 index 00000000..95e47d66 --- /dev/null +++ b/vscode-extension-ashes/package.json @@ -0,0 +1,95 @@ +{ + "name": "ashes-language-support", + "displayName": "ASHES Language Support", + "description": "Syntax highlighting and IntelliSense for ASHES (Adventure Scripting Helping Escoria) language", + "version": "0.1.0", + "publisher": "gymkhana-dev", + "engines": { + "vscode": "^1.74.0" + }, + "categories": [ + "Programming Languages", + "Snippets" + ], + "keywords": [ + "ashes", + "escoria", + "adventure", + "game", + "scripting" + ], + "activationEvents": [ + "onLanguage:ashes" + ], + "main": "./out/extension.js", + "contributes": { + "languages": [ + { + "id": "ashes", + "aliases": [ + "ASHES", + "ashes" + ], + "extensions": [ + ".esc" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "ashes", + "scopeName": "source.ashes", + "path": "./syntaxes/ashes.tmLanguage.json" + } + ], + "snippets": [ + { + "language": "ashes", + "path": "./snippets/ashes.json" + } + ], + "commands": [ + { + "command": "ashes.showCommandReference", + "title": "Show ASHES Command Reference", + "category": "ASHES" + }, + { + "command": "ashes.refreshCommands", + "title": "Refresh ASHES Commands", + "category": "ASHES" + }, + { + "command": "ashes.refreshDiagnostics", + "title": "Refresh ASHES Diagnostics", + "category": "ASHES" + } + ], + "menus": { + "commandPalette": [ + { + "command": "ashes.showCommandReference" + }, + { + "command": "ashes.refreshCommands" + }, + { + "command": "ashes.refreshDiagnostics" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "16.x", + "@types/vscode": "^1.74.0", + "typescript": "^4.9.4" + }, + "author": "", + "license": "ISC" +} diff --git a/vscode-extension-ashes/sample.esc b/vscode-extension-ashes/sample.esc new file mode 100644 index 00000000..8c79b670 --- /dev/null +++ b/vscode-extension-ashes/sample.esc @@ -0,0 +1,102 @@ +# Sample ASHES script for testing the VS Code extension +# This file demonstrates various ASHES language features + +:setup + # Global variable declarations + global game_started = false + global player_name = "Player" + global score = 0 + + # Local variables + var tutorial_completed = false + var current_room = "intro" + + # Set initial state + set_global("game_started", true) + set_active($player, true) + teleport($player, $start_position) + +:ready + # Check if this is the first time + if !game_started: + say($player, "Welcome to the game!", "welcome_message") + game_started = true + else: + say($player, "Welcome back!", "welcome_back") + +:action1 + # Simple interaction + say($player, "You examine the object carefully.", "examine_object") + + # Check inventory + if $magic_key in inventory: + say($player, "You have the magic key!", "has_key") + else: + say($player, "You need a key to proceed.", "needs_key") + +:action2 + # Dialog system example + say($player, "You approach the mysterious character.", "approach_character") + + ?! + - "Hello, who are you?" [!name_known] + say_last_dialog_option() + say($npc, "I am the guardian of this place.", "guardian_intro") + global name_known = true + - "Can you help me?" [name_known] + say_last_dialog_option() + say($npc, "I can guide you, but you must prove yourself.", "guardian_help") + ?! + - "How can I prove myself?" + say_last_dialog_option() + say($npc, "Find the three ancient artifacts.", "guardian_task") + - "I'm not interested." + say_last_dialog_option() + say($npc, "Very well, good luck on your own.", "guardian_dismiss") + done + - "Goodbye" + say_last_dialog_option() + say($npc, "Farewell, traveler.", "guardian_farewell") + done + +:action3 + # Complex logic example + var artifacts_found = 0 + + # Check for artifacts + if $artifact1_collected: + artifacts_found += 1 + if $artifact2_collected: + artifacts_found += 1 + if $artifact3_collected: + artifacts_found += 1 + + # Conditional responses + if artifacts_found == 3: + say($player, "I have found all three artifacts!", "all_artifacts") + change_scene("res://rooms/victory_room.tscn") + elif artifacts_found > 0: + say($player, "I have found " + str(artifacts_found) + " artifacts so far.", "some_artifacts") + else: + say($player, "I haven't found any artifacts yet.", "no_artifacts") + +:use | TK + # Use with item + if $magic_key in inventory: + say($player, "You use the magic key.", "use_key") + set_active($locked_door, false) + play_snd("res://sounds/door_open.ogg", _sfx) + else: + say($player, "You can't use this without the right item.", "cant_use") + +:look + # Look action + say($player, "You look around carefully.", "look_around") + + # Conditional descriptions + if $secret_door is active: + say($player, "You notice a hidden passage.", "hidden_passage") + else: + say($player, "Nothing unusual catches your eye.", "nothing_unusual") + +# End of sample script diff --git a/vscode-extension-ashes/snippets/ashes.json b/vscode-extension-ashes/snippets/ashes.json new file mode 100644 index 00000000..1cdb2767 --- /dev/null +++ b/vscode-extension-ashes/snippets/ashes.json @@ -0,0 +1,176 @@ +{ + "Event": { + "prefix": "event", + "body": [ + ":${1:event_name}", + "\t${2:// Event code here}" + ], + "description": "Create a new ASHES event" + }, + "Event with target": { + "prefix": "eventtarget", + "body": [ + ":${1:event_name} \"${2:target_id}\"", + "\t${3:// Event code here}" + ], + "description": "Create a new ASHES event with target" + }, + "Event with flags": { + "prefix": "eventflags", + "body": [ + ":${1:event_name} | ${2:NO_UI} | ${3:NO_TT}", + "\t${4:// Event code here}" + ], + "description": "Create a new ASHES event with flags" + }, + "Event with flags and target": { + "prefix": "eventflagstarget", + "body": [ + ":${1:event_name} | ${2:NO_UI} | ${3:NO_TT} \"${4:target_id}\"", + "\t${5:// Event code here}" + ], + "description": "Create a new ASHES event with flags and target" + }, + "Say command": { + "prefix": "say", + "body": [ + "say(${1:player}, \"${2:text}\"${3:, \"${4:translation_key}\"})" + ], + "description": "Say command for dialog" + }, + "Set global variable": { + "prefix": "setglobal", + "body": [ + "set_global(\"${1:variable_name}\", ${2:value})" + ], + "description": "Set a global variable" + }, + "Global variable declaration": { + "prefix": "global", + "body": [ + "global ${1:variable_name}${2: = ${3:value}}" + ], + "description": "Declare a global variable" + }, + "Local variable declaration": { + "prefix": "var", + "body": [ + "var ${1:variable_name}${2: = ${3:value}}" + ], + "description": "Declare a local variable" + }, + "If statement": { + "prefix": "if", + "body": [ + "if ${1:condition}:", + "\t${2:// Code here}" + ], + "description": "If statement" + }, + "If-else statement": { + "prefix": "ifelse", + "body": [ + "if ${1:condition}:", + "\t${2:// Code here}", + "else:", + "\t${3:// Code here}" + ], + "description": "If-else statement" + }, + "While loop": { + "prefix": "while", + "body": [ + "while ${1:condition}:", + "\t${2:// Code here}" + ], + "description": "While loop" + }, + "Dialog block": { + "prefix": "dialog", + "body": [ + "?!", + "\t- \"${1:choice_text}\"", + "\t\t${2:// Code here}" + ], + "description": "Dialog block with choice" + }, + "Dialog choice with condition": { + "prefix": "dialogif", + "body": [ + "?!", + "\t- \"${1:choice_text}\" [${2:condition}]", + "\t\t${3:// Code here}" + ], + "description": "Dialog choice with condition" + }, + "Change scene": { + "prefix": "changescene", + "body": [ + "change_scene(\"${1:scene_path}\"${2:, ${3:true}})" + ], + "description": "Change to a different scene" + }, + "Set active": { + "prefix": "setactive", + "body": [ + "set_active($${1:object_id}, ${2:true})" + ], + "description": "Set object active/inactive" + }, + "Teleport": { + "prefix": "teleport", + "body": [ + "teleport($${1:object_id}, $${2:target_id})" + ], + "description": "Teleport object to target" + }, + "Walk to": { + "prefix": "walk", + "body": [ + "walk($${1:object_id}, $${2:target_id})" + ], + "description": "Walk object to target" + }, + "Play sound": { + "prefix": "playsnd", + "body": [ + "play_snd(\"${1:sound_path}\"${2:, ${3:_music}})" + ], + "description": "Play a sound file" + }, + "Play video": { + "prefix": "playvideo", + "body": [ + "play_video(\"${1:video_path}\")" + ], + "description": "Play a video file" + }, + "Inventory add": { + "prefix": "inventoryadd", + "body": [ + "inventory_add($${1:item_id})" + ], + "description": "Add item to inventory" + }, + "Inventory remove": { + "prefix": "inventoryremove", + "body": [ + "inventory_remove($${1:item_id})" + ], + "description": "Remove item from inventory" + }, + "Comment": { + "prefix": "comment", + "body": [ + "# ${1:comment}" + ], + "description": "Add a comment" + }, + "Print": { + "prefix": "print", + "body": [ + "print(\"${1:message}\")" + ], + "description": "Print debug message" + } +} diff --git a/vscode-extension-ashes/src/commandParser.ts b/vscode-extension-ashes/src/commandParser.ts new file mode 100644 index 00000000..dba22523 --- /dev/null +++ b/vscode-extension-ashes/src/commandParser.ts @@ -0,0 +1,455 @@ +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; + filePath?: 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 signatureParams: string[] = []; + + 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) and parameter names from signature + if (line.startsWith('# `') && line.includes('`')) { + const match = line.match(/# `([^`]+)`/); + if (match) { + description = match[1] + '\n\n'; + foundFirstDescription = true; + + // Extract parameter names from command signature + // e.g., "anim object name [reverse]" -> ["object", "name", "reverse"] + const signature = match[1]; + const paramMatches = signature.match(/\b\w+\b/g); + if (paramMatches && paramMatches.length > 1) { + // Skip the first match (command name) and extract parameter names + signatureParams = paramMatches.slice(1); + } + } + 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 += '\n' + 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 with more comprehensive detection + let paramType = 'string'; + const descLower = paramDesc.toLowerCase(); + if (descLower.includes('boolean') || descLower.includes('true') || descLower.includes('false') || + descLower.includes('bool') || descLower.includes('flag')) { + paramType = 'boolean'; + } else if (descLower.includes('number') || descLower.includes('int') || descLower.includes('float') || + descLower.includes('integer') || descLower.includes('numeric')) { + paramType = 'number'; + } else if (descLower.includes('object') || descLower.includes('node') || descLower.includes('item')) { + paramType = 'object'; + } else if (descLower.includes('scene') || descLower.includes('room')) { + paramType = 'scene'; + } else if (descLower.includes('animation') || descLower.includes('anim')) { + paramType = 'animation'; + } + + 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 - look for the number on the next line + let minArgs = 0; + for (let k = j + 1; k < Math.min(j + 5, lines.length); k++) { + const nextLine = lines[k].trim(); + const numberMatch = nextLine.match(/^(\d+),?$/); + if (numberMatch) { + minArgs = parseInt(numberMatch[1]); + break; + } + } + + if (minArgs > 0) { + + // 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 (first array that doesn't contain TYPE_) + if (typeLine.includes('[') && !typeLine.includes('TYPE_') && !defaults.length) { + const defaultMatch = typeLine.match(/\[([^\]]+)\]/); + if (defaultMatch) { + defaults = defaultMatch[1].split(',').map(d => d.trim()); + } + } + + // Extract required flags array (second array that doesn't contain TYPE_ and has true/false) + if (typeLine.includes('[') && !typeLine.includes('TYPE_') && defaults.length > 0 && + (typeLine.includes('true') || typeLine.includes('false'))) { + 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]; + + // Use the total number of types, not just minArgs + const totalParams = types.length; + + for (let p = 0; p < totalParams; 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 with more comprehensive mapping + 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'; + } else if (type.includes('TYPE_VECTOR2')) { + paramType = 'vector2'; + } else if (type.includes('TYPE_VECTOR3')) { + paramType = 'vector3'; + } else if (type.includes('TYPE_ARRAY')) { + paramType = 'array'; + } else if (type.includes('TYPE_DICTIONARY')) { + paramType = 'dictionary'; + } else if (type.includes('TYPE_OBJECT')) { + paramType = 'object'; + } + + // Try to get parameter name from comments if available, otherwise use signature or generate one + let paramName = `param${p + 1}`; + if (p < originalParams.length && originalParams[p] && originalParams[p].name) { + paramName = originalParams[p].name; + } else if (p < signatureParams.length && signatureParams[p]) { + paramName = signatureParams[p]; + } + + 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++) { + // Use signature parameter name if available, otherwise generate one + const paramName = i < signatureParams.length ? signatureParams[i] : `param${i + 1}`; + parameters.push({ + name: paramName, + type: 'string', + required: true + }); + } + } + } + + // Clean up description - remove extra whitespace and fix common issues + if (description) { + description = description.trim(); + + // Remove extra whitespace but preserve newlines + description = description.replace(/[ \t]+/g, ' ').replace(/\n\s+/g, '\n'); + + // 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, + filePath: filePath + }; + + } 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[], filePath?: string}> { + const commands = this.parseCommands(); + + return commands.map(cmd => ({ + name: cmd.name, + description: cmd.description, + parameters: cmd.parameters, + filePath: cmd.filePath + })); + } +} diff --git a/vscode-extension-ashes/src/diagnosticProvider.ts b/vscode-extension-ashes/src/diagnosticProvider.ts new file mode 100644 index 00000000..ef52be83 --- /dev/null +++ b/vscode-extension-ashes/src/diagnosticProvider.ts @@ -0,0 +1,247 @@ +import * as vscode from 'vscode'; +import { ASHESSyntaxValidator } from './syntaxValidator'; + +export class ASHESDiagnosticProvider { + private diagnosticCollection: vscode.DiagnosticCollection; + private syntaxValidator: ASHESSyntaxValidator; + private commandCache: Map = new Map(); + private lastUpdateTime: Map = new Map(); + + constructor() { + this.diagnosticCollection = vscode.languages.createDiagnosticCollection('ashes'); + this.syntaxValidator = new ASHESSyntaxValidator(); + } + + /** + * Initialize the diagnostic provider + */ + public initialize(context: vscode.ExtensionContext): void { + // Register document change listener + const documentChangeListener = vscode.workspace.onDidChangeTextDocument( + (event) => this.onDocumentChanged(event) + ); + + // Register document open listener + const documentOpenListener = vscode.workspace.onDidOpenTextDocument( + (document) => this.validateDocument(document) + ); + + // Register document save listener + const documentSaveListener = vscode.workspace.onDidSaveTextDocument( + (document) => this.validateDocument(document) + ); + + // Register workspace change listener to update command cache + const workspaceChangeListener = vscode.workspace.onDidChangeWorkspaceFolders( + () => this.updateCommandCache() + ); + + // Validate all open ASHES documents + vscode.workspace.textDocuments.forEach(document => { + if (document.languageId === 'ashes') { + this.validateDocument(document); + } + }); + + // Add to subscriptions + context.subscriptions.push( + this.diagnosticCollection, + documentChangeListener, + documentOpenListener, + documentSaveListener, + workspaceChangeListener + ); + + // Initial command cache update + this.updateCommandCache(); + } + + /** + * Handle document changes with debouncing + */ + private onDocumentChanged(event: vscode.TextDocumentChangeEvent): void { + if (event.document.languageId !== 'ashes') { + return; + } + + const document = event.document; + const uri = document.uri.toString(); + const now = Date.now(); + + // Debounce validation to avoid excessive processing + const lastUpdate = this.lastUpdateTime.get(uri) || 0; + if (now - lastUpdate < 500) { // 500ms debounce + return; + } + + this.lastUpdateTime.set(uri, now); + + // Use setTimeout to debounce the validation + setTimeout(() => { + this.validateDocument(document); + }, 500); + } + + /** + * Validate a document and update diagnostics + */ + private validateDocument(document: vscode.TextDocument): void { + if (document.languageId !== 'ashes') { + return; + } + + try { + // Update command cache if needed + this.updateCommandCacheForDocument(document); + + // Get diagnostics from syntax validator + const diagnostics = this.syntaxValidator.validateDocument(document); + + // Update the diagnostic collection + this.diagnosticCollection.set(document.uri, diagnostics); + + } catch (error) { + console.error('Error validating ASHES document:', error); + + // Show error diagnostic + const errorDiagnostic = new vscode.Diagnostic( + new vscode.Range(0, 0, 0, 0), + `Error validating ASHES syntax: ${error}`, + vscode.DiagnosticSeverity.Error + ); + this.diagnosticCollection.set(document.uri, [errorDiagnostic]); + } + } + + /** + * Update command cache for all workspace folders + */ + private updateCommandCache(): void { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return; + } + + for (const folder of workspaceFolders) { + this.updateCommandCacheForWorkspace(folder.uri.fsPath); + } + } + + /** + * Update command cache for a specific document's workspace + */ + private updateCommandCacheForDocument(document: vscode.TextDocument): void { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder) { + return; + } + + const workspacePath = workspaceFolder.uri.fsPath; + this.updateCommandCacheForWorkspace(workspacePath); + } + + /** + * Update command cache for a specific workspace + */ + private updateCommandCacheForWorkspace(workspacePath: string): void { + const now = Date.now(); + const lastUpdate = this.lastUpdateTime.get(workspacePath) || 0; + + // Only update if cache is older than 5 minutes + if (now - lastUpdate < 5 * 60 * 1000) { + return; + } + + try { + const commands = this.extractCommandsFromWorkspace(workspacePath); + this.commandCache.set(workspacePath, commands); + this.syntaxValidator.updateCommands(commands); + this.lastUpdateTime.set(workspacePath, now); + + // Re-validate all ASHES documents in this workspace + vscode.workspace.textDocuments.forEach(document => { + if (document.languageId === 'ashes') { + const docWorkspace = vscode.workspace.getWorkspaceFolder(document.uri); + if (docWorkspace && docWorkspace.uri.fsPath === workspacePath) { + this.validateDocument(document); + } + } + }); + + } catch (error) { + console.error('Error updating command cache:', error); + } + } + + /** + * Extract commands from workspace using the existing CommandParser + */ + private extractCommandsFromWorkspace(workspacePath: string): string[] { + try { + // Import the CommandParser dynamically to avoid circular dependencies + const { CommandParser } = require('./commandParser'); + const parser = new CommandParser(workspacePath); + const commands = parser.parseCommands(); + return commands.map((cmd: any) => cmd.name); + } catch (error) { + console.error('Error extracting commands:', error); + // Return basic command set as fallback + return [ + 'accept_input', 'anim', 'anim_block', 'block_say', 'camera_push', 'camera_push_block', + 'camera_set_limits', 'camera_set_pos', 'camera_set_pos_block', 'camera_set_target', + 'camera_set_target_block', 'camera_set_zoom', 'camera_set_zoom_block', 'camera_set_zoom_height', + 'camera_set_zoom_height_block', 'camera_shift', 'camera_shift_block', 'change_scene', 'custom', + 'dec_global', 'enable_terrain', 'end_block_say', 'hide_menu', 'inc_global', 'inventory_add', + 'inventory_remove', 'item_count_add', 'play_lib_snd', 'play_snd', 'play_video', 'print', + 'print_internal', 'queue_event', 'queue_resource', 'rand_global', 'repeat', 'save_game', + 'say', 'say_last_dialog_option', 'say_random', 'say_sequence', 'sched_event', 'set_active', + 'set_active_if_exists', 'set_angle', 'set_animations', 'set_direction', 'set_global', + 'set_globals', 'set_gui_visible', 'set_interactive', 'set_item_custom_data', 'set_speed', + 'set_state', 'set_tooltip', 'show_menu', 'slide', 'slide_block', 'spawn', 'stop', 'stop_snd', + 'teleport', 'teleport_pos', 'transition', 'turn_to', 'wait', 'walk', 'walk_block', + 'walk_to_pos', 'walk_to_pos_block' + ]; + } + } + + /** + * Clear diagnostics for a document + */ + public clearDiagnostics(document: vscode.TextDocument): void { + this.diagnosticCollection.delete(document.uri); + } + + /** + * Clear all diagnostics + */ + public clearAllDiagnostics(): void { + this.diagnosticCollection.clear(); + } + + /** + * Get current diagnostics for a document + */ + public getDiagnostics(document: vscode.TextDocument): readonly vscode.Diagnostic[] { + return this.diagnosticCollection.get(document.uri) || []; + } + + /** + * Force refresh diagnostics for all ASHES documents + */ + public refreshAllDiagnostics(): void { + this.updateCommandCache(); + + vscode.workspace.textDocuments.forEach(document => { + if (document.languageId === 'ashes') { + this.validateDocument(document); + } + }); + } + + /** + * Get diagnostic collection for external access + */ + public getDiagnosticCollection(): vscode.DiagnosticCollection { + return this.diagnosticCollection; + } +} diff --git a/vscode-extension-ashes/src/extension.ts b/vscode-extension-ashes/src/extension.ts new file mode 100644 index 00000000..0cdda8e0 --- /dev/null +++ b/vscode-extension-ashes/src/extension.ts @@ -0,0 +1,481 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { CommandParser, CommandInfo, CommandParameter } from './commandParser'; +import { ASHESDiagnosticProvider } from './diagnosticProvider'; + +// Cache for dynamically loaded commands +let ASHES_COMMANDS: Array<{name: string, description: string, parameters: CommandParameter[], filePath?: string}> = []; +let COMMAND_CACHE_TIMESTAMP = 0; + +// Built-in variables +const BUILTIN_VARIABLES = [ + 'CURRENT_PLAYER', + 'ESC_LAST_SCENE', + 'ESC_CURRENT_SCENE', + 'FORCE_LAST_SCENE_NULL', + 'ANIMATION_RESOURCES' +]; + +// Keywords +const KEYWORDS = [ + 'var', 'global', 'if', 'elif', 'else', 'while', 'break', 'done', 'stop', 'pass', + '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[], filePath?: string}> { + 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[], filePath?: string}> { + 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!'); + + // Initialize diagnostic provider for syntax error detection + const diagnosticProvider = new ASHESDiagnosticProvider(); + diagnosticProvider.initialize(context); + + // Register completion provider + const completionProvider = vscode.languages.registerCompletionItemProvider( + 'ashes', + { + 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 + 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) => { + // Enhanced parameter formatting with clear indicators + const requiredIndicator = param.required ? '**Required**' : '**Optional**'; + const typeIndicator = `\`${param.type}\``; + const nameIndicator = `**${param.name}**`; + const defaultValue = param.defaultValue ? ` *(default: \`${param.defaultValue}\`)*` : ''; + + paramDocs += `- ${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}\n`; + + // Create example parameter with clear formatting + 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---\n\n${command.description}${paramDocs}${exampleUsage}` + ); + + // 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); + }); + + // Add built-in variable completions + BUILTIN_VARIABLES.forEach(variable => { + const completion = new vscode.CompletionItem(variable, vscode.CompletionItemKind.Variable); + completion.detail = 'Built-in variable'; + completion.documentation = new vscode.MarkdownString(`Built-in ASHES variable: **${variable}**`); + completions.push(completion); + }); + + // Add keyword completions + KEYWORDS.forEach(keyword => { + const completion = new vscode.CompletionItem(keyword, vscode.CompletionItemKind.Keyword); + completion.detail = 'ASHES keyword'; + completions.push(completion); + }); + + return completions; + } + }, + ' ', '(', '$' // Trigger characters + ); + + // Register hover provider for commands + const hoverProvider = vscode.languages.registerHoverProvider( + 'ashes', + { + provideHover(document: vscode.TextDocument, position: vscode.Position) { + const word = document.getText(document.getWordRangeAtPosition(position)); + + // 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) => { + // Enhanced parameter formatting with clear indicators + const requiredIndicator = param.required ? '**Required**' : '**Optional**'; + const typeIndicator = `\`${param.type}\``; + const nameIndicator = `**${param.name}**`; + const defaultValue = param.defaultValue ? ` *(default: \`${param.defaultValue}\`)*` : ''; + + paramDocs += `- ${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}\n`; + + // Create example parameter with clear formatting + 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---\n\n${command.description}${paramDocs}${exampleUsage}` + ) + ); + return hover; + } + + const builtinVar = BUILTIN_VARIABLES.find(variable => variable === word); + if (builtinVar) { + const hover = new vscode.Hover( + new vscode.MarkdownString(`Built-in ASHES variable: **${builtinVar}**`) + ); + return hover; + } + + return null; + } + } + ); + + // Register definition provider for Ctrl+click navigation + const definitionProvider = vscode.languages.registerDefinitionProvider( + 'ashes', + { + provideDefinition(document: vscode.TextDocument, position: vscode.Position) { + const word = document.getText(document.getWordRangeAtPosition(position)); + + // 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 && command.filePath) { + // Create a location pointing to the command file + const uri = vscode.Uri.file(command.filePath); + + // Try to find the class definition line in the file + try { + const fs = require('fs'); + const content = fs.readFileSync(command.filePath, 'utf8'); + const lines = content.split('\n'); + + // Look for the class definition line + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('class_name') && line.includes(command.name)) { + return new vscode.Location(uri, new vscode.Position(i, 0)); + } + } + + // If no class definition found, return the beginning of the file + return new vscode.Location(uri, new vscode.Position(0, 0)); + } catch (error) { + console.error('Error reading command file:', error); + return new vscode.Location(uri, new vscode.Position(0, 0)); + } + } + + return null; + } + } + ); + + // Register command for showing command reference + const showCommandReference = vscode.commands.registerCommand('ashes.showCommandReference', () => { + const panel = vscode.window.createWebviewPanel( + 'ashesCommandReference', + 'ASHES Command Reference', + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true + } + ); + + // 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 = ` + + + + + + +

ASHES Command Reference

+

No workspace found. Please open a workspace to view commands.

+ + + `; + return; + } + + // Get dynamic commands + const commands = getCommands(workspaceRoot); + + // Handle messages from the webview + panel.webview.onDidReceiveMessage( + message => { + switch (message.command) { + case 'navigateToCommand': + if (message.commandName && workspaceRoot) { + const command = commands.find(cmd => cmd.name === message.commandName); + if (command && command.filePath) { + const uri = vscode.Uri.file(command.filePath); + + // Try to find the class definition line + try { + const fs = require('fs'); + const content = fs.readFileSync(command.filePath, 'utf8'); + const lines = content.split('\n'); + + // Look for the class definition line + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('class_name') && line.includes(command.name)) { + vscode.window.showTextDocument(uri, { + selection: new vscode.Range(i, 0, i, 0) + }); + return; + } + } + + // If no class definition found, open at the beginning + vscode.window.showTextDocument(uri); + } catch (error) { + console.error('Error opening command file:', error); + vscode.window.showTextDocument(uri); + } + } + } + return; + } + }, + undefined, + context.subscriptions + ); + + const commandsHtml = commands.map(command => { + let paramInfo = ''; + let exampleUsage = ''; + + if (command.parameters && command.parameters.length > 0) { + const exampleParams: string[] = []; + + paramInfo = command.parameters.map(param => { + // Enhanced parameter formatting with clear indicators + const requiredIndicator = param.required ? 'Required' : 'Optional'; + const typeIndicator = `${param.type}`; + const nameIndicator = `${param.name}`; + 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 `${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}`; + }).join('
'); + + // Create example usage + exampleUsage = `

Example:
${command.name}(${exampleParams.join(', ')})`; + } + + return ` + ${command.name} + ${command.description}${exampleUsage} + ${paramInfo} + `; + }).join(''); + + panel.webview.html = ` + + + + + + +

ASHES Command Reference

+
+ 💡 Tip: Click on any command name to navigate to its source file! +
+ + + + + + + + + + ${commandsHtml} + +
CommandDescriptionParameters
+ + + + + `; + }); + + // Register command for refreshing command cache + const refreshCommands = vscode.commands.registerCommand('ashes.refreshCommands', () => { + // Clear cache to force reload + ASHES_COMMANDS = []; + COMMAND_CACHE_TIMESTAMP = 0; + + // Also refresh diagnostics + diagnosticProvider.refreshAllDiagnostics(); + + vscode.window.showInformationMessage('ASHES commands cache and diagnostics refreshed!'); + }); + + // Register command for refreshing diagnostics only + const refreshDiagnostics = vscode.commands.registerCommand('ashes.refreshDiagnostics', () => { + diagnosticProvider.refreshAllDiagnostics(); + vscode.window.showInformationMessage('ASHES diagnostics refreshed!'); + }); + + context.subscriptions.push(completionProvider, hoverProvider, definitionProvider, showCommandReference, refreshCommands, refreshDiagnostics); +} + +export function deactivate() {} diff --git a/vscode-extension-ashes/src/syntaxValidator.ts b/vscode-extension-ashes/src/syntaxValidator.ts new file mode 100644 index 00000000..dc35634f --- /dev/null +++ b/vscode-extension-ashes/src/syntaxValidator.ts @@ -0,0 +1,803 @@ +import * as vscode from 'vscode'; + +export interface SyntaxError { + line: number; + column: number; + message: string; + severity: vscode.DiagnosticSeverity; + code?: string; +} + +export class ASHESSyntaxValidator { + private commands: Set = new Set(); + private builtinVariables: Set = new Set([ + 'CURRENT_PLAYER', + 'ESC_LAST_SCENE', + 'ESC_CURRENT_SCENE', + 'FORCE_LAST_SCENE_NULL', + 'ANIMATION_RESOURCES' + ]); + private keywords: Set = new Set([ + 'var', 'global', 'if', 'elif', 'else', 'while', 'break', 'done', 'stop', 'pass', + 'true', 'false', 'nil', 'and', 'or', 'not', 'in', 'is', 'active' + ]); + + constructor(commands: string[] = []) { + this.commands = new Set(commands); + } + + /** + * Update the list of available commands + */ + public updateCommands(commands: string[]): void { + this.commands = new Set(commands); + } + + /** + * Validate ASHES syntax and return diagnostic errors + */ + public validateDocument(document: vscode.TextDocument): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + const text = document.getText(); + const lines = text.split('\n'); + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + const lineNumber = lineIndex; + + // Skip empty lines and comments + if (this.isEmptyOrComment(line)) { + continue; + } + + // Validate different types of lines + const lineDiagnostics = this.validateLine(line, lineNumber, lines, lineIndex); + diagnostics.push(...lineDiagnostics); + } + + // Validate overall document structure + const documentDiagnostics = this.validateDocumentStructure(lines); + diagnostics.push(...documentDiagnostics); + + return diagnostics; + } + + /** + * Check if a line is empty or a comment + */ + private isEmptyOrComment(line: string): boolean { + const trimmed = line.trim(); + return trimmed === '' || trimmed.startsWith('#'); + } + + /** + * Validate a single line + */ + private validateLine(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + const trimmed = line.trim(); + + // Validate event definitions + if (trimmed.startsWith(':')) { + diagnostics.push(...this.validateEventDefinition(line, lineNumber)); + } + // Validate dialog blocks + else if (trimmed.startsWith('?!')) { + diagnostics.push(...this.validateDialogBlock(line, lineNumber, allLines, lineIndex)); + } + // Validate dialog choices + else if (trimmed.startsWith('-')) { + diagnostics.push(...this.validateDialogChoice(line, lineNumber)); + } + // Validate commands and control flow + else { + diagnostics.push(...this.validateCommandOrControlFlow(line, lineNumber)); + } + + // Validate indentation + diagnostics.push(...this.validateIndentation(line, lineNumber, allLines, lineIndex)); + + // Validate indentation after control flow statements + if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('else') || line.includes('while '))) { + // Check if the next line has proper indentation + if (lineIndex + 1 < allLines.length) { + const currentIndent = line.match(/^(\s*)/)?.[1].length || 0; + + // Skip empty lines + let nextNonEmptyLineIndex = lineIndex + 1; + while (nextNonEmptyLineIndex < allLines.length && allLines[nextNonEmptyLineIndex].trim() === '') { + nextNonEmptyLineIndex++; + } + + if (nextNonEmptyLineIndex < allLines.length) { + const nextNonEmptyLine = allLines[nextNonEmptyLineIndex]; + const nextNonEmptyIndent = nextNonEmptyLine.match(/^(\s*)/)?.[1].length || 0; + + // The next non-empty line should have exactly one more level of indentation + if (nextNonEmptyIndent <= currentIndent) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Control flow statement requires indented block on the next line", + severity: vscode.DiagnosticSeverity.Error, + code: 'missing-indentation' + }); + } + } + } + } + + // Validate string literals + diagnostics.push(...this.validateStringLiterals(line, lineNumber)); + + // Validate parentheses matching + diagnostics.push(...this.validateParentheses(line, lineNumber)); + + return diagnostics; + } + + /** + * Validate event definitions + */ + private validateEventDefinition(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Event definition patterns based on real ESC files: + // :event_name + // :event_name | FLAGS + // :event_name "target" + // :event_name | FLAGS "target" + const eventMatch = line.match(/^(\s*):([a-zA-Z_][a-zA-Z0-9_]*)(\s*\|\s*[A-Z_]+)*(\s+("[^"]*"))?/); + + if (!eventMatch) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Invalid event definition. Expected format: :event_name [| FLAGS] [\"target\"]", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-definition' + }); + return diagnostics; + } + + const eventName = eventMatch[2]; + const flags = eventMatch[3]; + const target = eventMatch[5]; + + // Validate event name + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(eventName)) { + diagnostics.push({ + range: new vscode.Range(lineNumber, eventMatch.index! + 1, lineNumber, eventMatch.index! + 1 + eventName.length), + message: "Event name must start with a letter or underscore and contain only letters, numbers, and underscores", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-name' + }); + } + + // Check for common event names and suggest alternatives + const commonEvents = ['init', 'ready', 'setup', 'action1', 'action2', 'action3', 'use', 'look', 'talk', 'walk', 'interact']; + if (!commonEvents.includes(eventName.toLowerCase()) && eventName.length > 20) { + const eventIndex = line.indexOf(eventName); + diagnostics.push({ + range: new vscode.Range(lineNumber, eventIndex, lineNumber, eventIndex + eventName.length), + message: `Consider using a shorter event name. Common events: ${commonEvents.join(', ')}`, + severity: vscode.DiagnosticSeverity.Information, + code: 'event-naming-suggestion' + }); + } + + // Validate flags if present + if (flags) { + const flagText = flags.trim().substring(1).trim(); // Remove the | + const validFlags = ['TK', 'TR', 'TG', 'SKIP', 'GLOBAL']; + const providedFlags = flagText.split(/\s+/); + + for (const flag of providedFlags) { + if (flag && !validFlags.includes(flag)) { + const flagIndex = line.indexOf(flag); + diagnostics.push({ + range: new vscode.Range(lineNumber, flagIndex, lineNumber, flagIndex + flag.length), + message: `Invalid event flag '${flag}'. Valid flags: ${validFlags.join(', ')}`, + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-flag' + }); + } + } + } + + // Validate target if present + if (target && (!target.startsWith('"') || !target.endsWith('"'))) { + const targetIndex = line.indexOf(target); + diagnostics.push({ + range: new vscode.Range(lineNumber, targetIndex, lineNumber, targetIndex + target.length), + message: "Event target must be a quoted string", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-target' + }); + } + + return diagnostics; + } + + /** + * Validate dialog blocks + */ + private validateDialogBlock(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Dialog block should have proper indentation + const indentMatch = line.match(/^(\s*)(\?\!)/); + if (!indentMatch) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Invalid dialog block syntax. Expected: ?!", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-dialog-block' + }); + return diagnostics; + } + + const baseIndent = indentMatch[1].length; + + // Check if dialog block is properly closed + let foundEnd = false; + for (let i = lineIndex + 1; i < allLines.length; i++) { + const nextLine = allLines[i]; + if (nextLine.trim() === '') continue; + + const nextIndent = nextLine.match(/^(\s*)/)?.[1].length || 0; + + if (nextIndent <= baseIndent && !nextLine.trim().startsWith('-')) { + foundEnd = true; + break; + } + } + + if (!foundEnd) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Dialog block is not properly closed", + severity: vscode.DiagnosticSeverity.Warning, + code: 'unclosed-dialog-block' + }); + } + + return diagnostics; + } + + /** + * Validate dialog choices + */ + private validateDialogChoice(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Dialog choice pattern: - "choice text" [condition] + const choiceMatch = line.match(/^(\s*)-(\s*)("[^"]*")(\s*\[.*\])?/); + + if (!choiceMatch) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Invalid dialog choice syntax. Expected: - \"choice text\" [condition]", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-dialog-choice' + }); + return diagnostics; + } + + const choiceText = choiceMatch[3]; + const condition = choiceMatch[4]; + + // Validate choice text is not empty + if (choiceText === '""') { + const textIndex = line.indexOf(choiceText); + diagnostics.push({ + range: new vscode.Range(lineNumber, textIndex, lineNumber, textIndex + choiceText.length), + message: "Dialog choice text cannot be empty", + severity: vscode.DiagnosticSeverity.Error, + code: 'empty-dialog-choice' + }); + } + + // Validate condition if present + if (condition) { + const conditionContent = condition.slice(1, -1); // Remove brackets + if (conditionContent.trim() === '') { + const conditionIndex = line.indexOf(condition); + diagnostics.push({ + range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length), + message: "Dialog choice condition cannot be empty", + severity: vscode.DiagnosticSeverity.Error, + code: 'empty-dialog-condition' + }); + } + } + + return diagnostics; + } + + /** + * Validate commands and control flow statements + */ + private validateCommandOrControlFlow(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Check for function calls + const functionCallMatch = line.match(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/); + if (functionCallMatch) { + const functionName = functionCallMatch[1]; + + // Check if it's a known command + if (!this.commands.has(functionName) && !this.keywords.has(functionName)) { + const funcIndex = line.indexOf(functionName); + diagnostics.push({ + range: new vscode.Range(lineNumber, funcIndex, lineNumber, funcIndex + functionName.length), + message: `Unknown command '${functionName}'. Available commands: ${Array.from(this.commands).slice(0, 10).join(', ')}${this.commands.size > 10 ? '...' : ''}`, + severity: vscode.DiagnosticSeverity.Error, + code: 'unknown-command' + }); + } + } + + // Validate control flow statements + const controlFlowMatch = line.match(/\b(if|elif|while)\s+(.+):/); + if (controlFlowMatch) { + const condition = controlFlowMatch[2]; + + // Basic condition validation + if (condition.trim() === '') { + const conditionIndex = line.indexOf(condition); + diagnostics.push({ + range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length), + message: "Control flow condition cannot be empty", + severity: vscode.DiagnosticSeverity.Error, + code: 'empty-condition' + }); + } + } + + // Check for missing colon in control flow statements + const controlFlowWithoutColon = line.match(/\b(if|elif|else|while)\s+(.+)$/); + if (controlFlowWithoutColon && !line.trim().endsWith(':')) { + const keyword = controlFlowWithoutColon[1]; + const keywordIndex = line.indexOf(keyword); + diagnostics.push({ + range: new vscode.Range(lineNumber, keywordIndex, lineNumber, keywordIndex + keyword.length), + message: `'${keyword}' statement must end with ':'`, + severity: vscode.DiagnosticSeverity.Error, + code: 'missing-colon' + }); + } + + // Check for 'else' without colon + const elseMatch = line.match(/\belse\s*$/); + if (elseMatch && !line.trim().endsWith(':')) { + const elseIndex = line.indexOf('else'); + diagnostics.push({ + range: new vscode.Range(lineNumber, elseIndex, lineNumber, elseIndex + 4), + message: "'else' statement must end with ':'", + severity: vscode.DiagnosticSeverity.Error, + code: 'missing-colon' + }); + } + + + // Validate common condition patterns from real ESC files (for lines that do have colons) + if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('while '))) { + const conditionMatch = line.match(/\b(if|elif|while)\s+(.+):/); + if (conditionMatch) { + const conditionText = conditionMatch[2].trim(); + + // Check for proper inventory check patterns: "item" in inventory + const inventoryMatch = conditionText.match(/"([^"]*)"\s+in\s+inventory/); + if (inventoryMatch) { + // The pattern already matches quoted strings, so this is valid + // No need to check for quotes since the regex already requires them + } else { + // Check if there's an inventory check without proper quotes + const unquotedInventoryMatch = conditionText.match(/([^"]*)\s+in\s+inventory/); + if (unquotedInventoryMatch) { + const inventoryIndex = conditionText.indexOf('in inventory'); + diagnostics.push({ + range: new vscode.Range(lineNumber, line.indexOf(conditionText) + inventoryIndex, lineNumber, line.indexOf(conditionText) + inventoryIndex + 'in inventory'.length), + message: "Inventory check should use quoted string: \"item_name\" in inventory", + severity: vscode.DiagnosticSeverity.Warning, + code: 'inventory-check-pattern' + }); + } + } + + // Check for negation patterns: !variable + if (conditionText.startsWith('!')) { + const varName = conditionText.substring(1).trim(); + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(varName)) { + const varIndex = line.indexOf(varName); + diagnostics.push({ + range: new vscode.Range(lineNumber, varIndex, lineNumber, varIndex + varName.length), + message: "Variable name after ! must be valid identifier", + severity: vscode.DiagnosticSeverity.Warning, + code: 'invalid-negation-variable' + }); + } + } + + // Check for comparison patterns: variable == value, variable != value + const comparisonMatch = conditionText.match(/([a-zA-Z_][a-zA-Z0-9_]*)\s*(==|!=|<=|>=|<|>)\s*(.+)/); + if (comparisonMatch) { + const leftVar = comparisonMatch[1]; + const operator = comparisonMatch[2]; + const rightValue = comparisonMatch[3].trim(); + + // Check if right value is properly quoted for strings + if (rightValue.startsWith('"') && !rightValue.endsWith('"')) { + const rightIndex = line.indexOf(rightValue); + diagnostics.push({ + range: new vscode.Range(lineNumber, rightIndex, lineNumber, rightIndex + rightValue.length), + message: "String comparison value should be properly quoted", + severity: vscode.DiagnosticSeverity.Warning, + code: 'unclosed-comparison-string' + }); + } + } + + // Check for logical operators: and, or + if (conditionText.includes(' and ') || conditionText.includes(' or ')) { + // Basic validation for logical operators + const logicalMatch = conditionText.match(/(.+)\s+(and|or)\s+(.+)/); + if (logicalMatch) { + const leftPart = logicalMatch[1].trim(); + const rightPart = logicalMatch[3].trim(); + + if (leftPart === '' || rightPart === '') { + const operatorIndex = conditionText.indexOf(logicalMatch[2]); + diagnostics.push({ + range: new vscode.Range(lineNumber, line.indexOf(conditionText) + operatorIndex, lineNumber, line.indexOf(conditionText) + operatorIndex + logicalMatch[2].length), + message: "Logical operator requires expressions on both sides", + severity: vscode.DiagnosticSeverity.Warning, + code: 'incomplete-logical-expression' + }); + } + } + } + } + } + + // Validate variable assignments + const varAssignmentMatch = line.match(/\b(var|global)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/); + if (varAssignmentMatch) { + const varName = varAssignmentMatch[2]; + + // Check for reserved keywords + if (this.keywords.has(varName)) { + const varIndex = line.indexOf(varName); + diagnostics.push({ + range: new vscode.Range(lineNumber, varIndex, lineNumber, varIndex + varName.length), + message: `Variable name '${varName}' is a reserved keyword`, + severity: vscode.DiagnosticSeverity.Error, + code: 'reserved-variable-name' + }); + } + } + + // Validate specific command patterns from real ESC files + const sayMatch = line.match(/\bsay\s*\(/); + if (sayMatch) { + // Check for proper say command format: say(speaker, "text", "id") + // Handle both 2-argument and 3-argument say commands + // Use a more sophisticated regex to handle quoted strings with commas inside + const sayArgsMatch = line.match(/\bsay\s*\(\s*([^,]+),\s*("(?:[^"\\]|\\.)*")\s*(?:,\s*("(?:[^"\\]|\\.)*"))?\s*\)/); + if (sayArgsMatch) { + const speaker = sayArgsMatch[1].trim(); + const text = sayArgsMatch[2].trim(); + const id = sayArgsMatch[3] ? sayArgsMatch[3].trim() : null; + + // Check if speaker starts with $ (common pattern) + if (!speaker.startsWith('$') && !speaker.startsWith('current_player')) { + const speakerIndex = line.indexOf(speaker); + diagnostics.push({ + range: new vscode.Range(lineNumber, speakerIndex, lineNumber, speakerIndex + speaker.length), + message: "Speaker should typically start with $ or be 'current_player'", + severity: vscode.DiagnosticSeverity.Information, + code: 'say-speaker-pattern' + }); + } + + // Text should already be properly quoted by the regex + // Just check if ID is quoted (when present) + if (id && !this.isProperlyQuotedString(id)) { + const idIndex = line.indexOf(id); + diagnostics.push({ + range: new vscode.Range(lineNumber, idIndex, lineNumber, idIndex + id.length), + message: "Say ID should be quoted", + severity: vscode.DiagnosticSeverity.Warning, + code: 'say-id-quoted' + }); + } + } else { + // If the sophisticated regex doesn't match, try a simpler approach + const simpleSayMatch = line.match(/\bsay\s*\(\s*([^,]+),\s*([^,]+)(?:,\s*([^)]+))?\)/); + if (simpleSayMatch) { + const text = simpleSayMatch[2].trim(); + const id = simpleSayMatch[3] ? simpleSayMatch[3].trim() : null; + + // Check if text is properly quoted + if (!this.isProperlyQuotedString(text)) { + const textIndex = line.indexOf(text); + diagnostics.push({ + range: new vscode.Range(lineNumber, textIndex, lineNumber, textIndex + text.length), + message: "Say text should be properly quoted", + severity: vscode.DiagnosticSeverity.Warning, + code: 'say-text-quoted' + }); + } + + // Check if ID is quoted (when present) + if (id && !this.isProperlyQuotedString(id)) { + const idIndex = line.indexOf(id); + diagnostics.push({ + range: new vscode.Range(lineNumber, idIndex, lineNumber, idIndex + id.length), + message: "Say ID should be quoted", + severity: vscode.DiagnosticSeverity.Warning, + code: 'say-id-quoted' + }); + } + } + } + } + + // Validate inventory commands + const inventoryMatch = line.match(/\b(inventory_add|inventory_remove)\s*\(/); + if (inventoryMatch) { + const inventoryArgsMatch = line.match(/\b(inventory_add|inventory_remove)\s*\(\s*([^)]+)\)/); + if (inventoryArgsMatch) { + const arg = inventoryArgsMatch[2].trim(); + + // Check if argument starts with $ or is quoted + if (!arg.startsWith('$') && !arg.startsWith('"')) { + const argIndex = line.indexOf(arg); + diagnostics.push({ + range: new vscode.Range(lineNumber, argIndex, lineNumber, argIndex + arg.length), + message: "Inventory item should start with $ or be quoted", + severity: vscode.DiagnosticSeverity.Warning, + code: 'inventory-item-pattern' + }); + } + } + } + + return diagnostics; + } + + /** + * Validate indentation - ASHES requires tabs, not spaces + */ + private validateIndentation(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + if (line.trim() === '') return diagnostics; + + const indentMatch = line.match(/^(\s*)/); + if (!indentMatch) return diagnostics; + + const indent = indentMatch[1]; + const currentIndent = indent.length; + + // Check for spaces instead of tabs - ASHES requires tabs + if (indent.includes(' ')) { + const spaceIndex = indent.indexOf(' '); + diagnostics.push({ + range: new vscode.Range(lineNumber, spaceIndex, lineNumber, spaceIndex + 1), + message: "ASHES requires tabs for indentation, not spaces", + severity: vscode.DiagnosticSeverity.Error, + code: 'spaces-instead-of-tabs' + }); + } + + // Check for mixed tabs and spaces + if (indent.includes('\t') && indent.includes(' ')) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, currentIndent), + message: "Mixed tabs and spaces in indentation - use only tabs", + severity: vscode.DiagnosticSeverity.Error, + code: 'mixed-indentation' + }); + } + + // Find the previous non-empty line + let prevLineIndex = lineIndex - 1; + while (prevLineIndex >= 0 && allLines[prevLineIndex].trim() === '') { + prevLineIndex--; + } + + if (prevLineIndex >= 0) { + const prevLine = allLines[prevLineIndex]; + const prevIndentMatch = prevLine.match(/^(\s*)/); + if (prevIndentMatch) { + const prevIndent = prevIndentMatch[1].length; + + // Note: Removed excessive indentation limit to allow deeper nesting + // if (currentIndent > 3) { + // diagnostics.push({ + // range: new vscode.Range(lineNumber, 0, lineNumber, currentIndent), + // message: "Excessive indentation detected (more than 3 levels)", + // severity: vscode.DiagnosticSeverity.Warning, + // code: 'excessive-indentation' + // }); + // } + } + } + + return diagnostics; + } + + /** + * Validate string literals + */ + private validateStringLiterals(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Check for unclosed strings with proper quote handling + let inString = false; + let stringChar = ''; + let stringStart = -1; + let escapeNext = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === '\\') { + escapeNext = true; + continue; + } + + if (!inString && (char === '"' || char === "'")) { + inString = true; + stringChar = char; + stringStart = i; + } else if (inString && char === stringChar) { + inString = false; + stringChar = ''; + stringStart = -1; + } + } + + if (inString) { + diagnostics.push({ + range: new vscode.Range(lineNumber, stringStart, lineNumber, line.length), + message: `Unclosed ${stringChar === '"' ? 'double' : 'single'} quote`, + severity: vscode.DiagnosticSeverity.Error, + code: 'unclosed-string' + }); + } + + return diagnostics; + } + + /** + * Validate parentheses matching + */ + private validateParentheses(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + const stack: { char: string; index: number }[] = []; + const pairs: { [key: string]: string } = { '(': ')', '[': ']', '{': '}' }; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char in pairs) { + stack.push({ char, index: i }); + } else if (Object.values(pairs).includes(char)) { + if (stack.length === 0) { + diagnostics.push({ + range: new vscode.Range(lineNumber, i, lineNumber, i + 1), + message: `Unexpected closing '${char}'`, + severity: vscode.DiagnosticSeverity.Error, + code: 'unexpected-closing-bracket' + }); + } else { + const last = stack.pop()!; + if (pairs[last.char] !== char) { + diagnostics.push({ + range: new vscode.Range(lineNumber, last.index, lineNumber, last.index + 1), + message: `Mismatched brackets: expected '${pairs[last.char]}' but found '${char}'`, + severity: vscode.DiagnosticSeverity.Error, + code: 'mismatched-brackets' + }); + } + } + } + } + + // Check for unclosed brackets + for (const unclosed of stack) { + diagnostics.push({ + range: new vscode.Range(lineNumber, unclosed.index, lineNumber, unclosed.index + 1), + message: `Unclosed '${unclosed.char}'`, + severity: vscode.DiagnosticSeverity.Error, + code: 'unclosed-bracket' + }); + } + + return diagnostics; + } + + /** + * Check if a string is properly quoted (handles escaped quotes) + */ + private isProperlyQuotedString(str: string): boolean { + if (!str.startsWith('"') || !str.endsWith('"')) { + return false; + } + + // For simple validation, just check if it starts and ends with quotes + // and doesn't have unescaped quotes in the middle + let escapeNext = false; + + for (let i = 1; i < str.length - 1; i++) { + const char = str[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === '\\') { + escapeNext = true; + continue; + } + + if (char === '"') { + // Unescaped quote in the middle - this is actually valid in ASHES + // as long as the string is properly closed + continue; + } + } + + return true; + } + + /** + * Validate overall document structure + */ + private validateDocumentStructure(lines: string[]): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Check for at least one event definition + const hasEvent = lines.some(line => line.trim().startsWith(':')); + if (!hasEvent) { + diagnostics.push({ + range: new vscode.Range(0, 0, 0, 0), + message: "ASHES file should contain at least one event definition (starting with :)", + severity: vscode.DiagnosticSeverity.Warning, + code: 'no-events' + }); + } + + // Check for proper event naming conventions + const events = lines.filter(line => line.trim().startsWith(':')); + for (const eventLine of events) { + const eventMatch = eventLine.match(/^(\s*):([a-zA-Z_][a-zA-Z0-9_]*)/); + if (eventMatch) { + const eventName = eventMatch[2]; + + // Suggest common event names if the current one seems unusual + const commonEvents = ['init', 'ready', 'use', 'look', 'talk', 'walk', 'interact']; + if (!commonEvents.includes(eventName.toLowerCase()) && eventName.length > 20) { + const eventIndex = eventLine.indexOf(eventName); + const lineNumber = lines.indexOf(eventLine); + diagnostics.push({ + range: new vscode.Range(lineNumber, eventIndex, lineNumber, eventIndex + eventName.length), + message: `Consider using a shorter, more descriptive event name. Common events: ${commonEvents.join(', ')}`, + severity: vscode.DiagnosticSeverity.Information, + code: 'event-naming-suggestion' + }); + } + } + } + + return diagnostics; + } +} diff --git a/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json b/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json new file mode 100644 index 00000000..47e42f3e --- /dev/null +++ b/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json @@ -0,0 +1,418 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "ASHES", + "scopeName": "source.ashes", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#events" + }, + { + "include": "#dialog-blocks" + }, + { + "include": "#dialog-choices" + }, + { + "include": "#commands" + }, + { + "include": "#variables" + }, + { + "include": "#control-flow" + }, + { + "include": "#strings" + }, + { + "include": "#numbers" + }, + { + "include": "#operators" + }, + { + "include": "#keywords" + }, + { + "include": "#builtin-variables" + }, + { + "include": "#global-ids" + } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.ashes", + "begin": "#", + "end": "$", + "patterns": [ + { + "name": "comment.line.number-sign.ashes", + "match": "." + } + ] + } + ] + }, + "events": { + "patterns": [ + { + "name": "entity.name.function.event.ashes", + "match": "^\\s*(:)([a-zA-Z_][a-zA-Z0-9_]*)(\\s*\\|\\s*[A-Z_]+)*", + "captures": { + "1": { + "name": "punctuation.definition.event.ashes" + }, + "2": { + "name": "entity.name.function.event.ashes" + }, + "3": { + "name": "entity.name.tag.event-flags.ashes" + } + } + }, + { + "name": "entity.name.function.event-with-target.ashes", + "match": "^\\s*(:)([a-zA-Z_][a-zA-Z0-9_]*)(\\s*\\|\\s*[A-Z_]+)*\\s+(\"[^\"]*\")", + "captures": { + "1": { + "name": "punctuation.definition.event.ashes" + }, + "2": { + "name": "entity.name.function.event.ashes" + }, + "3": { + "name": "entity.name.tag.event-flags.ashes" + }, + "4": { + "name": "string.quoted.double.event-target.ashes" + } + } + } + ] + }, + "dialog-blocks": { + "patterns": [ + { + "name": "meta.dialog-block.ashes", + "begin": "^(\\s*)(\\?\\!)", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.indent.ashes" + }, + "2": { + "name": "keyword.control.dialog.ashes" + } + }, + "end": "^(?=\\1[^\\s]|\\s*$)", + "patterns": [ + { + "include": "#dialog-choices" + }, + { + "include": "#commands" + }, + { + "include": "#variables" + }, + { + "include": "#control-flow" + }, + { + "include": "#strings" + }, + { + "include": "#comments" + } + ] + } + ] + }, + "dialog-choices": { + "patterns": [ + { + "name": "meta.dialog-choice.ashes", + "begin": "^(\\s*)(-)\\s*(\"[^\"]*\")\\s*(\\[.*?\\])?", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.indent.ashes" + }, + "2": { + "name": "punctuation.definition.dialog-choice.ashes" + }, + "3": { + "name": "string.quoted.double.dialog-choice.ashes" + }, + "4": { + "name": "meta.condition.dialog-choice.ashes" + } + }, + "end": "^(?=\\1[^\\s-]|\\s*$)", + "patterns": [ + { + "include": "#commands" + }, + { + "include": "#variables" + }, + { + "include": "#control-flow" + }, + { + "include": "#strings" + }, + { + "include": "#comments" + } + ] + } + ] + }, + "commands": { + "patterns": [ + { + "name": "support.function.command.ashes", + "match": "\\b(accept_input|anim|anim_block|block_say|camera_push|camera_push_block|camera_set_limits|camera_set_pos|camera_set_pos_block|camera_set_target|camera_set_target_block|camera_set_zoom|camera_set_zoom_block|camera_set_zoom_height|camera_set_zoom_height_block|camera_shift|camera_shift_block|change_scene|custom|dec_global|enable_terrain|end_block_say|hide_menu|inc_global|inventory_add|inventory_remove|item_count_add|play_lib_snd|play_snd|play_video|print|print_internal|queue_event|queue_resource|rand_global|repeat|save_game|say|say_last_dialog_option|say_random|say_sequence|sched_event|set_active|set_active_if_exists|set_angle|set_animations|set_direction|set_global|set_globals|set_gui_visible|set_interactive|set_item_custom_data|set_speed|set_state|set_tooltip|show_menu|slide|slide_block|spawn|stop|stop_snd|teleport|teleport_pos|transition|turn_to|wait|walk|walk_block|walk_to_pos|walk_to_pos_block)\\b", + "captures": { + "1": { + "name": "support.function.command.ashes" + } + } + }, + { + "name": "meta.function-call.ashes", + "begin": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.function.command.ashes" + }, + "2": { + "name": "punctuation.definition.parameters.begin.ashes" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.parameters.end.ashes" + } + }, + "patterns": [ + { + "include": "#strings" + }, + { + "include": "#numbers" + }, + { + "include": "#global-ids" + }, + { + "include": "#builtin-variables" + }, + { + "include": "#operators" + } + ] + } + ] + }, + "variables": { + "patterns": [ + { + "name": "storage.type.variable.ashes", + "match": "\\b(var|global)\\b", + "captures": { + "1": { + "name": "storage.type.variable.ashes" + } + } + }, + { + "name": "variable.other.ashes", + "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=)", + "captures": { + "1": { + "name": "variable.other.ashes" + }, + "2": { + "name": "keyword.operator.assignment.ashes" + } + } + } + ] + }, + "control-flow": { + "patterns": [ + { + "name": "keyword.control.ashes", + "match": "\\b(if|elif|else|while|break|done|stop|pass)\\b", + "captures": { + "1": { + "name": "keyword.control.ashes" + } + } + }, + { + "name": "meta.control-flow.ashes", + "begin": "\\b(if|elif|while)\\s+", + "beginCaptures": { + "1": { + "name": "keyword.control.ashes" + } + }, + "end": ":", + "endCaptures": { + "0": { + "name": "punctuation.separator.condition.ashes" + } + }, + "patterns": [ + { + "include": "#operators" + }, + { + "include": "#builtin-variables" + }, + { + "include": "#global-ids" + }, + { + "include": "#strings" + }, + { + "include": "#numbers" + }, + { + "include": "#keywords" + }, + { + "include": "#variables" + }, + { + "include": "#conditional-expressions" + } + ] + } + ] + }, + "conditional-expressions": { + "patterns": [ + { + "name": "meta.conditional-expression.ashes", + "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(==|!=|<=|>=|<|>)\\s*([a-zA-Z_][a-zA-Z0-9_]*|\\d+)\\b", + "captures": { + "1": { + "name": "variable.other.conditional.ashes" + }, + "2": { + "name": "keyword.operator.comparison.ashes" + }, + "3": { + "name": "variable.other.conditional.ashes" + } + } + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.ashes", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "name": "constant.character.escape.ashes", + "match": "\\\\." + }, + { + "name": "variable.other.global.ashes", + "match": "\\{[^}]+\\}" + } + ] + }, + { + "name": "string.quoted.single.ashes", + "begin": "'", + "end": "'", + "patterns": [ + { + "name": "constant.character.escape.ashes", + "match": "\\\\." + } + ] + } + ] + }, + "numbers": { + "patterns": [ + { + "name": "constant.numeric.integer.ashes", + "match": "\\b\\d+\\b" + }, + { + "name": "constant.numeric.float.ashes", + "match": "\\b\\d+\\.\\d+\\b" + } + ] + }, + "operators": { + "patterns": [ + { + "name": "keyword.operator.arithmetic.ashes", + "match": "\\+|-|\\*|/" + }, + { + "name": "keyword.operator.comparison.ashes", + "match": "==|!=|<=|>=|<|>" + }, + { + "name": "keyword.operator.logical.ashes", + "match": "\\b(and|or|not)\\b|!" + }, + { + "name": "keyword.operator.assignment.ashes", + "match": "=" + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "constant.language.boolean.ashes", + "match": "\\b(true|false|nil)\\b" + }, + { + "name": "keyword.other.ashes", + "match": "\\b(in|is|active|inventory)\\b" + } + ] + }, + "builtin-variables": { + "patterns": [ + { + "name": "variable.language.builtin.ashes", + "match": "\\b(CURRENT_PLAYER|ESC_LAST_SCENE|ESC_CURRENT_SCENE|FORCE_LAST_SCENE_NULL|ANIMATION_RESOURCES)\\b" + } + ] + }, + "global-ids": { + "patterns": [ + { + "name": "variable.other.global-id.ashes", + "match": "\\$([a-zA-Z_][a-zA-Z0-9_]*)", + "captures": { + "1": { + "name": "variable.other.global-id.ashes" + } + } + } + ] + } + } +} diff --git a/vscode-extension-ashes/tsconfig.json b/vscode-extension-ashes/tsconfig.json new file mode 100644 index 00000000..3d2f8f67 --- /dev/null +++ b/vscode-extension-ashes/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": [ + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +}