Files
DcsMissionScriptingTools/dutchies-dcs-scripting-tools/src/extension.ts
T

450 lines
17 KiB
TypeScript

// 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<string, string> = 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<string>('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<boolean>('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<string>('luaSrcDirectory') || '${workspaceFolder}/src';
const outputPath = config.get<string>('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<string, CompilationError[]>();
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<boolean>('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<boolean>("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<boolean>("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<void> {
// 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<void> {
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<boolean>('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<string, string>();
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<string, string>): Promise<void> {
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<string[]>(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<void> {
try {
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
let librarySettings = luaSettings.get<string[]>(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;
}
}