From 7479320aab2eb5b8ad2954868912d75e9276f22d Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 01:47:30 +0200 Subject: [PATCH 1/8] Extension init, syntax highlighting, autocompletion, install script --- .vscode/settings.json | 3 +- install-extension.sh | 171 ++++++ vscode-extension-ashes/.gitignore | 4 + vscode-extension-ashes/.vscodeignore | 10 + vscode-extension-ashes/INSTALL.md | 84 +++ vscode-extension-ashes/README.md | 93 ++++ .../language-configuration.json | 35 ++ vscode-extension-ashes/package-lock.json | 49 ++ vscode-extension-ashes/package.json | 79 +++ vscode-extension-ashes/sample.esc | 102 ++++ vscode-extension-ashes/snippets/ashes.json | 176 +++++++ vscode-extension-ashes/src/extension.ts | 495 ++++++++++++++++++ .../syntaxes/ashes.tmLanguage.json | 390 ++++++++++++++ vscode-extension-ashes/tsconfig.json | 17 + vscode-extension-ashes/vs-escoria4-ashes | 1 + 15 files changed, 1708 insertions(+), 1 deletion(-) create mode 100755 install-extension.sh create mode 100644 vscode-extension-ashes/.gitignore create mode 100644 vscode-extension-ashes/.vscodeignore create mode 100644 vscode-extension-ashes/INSTALL.md create mode 100644 vscode-extension-ashes/README.md create mode 100644 vscode-extension-ashes/language-configuration.json create mode 100644 vscode-extension-ashes/package-lock.json create mode 100644 vscode-extension-ashes/package.json create mode 100644 vscode-extension-ashes/sample.esc create mode 100644 vscode-extension-ashes/snippets/ashes.json create mode 100644 vscode-extension-ashes/src/extension.ts create mode 100644 vscode-extension-ashes/syntaxes/ashes.tmLanguage.json create mode 100644 vscode-extension-ashes/tsconfig.json create mode 120000 vscode-extension-ashes/vs-escoria4-ashes diff --git a/.vscode/settings.json b/.vscode/settings.json index d057c896..ccdf8c37 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "editor.tabSize": 4, "editor.insertSpaces": false, - "editor.detectIndentation": false +"editor.detectIndentation": false, +"godotTools.editorPath.godot4": "/home/oier/Descargas/Godot_v4.4.1-stable_linux.x86_64" } diff --git a/install-extension.sh b/install-extension.sh new file mode 100755 index 00000000..8fa23908 --- /dev/null +++ b/install-extension.sh @@ -0,0 +1,171 @@ +#!/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..." + + # Check if destination already exists + if [ -e "$EXTENSION_DEST" ]; then + print_warning "Destination already exists: $EXTENSION_DEST" + read -p "Remove existing installation? (y/n) [y]: " remove_existing + remove_existing=${remove_existing:-y} + + if [[ $remove_existing =~ ^[Yy]$ ]]; then + print_info "Removing existing installation..." + rm -rf "$EXTENSION_DEST" || { + print_error "Failed to remove existing installation" + exit 1 + } + else + print_info "Installation cancelled." + exit 0 + fi + fi + + # Create the symlink + print_info "Creating symlink..." + if ln -s "$EXTENSION_SOURCE" "$EXTENSION_DEST"; then + print_success "Extension installed successfully!" + echo + print_info "Next steps:" + print_info "1. Restart VSCode/VSCodium" + print_info "2. Open any .esc file to test the extension" + print_info "3. The language should be automatically detected as 'ASHES'" + echo + print_info "To uninstall, simply remove the symlink:" + print_info " rm '$EXTENSION_DEST'" + else + print_error "Failed to create symlink" + print_error "Make sure you have write permissions to: $EXTENSIONS_DIR" + exit 1 + fi +} + +# Run main function +main "$@" diff --git a/vscode-extension-ashes/.gitignore b/vscode-extension-ashes/.gitignore new file mode 100644 index 00000000..154499d3 --- /dev/null +++ b/vscode-extension-ashes/.gitignore @@ -0,0 +1,4 @@ +out +node_modules +*.vsix +.DS_Store diff --git a/vscode-extension-ashes/.vscodeignore b/vscode-extension-ashes/.vscodeignore new file mode 100644 index 00000000..38999676 --- /dev/null +++ b/vscode-extension-ashes/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts diff --git a/vscode-extension-ashes/INSTALL.md b/vscode-extension-ashes/INSTALL.md new file mode 100644 index 00000000..34389c14 --- /dev/null +++ b/vscode-extension-ashes/INSTALL.md @@ -0,0 +1,84 @@ +# Installation Guide for ASHES Language Support Extension + +## 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` (or similar) + +3. **Reload VS Code** or restart the application + +4. **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. **Install dependencies**: + ```bash + npm install + ``` + +2. **Compile TypeScript**: + ```bash + npm run compile + ``` + +3. **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) + +## Uninstalling + +Simply delete the extension folder from your VS Code extensions directory and restart VS Code. diff --git a/vscode-extension-ashes/README.md b/vscode-extension-ashes/README.md new file mode 100644 index 00000000..33fe6a7a --- /dev/null +++ b/vscode-extension-ashes/README.md @@ -0,0 +1,93 @@ +# 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 +- **Hover Information**: Detailed information about commands and variables on hover +- **Code Snippets**: Pre-built snippets for common ASHES patterns +- **Command Reference**: Built-in command reference panel +- **Smart Indentation**: Proper indentation rules for ASHES code structure + +## 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 +- Command parameter hints + +### 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 + +### 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 parameters + +## Language Features + +### Syntax Highlighting +- Events are highlighted in blue +- Commands are highlighted in green +- Variables are highlighted in orange +- Strings are highlighted in yellow +- Comments are highlighted in gray +- Dialog blocks have special highlighting + +### Smart Indentation +- Automatic indentation for events, control flow, and dialog blocks +- Proper outdenting for `break`, `done`, `else`, etc. + +### Folding +- Events can be folded for better code organization +- Dialog blocks can be folded + +## Contributing + +This extension is designed specifically for the Escoria framework and ASHES language. If you find issues or want to add features, please contribute to the project. + +## License + +This extension is part of the Gymkhana project and follows the same license terms. diff --git a/vscode-extension-ashes/language-configuration.json b/vscode-extension-ashes/language-configuration.json new file mode 100644 index 00000000..09cf6aca --- /dev/null +++ b/vscode-extension-ashes/language-configuration.json @@ -0,0 +1,35 @@ +{ + "comments": { + "lineComment": "#" + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "folding": { + "markers": { + "start": "^\\s*:", + "end": "^\\s*$" + } + }, + "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)", + "indentationRules": { + "increaseIndentPattern": "^\\s*(?!\\s*#).*:\\s*$|^\\s*if\\s+.*:|^\\s*elif\\s+.*:|^\\s*else\\s*:|^\\s*while\\s+.*:|^\\s*-\\s+.*:|^\\s*\\?!\\s*$", + "decreaseIndentPattern": "^\\s*(elif|else|done|break|stop)\\b" + } +} diff --git a/vscode-extension-ashes/package-lock.json b/vscode-extension-ashes/package-lock.json new file mode 100644 index 00000000..c19417d1 --- /dev/null +++ b/vscode-extension-ashes/package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "ashes-language-support", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ashes-language-support", + "version": "0.1.0", + "license": "ISC", + "devDependencies": { + "@types/node": "16.x", + "@types/vscode": "^1.74.0", + "typescript": "^4.9.4" + }, + "engines": { + "vscode": "^1.74.0" + } + }, + "node_modules/@types/node": { + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.103.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.103.0.tgz", + "integrity": "sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + } +} diff --git a/vscode-extension-ashes/package.json b/vscode-extension-ashes/package.json new file mode 100644 index 00000000..0699ecd9 --- /dev/null +++ b/vscode-extension-ashes/package.json @@ -0,0 +1,79 @@ +{ + "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" + } + ], + "menus": { + "commandPalette": [ + { + "command": "ashes.showCommandReference" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "16.x", + "@types/vscode": "^1.74.0", + "typescript": "^4.9.4" + }, + "author": "", + "license": "ISC" +} diff --git a/vscode-extension-ashes/sample.esc b/vscode-extension-ashes/sample.esc new file mode 100644 index 00000000..8c79b670 --- /dev/null +++ b/vscode-extension-ashes/sample.esc @@ -0,0 +1,102 @@ +# Sample ASHES script for testing the VS Code extension +# This file demonstrates various ASHES language features + +:setup + # Global variable declarations + global game_started = false + global player_name = "Player" + global score = 0 + + # Local variables + var tutorial_completed = false + var current_room = "intro" + + # Set initial state + set_global("game_started", true) + set_active($player, true) + teleport($player, $start_position) + +:ready + # Check if this is the first time + if !game_started: + say($player, "Welcome to the game!", "welcome_message") + game_started = true + else: + say($player, "Welcome back!", "welcome_back") + +:action1 + # Simple interaction + say($player, "You examine the object carefully.", "examine_object") + + # Check inventory + if $magic_key in inventory: + say($player, "You have the magic key!", "has_key") + else: + say($player, "You need a key to proceed.", "needs_key") + +:action2 + # Dialog system example + say($player, "You approach the mysterious character.", "approach_character") + + ?! + - "Hello, who are you?" [!name_known] + say_last_dialog_option() + say($npc, "I am the guardian of this place.", "guardian_intro") + global name_known = true + - "Can you help me?" [name_known] + say_last_dialog_option() + say($npc, "I can guide you, but you must prove yourself.", "guardian_help") + ?! + - "How can I prove myself?" + say_last_dialog_option() + say($npc, "Find the three ancient artifacts.", "guardian_task") + - "I'm not interested." + say_last_dialog_option() + say($npc, "Very well, good luck on your own.", "guardian_dismiss") + done + - "Goodbye" + say_last_dialog_option() + say($npc, "Farewell, traveler.", "guardian_farewell") + done + +:action3 + # Complex logic example + var artifacts_found = 0 + + # Check for artifacts + if $artifact1_collected: + artifacts_found += 1 + if $artifact2_collected: + artifacts_found += 1 + if $artifact3_collected: + artifacts_found += 1 + + # Conditional responses + if artifacts_found == 3: + say($player, "I have found all three artifacts!", "all_artifacts") + change_scene("res://rooms/victory_room.tscn") + elif artifacts_found > 0: + say($player, "I have found " + str(artifacts_found) + " artifacts so far.", "some_artifacts") + else: + say($player, "I haven't found any artifacts yet.", "no_artifacts") + +:use | TK + # Use with item + if $magic_key in inventory: + say($player, "You use the magic key.", "use_key") + set_active($locked_door, false) + play_snd("res://sounds/door_open.ogg", _sfx) + else: + say($player, "You can't use this without the right item.", "cant_use") + +:look + # Look action + say($player, "You look around carefully.", "look_around") + + # Conditional descriptions + if $secret_door is active: + say($player, "You notice a hidden passage.", "hidden_passage") + else: + say($player, "Nothing unusual catches your eye.", "nothing_unusual") + +# End of sample script diff --git a/vscode-extension-ashes/snippets/ashes.json b/vscode-extension-ashes/snippets/ashes.json new file mode 100644 index 00000000..1cdb2767 --- /dev/null +++ b/vscode-extension-ashes/snippets/ashes.json @@ -0,0 +1,176 @@ +{ + "Event": { + "prefix": "event", + "body": [ + ":${1:event_name}", + "\t${2:// Event code here}" + ], + "description": "Create a new ASHES event" + }, + "Event with target": { + "prefix": "eventtarget", + "body": [ + ":${1:event_name} \"${2:target_id}\"", + "\t${3:// Event code here}" + ], + "description": "Create a new ASHES event with target" + }, + "Event with flags": { + "prefix": "eventflags", + "body": [ + ":${1:event_name} | ${2:NO_UI} | ${3:NO_TT}", + "\t${4:// Event code here}" + ], + "description": "Create a new ASHES event with flags" + }, + "Event with flags and target": { + "prefix": "eventflagstarget", + "body": [ + ":${1:event_name} | ${2:NO_UI} | ${3:NO_TT} \"${4:target_id}\"", + "\t${5:// Event code here}" + ], + "description": "Create a new ASHES event with flags and target" + }, + "Say command": { + "prefix": "say", + "body": [ + "say(${1:player}, \"${2:text}\"${3:, \"${4:translation_key}\"})" + ], + "description": "Say command for dialog" + }, + "Set global variable": { + "prefix": "setglobal", + "body": [ + "set_global(\"${1:variable_name}\", ${2:value})" + ], + "description": "Set a global variable" + }, + "Global variable declaration": { + "prefix": "global", + "body": [ + "global ${1:variable_name}${2: = ${3:value}}" + ], + "description": "Declare a global variable" + }, + "Local variable declaration": { + "prefix": "var", + "body": [ + "var ${1:variable_name}${2: = ${3:value}}" + ], + "description": "Declare a local variable" + }, + "If statement": { + "prefix": "if", + "body": [ + "if ${1:condition}:", + "\t${2:// Code here}" + ], + "description": "If statement" + }, + "If-else statement": { + "prefix": "ifelse", + "body": [ + "if ${1:condition}:", + "\t${2:// Code here}", + "else:", + "\t${3:// Code here}" + ], + "description": "If-else statement" + }, + "While loop": { + "prefix": "while", + "body": [ + "while ${1:condition}:", + "\t${2:// Code here}" + ], + "description": "While loop" + }, + "Dialog block": { + "prefix": "dialog", + "body": [ + "?!", + "\t- \"${1:choice_text}\"", + "\t\t${2:// Code here}" + ], + "description": "Dialog block with choice" + }, + "Dialog choice with condition": { + "prefix": "dialogif", + "body": [ + "?!", + "\t- \"${1:choice_text}\" [${2:condition}]", + "\t\t${3:// Code here}" + ], + "description": "Dialog choice with condition" + }, + "Change scene": { + "prefix": "changescene", + "body": [ + "change_scene(\"${1:scene_path}\"${2:, ${3:true}})" + ], + "description": "Change to a different scene" + }, + "Set active": { + "prefix": "setactive", + "body": [ + "set_active($${1:object_id}, ${2:true})" + ], + "description": "Set object active/inactive" + }, + "Teleport": { + "prefix": "teleport", + "body": [ + "teleport($${1:object_id}, $${2:target_id})" + ], + "description": "Teleport object to target" + }, + "Walk to": { + "prefix": "walk", + "body": [ + "walk($${1:object_id}, $${2:target_id})" + ], + "description": "Walk object to target" + }, + "Play sound": { + "prefix": "playsnd", + "body": [ + "play_snd(\"${1:sound_path}\"${2:, ${3:_music}})" + ], + "description": "Play a sound file" + }, + "Play video": { + "prefix": "playvideo", + "body": [ + "play_video(\"${1:video_path}\")" + ], + "description": "Play a video file" + }, + "Inventory add": { + "prefix": "inventoryadd", + "body": [ + "inventory_add($${1:item_id})" + ], + "description": "Add item to inventory" + }, + "Inventory remove": { + "prefix": "inventoryremove", + "body": [ + "inventory_remove($${1:item_id})" + ], + "description": "Remove item from inventory" + }, + "Comment": { + "prefix": "comment", + "body": [ + "# ${1:comment}" + ], + "description": "Add a comment" + }, + "Print": { + "prefix": "print", + "body": [ + "print(\"${1:message}\")" + ], + "description": "Print debug message" + } +} diff --git a/vscode-extension-ashes/src/extension.ts b/vscode-extension-ashes/src/extension.ts new file mode 100644 index 00000000..3e15de7b --- /dev/null +++ b/vscode-extension-ashes/src/extension.ts @@ -0,0 +1,495 @@ +import * as vscode from 'vscode'; + +// ASHES commands with their descriptions and parameters +const ASHES_COMMANDS = [ + { + name: 'accept_input', + description: 'Accept specific input types', + parameters: ['input_type'] + }, + { + name: 'anim', + description: 'Play animation on object', + parameters: ['object_id', 'animation_name'] + }, + { + name: 'anim_block', + description: 'Play animation and wait for completion', + parameters: ['object_id', 'animation_name'] + }, + { + name: 'block_say', + description: 'Start a block of say commands', + parameters: [] + }, + { + name: 'camera_push', + description: 'Push camera to new position', + parameters: ['x', 'y'] + }, + { + name: 'camera_push_block', + description: 'Push camera and wait for completion', + parameters: ['x', 'y'] + }, + { + name: 'camera_set_limits', + description: 'Set camera movement limits', + parameters: ['left', 'top', 'right', 'bottom'] + }, + { + name: 'camera_set_pos', + description: 'Set camera position', + parameters: ['x', 'y'] + }, + { + name: 'camera_set_pos_block', + description: 'Set camera position and wait', + parameters: ['x', 'y'] + }, + { + name: 'camera_set_target', + description: 'Set camera target', + parameters: ['object_id'] + }, + { + name: 'camera_set_target_block', + description: 'Set camera target and wait', + parameters: ['object_id'] + }, + { + name: 'camera_set_zoom', + description: 'Set camera zoom level', + parameters: ['zoom_level'] + }, + { + name: 'camera_set_zoom_block', + description: 'Set camera zoom and wait', + parameters: ['zoom_level'] + }, + { + name: 'camera_set_zoom_height', + description: 'Set camera zoom height', + parameters: ['height'] + }, + { + name: 'camera_set_zoom_height_block', + description: 'Set camera zoom height and wait', + parameters: ['height'] + }, + { + name: 'camera_shift', + description: 'Shift camera position', + parameters: ['x', 'y'] + }, + { + name: 'camera_shift_block', + description: 'Shift camera and wait', + parameters: ['x', 'y'] + }, + { + name: 'change_scene', + description: 'Change to a different scene', + parameters: ['scene_path', 'enable_transition', 'run_events'] + }, + { + name: 'custom', + description: 'Execute custom command', + parameters: ['command_name', '...args'] + }, + { + name: 'dec_global', + description: 'Decrement global variable', + parameters: ['variable_name'] + }, + { + name: 'enable_terrain', + description: 'Enable/disable terrain', + parameters: ['terrain_name', 'enabled'] + }, + { + name: 'end_block_say', + description: 'End a block of say commands', + parameters: [] + }, + { + name: 'hide_menu', + description: 'Hide menu', + parameters: ['menu_name'] + }, + { + name: 'inc_global', + description: 'Increment global variable', + parameters: ['variable_name'] + }, + { + name: 'inventory_add', + description: 'Add item to inventory', + parameters: ['item_id'] + }, + { + name: 'inventory_remove', + description: 'Remove item from inventory', + parameters: ['item_id'] + }, + { + name: 'item_count_add', + description: 'Add to item count', + parameters: ['item_id', 'count'] + }, + { + name: 'play_lib_snd', + description: 'Play library sound', + parameters: ['filename', 'namespace'] + }, + { + name: 'play_snd', + description: 'Play sound file', + parameters: ['sound_path', 'type'] + }, + { + name: 'play_video', + description: 'Play video file', + parameters: ['video_path'] + }, + { + name: 'print', + description: 'Print debug message', + parameters: ['message'] + }, + { + name: 'print_internal', + description: 'Print internal message', + parameters: ['message'] + }, + { + name: 'queue_event', + description: 'Queue event for later execution', + parameters: ['object_id', 'event_name'] + }, + { + name: 'queue_resource', + description: 'Queue resource for loading', + parameters: ['resource_path'] + }, + { + name: 'rand_global', + description: 'Set random value to global', + parameters: ['variable_name', 'min', 'max'] + }, + { + name: 'repeat', + description: 'Repeat command', + parameters: ['count', 'command'] + }, + { + name: 'save_game', + description: 'Save game state', + parameters: ['save_name'] + }, + { + name: 'say', + description: 'Display dialog text', + parameters: ['speaker', 'text', 'translation_key', 'type'] + }, + { + name: 'say_last_dialog_option', + description: 'Say the last dialog option', + parameters: [] + }, + { + name: 'say_random', + description: 'Say random text from list', + parameters: ['speaker', 'list_id', 'length'] + }, + { + name: 'say_sequence', + description: 'Say text sequence', + parameters: ['speaker', 'list_id', 'length', 'loop'] + }, + { + name: 'sched_event', + description: 'Schedule event for later', + parameters: ['delay', 'object_id', 'event_name'] + }, + { + name: 'set_active', + description: 'Set object active/inactive', + parameters: ['object_id', 'active'] + }, + { + name: 'set_active_if_exists', + description: 'Set object active if it exists', + parameters: ['object_id', 'active'] + }, + { + name: 'set_angle', + description: 'Set object angle', + parameters: ['object_id', 'angle'] + }, + { + name: 'set_animations', + description: 'Set object animations', + parameters: ['object_id', 'animations'] + }, + { + name: 'set_direction', + description: 'Set object direction', + parameters: ['object_id', 'direction'] + }, + { + name: 'set_global', + description: 'Set global variable', + parameters: ['variable_name', 'value', 'force'] + }, + { + name: 'set_globals', + description: 'Set multiple global variables', + parameters: ['variables_dict'] + }, + { + name: 'set_gui_visible', + description: 'Set GUI visibility', + parameters: ['visible'] + }, + { + name: 'set_interactive', + description: 'Set object interactive state', + parameters: ['object_id', 'interactive'] + }, + { + name: 'set_item_custom_data', + description: 'Set item custom data', + parameters: ['item_id', 'key', 'value'] + }, + { + name: 'set_speed', + description: 'Set object speed', + parameters: ['object_id', 'speed'] + }, + { + name: 'set_state', + description: 'Set object state', + parameters: ['object_id', 'state'] + }, + { + name: 'set_tooltip', + description: 'Set object tooltip', + parameters: ['object_id', 'action', 'text'] + }, + { + name: 'show_menu', + description: 'Show menu', + parameters: ['menu_name'] + }, + { + name: 'slide', + description: 'Slide object to position', + parameters: ['object_id', 'x', 'y', 'duration'] + }, + { + name: 'slide_block', + description: 'Slide object and wait', + parameters: ['object_id', 'x', 'y', 'duration'] + }, + { + name: 'spawn', + description: 'Spawn object', + parameters: ['object_id', 'x', 'y'] + }, + { + name: 'stop', + description: 'Stop current event', + parameters: [] + }, + { + name: 'stop_snd', + description: 'Stop sound', + parameters: ['sound_type'] + }, + { + name: 'teleport', + description: 'Teleport object to target', + parameters: ['object_id', 'target_id'] + }, + { + name: 'teleport_pos', + description: 'Teleport object to position', + parameters: ['object_id', 'x', 'y'] + }, + { + name: 'transition', + description: 'Play transition effect', + parameters: ['transition_type', 'duration'] + }, + { + name: 'turn_to', + description: 'Turn object to face target', + parameters: ['object_id', 'target_id'] + }, + { + name: 'wait', + description: 'Wait for specified time', + parameters: ['duration'] + }, + { + name: 'walk', + description: 'Walk object to target', + parameters: ['object_id', 'target_id'] + }, + { + name: 'walk_block', + description: 'Walk object and wait', + parameters: ['object_id', 'target_id'] + }, + { + name: 'walk_to_pos', + description: 'Walk object to position', + parameters: ['object_id', 'x', 'y'] + }, + { + name: 'walk_to_pos_block', + description: 'Walk object to position and wait', + parameters: ['object_id', 'x', 'y'] + } +]; + +// 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' +]; + +export function activate(context: vscode.ExtensionContext) { + console.log('ASHES Language Support extension is now active!'); + + // Register completion provider + const completionProvider = vscode.languages.registerCompletionItemProvider( + 'ashes', + { + provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { + const completions: vscode.CompletionItem[] = []; + + // Add command completions + ASHES_COMMANDS.forEach(command => { + const completion = new vscode.CompletionItem(command.name, vscode.CompletionItemKind.Function); + completion.detail = command.description; + completion.documentation = new vscode.MarkdownString( + `**${command.name}**\n\n${command.description}\n\n**Parameters:** ${command.parameters.join(', ')}` + ); + completion.insertText = new vscode.SnippetString(`${command.name}($1)`); + 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)); + const command = ASHES_COMMANDS.find(cmd => cmd.name === word); + + if (command) { + const hover = new vscode.Hover( + new vscode.MarkdownString( + `**${command.name}**\n\n${command.description}\n\n**Parameters:** ${command.parameters.join(', ')}` + ) + ); + 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 command for showing command reference + const showCommandReference = vscode.commands.registerCommand('ashes.showCommandReference', () => { + const panel = vscode.window.createWebviewPanel( + 'ashesCommandReference', + 'ASHES Command Reference', + vscode.ViewColumn.One, + {} + ); + + const commandsHtml = ASHES_COMMANDS.map(command => + ` + ${command.name} + ${command.description} + ${command.parameters.join(', ')} + ` + ).join(''); + + panel.webview.html = ` + + + + + + +

ASHES Command Reference

+ + + + + + + + + + ${commandsHtml} + +
CommandDescriptionParameters
+ + + `; + }); + + context.subscriptions.push(completionProvider, hoverProvider, showCommandReference); +} + +export function deactivate() {} diff --git a/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json b/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json new file mode 100644 index 00000000..174e2498 --- /dev/null +++ b/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json @@ -0,0 +1,390 @@ +{ + "$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" + } + ] + } + ] + }, + "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)\\b" + } + ] + }, + "builtin-variables": { + "patterns": [ + { + "name": "variable.language.builtin.ashes", + "match": "\\b(CURRENT_PLAYER|ESC_LAST_SCENE|ESC_CURRENT_SCENE|FORCE_LAST_SCENE_NULL|ANIMATION_RESOURCES)\\b" + } + ] + }, + "global-ids": { + "patterns": [ + { + "name": "variable.other.global-id.ashes", + "match": "\\$([a-zA-Z_][a-zA-Z0-9_]*)", + "captures": { + "1": { + "name": "variable.other.global-id.ashes" + } + } + } + ] + } + } +} diff --git a/vscode-extension-ashes/tsconfig.json b/vscode-extension-ashes/tsconfig.json new file mode 100644 index 00000000..3d2f8f67 --- /dev/null +++ b/vscode-extension-ashes/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": [ + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} diff --git a/vscode-extension-ashes/vs-escoria4-ashes b/vscode-extension-ashes/vs-escoria4-ashes new file mode 120000 index 00000000..11941b4a --- /dev/null +++ b/vscode-extension-ashes/vs-escoria4-ashes @@ -0,0 +1 @@ +/home/oier/.vscode-oss/extensions/vs-escoria4-ashes \ No newline at end of file From 11a7724c58086286d4651861af032328e5267daf Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 02:18:11 +0200 Subject: [PATCH 2/8] Dynamic commands --- vscode-extension-ashes/DYNAMIC_COMMANDS.md | 76 +++ vscode-extension-ashes/HOWTO.md | 355 +++++++++++++ vscode-extension-ashes/README.md | 1 + vscode-extension-ashes/package.json | 8 + vscode-extension-ashes/src/commandParser.ts | 407 +++++++++++++++ vscode-extension-ashes/src/extension.ts | 550 +++++++------------- vscode-extension-ashes/test.esc | 7 + 7 files changed, 1039 insertions(+), 365 deletions(-) create mode 100644 vscode-extension-ashes/DYNAMIC_COMMANDS.md create mode 100644 vscode-extension-ashes/HOWTO.md create mode 100644 vscode-extension-ashes/src/commandParser.ts create mode 100644 vscode-extension-ashes/test.esc diff --git a/vscode-extension-ashes/DYNAMIC_COMMANDS.md b/vscode-extension-ashes/DYNAMIC_COMMANDS.md new file mode 100644 index 00000000..0092f23d --- /dev/null +++ b/vscode-extension-ashes/DYNAMIC_COMMANDS.md @@ -0,0 +1,76 @@ +# Dynamic Command Generation + +The ASHES VSCode extension now supports dynamic command generation by parsing the `project.godot` file and extracting command information from the specified command directories. + +## How it Works + +1. **Project.godot Parsing**: The extension reads the `project.godot` file and looks for the `main/command_directories` setting in the `[escoria]` section. + +2. **Command Discovery**: It scans each directory specified in `command_directories` for `.gd` files (excluding `.gd.uid` files). + +3. **Command Parsing**: For each command file, it extracts: + - Command name (from filename) + - Description (from comment blocks) + - Parameters (from comment documentation and `configure()` method) + - Examples (if available) + +4. **Caching**: Commands are cached for 5 minutes to improve performance. + +## Configuration + +The extension automatically detects command directories from your `project.godot` file: + +```ini +[escoria] +main/command_directories=["res://addons/escoria-core/game/core-scripts/esc/commands", "res://addons/escoria-ui-return-monkey-island/esc/commands", "res://addons/escoria-ui-return-monkey-island-dialog-simple/commands"] +``` + +## Features + +- **Auto-completion**: Commands are automatically available in IntelliSense +- **Hover Documentation**: Hover over commands to see descriptions and parameters +- **Command Reference**: Use `Ctrl+Shift+P` → "ASHES: Show ASHES Command Reference" to see all commands +- **Cache Refresh**: Use `Ctrl+Shift+P` → "ASHES: Refresh ASHES Commands" to reload commands + +## Command File Format + +Commands should follow the standard Escoria format: + +```gdscript +# `command_name param1 param2 [optional_param]` +# +# Description of what the command does. +# +# **Parameters** +# +# - *param1*: Description of parameter 1 +# - *param2*: Description of parameter 2 +# - *optional_param*: Description of optional parameter (default: default_value) +# +# Example: `command_name("value1", "value2")` +# +# @ESC +extends ESCBaseCommand +class_name CommandNameCommand + +func configure() -> ESCCommandArgumentDescriptor: + return ESCCommandArgumentDescriptor.new( + 2, # Number of required parameters + [TYPE_STRING, TYPE_STRING, TYPE_STRING], # Parameter types + [null, null, "default_value"], # Default values + [true, true, false] # Required flags + ) +``` + +## Fallback Behavior + +If the extension cannot find or parse the `project.godot` file, it falls back to the default command directories: +- `res://addons/escoria-core/game/core-scripts/esc/commands` +- `res://addons/escoria-ui-return-monkey-island/esc/commands` +- `res://addons/escoria-ui-return-monkey-island-dialog-simple/commands` + +## Troubleshooting + +- **Commands not appearing**: Make sure your `project.godot` file has the correct `main/command_directories` setting +- **Outdated commands**: Use the "Refresh ASHES Commands" command to reload the cache +- **Missing descriptions**: Ensure your command files have proper comment documentation diff --git a/vscode-extension-ashes/HOWTO.md b/vscode-extension-ashes/HOWTO.md new file mode 100644 index 00000000..2b5509f1 --- /dev/null +++ b/vscode-extension-ashes/HOWTO.md @@ -0,0 +1,355 @@ +# ASHES Language Support - HowTo Guide + +This guide provides comprehensive instructions for configuring and using the ASHES Language Support extension for Visual Studio Code. + +## Table of Contents + +1. [Installation & Configuration](#installation--configuration) +2. [Available Commands & Functionality](#available-commands--functionality) +3. [Language Features](#language-features) +4. [Code Snippets](#code-snippets) +5. [Auto-completion & IntelliSense](#auto-completion--intellisense) +6. [Syntax Highlighting](#syntax-highlighting) +7. [Troubleshooting](#troubleshooting) + +## Installation & Configuration + +### Quick Installation + +1. **Copy the extension folder** to your VS Code extensions directory: + - **Linux**: `~/.vscode/extensions/` + - **macOS**: `~/.vscode/extensions/` + - **Windows**: `%USERPROFILE%\.vscode\extensions\` + +2. **Rename the folder** to `ashes-language-support-0.1.0` + +3. **Reload VS Code** or restart the application + +4. **Verify installation** by opening any `.esc` file - you should see "ASHES" in the language selector (bottom-right corner) + +### Development Mode Installation + +1. **Open VS Code** in the extension directory: + ```bash + cd vscode-extension-ashes + code . + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Compile TypeScript**: + ```bash + npm run compile + ``` + +4. **Press F5** to run the extension in a new Extension Development Host window + +### Configuration Options + +The extension automatically configures itself when you open `.esc` files. No additional configuration is required, but you can customize: + +- **File associations**: Ensure `.esc` files are associated with the ASHES language +- **Editor settings**: Configure indentation, word wrap, etc. in VS Code settings +- **Theme compatibility**: The extension works with all VS Code themes + +## Available Commands & Functionality + +### VSCode Commands + +#### `ASHES: Show Command Reference` +- **Access**: `Ctrl+Shift+P` → Type "ASHES: Show Command Reference" +- **Description**: Opens a webview panel with a complete reference of all ASHES commands +- **Features**: + - Searchable table of commands + - Parameter information + - Command descriptions + +### ASHES Language Commands + +The extension supports **60+ ASHES commands** organized by category: + +#### Animation Commands +- `anim(object_id, animation_name)` - Play animation on object +- `anim_block(object_id, animation_name)` - Play animation and wait for completion +- `set_animations(object_id, animations)` - Set object animations + +#### Camera Commands +- `camera_push(x, y)` - Push camera to new position +- `camera_set_pos(x, y)` - Set camera position +- `camera_set_target(object_id)` - Set camera target +- `camera_set_zoom(zoom_level)` - Set camera zoom level +- `camera_shift(x, y)` - Shift camera position + +#### Dialog & Text Commands +- `say(speaker, text, translation_key, type)` - Display dialog text +- `say_random(speaker, list_id, length)` - Say random text from list +- `say_sequence(speaker, list_id, length, loop)` - Say text sequence +- `block_say()` - Start a block of say commands +- `end_block_say()` - End a block of say commands + +#### Game State Commands +- `change_scene(scene_path, enable_transition, run_events)` - Change to a different scene +- `save_game(save_name)` - Save game state +- `set_global(variable_name, value, force)` - Set global variable +- `set_globals(variables_dict)` - Set multiple global variables +- `inc_global(variable_name)` - Increment global variable +- `dec_global(variable_name)` - Decrement global variable + +#### Inventory Commands +- `inventory_add(item_id)` - Add item to inventory +- `inventory_remove(item_id)` - Remove item from inventory +- `item_count_add(item_id, count)` - Add to item count +- `set_item_custom_data(item_id, key, value)` - Set item custom data + +#### Object Manipulation Commands +- `set_active(object_id, active)` - Set object active/inactive +- `set_interactive(object_id, interactive)` - Set object interactive state +- `set_state(object_id, state)` - Set object state +- `set_tooltip(object_id, action, text)` - Set object tooltip +- `teleport(object_id, target_id)` - Teleport object to target +- `teleport_pos(object_id, x, y)` - Teleport object to position +- `walk(object_id, target_id)` - Walk object to target +- `walk_to_pos(object_id, x, y)` - Walk object to position + +#### Audio/Video Commands +- `play_snd(sound_path, type)` - Play sound file +- `play_lib_snd(filename, namespace)` - Play library sound +- `play_video(video_path)` - Play video file +- `stop_snd(sound_type)` - Stop sound + +#### Utility Commands +- `print(message)` - Print debug message +- `wait(duration)` - Wait for specified time +- `transition(transition_type, duration)` - Play transition effect +- `custom(command_name, ...args)` - Execute custom command + +### Built-in Variables + +The extension provides auto-completion for these built-in variables: +- `CURRENT_PLAYER` - Current player object +- `ESC_LAST_SCENE` - Last scene identifier +- `ESC_CURRENT_SCENE` - Current scene identifier +- `FORCE_LAST_SCENE_NULL` - Force last scene to null +- `ANIMATION_RESOURCES` - Animation resources + +### Keywords + +Auto-completion for ASHES keywords: +- `var` - Local variable declaration +- `global` - Global variable declaration +- `if`, `elif`, `else` - Conditional statements +- `while` - Loop statement +- `break`, `done` - Loop control +- `stop`, `pass` - Flow control +- `true`, `false`, `nil` - Boolean values +- `and`, `or`, `not` - Logical operators +- `in`, `is`, `active` - Special operators + +## Language Features + +### Events +```ashes +:event_name + # Event code here + +:event_with_target "target_id" + # Event code here + +:event_with_flags | NO_UI | NO_TT + # Event code here +``` + +### Variables +```ashes +# Local variables +var my_variable = "value" + +# Global variables +global game_state = "playing" + +# Global IDs (with $ prefix) +set_active($player, true) +``` + +### Control Flow +```ashes +if condition: + # Code here +elif other_condition: + # Code here +else: + # Code here + +while condition: + # Code here + break # Exit loop + # or + done # Continue to next iteration +``` + +### Dialog System +```ashes +?! + - "Choice 1" + # Code for choice 1 + - "Choice 2" [condition] + # Code for choice 2 (only if condition is true) +``` + +### Comments +```ashes +# This is a line comment +``` + +## Code Snippets + +The extension provides **20+ code snippets** for common ASHES patterns: + +### Event Snippets +- `event` → Create new event +- `eventtarget` → Create event with target +- `eventflags` → Create event with flags +- `eventflagstarget` → Create event with flags and target + +### Command Snippets +- `say` → Say command +- `setglobal` → Set global variable +- `changescene` → Change scene +- `setactive` → Set object active +- `teleport` → Teleport object +- `walk` → Walk object +- `playsnd` → Play sound +- `playvideo` → Play video +- `inventoryadd` → Add to inventory +- `inventoryremove` → Remove from inventory + +### Control Flow Snippets +- `if` → If statement +- `ifelse` → If-else statement +- `while` → While loop + +### Dialog Snippets +- `dialog` → Dialog block with choice +- `dialogif` → Dialog choice with condition + +### Utility Snippets +- `var` → Local variable declaration +- `global` → Global variable declaration +- `comment` → Add comment +- `print` → Print debug message + +### Using Snippets +1. Type the snippet prefix (e.g., `event`) +2. Press `Tab` to expand +3. Use `Tab` to navigate between placeholders +4. Press `Enter` or `Escape` to finish + +## Auto-completion & IntelliSense + +### Trigger Characters +Auto-completion is triggered by: +- Space character (` `) +- Opening parenthesis (`(`) +- Dollar sign (`$`) for global IDs + +### Command Completion +- Type any ASHES command and press `Ctrl+Space` +- Commands show with descriptions and parameter hints +- Parameters are automatically inserted with placeholders + +### Variable Completion +- Built-in variables are automatically suggested +- Global IDs with `$` prefix are suggested +- Keywords are suggested in appropriate contexts + +### Hover Information +- Hover over any command to see detailed information +- Shows command description and parameter list +- Works for built-in variables and keywords + +## Syntax Highlighting + +The extension provides comprehensive syntax highlighting: + +### Color Scheme +- **Events** (`:event_name`) - Blue +- **Commands** (`say`, `set_global`, etc.) - Green +- **Variables** (`var`, `global`) - Orange +- **Strings** (`"text"`) - Yellow +- **Comments** (`# comment`) - Gray +- **Dialog blocks** (`?!`) - Special highlighting +- **Global IDs** (`$object_id`) - Distinct color + +### Smart Indentation +- Automatic indentation for events, control flow, and dialog blocks +- Proper outdenting for `break`, `done`, `else`, etc. +- Configurable through VS Code settings + +### Code Folding +- Events can be folded for better organization +- Dialog blocks can be folded +- Folding markers: `:event_name` (start), empty line (end) + +## Troubleshooting + +### Extension Not Loading +1. Check that the folder is in the correct extensions directory +2. Restart VS Code completely +3. Check the Developer Console for errors (`Help > Toggle Developer Tools`) + +### Syntax Highlighting Not Working +1. Ensure the file has a `.esc` extension +2. Check that the language is set to "ASHES" in the bottom-right corner +3. Reload the window (`Ctrl+Shift+P` → "Developer: Reload Window") + +### Auto-completion Not Working +1. Press `Ctrl+Space` to manually trigger completion +2. Check that the extension is activated (should show in the Extensions panel) +3. Ensure you're typing in a `.esc` file + +### Snippets Not Working +1. Make sure you're in a `.esc` file +2. Type the snippet prefix and press `Tab` +3. Check that the language is set to "ASHES" + +### Performance Issues +1. Large files may cause slower auto-completion +2. Consider splitting large `.esc` files into smaller ones +3. Disable other extensions if needed + +## Advanced Usage + +### Custom Commands +The extension supports custom commands through the `custom()` function: +```ashes +custom("my_custom_command", "param1", "param2") +``` + +### File Organization +- Use multiple `.esc` files for different scenes/characters +- Organize events logically within files +- Use comments to document complex logic + +### Integration with Escoria +- The extension is designed specifically for the Escoria framework +- Commands correspond to Escoria's ASHES implementation +- Global IDs should match your Escoria project structure + +## Contributing + +To contribute to the extension: +1. Fork the repository +2. Make your changes +3. Test thoroughly with `.esc` files +4. Submit a pull request + +## Support + +For issues or questions: +1. Check this HowTo guide first +2. Review the main README.md +3. Check the installation guide (INSTALL.md) +4. Report issues through the project's issue tracker diff --git a/vscode-extension-ashes/README.md b/vscode-extension-ashes/README.md index 33fe6a7a..80c205e9 100644 --- a/vscode-extension-ashes/README.md +++ b/vscode-extension-ashes/README.md @@ -10,6 +10,7 @@ A Visual Studio Code extension that provides syntax highlighting and IntelliSens - **Code Snippets**: Pre-built snippets for common ASHES patterns - **Command Reference**: Built-in command reference panel - **Smart Indentation**: Proper indentation rules for ASHES code structure +- **Dynamic Command Generation**: Automatically discovers and loads commands from your project's `project.godot` configuration - [Learn more about dynamic commands](DYNAMIC_COMMANDS.md) ## ASHES Language Features Supported diff --git a/vscode-extension-ashes/package.json b/vscode-extension-ashes/package.json index 0699ecd9..c83cd5a8 100644 --- a/vscode-extension-ashes/package.json +++ b/vscode-extension-ashes/package.json @@ -54,12 +54,20 @@ "command": "ashes.showCommandReference", "title": "Show ASHES Command Reference", "category": "ASHES" + }, + { + "command": "ashes.refreshCommands", + "title": "Refresh ASHES Commands", + "category": "ASHES" } ], "menus": { "commandPalette": [ { "command": "ashes.showCommandReference" + }, + { + "command": "ashes.refreshCommands" } ] } diff --git a/vscode-extension-ashes/src/commandParser.ts b/vscode-extension-ashes/src/commandParser.ts new file mode 100644 index 00000000..c55e2793 --- /dev/null +++ b/vscode-extension-ashes/src/commandParser.ts @@ -0,0 +1,407 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export interface CommandParameter { + name: string; + type: string; + required: boolean; + defaultValue?: string; +} + +export interface CommandInfo { + name: string; + description: string; + parameters: CommandParameter[]; + example?: string; +} + +export class CommandParser { + private projectRoot: string; + + constructor(projectRoot: string) { + this.projectRoot = projectRoot; + } + + /** + * Parse project.godot file to extract command directories + */ + private parseProjectGodot(): string[] { + const projectGodotPath = path.join(this.projectRoot, 'project.godot'); + + if (!fs.existsSync(projectGodotPath)) { + console.warn('project.godot not found, using default command directories'); + return [ + 'res://addons/escoria-core/game/core-scripts/esc/commands', + 'res://addons/escoria-ui-return-monkey-island/esc/commands', + 'res://addons/escoria-ui-return-monkey-island-dialog-simple/commands' + ]; + } + + const content = fs.readFileSync(projectGodotPath, 'utf8'); + const lines = content.split('\n'); + + let inEscoriaSection = false; + let commandDirectories: string[] = []; + + for (const line of lines) { + const trimmedLine = line.trim(); + + if (trimmedLine === '[escoria]') { + inEscoriaSection = true; + continue; + } + + if (inEscoriaSection && trimmedLine.startsWith('[')) { + break; // End of escoria section + } + + if (inEscoriaSection && trimmedLine.startsWith('main/command_directories=')) { + // Parse the array format: ["path1", "path2", "path3"] + const arrayMatch = trimmedLine.match(/main\/command_directories=\[(.*)\]/); + if (arrayMatch) { + const pathsString = arrayMatch[1]; + // Extract quoted paths + const pathMatches = pathsString.match(/"([^"]+)"/g); + if (pathMatches) { + commandDirectories = pathMatches.map(match => match.slice(1, -1)); // Remove quotes + } + } + break; + } + } + + return commandDirectories.length > 0 ? commandDirectories : [ + 'res://addons/escoria-core/game/core-scripts/esc/commands', + 'res://addons/escoria-ui-return-monkey-island/esc/commands', + 'res://addons/escoria-ui-return-monkey-island-dialog-simple/commands' + ]; + } + + /** + * Convert res:// path to actual filesystem path + */ + private resPathToFsPath(resPath: string): string { + if (resPath.startsWith('res://')) { + return path.join(this.projectRoot, resPath.substring(6)); + } + return path.join(this.projectRoot, resPath); + } + + /** + * Parse a single command file to extract command information + */ + private parseCommandFile(filePath: string): CommandInfo | null { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + + // Extract command name from filename + const fileName = path.basename(filePath, '.gd'); + const commandName = fileName; + + // Parse description from comments + let description = ''; + let parameters: CommandParameter[] = []; + let example = ''; + + let inParametersSection = false; + let inExampleSection = false; + let foundFirstDescription = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Skip empty lines + if (!line) { + continue; + } + + // Extract main description (first comment block) + if (line.startsWith('# `') && line.includes('`')) { + const match = line.match(/# `([^`]+)`/); + if (match) { + description = match[1]; + foundFirstDescription = true; + } + continue; + } + + // Look for description in subsequent comment lines (before parameters section) + if (line.startsWith('#') && !inParametersSection && !inExampleSection) { + const cleanLine = line.replace(/^#\s*/, ''); + + // Skip lines that are clearly not part of the main description + if (cleanLine.startsWith('**') || cleanLine.startsWith('@') || + cleanLine.startsWith('Example:') || cleanLine.startsWith('e.g.') || + cleanLine.includes('*') && cleanLine.includes(':') || + cleanLine.includes('Constructor') || cleanLine.includes('Use look-ahead') || + cleanLine.includes('Return the descriptor') || cleanLine.includes('Validate whether') || + cleanLine.includes('Run the command') || cleanLine.includes('Function called when')) { + continue; + } + + // If we haven't found the first description yet, this might be it + if (!foundFirstDescription && cleanLine) { + description = cleanLine; + foundFirstDescription = true; + continue; + } + + // If we have a description and this line looks like continuation + if (foundFirstDescription && cleanLine && !cleanLine.includes('*')) { + // Stop if we hit code-related content + if (cleanLine.includes('Constructor') || cleanLine.includes('Use look-ahead') || + cleanLine.includes('Return the descriptor') || cleanLine.includes('Validate whether') || + cleanLine.includes('Run the command') || cleanLine.includes('Function called when')) { + break; + } + description += ' ' + cleanLine; + } + continue; + } + + // Check for parameters section + if (line.includes('**Parameters**')) { + inParametersSection = true; + inExampleSection = false; + continue; + } + + // Check for example section + if (line.includes('Example:')) { + inParametersSection = false; + inExampleSection = true; + const exampleMatch = line.match(/Example:\s*(.+)/); + if (exampleMatch) { + example = exampleMatch[1]; + } + continue; + } + + // Parse parameters (support both # - *param*: and # * *param*: formats) + if (inParametersSection && (line.includes('# - *') || line.includes('# * *'))) { + const paramMatch = line.match(/#\s*[-*]\s*\*([^*]+)\*:\s*(.+)/); + if (paramMatch) { + const paramName = paramMatch[1].trim(); + const paramDesc = paramMatch[2].trim(); + + // Try to determine if parameter is required + const isRequired = !paramDesc.includes('default:') && !paramDesc.includes('(default:'); + + // Extract default value if present + let defaultValue: string | undefined; + const defaultMatch = paramDesc.match(/\(default:\s*([^)]+)\)/); + if (defaultMatch) { + defaultValue = defaultMatch[1].trim(); + } + + // Determine parameter type from description + let paramType = 'string'; + if (paramDesc.includes('boolean') || paramDesc.includes('true') || paramDesc.includes('false')) { + paramType = 'boolean'; + } else if (paramDesc.includes('number') || paramDesc.includes('int') || paramDesc.includes('float')) { + paramType = 'number'; + } + + parameters.push({ + name: paramName, + type: paramType, + required: isRequired, + defaultValue: defaultValue + }); + } + continue; + } + + // Parse configure() method to get more accurate parameter info + if (line.includes('func configure()') && i + 5 < lines.length) { + // Look for ESCCommandArgumentDescriptor in the next few lines + for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) { + const configLine = lines[j].trim(); + if (configLine.includes('ESCCommandArgumentDescriptor.new(')) { + // Parse the descriptor parameters + const descriptorMatch = configLine.match(/ESCCommandArgumentDescriptor\.new\(\s*(\d+)/); + if (descriptorMatch) { + const minArgs = parseInt(descriptorMatch[1]); + + // Look for type arrays, defaults, and required flags in subsequent lines + let types: string[] = []; + let defaults: string[] = []; + let requiredFlags: boolean[] = []; + + for (let k = j + 1; k < Math.min(j + 10, lines.length); k++) { + const typeLine = lines[k].trim(); + + // Extract types array + if (typeLine.includes('TYPE_') && typeLine.includes('[')) { + const typeMatch = typeLine.match(/\[([^\]]+)\]/); + if (typeMatch) { + types = typeMatch[1].split(',').map(t => t.trim()); + } + } + + // Extract defaults array + if (typeLine.includes('null') && typeLine.includes('[') && !typeLine.includes('TYPE_')) { + const defaultMatch = typeLine.match(/\[([^\]]+)\]/); + if (defaultMatch) { + defaults = defaultMatch[1].split(',').map(d => d.trim()); + } + } + + // Extract required flags array (look for true/false pattern) + if (typeLine.includes('true') && typeLine.includes('false') && typeLine.includes('[') && !typeLine.includes('TYPE_')) { + const requiredMatch = typeLine.match(/\[([^\]]+)\]/); + if (requiredMatch) { + requiredFlags = requiredMatch[1].split(',').map(f => f.trim() === 'true'); + } + } + } + + // Update or create parameters with accurate information + const maxParams = Math.max(types.length, defaults.length); + const newParameters: CommandParameter[] = []; + + // Store original parameters from comments for name preservation + const originalParams = [...parameters]; + + for (let p = 0; p < maxParams; p++) { + const type = types[p] || 'TYPE_STRING'; + const defaultValue = defaults[p] || 'null'; + // If requiredFlags array is not provided, use minArgs to determine required parameters + const isRequired = p < minArgs || (requiredFlags[p] !== undefined ? requiredFlags[p] : p < minArgs); + + // Convert Godot types to readable types + let paramType = 'string'; + if (type.includes('TYPE_BOOL')) { + paramType = 'boolean'; + } else if (type.includes('TYPE_INT') || type.includes('TYPE_FLOAT')) { + paramType = 'number'; + } else if (type.includes('TYPE_STRING')) { + paramType = 'string'; + } + + // Try to get parameter name from comments if available, otherwise generate one + let paramName = `param${p + 1}`; + if (p < originalParams.length && originalParams[p] && originalParams[p].name !== `param${p + 1}`) { + paramName = originalParams[p].name; + } + + newParameters.push({ + name: paramName, + type: paramType, + required: isRequired, + defaultValue: defaultValue !== 'null' ? defaultValue : undefined + }); + } + + // Replace parameters with the new ones from configure method + parameters = newParameters; + } + break; + } + } + break; + } + } + + // If we couldn't parse parameters from comments, try to infer from configure method + if (parameters.length === 0) { + // Look for configure method and try to extract parameter count + const configureMatch = content.match(/func configure\(\)[^}]*ESCCommandArgumentDescriptor\.new\(\s*(\d+)/); + if (configureMatch) { + const paramCount = parseInt(configureMatch[1]); + for (let i = 0; i < paramCount; i++) { + parameters.push({ + name: `param${i + 1}`, + type: 'string', + required: true + }); + } + } + } + + // Clean up description - remove extra whitespace and fix common issues + if (description) { + description = description.trim(); + + // Remove extra whitespace + description = description.replace(/\s+/g, ' '); + + // Truncate if too long (keep first sentence or first 200 chars) + const firstSentence = description.split('.')[0]; + if (firstSentence.length < 200 && firstSentence.length > 20) { + description = firstSentence + '.'; + } else if (description.length > 200) { + description = description.substring(0, 200).trim() + '...'; + } + + // If description is just the command name, try to get a better one + if (description === commandName || description === `${commandName} command`) { + description = `${commandName} command`; + } + } + + return { + name: commandName, + description: description || `${commandName} command`, + parameters: parameters, + example: example + }; + + } catch (error) { + console.error(`Error parsing command file ${filePath}:`, error); + return null; + } + } + + /** + * Parse all command files from the specified directories + */ + public parseCommands(): CommandInfo[] { + const commandDirectories = this.parseProjectGodot(); + const commands: CommandInfo[] = []; + + for (const dir of commandDirectories) { + const fsPath = this.resPathToFsPath(dir); + + if (!fs.existsSync(fsPath)) { + console.warn(`Command directory not found: ${fsPath}`); + continue; + } + + try { + const files = fs.readdirSync(fsPath); + const gdFiles = files.filter(file => file.endsWith('.gd') && !file.endsWith('.gd.uid')); + + for (const file of gdFiles) { + const filePath = path.join(fsPath, file); + const commandInfo = this.parseCommandFile(filePath); + + if (commandInfo) { + commands.push(commandInfo); + } + } + } catch (error) { + console.error(`Error reading directory ${fsPath}:`, error); + } + } + + // Sort commands alphabetically + commands.sort((a, b) => a.name.localeCompare(b.name)); + + return commands; + } + + /** + * Get commands in the format expected by the VSCode extension + */ + public getCommandsForExtension(): Array<{name: string, description: string, parameters: CommandParameter[]}> { + const commands = this.parseCommands(); + + return commands.map(cmd => ({ + name: cmd.name, + description: cmd.description, + parameters: cmd.parameters + })); + } +} diff --git a/vscode-extension-ashes/src/extension.ts b/vscode-extension-ashes/src/extension.ts index 3e15de7b..f576315d 100644 --- a/vscode-extension-ashes/src/extension.ts +++ b/vscode-extension-ashes/src/extension.ts @@ -1,358 +1,10 @@ import * as vscode from 'vscode'; +import * as path from 'path'; +import { CommandParser, CommandInfo, CommandParameter } from './commandParser'; -// ASHES commands with their descriptions and parameters -const ASHES_COMMANDS = [ - { - name: 'accept_input', - description: 'Accept specific input types', - parameters: ['input_type'] - }, - { - name: 'anim', - description: 'Play animation on object', - parameters: ['object_id', 'animation_name'] - }, - { - name: 'anim_block', - description: 'Play animation and wait for completion', - parameters: ['object_id', 'animation_name'] - }, - { - name: 'block_say', - description: 'Start a block of say commands', - parameters: [] - }, - { - name: 'camera_push', - description: 'Push camera to new position', - parameters: ['x', 'y'] - }, - { - name: 'camera_push_block', - description: 'Push camera and wait for completion', - parameters: ['x', 'y'] - }, - { - name: 'camera_set_limits', - description: 'Set camera movement limits', - parameters: ['left', 'top', 'right', 'bottom'] - }, - { - name: 'camera_set_pos', - description: 'Set camera position', - parameters: ['x', 'y'] - }, - { - name: 'camera_set_pos_block', - description: 'Set camera position and wait', - parameters: ['x', 'y'] - }, - { - name: 'camera_set_target', - description: 'Set camera target', - parameters: ['object_id'] - }, - { - name: 'camera_set_target_block', - description: 'Set camera target and wait', - parameters: ['object_id'] - }, - { - name: 'camera_set_zoom', - description: 'Set camera zoom level', - parameters: ['zoom_level'] - }, - { - name: 'camera_set_zoom_block', - description: 'Set camera zoom and wait', - parameters: ['zoom_level'] - }, - { - name: 'camera_set_zoom_height', - description: 'Set camera zoom height', - parameters: ['height'] - }, - { - name: 'camera_set_zoom_height_block', - description: 'Set camera zoom height and wait', - parameters: ['height'] - }, - { - name: 'camera_shift', - description: 'Shift camera position', - parameters: ['x', 'y'] - }, - { - name: 'camera_shift_block', - description: 'Shift camera and wait', - parameters: ['x', 'y'] - }, - { - name: 'change_scene', - description: 'Change to a different scene', - parameters: ['scene_path', 'enable_transition', 'run_events'] - }, - { - name: 'custom', - description: 'Execute custom command', - parameters: ['command_name', '...args'] - }, - { - name: 'dec_global', - description: 'Decrement global variable', - parameters: ['variable_name'] - }, - { - name: 'enable_terrain', - description: 'Enable/disable terrain', - parameters: ['terrain_name', 'enabled'] - }, - { - name: 'end_block_say', - description: 'End a block of say commands', - parameters: [] - }, - { - name: 'hide_menu', - description: 'Hide menu', - parameters: ['menu_name'] - }, - { - name: 'inc_global', - description: 'Increment global variable', - parameters: ['variable_name'] - }, - { - name: 'inventory_add', - description: 'Add item to inventory', - parameters: ['item_id'] - }, - { - name: 'inventory_remove', - description: 'Remove item from inventory', - parameters: ['item_id'] - }, - { - name: 'item_count_add', - description: 'Add to item count', - parameters: ['item_id', 'count'] - }, - { - name: 'play_lib_snd', - description: 'Play library sound', - parameters: ['filename', 'namespace'] - }, - { - name: 'play_snd', - description: 'Play sound file', - parameters: ['sound_path', 'type'] - }, - { - name: 'play_video', - description: 'Play video file', - parameters: ['video_path'] - }, - { - name: 'print', - description: 'Print debug message', - parameters: ['message'] - }, - { - name: 'print_internal', - description: 'Print internal message', - parameters: ['message'] - }, - { - name: 'queue_event', - description: 'Queue event for later execution', - parameters: ['object_id', 'event_name'] - }, - { - name: 'queue_resource', - description: 'Queue resource for loading', - parameters: ['resource_path'] - }, - { - name: 'rand_global', - description: 'Set random value to global', - parameters: ['variable_name', 'min', 'max'] - }, - { - name: 'repeat', - description: 'Repeat command', - parameters: ['count', 'command'] - }, - { - name: 'save_game', - description: 'Save game state', - parameters: ['save_name'] - }, - { - name: 'say', - description: 'Display dialog text', - parameters: ['speaker', 'text', 'translation_key', 'type'] - }, - { - name: 'say_last_dialog_option', - description: 'Say the last dialog option', - parameters: [] - }, - { - name: 'say_random', - description: 'Say random text from list', - parameters: ['speaker', 'list_id', 'length'] - }, - { - name: 'say_sequence', - description: 'Say text sequence', - parameters: ['speaker', 'list_id', 'length', 'loop'] - }, - { - name: 'sched_event', - description: 'Schedule event for later', - parameters: ['delay', 'object_id', 'event_name'] - }, - { - name: 'set_active', - description: 'Set object active/inactive', - parameters: ['object_id', 'active'] - }, - { - name: 'set_active_if_exists', - description: 'Set object active if it exists', - parameters: ['object_id', 'active'] - }, - { - name: 'set_angle', - description: 'Set object angle', - parameters: ['object_id', 'angle'] - }, - { - name: 'set_animations', - description: 'Set object animations', - parameters: ['object_id', 'animations'] - }, - { - name: 'set_direction', - description: 'Set object direction', - parameters: ['object_id', 'direction'] - }, - { - name: 'set_global', - description: 'Set global variable', - parameters: ['variable_name', 'value', 'force'] - }, - { - name: 'set_globals', - description: 'Set multiple global variables', - parameters: ['variables_dict'] - }, - { - name: 'set_gui_visible', - description: 'Set GUI visibility', - parameters: ['visible'] - }, - { - name: 'set_interactive', - description: 'Set object interactive state', - parameters: ['object_id', 'interactive'] - }, - { - name: 'set_item_custom_data', - description: 'Set item custom data', - parameters: ['item_id', 'key', 'value'] - }, - { - name: 'set_speed', - description: 'Set object speed', - parameters: ['object_id', 'speed'] - }, - { - name: 'set_state', - description: 'Set object state', - parameters: ['object_id', 'state'] - }, - { - name: 'set_tooltip', - description: 'Set object tooltip', - parameters: ['object_id', 'action', 'text'] - }, - { - name: 'show_menu', - description: 'Show menu', - parameters: ['menu_name'] - }, - { - name: 'slide', - description: 'Slide object to position', - parameters: ['object_id', 'x', 'y', 'duration'] - }, - { - name: 'slide_block', - description: 'Slide object and wait', - parameters: ['object_id', 'x', 'y', 'duration'] - }, - { - name: 'spawn', - description: 'Spawn object', - parameters: ['object_id', 'x', 'y'] - }, - { - name: 'stop', - description: 'Stop current event', - parameters: [] - }, - { - name: 'stop_snd', - description: 'Stop sound', - parameters: ['sound_type'] - }, - { - name: 'teleport', - description: 'Teleport object to target', - parameters: ['object_id', 'target_id'] - }, - { - name: 'teleport_pos', - description: 'Teleport object to position', - parameters: ['object_id', 'x', 'y'] - }, - { - name: 'transition', - description: 'Play transition effect', - parameters: ['transition_type', 'duration'] - }, - { - name: 'turn_to', - description: 'Turn object to face target', - parameters: ['object_id', 'target_id'] - }, - { - name: 'wait', - description: 'Wait for specified time', - parameters: ['duration'] - }, - { - name: 'walk', - description: 'Walk object to target', - parameters: ['object_id', 'target_id'] - }, - { - name: 'walk_block', - description: 'Walk object and wait', - parameters: ['object_id', 'target_id'] - }, - { - name: 'walk_to_pos', - description: 'Walk object to position', - parameters: ['object_id', 'x', 'y'] - }, - { - name: 'walk_to_pos_block', - description: 'Walk object to position and wait', - parameters: ['object_id', 'x', 'y'] - } -]; +// Cache for dynamically loaded commands +let ASHES_COMMANDS: Array<{name: string, description: string, parameters: CommandParameter[]}> = []; +let COMMAND_CACHE_TIMESTAMP = 0; // Built-in variables const BUILTIN_VARIABLES = [ @@ -369,6 +21,41 @@ const KEYWORDS = [ 'true', 'false', 'nil', 'and', 'or', 'not', 'in', 'is', 'active' ]; +/** + * Load commands dynamically from the project + */ +function loadCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[]}> { + try { + const parser = new CommandParser(workspaceRoot); + const commands = parser.getCommandsForExtension(); + + // Update cache timestamp + COMMAND_CACHE_TIMESTAMP = Date.now(); + + return commands; + } catch (error) { + console.error('Error loading commands:', error); + // Return empty array if loading fails + return []; + } +} + +/** + * Get commands, using cache if available and not too old + */ +function getCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[]}> { + const now = Date.now(); + const cacheAge = now - COMMAND_CACHE_TIMESTAMP; + const maxCacheAge = 5 * 60 * 1000; // 5 minutes + + // Reload commands if cache is empty or too old + if (ASHES_COMMANDS.length === 0 || cacheAge > maxCacheAge) { + ASHES_COMMANDS = loadCommands(workspaceRoot); + } + + return ASHES_COMMANDS; +} + export function activate(context: vscode.ExtensionContext) { console.log('ASHES Language Support extension is now active!'); @@ -379,14 +66,52 @@ export function activate(context: vscode.ExtensionContext) { provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { const completions: vscode.CompletionItem[] = []; + // Get workspace root + const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath; + if (!workspaceRoot) { + return completions; + } + + // Get dynamic commands + const commands = getCommands(workspaceRoot); + // Add command completions - ASHES_COMMANDS.forEach(command => { + commands.forEach(command => { const completion = new vscode.CompletionItem(command.name, vscode.CompletionItemKind.Function); completion.detail = command.description; + + // Create detailed parameter documentation + let paramDocs = ''; + let exampleUsage = ''; + + if (command.parameters && command.parameters.length > 0) { + paramDocs = '\n\n**Parameters:**\n'; + const exampleParams: string[] = []; + + command.parameters.forEach((param, index) => { + const required = param.required ? '**' : ''; + const optional = param.required ? '' : ' (optional)'; + const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : ''; + paramDocs += `- ${required}${param.name}${required} (${param.type})${optional}${defaultValue}\n`; + + // Create example parameter + const exampleParam = param.required ? + `${param.name}: ${param.type}` : + `[${param.name}: ${param.type}]`; + exampleParams.push(exampleParam); + }); + + // Create example usage + exampleUsage = `\n\n**Example:**\n\`${command.name}(${exampleParams.join(', ')})\``; + } + completion.documentation = new vscode.MarkdownString( - `**${command.name}**\n\n${command.description}\n\n**Parameters:** ${command.parameters.join(', ')}` + `## ${command.name}\n\n---\n\n${command.description}${paramDocs}${exampleUsage}` ); - completion.insertText = new vscode.SnippetString(`${command.name}($1)`); + + // Create snippet with parameter placeholders + const snippetParams = command.parameters.map((param, index) => `\${${index + 1}:${param.name}}`).join(', '); + completion.insertText = new vscode.SnippetString(`${command.name}(${snippetParams})`); completions.push(completion); }); @@ -417,12 +142,46 @@ export function activate(context: vscode.ExtensionContext) { { provideHover(document: vscode.TextDocument, position: vscode.Position) { const word = document.getText(document.getWordRangeAtPosition(position)); - const command = ASHES_COMMANDS.find(cmd => cmd.name === word); + + // Get workspace root + const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath; + if (!workspaceRoot) { + return null; + } + + // Get dynamic commands + const commands = getCommands(workspaceRoot); + const command = commands.find(cmd => cmd.name === word); if (command) { + // Create detailed parameter documentation + let paramDocs = ''; + let exampleUsage = ''; + + if (command.parameters && command.parameters.length > 0) { + paramDocs = '\n\n**Parameters:**\n'; + const exampleParams: string[] = []; + + command.parameters.forEach((param, index) => { + const required = param.required ? '**' : ''; + const optional = param.required ? '' : ' (optional)'; + const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : ''; + paramDocs += `- ${required}${param.name}${required} (${param.type})${optional}${defaultValue}\n`; + + // Create example parameter + const exampleParam = param.required ? + `${param.name}: ${param.type}` : + `[${param.name}: ${param.type}]`; + exampleParams.push(exampleParam); + }); + + // Create example usage + exampleUsage = `\n\n**Example:**\n\`${command.name}(${exampleParams.join(', ')})\``; + } + const hover = new vscode.Hover( new vscode.MarkdownString( - `**${command.name}**\n\n${command.description}\n\n**Parameters:** ${command.parameters.join(', ')}` + `## ${command.name}\n\n---\n\n${command.description}${paramDocs}${exampleUsage}` ) ); return hover; @@ -450,13 +209,65 @@ export function activate(context: vscode.ExtensionContext) { {} ); - const commandsHtml = ASHES_COMMANDS.map(command => - ` + // Get workspace root from active editor + const activeEditor = vscode.window.activeTextEditor; + const workspaceRoot = activeEditor ? + vscode.workspace.getWorkspaceFolder(activeEditor.document.uri)?.uri.fsPath : + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + + if (!workspaceRoot) { + panel.webview.html = ` + + + + + + +

ASHES Command Reference

+

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

+ + + `; + return; + } + + // Get dynamic commands + const commands = getCommands(workspaceRoot); + + const commandsHtml = commands.map(command => { + let paramInfo = ''; + let exampleUsage = ''; + + if (command.parameters && command.parameters.length > 0) { + const exampleParams: string[] = []; + + paramInfo = command.parameters.map(param => { + const required = param.required ? '' : ''; + const requiredEnd = param.required ? '' : ''; + const optional = param.required ? '' : ' (optional)'; + const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : ''; + + // Create example parameter + const exampleParam = param.required ? + `${param.name}: ${param.type}` : + `[${param.name}: ${param.type}]`; + exampleParams.push(exampleParam); + + return `${required}${param.name}${requiredEnd} (${param.type})${optional}${defaultValue}`; + }).join('
'); + + // Create example usage + exampleUsage = `

Example:
${command.name}(${exampleParams.join(', ')})`; + } + + return ` ${command.name} - ${command.description} - ${command.parameters.join(', ')} - ` - ).join(''); + ${command.description}${exampleUsage} + ${paramInfo} + `; + }).join(''); panel.webview.html = ` @@ -489,7 +300,16 @@ export function activate(context: vscode.ExtensionContext) { `; }); - context.subscriptions.push(completionProvider, hoverProvider, showCommandReference); + // Register command for refreshing command cache + const refreshCommands = vscode.commands.registerCommand('ashes.refreshCommands', () => { + // Clear cache to force reload + ASHES_COMMANDS = []; + COMMAND_CACHE_TIMESTAMP = 0; + + vscode.window.showInformationMessage('ASHES commands cache refreshed!'); + }); + + context.subscriptions.push(completionProvider, hoverProvider, showCommandReference, refreshCommands); } export function deactivate() {} diff --git a/vscode-extension-ashes/test.esc b/vscode-extension-ashes/test.esc new file mode 100644 index 00000000..96f19e5c --- /dev/null +++ b/vscode-extension-ashes/test.esc @@ -0,0 +1,7 @@ +# Test ASHES file for the VSCode extension + +# Test some commands +say player "Hello world!" +walk player target +set_global test_var 42 +play_snd "sound.ogg" sfx From 9c2a6cb7e7291625640302eb4ffcdee2ed2c5be8 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 02:45:51 +0200 Subject: [PATCH 3/8] Show me more --- vscode-extension-ashes/README.md | 21 +- vscode-extension-ashes/src/commandParser.ts | 98 +++++++--- vscode-extension-ashes/src/extension.ts | 204 +++++++++++++++++--- vscode-extension-ashes/test.esc | 7 - 4 files changed, 267 insertions(+), 63 deletions(-) delete mode 100644 vscode-extension-ashes/test.esc diff --git a/vscode-extension-ashes/README.md b/vscode-extension-ashes/README.md index 80c205e9..e91e5b7f 100644 --- a/vscode-extension-ashes/README.md +++ b/vscode-extension-ashes/README.md @@ -5,10 +5,11 @@ A Visual Studio Code extension that provides syntax highlighting and IntelliSens ## Features - **Syntax Highlighting**: Full syntax highlighting for ASHES language files (.esc) -- **Auto-completion**: IntelliSense for ASHES commands, built-in variables, and keywords -- **Hover Information**: Detailed information about commands and variables on hover +- **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 +- **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) @@ -21,7 +22,11 @@ A Visual Studio Code extension that provides syntax highlighting and IntelliSens ### Commands - All standard Escoria commands (say, set_global, change_scene, etc.) - Custom commands -- Command parameter hints +- 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` @@ -55,6 +60,10 @@ A Visual Studio Code extension that provides syntax highlighting and IntelliSens - 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 @@ -65,7 +74,9 @@ A Visual Studio Code extension that provides syntax highlighting and IntelliSens ### Command Reference - Press `Ctrl+Shift+P` and type "ASHES: Show Command Reference" -- View all available commands with descriptions and parameters +- 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 diff --git a/vscode-extension-ashes/src/commandParser.ts b/vscode-extension-ashes/src/commandParser.ts index c55e2793..dba22523 100644 --- a/vscode-extension-ashes/src/commandParser.ts +++ b/vscode-extension-ashes/src/commandParser.ts @@ -13,6 +13,7 @@ export interface CommandInfo { description: string; parameters: CommandParameter[]; example?: string; + filePath?: string; } export class CommandParser { @@ -103,6 +104,7 @@ export class CommandParser { let description = ''; let parameters: CommandParameter[] = []; let example = ''; + let signatureParams: string[] = []; let inParametersSection = false; let inExampleSection = false; @@ -116,12 +118,21 @@ export class CommandParser { continue; } - // Extract main description (first comment block) + // 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]; + 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; } @@ -142,7 +153,7 @@ export class CommandParser { // If we haven't found the first description yet, this might be it if (!foundFirstDescription && cleanLine) { - description = cleanLine; + description += cleanLine; foundFirstDescription = true; continue; } @@ -155,7 +166,7 @@ export class CommandParser { cleanLine.includes('Run the command') || cleanLine.includes('Function called when')) { break; } - description += ' ' + cleanLine; + description += '\n' + cleanLine; } continue; } @@ -195,12 +206,21 @@ export class CommandParser { defaultValue = defaultMatch[1].trim(); } - // Determine parameter type from description + // Determine parameter type from description with more comprehensive detection let paramType = 'string'; - if (paramDesc.includes('boolean') || paramDesc.includes('true') || paramDesc.includes('false')) { + const descLower = paramDesc.toLowerCase(); + if (descLower.includes('boolean') || descLower.includes('true') || descLower.includes('false') || + descLower.includes('bool') || descLower.includes('flag')) { paramType = 'boolean'; - } else if (paramDesc.includes('number') || paramDesc.includes('int') || paramDesc.includes('float')) { + } 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({ @@ -219,10 +239,18 @@ export class CommandParser { for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) { const configLine = lines[j].trim(); if (configLine.includes('ESCCommandArgumentDescriptor.new(')) { - // Parse the descriptor parameters - const descriptorMatch = configLine.match(/ESCCommandArgumentDescriptor\.new\(\s*(\d+)/); - if (descriptorMatch) { - const minArgs = parseInt(descriptorMatch[1]); + // 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[] = []; @@ -240,16 +268,17 @@ export class CommandParser { } } - // Extract defaults array - if (typeLine.includes('null') && typeLine.includes('[') && !typeLine.includes('TYPE_')) { + // 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 (look for true/false pattern) - if (typeLine.includes('true') && typeLine.includes('false') && typeLine.includes('[') && !typeLine.includes('TYPE_')) { + // 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'); @@ -264,13 +293,16 @@ export class CommandParser { // Store original parameters from comments for name preservation const originalParams = [...parameters]; - for (let p = 0; p < maxParams; p++) { + // 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 + // Convert Godot types to readable types with more comprehensive mapping let paramType = 'string'; if (type.includes('TYPE_BOOL')) { paramType = 'boolean'; @@ -278,12 +310,24 @@ export class CommandParser { 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 generate one + // 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 !== `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({ @@ -311,8 +355,10 @@ export class CommandParser { 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: `param${i + 1}`, + name: paramName, type: 'string', required: true }); @@ -324,8 +370,8 @@ export class CommandParser { if (description) { description = description.trim(); - // Remove extra whitespace - description = description.replace(/\s+/g, ' '); + // 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]; @@ -345,7 +391,8 @@ export class CommandParser { name: commandName, description: description || `${commandName} command`, parameters: parameters, - example: example + example: example, + filePath: filePath }; } catch (error) { @@ -395,13 +442,14 @@ export class CommandParser { /** * Get commands in the format expected by the VSCode extension */ - public getCommandsForExtension(): Array<{name: string, description: string, parameters: CommandParameter[]}> { + 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 + parameters: cmd.parameters, + filePath: cmd.filePath })); } } diff --git a/vscode-extension-ashes/src/extension.ts b/vscode-extension-ashes/src/extension.ts index f576315d..82a8c825 100644 --- a/vscode-extension-ashes/src/extension.ts +++ b/vscode-extension-ashes/src/extension.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { CommandParser, CommandInfo, CommandParameter } from './commandParser'; // Cache for dynamically loaded commands -let ASHES_COMMANDS: Array<{name: string, description: string, parameters: CommandParameter[]}> = []; +let ASHES_COMMANDS: Array<{name: string, description: string, parameters: CommandParameter[], filePath?: string}> = []; let COMMAND_CACHE_TIMESTAMP = 0; // Built-in variables @@ -24,7 +24,7 @@ const KEYWORDS = [ /** * Load commands dynamically from the project */ -function loadCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[]}> { +function loadCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[], filePath?: string}> { try { const parser = new CommandParser(workspaceRoot); const commands = parser.getCommandsForExtension(); @@ -43,7 +43,7 @@ function loadCommands(workspaceRoot: string): Array<{name: string, description: /** * Get commands, using cache if available and not too old */ -function getCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[]}> { +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 @@ -89,12 +89,15 @@ export function activate(context: vscode.ExtensionContext) { const exampleParams: string[] = []; command.parameters.forEach((param, index) => { - const required = param.required ? '**' : ''; - const optional = param.required ? '' : ' (optional)'; - const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : ''; - paramDocs += `- ${required}${param.name}${required} (${param.type})${optional}${defaultValue}\n`; + // Enhanced parameter formatting with clear indicators + const requiredIndicator = param.required ? '**Required**' : '**Optional**'; + const typeIndicator = `\`${param.type}\``; + const nameIndicator = `**${param.name}**`; + const defaultValue = param.defaultValue ? ` *(default: \`${param.defaultValue}\`)*` : ''; - // Create example parameter + paramDocs += `- ${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}\n`; + + // Create example parameter with clear formatting const exampleParam = param.required ? `${param.name}: ${param.type}` : `[${param.name}: ${param.type}]`; @@ -163,12 +166,15 @@ export function activate(context: vscode.ExtensionContext) { const exampleParams: string[] = []; command.parameters.forEach((param, index) => { - const required = param.required ? '**' : ''; - const optional = param.required ? '' : ' (optional)'; - const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : ''; - paramDocs += `- ${required}${param.name}${required} (${param.type})${optional}${defaultValue}\n`; + // Enhanced parameter formatting with clear indicators + const requiredIndicator = param.required ? '**Required**' : '**Optional**'; + const typeIndicator = `\`${param.type}\``; + const nameIndicator = `**${param.name}**`; + const defaultValue = param.defaultValue ? ` *(default: \`${param.defaultValue}\`)*` : ''; - // Create example parameter + paramDocs += `- ${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}\n`; + + // Create example parameter with clear formatting const exampleParam = param.required ? `${param.name}: ${param.type}` : `[${param.name}: ${param.type}]`; @@ -200,13 +206,64 @@ export function activate(context: vscode.ExtensionContext) { } ); + // 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 @@ -236,6 +293,48 @@ export function activate(context: vscode.ExtensionContext) { // 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 = ''; @@ -244,10 +343,11 @@ export function activate(context: vscode.ExtensionContext) { const exampleParams: string[] = []; paramInfo = command.parameters.map(param => { - const required = param.required ? '' : ''; - const requiredEnd = param.required ? '' : ''; - const optional = param.required ? '' : ' (optional)'; - const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : ''; + // Enhanced parameter formatting with clear indicators + const requiredIndicator = param.required ? 'Required' : 'Optional'; + const typeIndicator = `${param.type}`; + const nameIndicator = `${param.name}`; + const defaultValue = param.defaultValue ? ` (default: ${param.defaultValue})` : ''; // Create example parameter const exampleParam = param.required ? @@ -255,7 +355,7 @@ export function activate(context: vscode.ExtensionContext) { `[${param.name}: ${param.type}]`; exampleParams.push(exampleParam); - return `${required}${param.name}${requiredEnd} (${param.type})${optional}${defaultValue}`; + return `${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}`; }).join('
'); // Create example usage @@ -263,7 +363,7 @@ export function activate(context: vscode.ExtensionContext) { } return ` - ${command.name} + ${command.name} ${command.description}${exampleUsage} ${paramInfo} `; @@ -274,15 +374,47 @@ export function activate(context: vscode.ExtensionContext) {

ASHES Command Reference

+
+ 💡 Tip: Click on any command name to navigate to its source file! +
@@ -295,6 +427,26 @@ export function activate(context: vscode.ExtensionContext) { ${commandsHtml}
+ + `; @@ -309,7 +461,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.showInformationMessage('ASHES commands cache refreshed!'); }); - context.subscriptions.push(completionProvider, hoverProvider, showCommandReference, refreshCommands); + context.subscriptions.push(completionProvider, hoverProvider, definitionProvider, showCommandReference, refreshCommands); } export function deactivate() {} diff --git a/vscode-extension-ashes/test.esc b/vscode-extension-ashes/test.esc deleted file mode 100644 index 96f19e5c..00000000 --- a/vscode-extension-ashes/test.esc +++ /dev/null @@ -1,7 +0,0 @@ -# Test ASHES file for the VSCode extension - -# Test some commands -say player "Hello world!" -walk player target -set_global test_var 42 -play_snd "sound.ogg" sfx From c9a13afbe8d46bc1a981e902f006f4470bd3e931 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 02:50:17 +0200 Subject: [PATCH 4/8] compile before symlink --- install-extension.sh | 45 +++++++++++++++++++++++++ vscode-extension-ashes/.nvmrc | 1 + vscode-extension-ashes/INSTALL.md | 55 +++++++++++++++++++++++++++---- 3 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 vscode-extension-ashes/.nvmrc diff --git a/install-extension.sh b/install-extension.sh index 8fa23908..53bb2105 100755 --- a/install-extension.sh +++ b/install-extension.sh @@ -130,6 +130,51 @@ main() { 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" diff --git a/vscode-extension-ashes/.nvmrc b/vscode-extension-ashes/.nvmrc new file mode 100644 index 00000000..92f279e3 --- /dev/null +++ b/vscode-extension-ashes/.nvmrc @@ -0,0 +1 @@ +v22 \ No newline at end of file diff --git a/vscode-extension-ashes/INSTALL.md b/vscode-extension-ashes/INSTALL.md index 34389c14..38952889 100644 --- a/vscode-extension-ashes/INSTALL.md +++ b/vscode-extension-ashes/INSTALL.md @@ -2,16 +2,45 @@ ## Quick Installation -1. **Copy the extension folder** to your VS Code extensions directory: +### 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\` -2. **Rename the folder** to `ashes-language-support-0.1.0` (or similar) +3. **Rename the folder** to `ashes-language-support-0.1.0` (or similar) -3. **Reload VS Code** or restart the application +4. **Reload VS Code** or restart the application -4. **Test the extension** by opening any `.esc` file +5. **Test the extension** by opening any `.esc` file ## Alternative Installation (Development Mode) @@ -27,17 +56,23 @@ ## Building from Source -1. **Install dependencies**: +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 ``` -2. **Compile TypeScript**: +3. **Compile TypeScript**: ```bash npm run compile ``` -3. **Package the extension** (optional): +4. **Package the extension** (optional): ```bash npx vsce package ``` @@ -79,6 +114,12 @@ - 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. From e068e4160b1ca7a10954c9cd767f2e9a8a260946 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 03:11:57 +0200 Subject: [PATCH 5/8] Show me errors --- vscode-extension-ashes/package.json | 8 + .../src/diagnosticProvider.ts | 247 ++++++ vscode-extension-ashes/src/extension.ts | 18 +- vscode-extension-ashes/src/syntaxValidator.ts | 799 ++++++++++++++++++ 4 files changed, 1070 insertions(+), 2 deletions(-) create mode 100644 vscode-extension-ashes/src/diagnosticProvider.ts create mode 100644 vscode-extension-ashes/src/syntaxValidator.ts diff --git a/vscode-extension-ashes/package.json b/vscode-extension-ashes/package.json index c83cd5a8..95e47d66 100644 --- a/vscode-extension-ashes/package.json +++ b/vscode-extension-ashes/package.json @@ -59,6 +59,11 @@ "command": "ashes.refreshCommands", "title": "Refresh ASHES Commands", "category": "ASHES" + }, + { + "command": "ashes.refreshDiagnostics", + "title": "Refresh ASHES Diagnostics", + "category": "ASHES" } ], "menus": { @@ -68,6 +73,9 @@ }, { "command": "ashes.refreshCommands" + }, + { + "command": "ashes.refreshDiagnostics" } ] } diff --git a/vscode-extension-ashes/src/diagnosticProvider.ts b/vscode-extension-ashes/src/diagnosticProvider.ts new file mode 100644 index 00000000..ef52be83 --- /dev/null +++ b/vscode-extension-ashes/src/diagnosticProvider.ts @@ -0,0 +1,247 @@ +import * as vscode from 'vscode'; +import { ASHESSyntaxValidator } from './syntaxValidator'; + +export class ASHESDiagnosticProvider { + private diagnosticCollection: vscode.DiagnosticCollection; + private syntaxValidator: ASHESSyntaxValidator; + private commandCache: Map = new Map(); + private lastUpdateTime: Map = new Map(); + + constructor() { + this.diagnosticCollection = vscode.languages.createDiagnosticCollection('ashes'); + this.syntaxValidator = new ASHESSyntaxValidator(); + } + + /** + * Initialize the diagnostic provider + */ + public initialize(context: vscode.ExtensionContext): void { + // Register document change listener + const documentChangeListener = vscode.workspace.onDidChangeTextDocument( + (event) => this.onDocumentChanged(event) + ); + + // Register document open listener + const documentOpenListener = vscode.workspace.onDidOpenTextDocument( + (document) => this.validateDocument(document) + ); + + // Register document save listener + const documentSaveListener = vscode.workspace.onDidSaveTextDocument( + (document) => this.validateDocument(document) + ); + + // Register workspace change listener to update command cache + const workspaceChangeListener = vscode.workspace.onDidChangeWorkspaceFolders( + () => this.updateCommandCache() + ); + + // Validate all open ASHES documents + vscode.workspace.textDocuments.forEach(document => { + if (document.languageId === 'ashes') { + this.validateDocument(document); + } + }); + + // Add to subscriptions + context.subscriptions.push( + this.diagnosticCollection, + documentChangeListener, + documentOpenListener, + documentSaveListener, + workspaceChangeListener + ); + + // Initial command cache update + this.updateCommandCache(); + } + + /** + * Handle document changes with debouncing + */ + private onDocumentChanged(event: vscode.TextDocumentChangeEvent): void { + if (event.document.languageId !== 'ashes') { + return; + } + + const document = event.document; + const uri = document.uri.toString(); + const now = Date.now(); + + // Debounce validation to avoid excessive processing + const lastUpdate = this.lastUpdateTime.get(uri) || 0; + if (now - lastUpdate < 500) { // 500ms debounce + return; + } + + this.lastUpdateTime.set(uri, now); + + // Use setTimeout to debounce the validation + setTimeout(() => { + this.validateDocument(document); + }, 500); + } + + /** + * Validate a document and update diagnostics + */ + private validateDocument(document: vscode.TextDocument): void { + if (document.languageId !== 'ashes') { + return; + } + + try { + // Update command cache if needed + this.updateCommandCacheForDocument(document); + + // Get diagnostics from syntax validator + const diagnostics = this.syntaxValidator.validateDocument(document); + + // Update the diagnostic collection + this.diagnosticCollection.set(document.uri, diagnostics); + + } catch (error) { + console.error('Error validating ASHES document:', error); + + // Show error diagnostic + const errorDiagnostic = new vscode.Diagnostic( + new vscode.Range(0, 0, 0, 0), + `Error validating ASHES syntax: ${error}`, + vscode.DiagnosticSeverity.Error + ); + this.diagnosticCollection.set(document.uri, [errorDiagnostic]); + } + } + + /** + * Update command cache for all workspace folders + */ + private updateCommandCache(): void { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return; + } + + for (const folder of workspaceFolders) { + this.updateCommandCacheForWorkspace(folder.uri.fsPath); + } + } + + /** + * Update command cache for a specific document's workspace + */ + private updateCommandCacheForDocument(document: vscode.TextDocument): void { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder) { + return; + } + + const workspacePath = workspaceFolder.uri.fsPath; + this.updateCommandCacheForWorkspace(workspacePath); + } + + /** + * Update command cache for a specific workspace + */ + private updateCommandCacheForWorkspace(workspacePath: string): void { + const now = Date.now(); + const lastUpdate = this.lastUpdateTime.get(workspacePath) || 0; + + // Only update if cache is older than 5 minutes + if (now - lastUpdate < 5 * 60 * 1000) { + return; + } + + try { + const commands = this.extractCommandsFromWorkspace(workspacePath); + this.commandCache.set(workspacePath, commands); + this.syntaxValidator.updateCommands(commands); + this.lastUpdateTime.set(workspacePath, now); + + // Re-validate all ASHES documents in this workspace + vscode.workspace.textDocuments.forEach(document => { + if (document.languageId === 'ashes') { + const docWorkspace = vscode.workspace.getWorkspaceFolder(document.uri); + if (docWorkspace && docWorkspace.uri.fsPath === workspacePath) { + this.validateDocument(document); + } + } + }); + + } catch (error) { + console.error('Error updating command cache:', error); + } + } + + /** + * Extract commands from workspace using the existing CommandParser + */ + private extractCommandsFromWorkspace(workspacePath: string): string[] { + try { + // Import the CommandParser dynamically to avoid circular dependencies + const { CommandParser } = require('./commandParser'); + const parser = new CommandParser(workspacePath); + const commands = parser.parseCommands(); + return commands.map((cmd: any) => cmd.name); + } catch (error) { + console.error('Error extracting commands:', error); + // Return basic command set as fallback + return [ + 'accept_input', 'anim', 'anim_block', 'block_say', 'camera_push', 'camera_push_block', + 'camera_set_limits', 'camera_set_pos', 'camera_set_pos_block', 'camera_set_target', + 'camera_set_target_block', 'camera_set_zoom', 'camera_set_zoom_block', 'camera_set_zoom_height', + 'camera_set_zoom_height_block', 'camera_shift', 'camera_shift_block', 'change_scene', 'custom', + 'dec_global', 'enable_terrain', 'end_block_say', 'hide_menu', 'inc_global', 'inventory_add', + 'inventory_remove', 'item_count_add', 'play_lib_snd', 'play_snd', 'play_video', 'print', + 'print_internal', 'queue_event', 'queue_resource', 'rand_global', 'repeat', 'save_game', + 'say', 'say_last_dialog_option', 'say_random', 'say_sequence', 'sched_event', 'set_active', + 'set_active_if_exists', 'set_angle', 'set_animations', 'set_direction', 'set_global', + 'set_globals', 'set_gui_visible', 'set_interactive', 'set_item_custom_data', 'set_speed', + 'set_state', 'set_tooltip', 'show_menu', 'slide', 'slide_block', 'spawn', 'stop', 'stop_snd', + 'teleport', 'teleport_pos', 'transition', 'turn_to', 'wait', 'walk', 'walk_block', + 'walk_to_pos', 'walk_to_pos_block' + ]; + } + } + + /** + * Clear diagnostics for a document + */ + public clearDiagnostics(document: vscode.TextDocument): void { + this.diagnosticCollection.delete(document.uri); + } + + /** + * Clear all diagnostics + */ + public clearAllDiagnostics(): void { + this.diagnosticCollection.clear(); + } + + /** + * Get current diagnostics for a document + */ + public getDiagnostics(document: vscode.TextDocument): readonly vscode.Diagnostic[] { + return this.diagnosticCollection.get(document.uri) || []; + } + + /** + * Force refresh diagnostics for all ASHES documents + */ + public refreshAllDiagnostics(): void { + this.updateCommandCache(); + + vscode.workspace.textDocuments.forEach(document => { + if (document.languageId === 'ashes') { + this.validateDocument(document); + } + }); + } + + /** + * Get diagnostic collection for external access + */ + public getDiagnosticCollection(): vscode.DiagnosticCollection { + return this.diagnosticCollection; + } +} diff --git a/vscode-extension-ashes/src/extension.ts b/vscode-extension-ashes/src/extension.ts index 82a8c825..0cdda8e0 100644 --- a/vscode-extension-ashes/src/extension.ts +++ b/vscode-extension-ashes/src/extension.ts @@ -1,6 +1,7 @@ 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}> = []; @@ -59,6 +60,10 @@ function getCommands(workspaceRoot: string): Array<{name: string, description: s 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', @@ -458,10 +463,19 @@ export function activate(context: vscode.ExtensionContext) { ASHES_COMMANDS = []; COMMAND_CACHE_TIMESTAMP = 0; - vscode.window.showInformationMessage('ASHES commands cache refreshed!'); + // Also refresh diagnostics + diagnosticProvider.refreshAllDiagnostics(); + + vscode.window.showInformationMessage('ASHES commands cache and diagnostics refreshed!'); }); - context.subscriptions.push(completionProvider, hoverProvider, definitionProvider, showCommandReference, refreshCommands); + // Register command for refreshing diagnostics only + const refreshDiagnostics = vscode.commands.registerCommand('ashes.refreshDiagnostics', () => { + diagnosticProvider.refreshAllDiagnostics(); + vscode.window.showInformationMessage('ASHES diagnostics refreshed!'); + }); + + context.subscriptions.push(completionProvider, hoverProvider, definitionProvider, showCommandReference, refreshCommands, refreshDiagnostics); } export function deactivate() {} diff --git a/vscode-extension-ashes/src/syntaxValidator.ts b/vscode-extension-ashes/src/syntaxValidator.ts new file mode 100644 index 00000000..4ca812b0 --- /dev/null +++ b/vscode-extension-ashes/src/syntaxValidator.ts @@ -0,0 +1,799 @@ +import * as vscode from 'vscode'; + +export interface SyntaxError { + line: number; + column: number; + message: string; + severity: vscode.DiagnosticSeverity; + code?: string; +} + +export class ASHESSyntaxValidator { + private commands: Set = new Set(); + private builtinVariables: Set = new Set([ + 'CURRENT_PLAYER', + 'ESC_LAST_SCENE', + 'ESC_CURRENT_SCENE', + 'FORCE_LAST_SCENE_NULL', + 'ANIMATION_RESOURCES' + ]); + private keywords: Set = new Set([ + 'var', 'global', 'if', 'elif', 'else', 'while', 'break', 'done', 'stop', 'pass', + 'true', 'false', 'nil', 'and', 'or', 'not', 'in', 'is', 'active' + ]); + + constructor(commands: string[] = []) { + this.commands = new Set(commands); + } + + /** + * Update the list of available commands + */ + public updateCommands(commands: string[]): void { + this.commands = new Set(commands); + } + + /** + * Validate ASHES syntax and return diagnostic errors + */ + public validateDocument(document: vscode.TextDocument): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + const text = document.getText(); + const lines = text.split('\n'); + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + const lineNumber = lineIndex; + + // Skip empty lines and comments + if (this.isEmptyOrComment(line)) { + continue; + } + + // Validate different types of lines + const lineDiagnostics = this.validateLine(line, lineNumber, lines, lineIndex); + diagnostics.push(...lineDiagnostics); + } + + // Validate overall document structure + const documentDiagnostics = this.validateDocumentStructure(lines); + diagnostics.push(...documentDiagnostics); + + return diagnostics; + } + + /** + * Check if a line is empty or a comment + */ + private isEmptyOrComment(line: string): boolean { + const trimmed = line.trim(); + return trimmed === '' || trimmed.startsWith('#'); + } + + /** + * Validate a single line + */ + private validateLine(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + const trimmed = line.trim(); + + // Validate event definitions + if (trimmed.startsWith(':')) { + diagnostics.push(...this.validateEventDefinition(line, lineNumber)); + } + // Validate dialog blocks + else if (trimmed.startsWith('?!')) { + diagnostics.push(...this.validateDialogBlock(line, lineNumber, allLines, lineIndex)); + } + // Validate dialog choices + else if (trimmed.startsWith('-')) { + diagnostics.push(...this.validateDialogChoice(line, lineNumber)); + } + // Validate commands and control flow + else { + diagnostics.push(...this.validateCommandOrControlFlow(line, lineNumber)); + } + + // Validate indentation + diagnostics.push(...this.validateIndentation(line, lineNumber, allLines, lineIndex)); + + // Validate indentation after control flow statements + if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('else') || line.includes('while '))) { + // Check if the next line has proper indentation + if (lineIndex + 1 < allLines.length) { + const currentIndent = line.match(/^(\s*)/)?.[1].length || 0; + + // Skip empty lines + let nextNonEmptyLineIndex = lineIndex + 1; + while (nextNonEmptyLineIndex < allLines.length && allLines[nextNonEmptyLineIndex].trim() === '') { + nextNonEmptyLineIndex++; + } + + if (nextNonEmptyLineIndex < allLines.length) { + const nextNonEmptyLine = allLines[nextNonEmptyLineIndex]; + const nextNonEmptyIndent = nextNonEmptyLine.match(/^(\s*)/)?.[1].length || 0; + + // The next non-empty line should have exactly one more level of indentation + if (nextNonEmptyIndent <= currentIndent) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Control flow statement requires indented block on the next line", + severity: vscode.DiagnosticSeverity.Error, + code: 'missing-indentation' + }); + } + } + } + } + + // Validate string literals + diagnostics.push(...this.validateStringLiterals(line, lineNumber)); + + // Validate parentheses matching + diagnostics.push(...this.validateParentheses(line, lineNumber)); + + return diagnostics; + } + + /** + * Validate event definitions + */ + private validateEventDefinition(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Event definition patterns based on real ESC files: + // :event_name + // :event_name | FLAGS + // :event_name "target" + // :event_name | FLAGS "target" + const eventMatch = line.match(/^(\s*):([a-zA-Z_][a-zA-Z0-9_]*)(\s*\|\s*[A-Z_]+)*(\s+("[^"]*"))?/); + + if (!eventMatch) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Invalid event definition. Expected format: :event_name [| FLAGS] [\"target\"]", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-definition' + }); + return diagnostics; + } + + const eventName = eventMatch[2]; + const flags = eventMatch[3]; + const target = eventMatch[5]; + + // Validate event name + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(eventName)) { + diagnostics.push({ + range: new vscode.Range(lineNumber, eventMatch.index! + 1, lineNumber, eventMatch.index! + 1 + eventName.length), + message: "Event name must start with a letter or underscore and contain only letters, numbers, and underscores", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-name' + }); + } + + // Check for common event names and suggest alternatives + const commonEvents = ['init', 'ready', 'setup', 'action1', 'action2', 'action3', 'use', 'look', 'talk', 'walk', 'interact']; + if (!commonEvents.includes(eventName.toLowerCase()) && eventName.length > 20) { + const eventIndex = line.indexOf(eventName); + diagnostics.push({ + range: new vscode.Range(lineNumber, eventIndex, lineNumber, eventIndex + eventName.length), + message: `Consider using a shorter event name. Common events: ${commonEvents.join(', ')}`, + severity: vscode.DiagnosticSeverity.Information, + code: 'event-naming-suggestion' + }); + } + + // Validate flags if present + if (flags) { + const flagText = flags.trim().substring(1).trim(); // Remove the | + const validFlags = ['TK', 'TR', 'TG', 'SKIP', 'GLOBAL']; + const providedFlags = flagText.split(/\s+/); + + for (const flag of providedFlags) { + if (flag && !validFlags.includes(flag)) { + const flagIndex = line.indexOf(flag); + diagnostics.push({ + range: new vscode.Range(lineNumber, flagIndex, lineNumber, flagIndex + flag.length), + message: `Invalid event flag '${flag}'. Valid flags: ${validFlags.join(', ')}`, + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-flag' + }); + } + } + } + + // Validate target if present + if (target && (!target.startsWith('"') || !target.endsWith('"'))) { + const targetIndex = line.indexOf(target); + diagnostics.push({ + range: new vscode.Range(lineNumber, targetIndex, lineNumber, targetIndex + target.length), + message: "Event target must be a quoted string", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-event-target' + }); + } + + return diagnostics; + } + + /** + * Validate dialog blocks + */ + private validateDialogBlock(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Dialog block should have proper indentation + const indentMatch = line.match(/^(\s*)(\?\!)/); + if (!indentMatch) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Invalid dialog block syntax. Expected: ?!", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-dialog-block' + }); + return diagnostics; + } + + const baseIndent = indentMatch[1].length; + + // Check if dialog block is properly closed + let foundEnd = false; + for (let i = lineIndex + 1; i < allLines.length; i++) { + const nextLine = allLines[i]; + if (nextLine.trim() === '') continue; + + const nextIndent = nextLine.match(/^(\s*)/)?.[1].length || 0; + + if (nextIndent <= baseIndent && !nextLine.trim().startsWith('-')) { + foundEnd = true; + break; + } + } + + if (!foundEnd) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Dialog block is not properly closed", + severity: vscode.DiagnosticSeverity.Warning, + code: 'unclosed-dialog-block' + }); + } + + return diagnostics; + } + + /** + * Validate dialog choices + */ + private validateDialogChoice(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Dialog choice pattern: - "choice text" [condition] + const choiceMatch = line.match(/^(\s*)-(\s*)("[^"]*")(\s*\[.*\])?/); + + if (!choiceMatch) { + diagnostics.push({ + range: new vscode.Range(lineNumber, 0, lineNumber, line.length), + message: "Invalid dialog choice syntax. Expected: - \"choice text\" [condition]", + severity: vscode.DiagnosticSeverity.Error, + code: 'invalid-dialog-choice' + }); + return diagnostics; + } + + const choiceText = choiceMatch[3]; + const condition = choiceMatch[4]; + + // Validate choice text is not empty + if (choiceText === '""') { + const textIndex = line.indexOf(choiceText); + diagnostics.push({ + range: new vscode.Range(lineNumber, textIndex, lineNumber, textIndex + choiceText.length), + message: "Dialog choice text cannot be empty", + severity: vscode.DiagnosticSeverity.Error, + code: 'empty-dialog-choice' + }); + } + + // Validate condition if present + if (condition) { + const conditionContent = condition.slice(1, -1); // Remove brackets + if (conditionContent.trim() === '') { + const conditionIndex = line.indexOf(condition); + diagnostics.push({ + range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length), + message: "Dialog choice condition cannot be empty", + severity: vscode.DiagnosticSeverity.Error, + code: 'empty-dialog-condition' + }); + } + } + + return diagnostics; + } + + /** + * Validate commands and control flow statements + */ + private validateCommandOrControlFlow(line: string, lineNumber: number): vscode.Diagnostic[] { + const diagnostics: vscode.Diagnostic[] = []; + + // Check for function calls + const functionCallMatch = line.match(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/); + if (functionCallMatch) { + const functionName = functionCallMatch[1]; + + // Check if it's a known command + if (!this.commands.has(functionName) && !this.keywords.has(functionName)) { + const funcIndex = line.indexOf(functionName); + diagnostics.push({ + range: new vscode.Range(lineNumber, funcIndex, lineNumber, funcIndex + functionName.length), + message: `Unknown command '${functionName}'. Available commands: ${Array.from(this.commands).slice(0, 10).join(', ')}${this.commands.size > 10 ? '...' : ''}`, + severity: vscode.DiagnosticSeverity.Error, + code: 'unknown-command' + }); + } + } + + // Validate control flow statements + const controlFlowMatch = line.match(/\b(if|elif|while)\s+(.+):/); + if (controlFlowMatch) { + const condition = controlFlowMatch[2]; + + // Basic condition validation + if (condition.trim() === '') { + const conditionIndex = line.indexOf(condition); + diagnostics.push({ + range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length), + message: "Control flow condition cannot be empty", + severity: vscode.DiagnosticSeverity.Error, + code: 'empty-condition' + }); + } + } + + // Check for missing colon in control flow statements + const controlFlowWithoutColon = line.match(/\b(if|elif|else|while)\s+(.+)$/); + if (controlFlowWithoutColon && !line.trim().endsWith(':')) { + const keyword = controlFlowWithoutColon[1]; + const keywordIndex = line.indexOf(keyword); + diagnostics.push({ + range: new vscode.Range(lineNumber, keywordIndex, lineNumber, keywordIndex + keyword.length), + message: `'${keyword}' statement must end with ':'`, + severity: vscode.DiagnosticSeverity.Error, + code: 'missing-colon' + }); + } + + // Check for 'else' without colon + const elseMatch = line.match(/\belse\s*$/); + if (elseMatch && !line.trim().endsWith(':')) { + const elseIndex = line.indexOf('else'); + diagnostics.push({ + range: new vscode.Range(lineNumber, elseIndex, lineNumber, elseIndex + 4), + message: "'else' statement must end with ':'", + severity: vscode.DiagnosticSeverity.Error, + code: 'missing-colon' + }); + } + + + // Validate common condition patterns from real ESC files (for lines that do have colons) + if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('while '))) { + const conditionMatch = line.match(/\b(if|elif|while)\s+(.+):/); + if (conditionMatch) { + const conditionText = conditionMatch[2].trim(); + + // Check for proper inventory check patterns: "item" in inventory + const inventoryMatch = conditionText.match(/([^"]*)\s+in\s+inventory/); + if (inventoryMatch) { + const itemPart = inventoryMatch[1].trim(); + if (!itemPart.startsWith('"') || !itemPart.endsWith('"')) { + 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; + + // Check for excessive indentation (more than 3 levels deep) + 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; + } +} From f8feee5fd7391d7c572f29948657f00f732e00ce Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 03:27:16 +0200 Subject: [PATCH 6/8] Requirements --- vscode-extension-ashes/Requirements.md | 594 +++++++++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 vscode-extension-ashes/Requirements.md diff --git a/vscode-extension-ashes/Requirements.md b/vscode-extension-ashes/Requirements.md new file mode 100644 index 00000000..61a401f6 --- /dev/null +++ b/vscode-extension-ashes/Requirements.md @@ -0,0 +1,594 @@ +# ASHES Language Support Extension - Requirements + +## Project Overview + +The ASHES Language Support extension is a Visual Studio Code extension that provides comprehensive language support for the ASHES (Adventure Scripting Helping Escoria) language used in the Escoria adventure game framework. ASHES is a Python-like scripting language specifically designed for creating adventure games, borrowing heavily from Pythonic languages while being optimized for the Escoria framework. + +This extension enhances the development experience for game developers working with ASHES scripts by providing syntax highlighting, IntelliSense, error detection, and other modern IDE features. The extension dynamically discovers commands from the project's configuration and provides real-time validation based on the actual Escoria command implementations. + +## ASHES Language Specifications + +Based on the Escoria core documentation, ASHES supports the following language features: + +### Data Types +- **Integers**: Whole numbers +- **Floats/Reals**: Decimal numbers +- **Booleans**: `true` and `false` values +- **Strings**: Text enclosed in quotes, can be concatenated with `+` operator +- **Nil**: The keyword `nil` is used in place of "null" and/or where empty values are desired + +### Language Features +- **Events**: Defined with `:event_name` at the highest level of scope (no indentation) +- **Variables**: Local variables with `var`, global variables with `global` +- **Comments**: Line comments using `#` character (block comments not supported) +- **Branching**: `if`, `elif`, and `else` statements +- **Looping**: `while` loops with `break` keyword to exit +- **Dialog System**: Dialog blocks with `?!` and choices with `-` +- **Global IDs**: Can be prefixed with `$` to avoid quotation marks +- **Inventory Checking**: `if $item in inventory:` syntax +- **State Checking**: `if $object is active:` and `if $object is "state":` syntax + +### Operators +- **Boolean**: `not`, `and`, `or`, `!` (exclamation mark for not) +- **Arithmetic**: `+`, `-`, `*`, `/` +- **Comparison**: `>`, `>=`, `<`, `<=`, `==` + +## Functional Requirements + +### 1. Language Recognition and File Association + +**FR-1.1: File Extension Association** +- The extension MUST recognize `.esc` files as ASHES language files +- The extension MUST set the language ID to `ashes` for `.esc` files +- The extension MUST activate automatically when `.esc` files are opened + +**FR-1.2: Language Configuration** +- The extension MUST provide proper language configuration including: + - Line comments using `#` character + - Bracket matching for `{}`, `[]`, `()` + - Auto-closing pairs for brackets and quotes + - Word pattern recognition for identifiers + - Smart indentation rules for ASHES syntax + +### 2. Syntax Highlighting + +**FR-2.1: Comprehensive Syntax Highlighting** +- The extension MUST provide syntax highlighting for ASHES language features as specified in the Escoria core documentation: + - **Events**: `:event_name` (blue highlighting) - Events are denoted using `:event` where "event" is the name of the event + - **Commands**: Function calls like `say()`, `set_global()` (green highlighting) - Commands are called just as functions normally are + - **Variables**: `var`, `global` declarations (orange highlighting) - Local variables with `var`, global variables with `global` + - **Strings**: Quoted text `"text"` (yellow highlighting) - Strings can be concatenated using the `+` operator + - **Comments**: `# comment` (gray highlighting) - Line comments using hashtag/pound sign + - **Dialog blocks**: `?!` syntax (special highlighting) - Dialog blocks begin with `?!` + - **Dialog choices**: `- "choice text" [condition]` (special highlighting) - Each dialog choice is prefixed with `-` + - **Global IDs**: `$object_id` (distinct color) - Global IDs can be prefixed with `$` to avoid quotation marks + - **Keywords**: `if`, `elif`, `else`, `while`, `break`, `done`, `stop`, `pass`, `true`, `false`, `nil`, `and`, `or`, `not`, `in`, `is`, `active` + +**FR-2.2: Advanced Syntax Patterns** +- The extension MUST highlight event definitions with flags: `:event_name | FLAG_NAME` (e.g., `:look | NO_UI | NO_TT`) +- The extension MUST highlight event definitions with targets: `:event_name "target"` +- The extension MUST highlight dialog choices with conditions: `- "choice text" [condition]` +- The extension MUST highlight control flow statements with proper indentation +- The extension MUST highlight boolean operators: `not`, `and`, `or`, `!` (exclamation mark for not) +- The extension MUST highlight arithmetic operators: `+`, `-`, `*`, `/` +- The extension MUST highlight comparison operators: `>`, `>=`, `<`, `<=`, `==` +- The extension MUST highlight the `in inventory` feature: `if $gold_brick in inventory:` +- The extension MUST highlight the `is active` feature: `if $room_monster is active:` +- The extension MUST highlight the `is` keyword for state checking: `if $room_monster is "running_away":` + +### 3. IntelliSense and Auto-completion + +**FR-3.1: Command Auto-completion** +- The extension MUST provide auto-completion for all ASHES commands from the configured command directories: + - **Core Commands** (from `escoria-core`): `accept_input`, `anim`, `anim_block`, `block_say`, `camera_push`, `camera_set_pos`, `camera_set_target`, `camera_set_zoom`, `change_scene`, `custom`, `dec_global`, `enable_terrain`, `end_block_say`, `hide_menu`, `inc_global`, `inventory_add`, `inventory_remove`, `play_snd`, `print_internal`, `queue_event`, `queue_resource`, `rand_global`, `repeat`, `save_game`, `say`, `sched_event`, `set_active`, `set_angle`, `set_animations`, `set_direction`, `set_global`, `set_globals`, `set_gui_visible`, `set_interactive`, `set_item_custom_data`, `set_speed`, `set_state`, `show_menu`, `slide`, `spawn`, `stop`, `stop_snd`, `teleport`, `teleport_pos`, `transition`, `turn_to`, `wait`, `walk`, `walk_to_pos` + - **UI Commands** (from `escoria-ui-return-monkey-island`): `item_count_add`, `music_enable`, `play_lib_snd`, `play_video`, `set_tooltip` + - **Dialog Commands** (from `escoria-ui-return-monkey-island-dialog-simple`): `say_last_dialog_option`, `say_random`, `say_sequence` +- The extension MUST dynamically load commands from project configuration +- The extension MUST show command descriptions and parameter information +- The extension MUST support trigger characters: space, `(`, `$` + +**FR-3.2: Enhanced Parameter Information** +- The extension MUST display parameter information with: + - **Required** / **Optional** indicators + - Parameter types (string, boolean, number, object, scene, animation, etc.) + - Variable names in bold formatting + - Default values when available + - Example usage patterns + +**FR-3.3: Variable and Keyword Completion** +- The extension MUST provide auto-completion for built-in variables as specified in the Escoria core documentation: + - `CURRENT_PLAYER` - References the current player scene being used, irrespective of its global ID + - `ESC_LAST_SCENE` - Holds the global ID of the last visited room + - `ESC_CURRENT_SCENE` - Holds the global ID of the current room + - `FORCE_LAST_SCENE_NULL` - Internal use only + - `ANIMATION_RESOURCES` - Internal use only +- The extension MUST provide auto-completion for ASHES keywords: `var`, `global`, `if`, `elif`, `else`, `while`, `break`, `done`, `stop`, `pass`, `true`, `false`, `nil`, `and`, `or`, `not`, `in`, `is`, `active` +- The extension MUST provide auto-completion for global IDs with `$` prefix (e.g., `$object_1_id`) + +### 4. Dynamic Command Discovery + +**FR-4.1: Project Configuration Parsing** +- The extension MUST parse `project.godot` files to extract command directories from the `[escoria]` section +- The extension MUST support the `main/command_directories` setting with array format: `["path1", "path2", "path3"]` +- The extension MUST handle the actual project configuration format as seen in real projects: + ```ini + [escoria] + main/command_directories=["res://addons/escoria-core/game/core-scripts/esc/commands", "res://addons/escoria-ui-return-monkey-island/esc/commands", "res://addons/escoria-ui-return-monkey-island-dialog-simple/commands"] + ``` +- The extension MUST provide fallback to default command directories if parsing fails: + - `res://addons/escoria-core/game/core-scripts/esc/commands` (core commands) + - `res://addons/escoria-ui-return-monkey-island/esc/commands` (UI commands) + - `res://addons/escoria-ui-return-monkey-island-dialog-simple/commands` (dialog commands) + +**FR-4.2: Command File Parsing** +- The extension MUST scan specified directories for `.gd` files (excluding `.gd.uid`) +- The extension MUST extract command information from GDScript files following the Escoria command format: + - Command name from filename (e.g., `say.gd` → `say` command) + - Description from comment blocks starting with `# \`command_name param1 param2 [optional_param]\`` + - Parameters from comment documentation in `**Parameters**` section with `- *param*: description` format + - Parameter types and requirements from `configure()` method using `ESCCommandArgumentDescriptor` + - Examples from comment blocks with `Example: \`command_name("value1", "value2")\`` format +- The extension MUST handle the standard Escoria command documentation format: + ```gdscript + # `command_name param1 param2 [optional_param]` + # + # Description of what the command does. + # + # **Parameters** + # + # - *param1*: Description of parameter 1 + # - *param2*: Description of parameter 2 + # - *optional_param*: Description of optional parameter (default: default_value) + # + # Example: `command_name("value1", "value2")` + # + # @ESC + extends ESCBaseCommand + class_name CommandNameCommand + ``` + +**FR-4.3: Command Caching** +- The extension MUST cache discovered commands for 5 minutes to improve performance +- The extension MUST provide manual cache refresh functionality +- The extension MUST update cache when workspace changes + +### 5. Hover Information + +**FR-5.1: Command Documentation** +- The extension MUST provide detailed hover information for commands +- The extension MUST show command descriptions and parameter details +- The extension MUST format parameter information with clear indicators +- The extension MUST provide example usage in hover tooltips + +**FR-5.2: Variable Documentation** +- The extension MUST provide hover information for built-in variables +- The extension MUST explain the purpose of each built-in variable + +### 6. Go to Definition + +**FR-6.1: Command Navigation** +- The extension MUST support Ctrl+click navigation to command source files +- The extension MUST find the class definition line in command files +- The extension MUST open the correct file and position the cursor appropriately +- The extension MUST work in both the editor and command reference panel + +### 7. Code Snippets + +**FR-7.1: Event Snippets** +- The extension MUST provide snippets for: + - `event` → Create new event + - `eventtarget` → Create event with target + - `eventflags` → Create event with flags + - `eventflagstarget` → Create event with flags and target + +**FR-7.2: Command Snippets** +- The extension MUST provide snippets for common commands: + - `say` → Say command + - `setglobal` → Set global variable + - `changescene` → Change scene + - `setactive` → Set object active + - `teleport` → Teleport object + - `walk` → Walk object + - `playsnd` → Play sound + - `playvideo` → Play video + - `inventoryadd` → Add to inventory + - `inventoryremove` → Remove from inventory + +**FR-7.3: Control Flow Snippets** +- The extension MUST provide snippets for: + - `if` → If statement + - `ifelse` → If-else statement + - `while` → While loop + +**FR-7.4: Dialog Snippets** +- The extension MUST provide snippets for: + - `dialog` → Dialog block with choice + - `dialogif` → Dialog choice with condition + +**FR-7.5: Utility Snippets** +- The extension MUST provide snippets for: + - `var` → Local variable declaration + - `global` → Global variable declaration + - `comment` → Add comment + - `print` → Print debug message + +### 8. Command Reference Panel + +**FR-8.1: Interactive Command Reference** +- The extension MUST provide a webview panel showing all available commands +- The extension MUST display commands in a searchable table format +- The extension MUST show command descriptions and parameter information +- The extension MUST support clickable command names for navigation + +**FR-8.2: Command Reference Features** +- The extension MUST format parameter information with clear indicators +- The extension MUST provide example usage for each command +- The extension MUST support navigation to source files from the reference panel + +### 9. Syntax Validation and Error Detection + +**FR-9.1: Real-time Syntax Validation** +- The extension MUST validate ASHES syntax in real-time +- The extension MUST provide error diagnostics with appropriate severity levels +- The extension MUST debounce validation to avoid excessive processing (500ms) +- The extension MUST validate on document changes, open, and save + +**FR-9.2: Event Definition Validation** +- The extension MUST validate event definitions: `:event_name [| FLAGS] ["target"]` +- The extension MUST check event name format (letters, numbers, underscores) +- The extension MUST validate event flags against known valid flags (e.g., `TK`, `TR`, `TG`, `SKIP`, `GLOBAL`) +- The extension MUST validate event targets as quoted strings +- The extension MUST suggest common event names for better practices (e.g., `init`, `ready`, `setup`, `action1`, `action2`, `action3`, `use`, `look`, `talk`, `walk`, `interact`) +- The extension MUST validate that events are at the highest level of scope (no indentation) + +**FR-9.3: Dialog System Validation** +- The extension MUST validate dialog blocks (`?!`) syntax +- The extension MUST validate dialog choices (`- "text" [condition]`) +- The extension MUST check for proper dialog block closure +- The extension MUST validate choice text is not empty +- The extension MUST validate condition syntax in dialog choices +- The extension MUST support nested dialog blocks with proper indentation +- The extension MUST validate `break` and `done` keywords in dialog context +- The extension MUST validate `break` with level specification (e.g., `break 2`) + +**FR-9.4: Command and Control Flow Validation** +- The extension MUST validate function calls against known commands from all configured directories +- The extension MUST validate control flow statements (`if`, `elif`, `else`, `while`) +- The extension MUST check for missing colons in control flow statements +- The extension MUST validate condition expressions including: + - Boolean operators (`and`, `or`, `not`, `!`) + - Comparison operators (`>`, `>=`, `<`, `<=`, `==`) + - Inventory checks (`if $item in inventory:`) + - State checks (`if $object is active:`, `if $object is "state":`) + - Global ID comparisons (`if $object1 == $object2:`) +- The extension MUST check for proper indentation after control flow statements +- The extension MUST validate `while` loops with `break` keyword support + +**FR-9.5: String and Bracket Validation** +- The extension MUST validate string literals for proper quoting +- The extension MUST check for unclosed strings and brackets +- The extension MUST validate parentheses, brackets, and braces matching +- The extension MUST handle escaped characters in strings +- The extension MUST validate string concatenation with `+` operator +- The extension MUST validate global variable substitution in strings using `{variable_name}` syntax + +**FR-9.6: Indentation Validation** +- The extension MUST enforce tab-based indentation (not spaces) as required by ASHES +- The extension MUST detect mixed tabs and spaces +- The extension MUST warn about excessive indentation (more than 3 levels) +- 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 From 1ba6fe4c3fd149794390a001bb9e51b5a070080c Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 15:50:35 +0200 Subject: [PATCH 7/8] Requirents... more syntax validator --- vscode-extension-ashes/Requirements.md | 2 +- vscode-extension-ashes/src/syntaxValidator.ts | 28 +++++++++-------- .../syntaxes/ashes.tmLanguage.json | 30 ++++++++++++++++++- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/vscode-extension-ashes/Requirements.md b/vscode-extension-ashes/Requirements.md index 61a401f6..b990ed01 100644 --- a/vscode-extension-ashes/Requirements.md +++ b/vscode-extension-ashes/Requirements.md @@ -275,7 +275,7 @@ Based on the Escoria core documentation, ASHES supports the following language f **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 warn about excessive indentation (more than 3 levels) +- 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 diff --git a/vscode-extension-ashes/src/syntaxValidator.ts b/vscode-extension-ashes/src/syntaxValidator.ts index 4ca812b0..dc35634f 100644 --- a/vscode-extension-ashes/src/syntaxValidator.ts +++ b/vscode-extension-ashes/src/syntaxValidator.ts @@ -386,10 +386,14 @@ export class ASHESSyntaxValidator { const conditionText = conditionMatch[2].trim(); // Check for proper inventory check patterns: "item" in inventory - const inventoryMatch = conditionText.match(/([^"]*)\s+in\s+inventory/); + const inventoryMatch = conditionText.match(/"([^"]*)"\s+in\s+inventory/); if (inventoryMatch) { - const itemPart = inventoryMatch[1].trim(); - if (!itemPart.startsWith('"') || !itemPart.endsWith('"')) { + // 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), @@ -608,15 +612,15 @@ export class ASHESSyntaxValidator { if (prevIndentMatch) { const prevIndent = prevIndentMatch[1].length; - // Check for excessive indentation (more than 3 levels deep) - 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' - }); - } + // 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' + // }); + // } } } diff --git a/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json b/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json index 174e2498..47e42f3e 100644 --- a/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json +++ b/vscode-extension-ashes/syntaxes/ashes.tmLanguage.json @@ -286,11 +286,39 @@ }, { "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": [ { @@ -361,7 +389,7 @@ }, { "name": "keyword.other.ashes", - "match": "\\b(in|is|active)\\b" + "match": "\\b(in|is|active|inventory)\\b" } ] }, From 3d694755c4cdf3ac0b425d613f6323327680d993 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 19:12:58 +0200 Subject: [PATCH 8/8] cleanup --- vscode-extension-ashes/vs-escoria4-ashes | 1 - 1 file changed, 1 deletion(-) delete mode 120000 vscode-extension-ashes/vs-escoria4-ashes diff --git a/vscode-extension-ashes/vs-escoria4-ashes b/vscode-extension-ashes/vs-escoria4-ashes deleted file mode 120000 index 11941b4a..00000000 --- a/vscode-extension-ashes/vs-escoria4-ashes +++ /dev/null @@ -1 +0,0 @@ -/home/oier/.vscode-oss/extensions/vs-escoria4-ashes \ No newline at end of file