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}