Show me errors
This commit is contained in:
@@ -59,6 +59,11 @@
|
|||||||
"command": "ashes.refreshCommands",
|
"command": "ashes.refreshCommands",
|
||||||
"title": "Refresh ASHES Commands",
|
"title": "Refresh ASHES Commands",
|
||||||
"category": "ASHES"
|
"category": "ASHES"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "ashes.refreshDiagnostics",
|
||||||
|
"title": "Refresh ASHES Diagnostics",
|
||||||
|
"category": "ASHES"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
@@ -68,6 +73,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "ashes.refreshCommands"
|
"command": "ashes.refreshCommands"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "ashes.refreshDiagnostics"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
247
vscode-extension-ashes/src/diagnosticProvider.ts
Normal file
247
vscode-extension-ashes/src/diagnosticProvider.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { ASHESSyntaxValidator } from './syntaxValidator';
|
||||||
|
|
||||||
|
export class ASHESDiagnosticProvider {
|
||||||
|
private diagnosticCollection: vscode.DiagnosticCollection;
|
||||||
|
private syntaxValidator: ASHESSyntaxValidator;
|
||||||
|
private commandCache: Map<string, string[]> = new Map();
|
||||||
|
private lastUpdateTime: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.diagnosticCollection = vscode.languages.createDiagnosticCollection('ashes');
|
||||||
|
this.syntaxValidator = new ASHESSyntaxValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the diagnostic provider
|
||||||
|
*/
|
||||||
|
public initialize(context: vscode.ExtensionContext): void {
|
||||||
|
// Register document change listener
|
||||||
|
const documentChangeListener = vscode.workspace.onDidChangeTextDocument(
|
||||||
|
(event) => this.onDocumentChanged(event)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register document open listener
|
||||||
|
const documentOpenListener = vscode.workspace.onDidOpenTextDocument(
|
||||||
|
(document) => this.validateDocument(document)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register document save listener
|
||||||
|
const documentSaveListener = vscode.workspace.onDidSaveTextDocument(
|
||||||
|
(document) => this.validateDocument(document)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register workspace change listener to update command cache
|
||||||
|
const workspaceChangeListener = vscode.workspace.onDidChangeWorkspaceFolders(
|
||||||
|
() => this.updateCommandCache()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate all open ASHES documents
|
||||||
|
vscode.workspace.textDocuments.forEach(document => {
|
||||||
|
if (document.languageId === 'ashes') {
|
||||||
|
this.validateDocument(document);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to subscriptions
|
||||||
|
context.subscriptions.push(
|
||||||
|
this.diagnosticCollection,
|
||||||
|
documentChangeListener,
|
||||||
|
documentOpenListener,
|
||||||
|
documentSaveListener,
|
||||||
|
workspaceChangeListener
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial command cache update
|
||||||
|
this.updateCommandCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle document changes with debouncing
|
||||||
|
*/
|
||||||
|
private onDocumentChanged(event: vscode.TextDocumentChangeEvent): void {
|
||||||
|
if (event.document.languageId !== 'ashes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = event.document;
|
||||||
|
const uri = document.uri.toString();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Debounce validation to avoid excessive processing
|
||||||
|
const lastUpdate = this.lastUpdateTime.get(uri) || 0;
|
||||||
|
if (now - lastUpdate < 500) { // 500ms debounce
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastUpdateTime.set(uri, now);
|
||||||
|
|
||||||
|
// Use setTimeout to debounce the validation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.validateDocument(document);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a document and update diagnostics
|
||||||
|
*/
|
||||||
|
private validateDocument(document: vscode.TextDocument): void {
|
||||||
|
if (document.languageId !== 'ashes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update command cache if needed
|
||||||
|
this.updateCommandCacheForDocument(document);
|
||||||
|
|
||||||
|
// Get diagnostics from syntax validator
|
||||||
|
const diagnostics = this.syntaxValidator.validateDocument(document);
|
||||||
|
|
||||||
|
// Update the diagnostic collection
|
||||||
|
this.diagnosticCollection.set(document.uri, diagnostics);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error validating ASHES document:', error);
|
||||||
|
|
||||||
|
// Show error diagnostic
|
||||||
|
const errorDiagnostic = new vscode.Diagnostic(
|
||||||
|
new vscode.Range(0, 0, 0, 0),
|
||||||
|
`Error validating ASHES syntax: ${error}`,
|
||||||
|
vscode.DiagnosticSeverity.Error
|
||||||
|
);
|
||||||
|
this.diagnosticCollection.set(document.uri, [errorDiagnostic]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update command cache for all workspace folders
|
||||||
|
*/
|
||||||
|
private updateCommandCache(): void {
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const folder of workspaceFolders) {
|
||||||
|
this.updateCommandCacheForWorkspace(folder.uri.fsPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update command cache for a specific document's workspace
|
||||||
|
*/
|
||||||
|
private updateCommandCacheForDocument(document: vscode.TextDocument): void {
|
||||||
|
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspacePath = workspaceFolder.uri.fsPath;
|
||||||
|
this.updateCommandCacheForWorkspace(workspacePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update command cache for a specific workspace
|
||||||
|
*/
|
||||||
|
private updateCommandCacheForWorkspace(workspacePath: string): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const lastUpdate = this.lastUpdateTime.get(workspacePath) || 0;
|
||||||
|
|
||||||
|
// Only update if cache is older than 5 minutes
|
||||||
|
if (now - lastUpdate < 5 * 60 * 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commands = this.extractCommandsFromWorkspace(workspacePath);
|
||||||
|
this.commandCache.set(workspacePath, commands);
|
||||||
|
this.syntaxValidator.updateCommands(commands);
|
||||||
|
this.lastUpdateTime.set(workspacePath, now);
|
||||||
|
|
||||||
|
// Re-validate all ASHES documents in this workspace
|
||||||
|
vscode.workspace.textDocuments.forEach(document => {
|
||||||
|
if (document.languageId === 'ashes') {
|
||||||
|
const docWorkspace = vscode.workspace.getWorkspaceFolder(document.uri);
|
||||||
|
if (docWorkspace && docWorkspace.uri.fsPath === workspacePath) {
|
||||||
|
this.validateDocument(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating command cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract commands from workspace using the existing CommandParser
|
||||||
|
*/
|
||||||
|
private extractCommandsFromWorkspace(workspacePath: string): string[] {
|
||||||
|
try {
|
||||||
|
// Import the CommandParser dynamically to avoid circular dependencies
|
||||||
|
const { CommandParser } = require('./commandParser');
|
||||||
|
const parser = new CommandParser(workspacePath);
|
||||||
|
const commands = parser.parseCommands();
|
||||||
|
return commands.map((cmd: any) => cmd.name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting commands:', error);
|
||||||
|
// Return basic command set as fallback
|
||||||
|
return [
|
||||||
|
'accept_input', 'anim', 'anim_block', 'block_say', 'camera_push', 'camera_push_block',
|
||||||
|
'camera_set_limits', 'camera_set_pos', 'camera_set_pos_block', 'camera_set_target',
|
||||||
|
'camera_set_target_block', 'camera_set_zoom', 'camera_set_zoom_block', 'camera_set_zoom_height',
|
||||||
|
'camera_set_zoom_height_block', 'camera_shift', 'camera_shift_block', 'change_scene', 'custom',
|
||||||
|
'dec_global', 'enable_terrain', 'end_block_say', 'hide_menu', 'inc_global', 'inventory_add',
|
||||||
|
'inventory_remove', 'item_count_add', 'play_lib_snd', 'play_snd', 'play_video', 'print',
|
||||||
|
'print_internal', 'queue_event', 'queue_resource', 'rand_global', 'repeat', 'save_game',
|
||||||
|
'say', 'say_last_dialog_option', 'say_random', 'say_sequence', 'sched_event', 'set_active',
|
||||||
|
'set_active_if_exists', 'set_angle', 'set_animations', 'set_direction', 'set_global',
|
||||||
|
'set_globals', 'set_gui_visible', 'set_interactive', 'set_item_custom_data', 'set_speed',
|
||||||
|
'set_state', 'set_tooltip', 'show_menu', 'slide', 'slide_block', 'spawn', 'stop', 'stop_snd',
|
||||||
|
'teleport', 'teleport_pos', 'transition', 'turn_to', 'wait', 'walk', 'walk_block',
|
||||||
|
'walk_to_pos', 'walk_to_pos_block'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear diagnostics for a document
|
||||||
|
*/
|
||||||
|
public clearDiagnostics(document: vscode.TextDocument): void {
|
||||||
|
this.diagnosticCollection.delete(document.uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all diagnostics
|
||||||
|
*/
|
||||||
|
public clearAllDiagnostics(): void {
|
||||||
|
this.diagnosticCollection.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current diagnostics for a document
|
||||||
|
*/
|
||||||
|
public getDiagnostics(document: vscode.TextDocument): readonly vscode.Diagnostic[] {
|
||||||
|
return this.diagnosticCollection.get(document.uri) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force refresh diagnostics for all ASHES documents
|
||||||
|
*/
|
||||||
|
public refreshAllDiagnostics(): void {
|
||||||
|
this.updateCommandCache();
|
||||||
|
|
||||||
|
vscode.workspace.textDocuments.forEach(document => {
|
||||||
|
if (document.languageId === 'ashes') {
|
||||||
|
this.validateDocument(document);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get diagnostic collection for external access
|
||||||
|
*/
|
||||||
|
public getDiagnosticCollection(): vscode.DiagnosticCollection {
|
||||||
|
return this.diagnosticCollection;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { CommandParser, CommandInfo, CommandParameter } from './commandParser';
|
import { CommandParser, CommandInfo, CommandParameter } from './commandParser';
|
||||||
|
import { ASHESDiagnosticProvider } from './diagnosticProvider';
|
||||||
|
|
||||||
// Cache for dynamically loaded commands
|
// Cache for dynamically loaded commands
|
||||||
let ASHES_COMMANDS: Array<{name: string, description: string, parameters: CommandParameter[], filePath?: string}> = [];
|
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) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
console.log('ASHES Language Support extension is now active!');
|
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
|
// Register completion provider
|
||||||
const completionProvider = vscode.languages.registerCompletionItemProvider(
|
const completionProvider = vscode.languages.registerCompletionItemProvider(
|
||||||
'ashes',
|
'ashes',
|
||||||
@@ -458,10 +463,19 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
ASHES_COMMANDS = [];
|
ASHES_COMMANDS = [];
|
||||||
COMMAND_CACHE_TIMESTAMP = 0;
|
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() {}
|
export function deactivate() {}
|
||||||
|
|||||||
799
vscode-extension-ashes/src/syntaxValidator.ts
Normal file
799
vscode-extension-ashes/src/syntaxValidator.ts
Normal file
@@ -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<string> = new Set();
|
||||||
|
private builtinVariables: Set<string> = new Set([
|
||||||
|
'CURRENT_PLAYER',
|
||||||
|
'ESC_LAST_SCENE',
|
||||||
|
'ESC_CURRENT_SCENE',
|
||||||
|
'FORCE_LAST_SCENE_NULL',
|
||||||
|
'ANIMATION_RESOURCES'
|
||||||
|
]);
|
||||||
|
private keywords: Set<string> = new Set([
|
||||||
|
'var', 'global', 'if', 'elif', 'else', 'while', 'break', 'done', 'stop', 'pass',
|
||||||
|
'true', 'false', 'nil', 'and', 'or', 'not', 'in', 'is', 'active'
|
||||||
|
]);
|
||||||
|
|
||||||
|
constructor(commands: string[] = []) {
|
||||||
|
this.commands = new Set(commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the list of available commands
|
||||||
|
*/
|
||||||
|
public updateCommands(commands: string[]): void {
|
||||||
|
this.commands = new Set(commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate ASHES syntax and return diagnostic errors
|
||||||
|
*/
|
||||||
|
public validateDocument(document: vscode.TextDocument): vscode.Diagnostic[] {
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
const text = document.getText();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
||||||
|
const line = lines[lineIndex];
|
||||||
|
const lineNumber = lineIndex;
|
||||||
|
|
||||||
|
// Skip empty lines and comments
|
||||||
|
if (this.isEmptyOrComment(line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate different types of lines
|
||||||
|
const lineDiagnostics = this.validateLine(line, lineNumber, lines, lineIndex);
|
||||||
|
diagnostics.push(...lineDiagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate overall document structure
|
||||||
|
const documentDiagnostics = this.validateDocumentStructure(lines);
|
||||||
|
diagnostics.push(...documentDiagnostics);
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a line is empty or a comment
|
||||||
|
*/
|
||||||
|
private isEmptyOrComment(line: string): boolean {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
return trimmed === '' || trimmed.startsWith('#');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a single line
|
||||||
|
*/
|
||||||
|
private validateLine(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] {
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
// Validate event definitions
|
||||||
|
if (trimmed.startsWith(':')) {
|
||||||
|
diagnostics.push(...this.validateEventDefinition(line, lineNumber));
|
||||||
|
}
|
||||||
|
// Validate dialog blocks
|
||||||
|
else if (trimmed.startsWith('?!')) {
|
||||||
|
diagnostics.push(...this.validateDialogBlock(line, lineNumber, allLines, lineIndex));
|
||||||
|
}
|
||||||
|
// Validate dialog choices
|
||||||
|
else if (trimmed.startsWith('-')) {
|
||||||
|
diagnostics.push(...this.validateDialogChoice(line, lineNumber));
|
||||||
|
}
|
||||||
|
// Validate commands and control flow
|
||||||
|
else {
|
||||||
|
diagnostics.push(...this.validateCommandOrControlFlow(line, lineNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate indentation
|
||||||
|
diagnostics.push(...this.validateIndentation(line, lineNumber, allLines, lineIndex));
|
||||||
|
|
||||||
|
// Validate indentation after control flow statements
|
||||||
|
if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('else') || line.includes('while '))) {
|
||||||
|
// Check if the next line has proper indentation
|
||||||
|
if (lineIndex + 1 < allLines.length) {
|
||||||
|
const currentIndent = line.match(/^(\s*)/)?.[1].length || 0;
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
let nextNonEmptyLineIndex = lineIndex + 1;
|
||||||
|
while (nextNonEmptyLineIndex < allLines.length && allLines[nextNonEmptyLineIndex].trim() === '') {
|
||||||
|
nextNonEmptyLineIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextNonEmptyLineIndex < allLines.length) {
|
||||||
|
const nextNonEmptyLine = allLines[nextNonEmptyLineIndex];
|
||||||
|
const nextNonEmptyIndent = nextNonEmptyLine.match(/^(\s*)/)?.[1].length || 0;
|
||||||
|
|
||||||
|
// The next non-empty line should have exactly one more level of indentation
|
||||||
|
if (nextNonEmptyIndent <= currentIndent) {
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, 0, lineNumber, line.length),
|
||||||
|
message: "Control flow statement requires indented block on the next line",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'missing-indentation'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate string literals
|
||||||
|
diagnostics.push(...this.validateStringLiterals(line, lineNumber));
|
||||||
|
|
||||||
|
// Validate parentheses matching
|
||||||
|
diagnostics.push(...this.validateParentheses(line, lineNumber));
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate event definitions
|
||||||
|
*/
|
||||||
|
private validateEventDefinition(line: string, lineNumber: number): vscode.Diagnostic[] {
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
|
||||||
|
// Event definition patterns based on real ESC files:
|
||||||
|
// :event_name
|
||||||
|
// :event_name | FLAGS
|
||||||
|
// :event_name "target"
|
||||||
|
// :event_name | FLAGS "target"
|
||||||
|
const eventMatch = line.match(/^(\s*):([a-zA-Z_][a-zA-Z0-9_]*)(\s*\|\s*[A-Z_]+)*(\s+("[^"]*"))?/);
|
||||||
|
|
||||||
|
if (!eventMatch) {
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, 0, lineNumber, line.length),
|
||||||
|
message: "Invalid event definition. Expected format: :event_name [| FLAGS] [\"target\"]",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'invalid-event-definition'
|
||||||
|
});
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventName = eventMatch[2];
|
||||||
|
const flags = eventMatch[3];
|
||||||
|
const target = eventMatch[5];
|
||||||
|
|
||||||
|
// Validate event name
|
||||||
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(eventName)) {
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, eventMatch.index! + 1, lineNumber, eventMatch.index! + 1 + eventName.length),
|
||||||
|
message: "Event name must start with a letter or underscore and contain only letters, numbers, and underscores",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'invalid-event-name'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common event names and suggest alternatives
|
||||||
|
const commonEvents = ['init', 'ready', 'setup', 'action1', 'action2', 'action3', 'use', 'look', 'talk', 'walk', 'interact'];
|
||||||
|
if (!commonEvents.includes(eventName.toLowerCase()) && eventName.length > 20) {
|
||||||
|
const eventIndex = line.indexOf(eventName);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, eventIndex, lineNumber, eventIndex + eventName.length),
|
||||||
|
message: `Consider using a shorter event name. Common events: ${commonEvents.join(', ')}`,
|
||||||
|
severity: vscode.DiagnosticSeverity.Information,
|
||||||
|
code: 'event-naming-suggestion'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate flags if present
|
||||||
|
if (flags) {
|
||||||
|
const flagText = flags.trim().substring(1).trim(); // Remove the |
|
||||||
|
const validFlags = ['TK', 'TR', 'TG', 'SKIP', 'GLOBAL'];
|
||||||
|
const providedFlags = flagText.split(/\s+/);
|
||||||
|
|
||||||
|
for (const flag of providedFlags) {
|
||||||
|
if (flag && !validFlags.includes(flag)) {
|
||||||
|
const flagIndex = line.indexOf(flag);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, flagIndex, lineNumber, flagIndex + flag.length),
|
||||||
|
message: `Invalid event flag '${flag}'. Valid flags: ${validFlags.join(', ')}`,
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'invalid-event-flag'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate target if present
|
||||||
|
if (target && (!target.startsWith('"') || !target.endsWith('"'))) {
|
||||||
|
const targetIndex = line.indexOf(target);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, targetIndex, lineNumber, targetIndex + target.length),
|
||||||
|
message: "Event target must be a quoted string",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'invalid-event-target'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate dialog blocks
|
||||||
|
*/
|
||||||
|
private validateDialogBlock(line: string, lineNumber: number, allLines: string[], lineIndex: number): vscode.Diagnostic[] {
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
|
||||||
|
// Dialog block should have proper indentation
|
||||||
|
const indentMatch = line.match(/^(\s*)(\?\!)/);
|
||||||
|
if (!indentMatch) {
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, 0, lineNumber, line.length),
|
||||||
|
message: "Invalid dialog block syntax. Expected: ?!",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'invalid-dialog-block'
|
||||||
|
});
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseIndent = indentMatch[1].length;
|
||||||
|
|
||||||
|
// Check if dialog block is properly closed
|
||||||
|
let foundEnd = false;
|
||||||
|
for (let i = lineIndex + 1; i < allLines.length; i++) {
|
||||||
|
const nextLine = allLines[i];
|
||||||
|
if (nextLine.trim() === '') continue;
|
||||||
|
|
||||||
|
const nextIndent = nextLine.match(/^(\s*)/)?.[1].length || 0;
|
||||||
|
|
||||||
|
if (nextIndent <= baseIndent && !nextLine.trim().startsWith('-')) {
|
||||||
|
foundEnd = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundEnd) {
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, 0, lineNumber, line.length),
|
||||||
|
message: "Dialog block is not properly closed",
|
||||||
|
severity: vscode.DiagnosticSeverity.Warning,
|
||||||
|
code: 'unclosed-dialog-block'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate dialog choices
|
||||||
|
*/
|
||||||
|
private validateDialogChoice(line: string, lineNumber: number): vscode.Diagnostic[] {
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
|
||||||
|
// Dialog choice pattern: - "choice text" [condition]
|
||||||
|
const choiceMatch = line.match(/^(\s*)-(\s*)("[^"]*")(\s*\[.*\])?/);
|
||||||
|
|
||||||
|
if (!choiceMatch) {
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, 0, lineNumber, line.length),
|
||||||
|
message: "Invalid dialog choice syntax. Expected: - \"choice text\" [condition]",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'invalid-dialog-choice'
|
||||||
|
});
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const choiceText = choiceMatch[3];
|
||||||
|
const condition = choiceMatch[4];
|
||||||
|
|
||||||
|
// Validate choice text is not empty
|
||||||
|
if (choiceText === '""') {
|
||||||
|
const textIndex = line.indexOf(choiceText);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, textIndex, lineNumber, textIndex + choiceText.length),
|
||||||
|
message: "Dialog choice text cannot be empty",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'empty-dialog-choice'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate condition if present
|
||||||
|
if (condition) {
|
||||||
|
const conditionContent = condition.slice(1, -1); // Remove brackets
|
||||||
|
if (conditionContent.trim() === '') {
|
||||||
|
const conditionIndex = line.indexOf(condition);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length),
|
||||||
|
message: "Dialog choice condition cannot be empty",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'empty-dialog-condition'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate commands and control flow statements
|
||||||
|
*/
|
||||||
|
private validateCommandOrControlFlow(line: string, lineNumber: number): vscode.Diagnostic[] {
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
|
||||||
|
// Check for function calls
|
||||||
|
const functionCallMatch = line.match(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
|
||||||
|
if (functionCallMatch) {
|
||||||
|
const functionName = functionCallMatch[1];
|
||||||
|
|
||||||
|
// Check if it's a known command
|
||||||
|
if (!this.commands.has(functionName) && !this.keywords.has(functionName)) {
|
||||||
|
const funcIndex = line.indexOf(functionName);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, funcIndex, lineNumber, funcIndex + functionName.length),
|
||||||
|
message: `Unknown command '${functionName}'. Available commands: ${Array.from(this.commands).slice(0, 10).join(', ')}${this.commands.size > 10 ? '...' : ''}`,
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'unknown-command'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate control flow statements
|
||||||
|
const controlFlowMatch = line.match(/\b(if|elif|while)\s+(.+):/);
|
||||||
|
if (controlFlowMatch) {
|
||||||
|
const condition = controlFlowMatch[2];
|
||||||
|
|
||||||
|
// Basic condition validation
|
||||||
|
if (condition.trim() === '') {
|
||||||
|
const conditionIndex = line.indexOf(condition);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, conditionIndex, lineNumber, conditionIndex + condition.length),
|
||||||
|
message: "Control flow condition cannot be empty",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'empty-condition'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for missing colon in control flow statements
|
||||||
|
const controlFlowWithoutColon = line.match(/\b(if|elif|else|while)\s+(.+)$/);
|
||||||
|
if (controlFlowWithoutColon && !line.trim().endsWith(':')) {
|
||||||
|
const keyword = controlFlowWithoutColon[1];
|
||||||
|
const keywordIndex = line.indexOf(keyword);
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, keywordIndex, lineNumber, keywordIndex + keyword.length),
|
||||||
|
message: `'${keyword}' statement must end with ':'`,
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'missing-colon'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for 'else' without colon
|
||||||
|
const elseMatch = line.match(/\belse\s*$/);
|
||||||
|
if (elseMatch && !line.trim().endsWith(':')) {
|
||||||
|
const elseIndex = line.indexOf('else');
|
||||||
|
diagnostics.push({
|
||||||
|
range: new vscode.Range(lineNumber, elseIndex, lineNumber, elseIndex + 4),
|
||||||
|
message: "'else' statement must end with ':'",
|
||||||
|
severity: vscode.DiagnosticSeverity.Error,
|
||||||
|
code: 'missing-colon'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Validate common condition patterns from real ESC files (for lines that do have colons)
|
||||||
|
if (line.trim().endsWith(':') && (line.includes('if ') || line.includes('elif ') || line.includes('while '))) {
|
||||||
|
const conditionMatch = line.match(/\b(if|elif|while)\s+(.+):/);
|
||||||
|
if (conditionMatch) {
|
||||||
|
const conditionText = conditionMatch[2].trim();
|
||||||
|
|
||||||
|
// Check for proper inventory check patterns: "item" in inventory
|
||||||
|
const inventoryMatch = conditionText.match(/([^"]*)\s+in\s+inventory/);
|
||||||
|
if (inventoryMatch) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user