// 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 * as path from 'path'; import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger, CompilationErrorType } 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'); // Diagnostic codes const DIAGNOSTIC_CODE_MISSING_GLOBAL = 'lua-missing-global-dependency'; const logger = new Logger(); const luaAddonNames: string[] = [ "dcs-types" ]; const extensionName = 'dutchies-dcs-scripting-tools'; const publisherName = 'dutchie031'; const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`; let luaAddonsManagerInstance: luaAddonsManager.LuaAddonsManager | undefined; // State tracking for addon updates let extensionPath: string | undefined; let workspaceRoot: string | undefined; let luaAddonsTargetPath: string | undefined; let currentExtensionVersion: string | undefined; 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; if (workspaceRoot === undefined) { vscode.window.showErrorMessage('Workspace root not found. Lua addons manager cannot be initialized.'); return; } luaAddonsTargetPath = path.join(workspaceRoot, '.vscode' , 'lua-addons'); luaAddonsManagerInstance = new luaAddonsManager.LuaAddonsManager(luaAddonsTargetPath, context.extensionPath); if (!luaAddonsManagerInstance) { vscode.window.showErrorMessage('Failed to initialize Lua addons manager.'); return; } // Initialize extension version try { currentExtensionVersion = await luaAddonsManagerInstance.getPackageVersion(); 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); } }) ); // Register code actions provider for quick fixes context.subscriptions.push( vscode.languages.registerCodeActionsProvider('lua', new LuaQuickFixProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }) ); // Register command to add global requirable context.subscriptions.push( vscode.commands.registerCommand( 'dutchies-dcs-scripting-tools.addGlobalRequirable', async (dependency: string) => { await addGlobalRequirable(dependency); } ) ); 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); } const globalRequirables: string[] = config.get('globalRequirables') || []; // Update diagnostics diagnosticCollection.clear(); for (const [filePath, errors] of errorsByFile) { const uri = vscode.Uri.file(filePath); const diagnostics = errors.map(error => { if (error.type === CompilationErrorType.DependencyNotFound) { const dependencyStr = error.metaData?.get("dependency") ?? undefined; if (dependencyStr) { if (globalRequirables.includes(dependencyStr)) { // Do nothing, it is a globally requirable dependency const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0); const diagnostic = new vscode.Diagnostic(range, "Unchecked: Globally marked dependency", vscode.DiagnosticSeverity.Hint); diagnostic.source = 'DCS Lua Transpiler'; return diagnostic; } else { 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.code = { value: DIAGNOSTIC_CODE_MISSING_GLOBAL, target: vscode.Uri.parse('https://example.com') }; diagnostic.source = 'DCS Lua Transpiler'; // Store the dependency name for the quick fix to access (diagnostic as any).dependency = dependencyStr; return diagnostic; } } return undefined; //Something weird happened, let's ignore it for now. } else { 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; } }).filter(diagnostic => diagnostic !== undefined); 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 addPluginPathsToSettings(); 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(); if(luaAddonsManagerInstance){ await luaAddonsManagerInstance.removeAllExtensions(luaAddonNames); } 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; if (luaAddonsManagerInstance === undefined) { logger.warn('Lua Addons Manager instance is not available.'); isUpdatingAddons = false; return; } try { logger.info('Starting lua-addons update...'); for (const addonName of luaAddonNames) { const packageVersion = await luaAddonsManagerInstance.getPackageVersion(); const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName); if (packageVersion && packageVersion !== addonVersion) { logger.warn( `Version mismatch for addon ${addonName}: package=${packageVersion}, installed=${addonVersion}` ); luaAddonsManagerInstance.removeExtension(addonName); luaAddonsManagerInstance.installExtension(addonName); } } 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...'); if (!luaAddonsManagerInstance) { logger.debug('Lua Addons Manager instance is not available, skipping addon version check.'); return; } let updateNeeded = false; for (const addonName of luaAddonNames) { const packageVersion = await luaAddonsManagerInstance.getPackageVersion(); const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName); if (packageVersion && packageVersion !== addonVersion) { updateNeeded = true; break; } } if (updateNeeded) { 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.'); } } } 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 the lua-addons directory to the Lua workspace library settings. * Lua Language Server automatically discovers versioned addons within this directory. */ async function addPluginPathsToSettings(): Promise { if (!luaAddonsTargetPath) { logger.warn('Lua addons target path not available.'); return; } try { const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey); let librarySettings = luaSettings.get(librarySettingsKey) || []; const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath); if (!librarySettings.includes(workspaceFolderPath)) { librarySettings.push(workspaceFolderPath); logger.debug(`Added lua-addons path to Lua settings: ${workspaceFolderPath}`); await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace); } else { logger.debug('Lua-addons path already in settings.'); } } catch (err) { logger.error(`Failed to add lua-addons path to settings: ${err instanceof Error ? err.message : String(err)}`); throw err; } } /** * Removes the lua-addons directory from the Lua workspace library settings */ async function removeVersionedPluginPathsFromSettings(): Promise { if (!luaAddonsTargetPath) { logger.debug('Lua addons target path not available, skipping removal.'); return; } try { const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey); let librarySettings = luaSettings.get(librarySettingsKey) || []; const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath); const filteredSettings = librarySettings.filter(path => path !== workspaceFolderPath); if (filteredSettings.length !== librarySettings.length) { await luaSettings.update(librarySettingsKey, filteredSettings, vscode.ConfigurationTarget.Workspace); logger.debug(`Removed lua-addons path from Lua settings: ${workspaceFolderPath}`); } else { logger.debug('Lua-addons path not found in settings.'); } } catch (err) { logger.error(`Failed to remove lua-addons path from settings: ${err instanceof Error ? err.message : String(err)}`); throw err; } } /** * Code actions provider for Lua quick fixes */ class LuaQuickFixProvider implements vscode.CodeActionProvider { provideCodeActions( document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, ): vscode.CodeAction[] { const codeActions: vscode.CodeAction[] = []; // Check for missing global dependency diagnostics for (const diagnostic of context.diagnostics) { const codeValue = typeof diagnostic.code === 'object' && diagnostic.code !== null ? (diagnostic.code as any).value : diagnostic.code; if (codeValue === DIAGNOSTIC_CODE_MISSING_GLOBAL) { const dependency = (diagnostic as any).dependency; if (dependency) { const action = new vscode.CodeAction( `Mark '${dependency}' as globally available`, vscode.CodeActionKind.QuickFix ); action.command = { title: `Add '${dependency}' to global requireables`, command: 'dutchies-dcs-scripting-tools.addGlobalRequirable', arguments: [dependency] }; action.diagnostics = [diagnostic]; codeActions.push(action); } } } return codeActions; } } /** * Adds a dependency to the global requireables list in workspace settings * @param dependency - The dependency name to add */ async function addGlobalRequirable(dependency: string): Promise { try { const config = vscode.workspace.getConfiguration(extensionName); const globalRequirables = config.get('globalRequirables') || []; if (!globalRequirables.includes(dependency)) { globalRequirables.push(dependency); await config.update('globalRequirables', globalRequirables, vscode.ConfigurationTarget.Workspace); vscode.window.showInformationMessage(`Added '${dependency}' to global requireables.`); // Recompile to update diagnostics await compileLuaScripts(); } else { vscode.window.showInformationMessage(`'${dependency}' is already in global requireables.`); } } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); vscode.window.showErrorMessage(`Failed to add global requirable: ${errorMsg}`); logger.error(`Error adding global requirable: ${errorMsg}`); } }