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

525 lines
21 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 * 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<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);
}
const globalRequirables: string[] = config.get<string[]>('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<boolean>("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<boolean>("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<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;
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<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...');
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<void> {
if (!luaAddonsTargetPath) {
logger.warn('Lua addons target path not available.');
return;
}
try {
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
let librarySettings = luaSettings.get<string[]>(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<void> {
if (!luaAddonsTargetPath) {
logger.debug('Lua addons target path not available, skipping removal.');
return;
}
try {
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
let librarySettings = luaSettings.get<string[]>(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<void> {
try {
const config = vscode.workspace.getConfiguration(extensionName);
const globalRequirables = config.get<string[]>('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}`);
}
}