Merge branch 'vscode-extension'

This commit is contained in:
2025-09-07 20:11:23 +02:00
21 changed files with 4366 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
{ {
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.insertSpaces": false, "editor.insertSpaces": false,
"editor.detectIndentation": false "editor.detectIndentation": false,
"godotTools.editorPath.godot4": "/home/oier/Descargas/Godot_v4.4.1-stable_linux.x86_64"
} }

216
install-extension.sh Executable file
View File

@@ -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 "$@"

4
vscode-extension-ashes/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
out
node_modules
*.vsix
.DS_Store

View File

@@ -0,0 +1 @@
v22

View File

@@ -0,0 +1,10 @@
.vscode/**
.vscode-test/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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"
}
}

49
vscode-extension-ashes/package-lock.json generated Normal file
View File

@@ -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"
}
}
}
}

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}));
}
}

View File

@@ -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<string, string[]> = new Map();
private lastUpdateTime: Map<string, number> = 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;
}
}

View File

@@ -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 = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: var(--vscode-font-family); }
</style>
</head>
<body>
<h1>ASHES Command Reference</h1>
<p>No workspace found. Please open a workspace to view commands.</p>
</body>
</html>
`;
return;
}
// Get dynamic commands
const commands = getCommands(workspaceRoot);
// 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 ? '<strong>Required</strong>' : '<strong>Optional</strong>';
const typeIndicator = `<code>${param.type}</code>`;
const nameIndicator = `<strong>${param.name}</strong>`;
const defaultValue = param.defaultValue ? ` <em>(default: <code>${param.defaultValue}</code>)</em>` : '';
// Create example parameter
const exampleParam = param.required ?
`${param.name}: ${param.type}` :
`[${param.name}: ${param.type}]`;
exampleParams.push(exampleParam);
return `${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}`;
}).join('<br>');
// Create example usage
exampleUsage = `<br><br><strong>Example:</strong><br><code>${command.name}(${exampleParams.join(', ')})</code>`;
}
return `<tr>
<td><code class="command-link" data-command="${command.name}">${command.name}</code></td>
<td>${command.description}${exampleUsage}</td>
<td>${paramInfo}</td>
</tr>`;
}).join('');
panel.webview.html = `
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: var(--vscode-font-family);
margin: 20px;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid var(--vscode-panel-border);
padding: 8px;
text-align: left;
}
th {
background-color: var(--vscode-panel-background);
}
code {
background-color: var(--vscode-textCodeBlock-background);
padding: 2px 4px;
}
.command-link {
cursor: pointer;
color: var(--vscode-textLink-foreground);
text-decoration: underline;
}
.command-link:hover {
color: var(--vscode-textLink-activeForeground);
}
.info {
margin-bottom: 20px;
padding: 10px;
background-color: var(--vscode-textBlockQuote-background);
border-left: 4px solid var(--vscode-textBlockQuote-border);
}
</style>
</head>
<body>
<h1>ASHES Command Reference</h1>
<div class="info">
💡 <strong>Tip:</strong> Click on any command name to navigate to its source file!
</div>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
<th>Parameters</th>
</tr>
</thead>
<tbody>
${commandsHtml}
</tbody>
</table>
<script>
const vscode = acquireVsCodeApi();
// Add click handlers to all command links
document.addEventListener('DOMContentLoaded', function() {
const commandLinks = document.querySelectorAll('.command-link');
commandLinks.forEach(link => {
link.addEventListener('click', function() {
const commandName = this.getAttribute('data-command');
if (commandName) {
vscode.postMessage({
command: 'navigateToCommand',
commandName: commandName
});
}
});
});
});
</script>
</body>
</html>
`;
});
// 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() {}

View File

@@ -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<string> = new Set();
private builtinVariables: Set<string> = new Set([
'CURRENT_PLAYER',
'ESC_LAST_SCENE',
'ESC_CURRENT_SCENE',
'FORCE_LAST_SCENE_NULL',
'ANIMATION_RESOURCES'
]);
private keywords: Set<string> = 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;
}
}

View File

@@ -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"
}
}
}
]
}
}
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "out",
"lib": [
"ES2020"
],
"sourceMap": true,
"rootDir": "src",
"strict": true
},
"exclude": [
"node_modules",
".vscode-test"
]
}