From 9c2a6cb7e7291625640302eb4ffcdee2ed2c5be8 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 7 Sep 2025 02:45:51 +0200 Subject: [PATCH] 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