482 lines
21 KiB
TypeScript
482 lines
21 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as path from 'path';
|
|
import { CommandParser, CommandInfo, CommandParameter } from './commandParser';
|
|
import { ASHESDiagnosticProvider } from './diagnosticProvider';
|
|
|
|
// Cache for dynamically loaded commands
|
|
let ASHES_COMMANDS: Array<{name: string, description: string, parameters: CommandParameter[], filePath?: string}> = [];
|
|
let COMMAND_CACHE_TIMESTAMP = 0;
|
|
|
|
// Built-in variables
|
|
const BUILTIN_VARIABLES = [
|
|
'CURRENT_PLAYER',
|
|
'ESC_LAST_SCENE',
|
|
'ESC_CURRENT_SCENE',
|
|
'FORCE_LAST_SCENE_NULL',
|
|
'ANIMATION_RESOURCES'
|
|
];
|
|
|
|
// Keywords
|
|
const KEYWORDS = [
|
|
'var', 'global', 'if', 'elif', 'else', 'while', 'break', 'done', 'stop', 'pass',
|
|
'true', 'false', 'nil', 'and', 'or', 'not', 'in', 'is', 'active'
|
|
];
|
|
|
|
/**
|
|
* Load commands dynamically from the project
|
|
*/
|
|
function loadCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[], filePath?: string}> {
|
|
try {
|
|
const parser = new CommandParser(workspaceRoot);
|
|
const commands = parser.getCommandsForExtension();
|
|
|
|
// Update cache timestamp
|
|
COMMAND_CACHE_TIMESTAMP = Date.now();
|
|
|
|
return commands;
|
|
} catch (error) {
|
|
console.error('Error loading commands:', error);
|
|
// Return empty array if loading fails
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get commands, using cache if available and not too old
|
|
*/
|
|
function getCommands(workspaceRoot: string): Array<{name: string, description: string, parameters: CommandParameter[], filePath?: string}> {
|
|
const now = Date.now();
|
|
const cacheAge = now - COMMAND_CACHE_TIMESTAMP;
|
|
const maxCacheAge = 5 * 60 * 1000; // 5 minutes
|
|
|
|
// Reload commands if cache is empty or too old
|
|
if (ASHES_COMMANDS.length === 0 || cacheAge > maxCacheAge) {
|
|
ASHES_COMMANDS = loadCommands(workspaceRoot);
|
|
}
|
|
|
|
return ASHES_COMMANDS;
|
|
}
|
|
|
|
export function activate(context: vscode.ExtensionContext) {
|
|
console.log('ASHES Language Support extension is now active!');
|
|
|
|
// Initialize diagnostic provider for syntax error detection
|
|
const diagnosticProvider = new ASHESDiagnosticProvider();
|
|
diagnosticProvider.initialize(context);
|
|
|
|
// Register completion provider
|
|
const completionProvider = vscode.languages.registerCompletionItemProvider(
|
|
'ashes',
|
|
{
|
|
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
|
|
const completions: vscode.CompletionItem[] = [];
|
|
|
|
// Get workspace root
|
|
const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath;
|
|
if (!workspaceRoot) {
|
|
return completions;
|
|
}
|
|
|
|
// Get dynamic commands
|
|
const commands = getCommands(workspaceRoot);
|
|
|
|
// Add command completions
|
|
commands.forEach(command => {
|
|
const completion = new vscode.CompletionItem(command.name, vscode.CompletionItemKind.Function);
|
|
completion.detail = command.description;
|
|
|
|
// Create detailed parameter documentation
|
|
let paramDocs = '';
|
|
let exampleUsage = '';
|
|
|
|
if (command.parameters && command.parameters.length > 0) {
|
|
paramDocs = '\n\n**Parameters:**\n';
|
|
const exampleParams: string[] = [];
|
|
|
|
command.parameters.forEach((param, index) => {
|
|
// 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}\`)*` : '';
|
|
|
|
paramDocs += `- ${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}\n`;
|
|
|
|
// Create example parameter with clear formatting
|
|
const exampleParam = param.required ?
|
|
`${param.name}: ${param.type}` :
|
|
`[${param.name}: ${param.type}]`;
|
|
exampleParams.push(exampleParam);
|
|
});
|
|
|
|
// Create example usage
|
|
exampleUsage = `\n\n**Example:**\n\`${command.name}(${exampleParams.join(', ')})\``;
|
|
}
|
|
|
|
completion.documentation = new vscode.MarkdownString(
|
|
`## ${command.name}\n\n---\n\n${command.description}${paramDocs}${exampleUsage}`
|
|
);
|
|
|
|
// Create snippet with parameter placeholders
|
|
const snippetParams = command.parameters.map((param, index) => `\${${index + 1}:${param.name}}`).join(', ');
|
|
completion.insertText = new vscode.SnippetString(`${command.name}(${snippetParams})`);
|
|
completions.push(completion);
|
|
});
|
|
|
|
// Add built-in variable completions
|
|
BUILTIN_VARIABLES.forEach(variable => {
|
|
const completion = new vscode.CompletionItem(variable, vscode.CompletionItemKind.Variable);
|
|
completion.detail = 'Built-in variable';
|
|
completion.documentation = new vscode.MarkdownString(`Built-in ASHES variable: **${variable}**`);
|
|
completions.push(completion);
|
|
});
|
|
|
|
// Add keyword completions
|
|
KEYWORDS.forEach(keyword => {
|
|
const completion = new vscode.CompletionItem(keyword, vscode.CompletionItemKind.Keyword);
|
|
completion.detail = 'ASHES keyword';
|
|
completions.push(completion);
|
|
});
|
|
|
|
return completions;
|
|
}
|
|
},
|
|
' ', '(', '$' // Trigger characters
|
|
);
|
|
|
|
// Register hover provider for commands
|
|
const hoverProvider = vscode.languages.registerHoverProvider(
|
|
'ashes',
|
|
{
|
|
provideHover(document: vscode.TextDocument, position: vscode.Position) {
|
|
const word = document.getText(document.getWordRangeAtPosition(position));
|
|
|
|
// Get workspace root
|
|
const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath;
|
|
if (!workspaceRoot) {
|
|
return null;
|
|
}
|
|
|
|
// Get dynamic commands
|
|
const commands = getCommands(workspaceRoot);
|
|
const command = commands.find(cmd => cmd.name === word);
|
|
|
|
if (command) {
|
|
// Create detailed parameter documentation
|
|
let paramDocs = '';
|
|
let exampleUsage = '';
|
|
|
|
if (command.parameters && command.parameters.length > 0) {
|
|
paramDocs = '\n\n**Parameters:**\n';
|
|
const exampleParams: string[] = [];
|
|
|
|
command.parameters.forEach((param, index) => {
|
|
// 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}\`)*` : '';
|
|
|
|
paramDocs += `- ${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}\n`;
|
|
|
|
// Create example parameter with clear formatting
|
|
const exampleParam = param.required ?
|
|
`${param.name}: ${param.type}` :
|
|
`[${param.name}: ${param.type}]`;
|
|
exampleParams.push(exampleParam);
|
|
});
|
|
|
|
// Create example usage
|
|
exampleUsage = `\n\n**Example:**\n\`${command.name}(${exampleParams.join(', ')})\``;
|
|
}
|
|
|
|
const hover = new vscode.Hover(
|
|
new vscode.MarkdownString(
|
|
`## ${command.name}\n\n---\n\n${command.description}${paramDocs}${exampleUsage}`
|
|
)
|
|
);
|
|
return hover;
|
|
}
|
|
|
|
const builtinVar = BUILTIN_VARIABLES.find(variable => variable === word);
|
|
if (builtinVar) {
|
|
const hover = new vscode.Hover(
|
|
new vscode.MarkdownString(`Built-in ASHES variable: **${builtinVar}**`)
|
|
);
|
|
return hover;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Register 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
|
|
const activeEditor = vscode.window.activeTextEditor;
|
|
const workspaceRoot = activeEditor ?
|
|
vscode.workspace.getWorkspaceFolder(activeEditor.document.uri)?.uri.fsPath :
|
|
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
|
|
if (!workspaceRoot) {
|
|
panel.webview.html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body { font-family: var(--vscode-font-family); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>ASHES Command Reference</h1>
|
|
<p>No workspace found. Please open a workspace to view commands.</p>
|
|
</body>
|
|
</html>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// 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 = '';
|
|
|
|
if (command.parameters && command.parameters.length > 0) {
|
|
const exampleParams: string[] = [];
|
|
|
|
paramInfo = command.parameters.map(param => {
|
|
// Enhanced parameter formatting with clear indicators
|
|
const requiredIndicator = param.required ? '<strong>Required</strong>' : '<strong>Optional</strong>';
|
|
const typeIndicator = `<code>${param.type}</code>`;
|
|
const nameIndicator = `<strong>${param.name}</strong>`;
|
|
const defaultValue = param.defaultValue ? ` <em>(default: <code>${param.defaultValue}</code>)</em>` : '';
|
|
|
|
// Create example parameter
|
|
const exampleParam = param.required ?
|
|
`${param.name}: ${param.type}` :
|
|
`[${param.name}: ${param.type}]`;
|
|
exampleParams.push(exampleParam);
|
|
|
|
return `${requiredIndicator} ${nameIndicator} (${typeIndicator})${defaultValue}`;
|
|
}).join('<br>');
|
|
|
|
// Create example usage
|
|
exampleUsage = `<br><br><strong>Example:</strong><br><code>${command.name}(${exampleParams.join(', ')})</code>`;
|
|
}
|
|
|
|
return `<tr>
|
|
<td><code class="command-link" data-command="${command.name}">${command.name}</code></td>
|
|
<td>${command.description}${exampleUsage}</td>
|
|
<td>${paramInfo}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
panel.webview.html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {
|
|
font-family: var(--vscode-font-family);
|
|
margin: 20px;
|
|
}
|
|
table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
}
|
|
th, td {
|
|
border: 1px solid var(--vscode-panel-border);
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
th {
|
|
background-color: var(--vscode-panel-background);
|
|
}
|
|
code {
|
|
background-color: var(--vscode-textCodeBlock-background);
|
|
padding: 2px 4px;
|
|
}
|
|
.command-link {
|
|
cursor: pointer;
|
|
color: var(--vscode-textLink-foreground);
|
|
text-decoration: underline;
|
|
}
|
|
.command-link:hover {
|
|
color: var(--vscode-textLink-activeForeground);
|
|
}
|
|
.info {
|
|
margin-bottom: 20px;
|
|
padding: 10px;
|
|
background-color: var(--vscode-textBlockQuote-background);
|
|
border-left: 4px solid var(--vscode-textBlockQuote-border);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>ASHES Command Reference</h1>
|
|
<div class="info">
|
|
💡 <strong>Tip:</strong> Click on any command name to navigate to its source file!
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Command</th>
|
|
<th>Description</th>
|
|
<th>Parameters</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${commandsHtml}
|
|
</tbody>
|
|
</table>
|
|
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
|
|
// Add click handlers to all command links
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const commandLinks = document.querySelectorAll('.command-link');
|
|
commandLinks.forEach(link => {
|
|
link.addEventListener('click', function() {
|
|
const commandName = this.getAttribute('data-command');
|
|
if (commandName) {
|
|
vscode.postMessage({
|
|
command: 'navigateToCommand',
|
|
commandName: commandName
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
});
|
|
|
|
// Register command for refreshing command cache
|
|
const refreshCommands = vscode.commands.registerCommand('ashes.refreshCommands', () => {
|
|
// Clear cache to force reload
|
|
ASHES_COMMANDS = [];
|
|
COMMAND_CACHE_TIMESTAMP = 0;
|
|
|
|
// Also refresh diagnostics
|
|
diagnosticProvider.refreshAllDiagnostics();
|
|
|
|
vscode.window.showInformationMessage('ASHES commands cache and diagnostics refreshed!');
|
|
});
|
|
|
|
// 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() {}
|