// The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler'; import { Logger } from './logger'; import * as luaAddonsManager from './lua-addons-manager'; const luaWorkSpaceSettingKey = "Lua.workspace"; const librarySettingsKey = "library"; const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler'); const logger = new Logger(); const extensionName = 'dutchies-dcs-scripting-tools'; const publisherName = 'dutchie031'; const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`; // State tracking for addon updates let extensionPath: string | undefined; let workspaceRoot: string | undefined; let currentExtensionVersion: string | undefined; let installedAddonPaths: Map = new Map(); let isUpdatingAddons = false; //TODO: // - ENABLE/DISABLE with settings instead of commands (or both) // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export async function activate(context: vscode.ExtensionContext) { extensionPath = context.extensionPath; workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; // Initialize extension version try { currentExtensionVersion = await luaAddonsManager.getExtensionVersion(extensionPath); logger.debug(`Extension version: ${currentExtensionVersion}`); } catch (err) { logger.error(`Failed to determine extension version: ${err instanceof Error ? err.message : String(err)}`); } context.subscriptions.push( vscode.commands.registerCommand('dutchies-dcs-scripting-tools.enable', async () => { await enableIntellisense(); }) ); context.subscriptions.push( vscode.commands.registerCommand('dutchies-dcs-scripting-tools.disable', async () => { await disableIntellisense(); }) ); context.subscriptions.push( vscode.commands.registerCommand('dutchies-dcs-scripting-tools.openSettings', async () => { await vscode.commands.executeCommand("workbench.action.openSettings", `${extensionSettingsFilter}`); })); context.subscriptions.push( vscode.commands.registerCommand('dutchies-dcs-scripting-tools.compileLuaScripts', async () => { try{ const start = Date.now(); await compileLuaScripts(); const end = Date.now(); vscode.window.showInformationMessage(`Lua scripts compiled successfully in ${(end - start) / 1000} seconds.`); }catch(err){ vscode.window.showErrorMessage('Error compiling Lua scripts: ' + (err as Error).message); } }) ); vscode.workspace.onDidSaveTextDocument(async(document) => { logger.info(`Document saved: ${document.uri.fsPath} | Language: ${document.languageId}`); if (document.languageId === 'lua') { const config = vscode.workspace.getConfiguration(extensionName); const compileAt = config.get('compileAt') || "undefined"; if (compileAt === 'onSave') { await compileLuaScripts(); } } }); vscode.workspace.onDidChangeConfiguration(async(event) => { if (event.affectsConfiguration(`${extensionName}.dcsTypes`)) { const config = vscode.workspace.getConfiguration(extensionName); const dcsTypesEnabled = config.get('dcsTypes') || false; if (dcsTypesEnabled) { await updateLuaAddons(); } else { await disableIntellisense(); } } }); // Check and update lua-addons on activation if dcsTypes is enabled await checkAndUpdateAddonsOnStartup(); logger.info('Dutchies DCS Scripting Tools extension activated'); } // This method is called when your extension is deactivated export async function deactivate() { // Clean up addon paths from settings on deactivation try { await removeVersionedPluginPathsFromSettings(); } catch (err) { logger.error(`Error cleaning up on deactivation: ${err instanceof Error ? err.message : String(err)}`); } logger.info('Dutchies DCS Scripting Tools extension deactivated'); } class CompilationLogger implements ICompilationLogger { constructor( private readonly logger: Logger) { } info(message: string): void { this.logger.info(message); } error(message: string): void { this.logger.error(message); } writeLine(message: string): void { this.logger.log(message); } } async function compileLuaScripts() { logger.clear(); logger.info("Compiling..."); const config = vscode.workspace.getConfiguration(extensionName); const sourcePath = config.get('luaSrcDirectory') || '${workspaceFolder}/src'; const outputPath = config.get('luaOutputPath') || '${workspaceFolder}/dist'; const resolvedSourcePath = sourcePath.replace('${workspaceFolder}', vscode.workspace.workspaceFolders?.[0].uri.fsPath || ''); const resolvedOutputPath = outputPath.replace('${workspaceFolder}', vscode.workspace.workspaceFolders?.[0].uri.fsPath || ''); diagnosticCollection.clear(); const errorsByFile = new Map(); const options: ScriptCompilerOptions = { sourcePath: resolvedSourcePath, outputPath: resolvedOutputPath, minify: false, onError: (error: CompilationError) => { const errors = errorsByFile.get(error.filePath) || []; errors.push(error); errorsByFile.set(error.filePath, errors); } }; const compilationLogger = new CompilationLogger(logger); const compiler = new ScriptCompiler(options, compilationLogger); const includeDevScript = config.get('includeDevelopmentScript') || false; try { await compiler.compile(includeDevScript); } catch (err) { vscode.window.showErrorMessage('Compilation failed: ' + (err as Error).message); logger.error('Compilation failed: ' + (err as Error).message + '\n' + (err as Error).stack); } // Update diagnostics diagnosticCollection.clear(); for (const [filePath, errors] of errorsByFile) { const uri = vscode.Uri.file(filePath); const diagnostics = errors.map(error => { const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0); const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error); diagnostic.source = 'DCS Lua Transpiler'; return diagnostic; }); diagnosticCollection.set(uri, diagnostics); } } async function enableIntellisense() { const config = vscode.workspace.getConfiguration(extensionName); if (config.get("dcsTypes") === false) { config.update("dcsTypes", true, vscode.ConfigurationTarget.Workspace); } await updateLuaAddons(); await vscode.commands.executeCommand( "lua.startServer" ); vscode.window.showInformationMessage(`DCS-Types installed. Version ${currentExtensionVersion}.`); } async function disableIntellisense() { const config = vscode.workspace.getConfiguration(extensionName); if (config.get("dcsTypes") === true) { config.update("dcsTypes", false, vscode.ConfigurationTarget.Workspace); } await removeVersionedPluginPathsFromSettings(); // Clean up all addon versions from workspace if (workspaceRoot) { try { const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath || ''); for (const addonName of discoveredAddons.keys()) { await luaAddonsManager.deleteAllVersionsOfAddon(workspaceRoot, addonName); logger.debug(`Deleted all versions of ${addonName} from workspace.`); } } catch (err) { logger.error(`Failed to clean up addons: ${err instanceof Error ? err.message : String(err)}`); } } await vscode.commands.executeCommand( "lua.startServer" ); vscode.window.showInformationMessage('Dutchies DCS Scripting Tools disabled.'); } /** * Updates lua-addons by copying them from the extension bundle to the workspace * with versioned naming, cleaning old versions, and updating Lua settings */ async function updateLuaAddons(): Promise { // Prevent concurrent update operations if (isUpdatingAddons) { logger.debug('Addon update already in progress, skipping.'); return; } if (!extensionPath || !workspaceRoot || !currentExtensionVersion) { logger.warn('Cannot update addons: extension path, workspace root, or version not available.'); return; } isUpdatingAddons = true; try { logger.info('Starting lua-addons update...'); // Discover bundled addons const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath); if (discoveredAddons.size === 0) { logger.info('No lua-addons found in extension bundle.'); isUpdatingAddons = false; return; } logger.debug(`Discovered ${discoveredAddons.size} lua-addon(s): ${Array.from(discoveredAddons.keys()).join(', ')}`); // Copy addons with versioned names const copiedAddons = await luaAddonsManager.copyLuaAddons( extensionPath, workspaceRoot, currentExtensionVersion, discoveredAddons ); // Store paths for settings management installedAddonPaths = copiedAddons; logger.debug(`Copied ${copiedAddons.size} addon(s) to workspace.`); // Clean up old versions for (const addonName of discoveredAddons.keys()) { await luaAddonsManager.deleteOldVersions(workspaceRoot, addonName, currentExtensionVersion); logger.debug(`Cleaned old versions of ${addonName}.`); } // Update Lua settings with new paths await addVersionedPluginPathsToSettings(copiedAddons); logger.info(`Lua-addons update completed successfully (version ${currentExtensionVersion}).`); } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); logger.error(`Failed to update lua-addons: ${errorMsg}`); vscode.window.showErrorMessage(`Failed to update Dcs Lua Types: ${errorMsg}`); } finally { isUpdatingAddons = false; } } /** * Checks on extension startup if lua-addons need to be updated * Runs only if dcsTypes is enabled in the workspace */ async function checkAndUpdateAddonsOnStartup(): Promise { if (!extensionPath || !workspaceRoot || !currentExtensionVersion) { logger.debug('Skipping addon version check on startup: missing initialization data.'); return; } try { const config = vscode.workspace.getConfiguration(extensionName); const dcsTypesEnabled = config.get('dcsTypes') || false; if (!dcsTypesEnabled) { logger.debug('dcsTypes not enabled, skipping addon check on startup.'); return; } logger.debug('Checking lua-addons versions on startup...'); // Get installed addons const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot); const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath); // Check each discovered addon let updateNeeded = false; for (const addonName of discoveredAddons.keys()) { const installed = installedAddons.get(addonName); const installedVersion = installed?.version; if (luaAddonsManager.requiresUpdate(installedVersion, currentExtensionVersion)) { logger.info( `Version mismatch for ${addonName}: installed=${installedVersion || 'none'}, current=${currentExtensionVersion}` ); updateNeeded = true; break; } } if (updateNeeded) { logger.info('Lua-addons update needed, prompting user...'); const userChoice = await vscode.window.showInformationMessage( 'DCS-Types are not up to date with the current extension version. Do you want to update them?', { modal: false }, 'Update' ); if (userChoice === 'Update') { await updateLuaAddons(); vscode.window.showInformationMessage(`DCS-Types updated to version ${currentExtensionVersion}.`); } else { logger.info('User declined DCS-Types update.'); } } else { logger.debug('Lua-addons versions are current, no update needed.'); // Ensure paths are in settings even if no update needed const pathsMap = new Map(); for (const [name, info] of installedAddons.entries()) { pathsMap.set(name, info.path); } installedAddonPaths = pathsMap; await addVersionedPluginPathsToSettings(installedAddonPaths); } } catch (err) { logger.error( `Error checking lua-addons on startup: ${err instanceof Error ? err.message : String(err)}` ); // Don't fail activation if check fails; user can manually enable/disable } } /** * Converts an absolute path to a workspace-relative path using ${workspaceFolder} */ function convertToWorkspaceFolderPath(absolutePath: string): string { if (!workspaceRoot) { return absolutePath; } // Normalize paths for comparison (handle both forward and back slashes) const normalizedAbsolute = absolutePath.replace(/\\/g, '/'); const normalizedWorkspace = workspaceRoot.replace(/\\/g, '/'); if (normalizedAbsolute.startsWith(normalizedWorkspace)) { const relativePath = normalizedAbsolute.substring(normalizedWorkspace.length); // Remove leading slash if present const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; return `\${workspaceFolder}/${cleanPath}`; } return absolutePath; } /** * Adds versioned addon paths to the Lua workspace library settings * @param addonPaths - Map of addon name to installed path */ async function addVersionedPluginPathsToSettings(addonPaths?: Map): Promise { const pathsToAdd = addonPaths || installedAddonPaths; if (pathsToAdd.size === 0) { logger.debug('No addon paths to add to settings.'); return; } try { const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey); let librarySettings = luaSettings.get(librarySettingsKey) || []; // Add each addon path if not already present for (const addonPath of pathsToAdd.values()) { const workspaceFolderPath = convertToWorkspaceFolderPath(addonPath); if (!librarySettings.includes(workspaceFolderPath)) { librarySettings.push(workspaceFolderPath); logger.debug(`Added addon path to settings: ${workspaceFolderPath}`); } } await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace); } catch (err) { logger.error(`Failed to add addon paths to settings: ${err instanceof Error ? err.message : String(err)}`); throw err; } } /** * Removes all addon paths from the Lua workspace library settings */ async function removeVersionedPluginPathsFromSettings(): Promise { try { const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey); let librarySettings = luaSettings.get(librarySettingsKey) || []; if (workspaceRoot) { // Get all installed addons to know what paths to remove const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot); for (const addonInfo of installedAddons.values()) { // Try both absolute and workspace-relative paths for compatibility const absolutePath = addonInfo.path; const relativePath = convertToWorkspaceFolderPath(absolutePath); // Remove absolute path if present let index = librarySettings.indexOf(absolutePath); if (index !== -1) { librarySettings.splice(index, 1); logger.debug(`Removed addon path from settings: ${absolutePath}`); } // Remove relative path if present index = librarySettings.indexOf(relativePath); if (index !== -1) { librarySettings.splice(index, 1); logger.debug(`Removed addon path from settings: ${relativePath}`); } } } await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace); } catch (err) { logger.error(`Failed to remove addon paths from settings: ${err instanceof Error ? err.message : String(err)}`); throw err; } }