import * as vscode from 'vscode'; export interface SyntaxError { line: number; column: number; message: string; severity: vscode.DiagnosticSeverity; code?: string; } export class ASHESSyntaxValidator { private commands: Set = new Set(); private builtinVariables: Set = new Set([ 'CURRENT_PLAYER', 'ESC_LAST_SCENE', 'ESC_CURRENT_SCENE', 'FORCE_LAST_SCENE_NULL', 'ANIMATION_RESOURCES' ]); private keywords: Set = new Set([ 'var', 'global', 'if', 'elif', 'else', 'while', 'break', 'done', 'stop', 'pass', 'true', 'false', 'nil', 'and', 'or', 'not', 'in', 'is', 'active' ]); constructor(commands: string[] = []) { this.commands = new Set(commands); } /** * Update the list of available commands */ public updateCommands(commands: string[]): void { this.commands = new Set(commands); } /** * Validate ASHES syntax and return diagnostic errors */ public validateDocument(document: vscode.TextDocument): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; const text = document.getText(); const lines = text.split('\n'); for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { const line = lines[lineIndex]; const lineNumber = lineIndex; // Skip empty lines and comments if (this.isEmptyOrComment(line)) { continue; } // Validate different types of lines const lineDiagnostics = this.validateLine(line, lineNumber, lines, lineIndex); diagnostics.push(...lineDiagnostics); } // Validate overall document structure const documentDiagnostics = this.validateDocumentStructure(lines); diagnostics.push(...documentDiagnostics); return diagnostics; } /** * Check if a line is empty or a comment */ private isEmptyOrComment(line: string): boolean { const trimmed = line.trim(); return trimmed === '' || trimmed.startsWith('#'); } /** * Validate a single line */ private validateLine(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; const trimmed = line.trim(); // Validate event definitions if (trimmed.startsWith(':')) { diagnostics.push(...this.validateEventDefinition(line, lineNumber)); } // Validate dialog blocks else if (trimmed.startsWith('?!')) { diagnostics.push(...this.validateDialogBlock(line, lineNumber, allLines, lineIndex)); } // Validate dialog choices else if (trimmed.startsWith('-')) { diagnostics.push(...this.validateDialogChoice(line, lineNumber)); } // Validate commands and control flow else { diagnostics.push(...this.validateCommandOrControlFlow(line, lineNumber)); } // Validate indentation diagnostics.push(...this.validateIndentation(line, lineNumber, allLines, lineIndex)); // Validate indentation after control flow statements if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('else') || line.includes('while '))) { // Check if the next line has proper indentation if (lineIndex + 1 < allLines.length) { const currentIndent = line.match(/^(\s*)/)?.[1].length || 0; // Skip empty lines let nextNonEmptyLineIndex = lineIndex + 1; while (nextNonEmptyLineIndex < allLines.length && allLines[nextNonEmptyLineIndex].trim() === '') { nextNonEmptyLineIndex++; } if (nextNonEmptyLineIndex < allLines.length) { const nextNonEmptyLine = allLines[nextNonEmptyLineIndex]; const nextNonEmptyIndent = nextNonEmptyLine.match(/^(\s*)/)?.[1].length || 0; // The next non-empty line should have exactly one more level of indentation if (nextNonEmptyIndent <= currentIndent) { diagnostics.push({ range: new vscode.Range(lineNumber, 0, lineNumber, line.length), message: "Control flow statement requires indented block on the next line", severity: vscode.DiagnosticSeverity.Error, code: 'missing-indentation' }); } } } } // Validate string literals diagnostics.push(...this.validateStringLiterals(line, lineNumber)); // Validate parentheses matching diagnostics.push(...this.validateParentheses(line, lineNumber)); return diagnostics; } /** * Validate event definitions */ private validateEventDefinition(line: string, lineNumber: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; // Event definition patterns based on real ESC files: // :event_name // :event_name | FLAGS // :event_name "target" // :event_name | FLAGS "target" const eventMatch = line.match(/^(\s*):([a-zA-Z_][a-zA-Z0-9_]*)(\s*\|\s*[A-Z_]+)*(\s+("[^"]*"))?/); if (!eventMatch) { diagnostics.push({ range: new vscode.Range(lineNumber, 0, lineNumber, line.length), message: "Invalid event definition. Expected format: :event_name [| FLAGS] [\"target\"]", severity: vscode.DiagnosticSeverity.Error, code: 'invalid-event-definition' }); return diagnostics; } const eventName = eventMatch[2]; const flags = eventMatch[3]; const target = eventMatch[5]; // Validate event name if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(eventName)) { diagnostics.push({ range: new vscode.Range(lineNumber, eventMatch.index! + 1, lineNumber, eventMatch.index! + 1 + eventName.length), message: "Event name must start with a letter or underscore and contain only letters, numbers, and underscores", severity: vscode.DiagnosticSeverity.Error, code: 'invalid-event-name' }); } // Check for common event names and suggest alternatives const commonEvents = ['init', 'ready', 'setup', 'action1', 'action2', 'action3', 'use', 'look', 'talk', 'walk', 'interact']; if (!commonEvents.includes(eventName.toLowerCase()) && eventName.length > 20) { const eventIndex = line.indexOf(eventName); diagnostics.push({ range: new vscode.Range(lineNumber, eventIndex, lineNumber, eventIndex + eventName.length), message: `Consider using a shorter event name. Common events: ${commonEvents.join(', ')}`, severity: vscode.DiagnosticSeverity.Information, code: 'event-naming-suggestion' }); } // Validate flags if present if (flags) { const flagText = flags.trim().substring(1).trim(); // Remove the | const validFlags = ['TK', 'TR', 'TG', 'SKIP', 'GLOBAL']; const providedFlags = flagText.split(/\s+/); for (const flag of providedFlags) { if (flag && !validFlags.includes(flag)) { const flagIndex = line.indexOf(flag); diagnostics.push({ range: new vscode.Range(lineNumber, flagIndex, lineNumber, flagIndex + flag.length), message: `Invalid event flag '${flag}'. Valid flags: ${validFlags.join(', ')}`, severity: vscode.DiagnosticSeverity.Error, code: 'invalid-event-flag' }); } } } // Validate target if present if (target && (!target.startsWith('"') || !target.endsWith('"'))) { const targetIndex = line.indexOf(target); diagnostics.push({ range: new vscode.Range(lineNumber, targetIndex, lineNumber, targetIndex + target.length), message: "Event target must be a quoted string", severity: vscode.DiagnosticSeverity.Error, code: 'invalid-event-target' }); } return diagnostics; } /** * Validate dialog blocks */ private validateDialogBlock(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; // Dialog block should have proper indentation const indentMatch = line.match(/^(\s*)(\?\!)/); if (!indentMatch) { diagnostics.push({ range: new vscode.Range(lineNumber, 0, lineNumber, line.length), message: "Invalid dialog block syntax. Expected: ?!", severity: vscode.DiagnosticSeverity.Error, code: 'invalid-dialog-block' }); return diagnostics; } const baseIndent = indentMatch[1].length; // Check if dialog block is properly closed let foundEnd = false; for (let i = lineIndex + 1; i < allLines.length; i++) { const nextLine = allLines[i]; if (nextLine.trim() === '') continue; const nextIndent = nextLine.match(/^(\s*)/)?.[1].length || 0; if (nextIndent <= baseIndent && !nextLine.trim().startsWith('-')) { foundEnd = true; break; } } if (!foundEnd) { diagnostics.push({ range: new vscode.Range(lineNumber, 0, lineNumber, line.length), message: "Dialog block is not properly closed", severity: vscode.DiagnosticSeverity.Warning, code: 'unclosed-dialog-block' }); } return diagnostics; } /** * Validate dialog choices */ private validateDialogChoice(line: string, lineNumber: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; // Dialog choice pattern: - "choice text" [condition] const choiceMatch = line.match(/^(\s*)-(\s*)("[^"]*")(\s*\[.*\])?/); if (!choiceMatch) { diagnostics.push({ range: new vscode.Range(lineNumber, 0, lineNumber, line.length), message: "Invalid dialog choice syntax. Expected: - \"choice text\" [condition]", severity: vscode.DiagnosticSeverity.Error, code: 'invalid-dialog-choice' }); return diagnostics; } const choiceText = choiceMatch[3]; const condition = choiceMatch[4]; // Validate choice text is not empty if (choiceText === '""') { const textIndex = line.indexOf(choiceText); diagnostics.push({ range: new vscode.Range(lineNumber, textIndex, lineNumber, textIndex + choiceText.length), message: "Dialog choice text cannot be empty", severity: vscode.DiagnosticSeverity.Error, code: 'empty-dialog-choice' }); } // Validate condition if present if (condition) { const conditionContent = condition.slice(1, -1); // Remove brackets if (conditionContent.trim() === '') { const conditionIndex = line.indexOf(condition); diagnostics.push({ range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length), message: "Dialog choice condition cannot be empty", severity: vscode.DiagnosticSeverity.Error, code: 'empty-dialog-condition' }); } } return diagnostics; } /** * Validate commands and control flow statements */ private validateCommandOrControlFlow(line: string, lineNumber: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; // Check for function calls const functionCallMatch = line.match(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/); if (functionCallMatch) { const functionName = functionCallMatch[1]; // Check if it's a known command if (!this.commands.has(functionName) && !this.keywords.has(functionName)) { const funcIndex = line.indexOf(functionName); diagnostics.push({ range: new vscode.Range(lineNumber, funcIndex, lineNumber, funcIndex + functionName.length), message: `Unknown command '${functionName}'. Available commands: ${Array.from(this.commands).slice(0, 10).join(', ')}${this.commands.size > 10 ? '...' : ''}`, severity: vscode.DiagnosticSeverity.Error, code: 'unknown-command' }); } } // Validate control flow statements const controlFlowMatch = line.match(/\b(if|elif|while)\s+(.+):/); if (controlFlowMatch) { const condition = controlFlowMatch[2]; // Basic condition validation if (condition.trim() === '') { const conditionIndex = line.indexOf(condition); diagnostics.push({ range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length), message: "Control flow condition cannot be empty", severity: vscode.DiagnosticSeverity.Error, code: 'empty-condition' }); } } // Check for missing colon in control flow statements const controlFlowWithoutColon = line.match(/\b(if|elif|else|while)\s+(.+)$/); if (controlFlowWithoutColon && !line.trim().endsWith(':')) { const keyword = controlFlowWithoutColon[1]; const keywordIndex = line.indexOf(keyword); diagnostics.push({ range: new vscode.Range(lineNumber, keywordIndex, lineNumber, keywordIndex + keyword.length), message: `'${keyword}' statement must end with ':'`, severity: vscode.DiagnosticSeverity.Error, code: 'missing-colon' }); } // Check for 'else' without colon const elseMatch = line.match(/\belse\s*$/); if (elseMatch && !line.trim().endsWith(':')) { const elseIndex = line.indexOf('else'); diagnostics.push({ range: new vscode.Range(lineNumber, elseIndex, lineNumber, elseIndex + 4), message: "'else' statement must end with ':'", severity: vscode.DiagnosticSeverity.Error, code: 'missing-colon' }); } // Validate common condition patterns from real ESC files (for lines that do have colons) if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('while '))) { const conditionMatch = line.match(/\b(if|elif|while)\s+(.+):/); if (conditionMatch) { const conditionText = conditionMatch[2].trim(); // Check for proper inventory check patterns: "item" in inventory const inventoryMatch = conditionText.match(/"([^"]*)"\s+in\s+inventory/); if (inventoryMatch) { // The pattern already matches quoted strings, so this is valid // No need to check for quotes since the regex already requires them } else { // Check if there's an inventory check without proper quotes const unquotedInventoryMatch = conditionText.match(/([^"]*)\s+in\s+inventory/); if (unquotedInventoryMatch) { const inventoryIndex = conditionText.indexOf('in inventory'); diagnostics.push({ range: new vscode.Range(lineNumber, line.indexOf(conditionText) + inventoryIndex, lineNumber, line.indexOf(conditionText) + inventoryIndex + 'in inventory'.length), message: "Inventory check should use quoted string: \"item_name\" in inventory", severity: vscode.DiagnosticSeverity.Warning, code: 'inventory-check-pattern' }); } } // Check for negation patterns: !variable if (conditionText.startsWith('!')) { const varName = conditionText.substring(1).trim(); if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(varName)) { const varIndex = line.indexOf(varName); diagnostics.push({ range: new vscode.Range(lineNumber, varIndex, lineNumber, varIndex + varName.length), message: "Variable name after ! must be valid identifier", severity: vscode.DiagnosticSeverity.Warning, code: 'invalid-negation-variable' }); } } // Check for comparison patterns: variable == value, variable != value const comparisonMatch = conditionText.match(/([a-zA-Z_][a-zA-Z0-9_]*)\s*(==|!=|<=|>=|<|>)\s*(.+)/); if (comparisonMatch) { const leftVar = comparisonMatch[1]; const operator = comparisonMatch[2]; const rightValue = comparisonMatch[3].trim(); // Check if right value is properly quoted for strings if (rightValue.startsWith('"') && !rightValue.endsWith('"')) { const rightIndex = line.indexOf(rightValue); diagnostics.push({ range: new vscode.Range(lineNumber, rightIndex, lineNumber, rightIndex + rightValue.length), message: "String comparison value should be properly quoted", severity: vscode.DiagnosticSeverity.Warning, code: 'unclosed-comparison-string' }); } } // Check for logical operators: and, or if (conditionText.includes(' and ') || conditionText.includes(' or ')) { // Basic validation for logical operators const logicalMatch = conditionText.match(/(.+)\s+(and|or)\s+(.+)/); if (logicalMatch) { const leftPart = logicalMatch[1].trim(); const rightPart = logicalMatch[3].trim(); if (leftPart === '' || rightPart === '') { const operatorIndex = conditionText.indexOf(logicalMatch[2]); diagnostics.push({ range: new vscode.Range(lineNumber, line.indexOf(conditionText) + operatorIndex, lineNumber, line.indexOf(conditionText) + operatorIndex + logicalMatch[2].length), message: "Logical operator requires expressions on both sides", severity: vscode.DiagnosticSeverity.Warning, code: 'incomplete-logical-expression' }); } } } } } // Validate variable assignments const varAssignmentMatch = line.match(/\b(var|global)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/); if (varAssignmentMatch) { const varName = varAssignmentMatch[2]; // Check for reserved keywords if (this.keywords.has(varName)) { const varIndex = line.indexOf(varName); diagnostics.push({ range: new vscode.Range(lineNumber, varIndex, lineNumber, varIndex + varName.length), message: `Variable name '${varName}' is a reserved keyword`, severity: vscode.DiagnosticSeverity.Error, code: 'reserved-variable-name' }); } } // Validate specific command patterns from real ESC files const sayMatch = line.match(/\bsay\s*\(/); if (sayMatch) { // Check for proper say command format: say(speaker, "text", "id") // Handle both 2-argument and 3-argument say commands // Use a more sophisticated regex to handle quoted strings with commas inside const sayArgsMatch = line.match(/\bsay\s*\(\s*([^,]+),\s*("(?:[^"\\]|\\.)*")\s*(?:,\s*("(?:[^"\\]|\\.)*"))?\s*\)/); if (sayArgsMatch) { const speaker = sayArgsMatch[1].trim(); const text = sayArgsMatch[2].trim(); const id = sayArgsMatch[3] ? sayArgsMatch[3].trim() : null; // Check if speaker starts with $ (common pattern) if (!speaker.startsWith('$') && !speaker.startsWith('current_player')) { const speakerIndex = line.indexOf(speaker); diagnostics.push({ range: new vscode.Range(lineNumber, speakerIndex, lineNumber, speakerIndex + speaker.length), message: "Speaker should typically start with $ or be 'current_player'", severity: vscode.DiagnosticSeverity.Information, code: 'say-speaker-pattern' }); } // Text should already be properly quoted by the regex // Just check if ID is quoted (when present) if (id && !this.isProperlyQuotedString(id)) { const idIndex = line.indexOf(id); diagnostics.push({ range: new vscode.Range(lineNumber, idIndex, lineNumber, idIndex + id.length), message: "Say ID should be quoted", severity: vscode.DiagnosticSeverity.Warning, code: 'say-id-quoted' }); } } else { // If the sophisticated regex doesn't match, try a simpler approach const simpleSayMatch = line.match(/\bsay\s*\(\s*([^,]+),\s*([^,]+)(?:,\s*([^)]+))?\)/); if (simpleSayMatch) { const text = simpleSayMatch[2].trim(); const id = simpleSayMatch[3] ? simpleSayMatch[3].trim() : null; // Check if text is properly quoted if (!this.isProperlyQuotedString(text)) { const textIndex = line.indexOf(text); diagnostics.push({ range: new vscode.Range(lineNumber, textIndex, lineNumber, textIndex + text.length), message: "Say text should be properly quoted", severity: vscode.DiagnosticSeverity.Warning, code: 'say-text-quoted' }); } // Check if ID is quoted (when present) if (id && !this.isProperlyQuotedString(id)) { const idIndex = line.indexOf(id); diagnostics.push({ range: new vscode.Range(lineNumber, idIndex, lineNumber, idIndex + id.length), message: "Say ID should be quoted", severity: vscode.DiagnosticSeverity.Warning, code: 'say-id-quoted' }); } } } } // Validate inventory commands const inventoryMatch = line.match(/\b(inventory_add|inventory_remove)\s*\(/); if (inventoryMatch) { const inventoryArgsMatch = line.match(/\b(inventory_add|inventory_remove)\s*\(\s*([^)]+)\)/); if (inventoryArgsMatch) { const arg = inventoryArgsMatch[2].trim(); // Check if argument starts with $ or is quoted if (!arg.startsWith('$') && !arg.startsWith('"')) { const argIndex = line.indexOf(arg); diagnostics.push({ range: new vscode.Range(lineNumber, argIndex, lineNumber, argIndex + arg.length), message: "Inventory item should start with $ or be quoted", severity: vscode.DiagnosticSeverity.Warning, code: 'inventory-item-pattern' }); } } } return diagnostics; } /** * Validate indentation - ASHES requires tabs, not spaces */ private validateIndentation(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; if (line.trim() === '') return diagnostics; const indentMatch = line.match(/^(\s*)/); if (!indentMatch) return diagnostics; const indent = indentMatch[1]; const currentIndent = indent.length; // Check for spaces instead of tabs - ASHES requires tabs if (indent.includes(' ')) { const spaceIndex = indent.indexOf(' '); diagnostics.push({ range: new vscode.Range(lineNumber, spaceIndex, lineNumber, spaceIndex + 1), message: "ASHES requires tabs for indentation, not spaces", severity: vscode.DiagnosticSeverity.Error, code: 'spaces-instead-of-tabs' }); } // Check for mixed tabs and spaces if (indent.includes('\t') && indent.includes(' ')) { diagnostics.push({ range: new vscode.Range(lineNumber, 0, lineNumber, currentIndent), message: "Mixed tabs and spaces in indentation - use only tabs", severity: vscode.DiagnosticSeverity.Error, code: 'mixed-indentation' }); } // Find the previous non-empty line let prevLineIndex = lineIndex - 1; while (prevLineIndex >= 0 && allLines[prevLineIndex].trim() === '') { prevLineIndex--; } if (prevLineIndex >= 0) { const prevLine = allLines[prevLineIndex]; const prevIndentMatch = prevLine.match(/^(\s*)/); if (prevIndentMatch) { const prevIndent = prevIndentMatch[1].length; // Note: Removed excessive indentation limit to allow deeper nesting // if (currentIndent > 3) { // diagnostics.push({ // range: new vscode.Range(lineNumber, 0, lineNumber, currentIndent), // message: "Excessive indentation detected (more than 3 levels)", // severity: vscode.DiagnosticSeverity.Warning, // code: 'excessive-indentation' // }); // } } } return diagnostics; } /** * Validate string literals */ private validateStringLiterals(line: string, lineNumber: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; // Check for unclosed strings with proper quote handling let inString = false; let stringChar = ''; let stringStart = -1; let escapeNext = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (!inString && (char === '"' || char === "'")) { inString = true; stringChar = char; stringStart = i; } else if (inString && char === stringChar) { inString = false; stringChar = ''; stringStart = -1; } } if (inString) { diagnostics.push({ range: new vscode.Range(lineNumber, stringStart, lineNumber, line.length), message: `Unclosed ${stringChar === '"' ? 'double' : 'single'} quote`, severity: vscode.DiagnosticSeverity.Error, code: 'unclosed-string' }); } return diagnostics; } /** * Validate parentheses matching */ private validateParentheses(line: string, lineNumber: number): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; const stack: { char: string; index: number }[] = []; const pairs: { [key: string]: string } = { '(': ')', '[': ']', '{': '}' }; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char in pairs) { stack.push({ char, index: i }); } else if (Object.values(pairs).includes(char)) { if (stack.length === 0) { diagnostics.push({ range: new vscode.Range(lineNumber, i, lineNumber, i + 1), message: `Unexpected closing '${char}'`, severity: vscode.DiagnosticSeverity.Error, code: 'unexpected-closing-bracket' }); } else { const last = stack.pop()!; if (pairs[last.char] !== char) { diagnostics.push({ range: new vscode.Range(lineNumber, last.index, lineNumber, last.index + 1), message: `Mismatched brackets: expected '${pairs[last.char]}' but found '${char}'`, severity: vscode.DiagnosticSeverity.Error, code: 'mismatched-brackets' }); } } } } // Check for unclosed brackets for (const unclosed of stack) { diagnostics.push({ range: new vscode.Range(lineNumber, unclosed.index, lineNumber, unclosed.index + 1), message: `Unclosed '${unclosed.char}'`, severity: vscode.DiagnosticSeverity.Error, code: 'unclosed-bracket' }); } return diagnostics; } /** * Check if a string is properly quoted (handles escaped quotes) */ private isProperlyQuotedString(str: string): boolean { if (!str.startsWith('"') || !str.endsWith('"')) { return false; } // For simple validation, just check if it starts and ends with quotes // and doesn't have unescaped quotes in the middle let escapeNext = false; for (let i = 1; i < str.length - 1; i++) { const char = str[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { // Unescaped quote in the middle - this is actually valid in ASHES // as long as the string is properly closed continue; } } return true; } /** * Validate overall document structure */ private validateDocumentStructure(lines: string[]): vscode.Diagnostic[] { const diagnostics: vscode.Diagnostic[] = []; // Check for at least one event definition const hasEvent = lines.some(line => line.trim().startsWith(':')); if (!hasEvent) { diagnostics.push({ range: new vscode.Range(0, 0, 0, 0), message: "ASHES file should contain at least one event definition (starting with :)", severity: vscode.DiagnosticSeverity.Warning, code: 'no-events' }); } // Check for proper event naming conventions const events = lines.filter(line => line.trim().startsWith(':')); for (const eventLine of events) { const eventMatch = eventLine.match(/^(\s*):([a-zA-Z_][a-zA-Z0-9_]*)/); if (eventMatch) { const eventName = eventMatch[2]; // Suggest common event names if the current one seems unusual const commonEvents = ['init', 'ready', 'use', 'look', 'talk', 'walk', 'interact']; if (!commonEvents.includes(eventName.toLowerCase()) && eventName.length > 20) { const eventIndex = eventLine.indexOf(eventName); const lineNumber = lines.indexOf(eventLine); diagnostics.push({ range: new vscode.Range(lineNumber, eventIndex, lineNumber, eventIndex + eventName.length), message: `Consider using a shorter, more descriptive event name. Common events: ${commonEvents.join(', ')}`, severity: vscode.DiagnosticSeverity.Information, code: 'event-naming-suggestion' }); } } } return diagnostics; } }