525 lines
21 KiB
TypeScript
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}`);
|
|
}
|
|
}
|