From a7c406c6772400f6b6f46b9a50de175447a9f439 Mon Sep 17 00:00:00 2001 From: Raphael Martin Date: Tue, 6 Jan 2026 15:45:32 +0100 Subject: [PATCH 1/3] init `export decompiled code to disk` feature --- vscode-extension/README.md | 5 +- vscode-extension/package.json | 17 +- .../src/commands/exportDecompiledAssembly.ts | 344 ++++++++++++++++++ vscode-extension/src/extension.ts | 4 + 4 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 vscode-extension/src/commands/exportDecompiledAssembly.ts diff --git a/vscode-extension/README.md b/vscode-extension/README.md index 5c1dae9..ce1af14 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -9,6 +9,10 @@ This is the actual VSCode extension part of the project, which fulfills followin A description of the extension's features can be found in [Feature Tour](https://github.com/icsharpcode/ilspy-vscode/wiki/Feature-Tour). +## Export decompiled code + +To export all decompiled code of a loaded assembly (keeping the namespace directory structure), right-click the assembly node in the `ILSpy: Assemblies` view and run `Export Decompiled Code...`. + ## Requirements - Visual Studio Code >= 1.101 @@ -34,4 +38,3 @@ npm install ``` Open this directory in Visual Studio Code and start debugging with F5. A development instance of VS Code will open with the latest extension code running. - diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 6778121..446ff55 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -185,6 +185,12 @@ "title": "Analyze Element", "category": "ILSpy", "icon": "$(search-view-icon)" + }, + { + "command": "ilspy.exportDecompiledAssembly", + "title": "Export Decompiled Code...", + "category": "ILSpy", + "icon": "$(export)" } ], "menus": { @@ -234,6 +240,11 @@ "when": "view == ilspyDecompiledMembers && viewItem == assemblyNode", "group": "inline" }, + { + "command": "ilspy.exportDecompiledAssembly", + "when": "view == ilspyDecompiledMembers && viewItem == assemblyNode", + "group": "navigation" + }, { "command": "ilspy.analyze", "when": "(view == ilspyDecompiledMembers || view == ilspySearchResultsContainer || view == ilspyAnalyzeResultsContainer) && viewItem == analyzableNode", @@ -276,6 +287,10 @@ { "command": "ilspy.unloadAssembly", "when": "false" + }, + { + "command": "ilspy.exportDecompiledAssembly", + "when": "ilspy.backendAvailable && ilspy.treeWithNodes" } ] }, @@ -339,4 +354,4 @@ "extensionDependencies": [ "ms-dotnettools.vscode-dotnet-runtime" ] -} \ No newline at end of file +} diff --git a/vscode-extension/src/commands/exportDecompiledAssembly.ts b/vscode-extension/src/commands/exportDecompiledAssembly.ts new file mode 100644 index 0000000..de56e90 --- /dev/null +++ b/vscode-extension/src/commands/exportDecompiledAssembly.ts @@ -0,0 +1,344 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from "path"; +import * as vscode from "vscode"; +import { DecompiledTreeProvider } from "../decompiler/DecompiledTreeProvider"; +import { + getDefaultOutputLanguage, + getShowCompilerGeneratedSymbolsSetting, +} from "../decompiler/settings"; +import { hasNodeFlag, isTypeNode } from "../decompiler/utils"; +import IILSpyBackend from "../decompiler/IILSpyBackend"; +import { LanguageName } from "../protocol/LanguageName"; +import Node from "../protocol/Node"; +import { NodeFlags } from "../protocol/NodeFlags"; +import { NodeType } from "../protocol/NodeType"; + +class ExportCancelledError extends Error {} + +export function registerExportDecompiledAssemblyCommand( + decompiledTreeProvider: DecompiledTreeProvider, + backend: IILSpyBackend +) { + return vscode.commands.registerCommand( + "ilspy.exportDecompiledAssembly", + async (node?: Node) => { + const assemblyNode = await getOrPickAssemblyNode( + node, + decompiledTreeProvider + ); + if (!assemblyNode?.metadata) { + return; + } + const assemblyMetadata = assemblyNode.metadata; + + const baseOutputDir = await promptForExportDirectory(); + if (!baseOutputDir) { + return; + } + + const outputLanguage = getDefaultOutputLanguage(); + const fileExtension = outputLanguage === LanguageName.IL ? ".il" : ".cs"; + + const assemblyFolderName = getAssemblyFolderName(assemblyNode); + const assemblyOutputDir = await createUniqueDirectory( + vscode.Uri.joinPath(baseOutputDir, assemblyFolderName) + ); + + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `ILSpy: Export decompiled code`, + cancellable: true, + }, + async (progress, token) => { + const errors: string[] = []; + + progress.report({ message: "Collecting types..." }); + const namespaces = (await decompiledTreeProvider.getChildNodes( + assemblyNode + )).filter((n) => n.metadata?.type === NodeType.Namespace); + + const exportItems: ExportItem[] = []; + const usedOutputPaths = new Set(); + const includeCompilerGenerated = + getShowCompilerGeneratedSymbolsSetting(); + + for (const nsNode of namespaces) { + throwIfCancelled(token); + + const nsName = nsNode.metadata?.name ?? ""; + const nsDir = getNamespaceDirectory(assemblyOutputDir, nsName); + + const typeNodes = (await decompiledTreeProvider.getChildNodes( + nsNode + )) + .filter((n) => isTypeNode(n.metadata?.type ?? NodeType.Unknown)) + .filter( + (n) => + includeCompilerGenerated || + !hasNodeFlag(n, NodeFlags.CompilerGenerated) + ); + + for (const typeNode of typeNodes) { + const symbolToken = typeNode.metadata?.symbolToken ?? 0; + const fileNameBase = sanitizeFileBaseName( + typeNode.metadata?.name ?? `type_${symbolToken}` + ); + + const baseFileName = `${fileNameBase}${fileExtension}`; + const baseUri = vscode.Uri.joinPath(nsDir, baseFileName); + const outputUri = usedOutputPaths.has(baseUri.toString()) + ? vscode.Uri.joinPath( + nsDir, + `${fileNameBase}_${symbolToken}${fileExtension}` + ) + : baseUri; + usedOutputPaths.add(outputUri.toString()); + + exportItems.push({ + node: typeNode, + outputFile: outputUri, + outputDir: nsDir, + }); + } + } + + const totalFiles = exportItems.length + 1; + let completedFiles = 0; + const reportFileDone = (message?: string) => { + completedFiles++; + progress.report({ + message, + increment: (1 / totalFiles) * 100, + }); + }; + + throwIfCancelled(token); + progress.report({ message: "Writing AssemblyInfo..." }); + + const assemblyInfo = await backend.sendDecompileNode({ + nodeMetadata: assemblyMetadata, + outputLanguage, + }); + + if (assemblyInfo?.shouldUpdateAssemblyList) { + vscode.commands.executeCommand("ilspy.refreshAssemblyList"); + } + + await vscode.workspace.fs.writeFile( + vscode.Uri.joinPath( + assemblyOutputDir, + `AssemblyInfo${fileExtension}` + ), + Buffer.from( + ensureTrailingNewline( + assemblyInfo?.decompiledCode ?? + `// Failed to decompile assembly.\n// ${ + assemblyInfo?.errorMessage ?? "" + }\n` + ), + "utf8" + ) + ); + if (assemblyInfo?.isError) { + errors.push( + `AssemblyInfo: ${assemblyInfo.errorMessage ?? "unknown error"}` + ); + } + reportFileDone(); + + const createdDirs = new Set(); + for (const item of exportItems) { + throwIfCancelled(token); + + const dirKey = item.outputDir.toString(); + if (!createdDirs.has(dirKey)) { + await vscode.workspace.fs.createDirectory(item.outputDir); + createdDirs.add(dirKey); + } + + const response = await backend.sendDecompileNode({ + nodeMetadata: item.node.metadata!, + outputLanguage, + }); + + if (response?.shouldUpdateAssemblyList) { + vscode.commands.executeCommand("ilspy.refreshAssemblyList"); + } + + const code = + response?.decompiledCode ?? + `// Failed to decompile.\n// ${response?.errorMessage ?? ""}\n`; + await vscode.workspace.fs.writeFile( + item.outputFile, + Buffer.from(ensureTrailingNewline(code), "utf8") + ); + + if (response?.isError) { + errors.push( + `${item.node.metadata?.name ?? "type"}: ${ + response.errorMessage ?? "unknown error" + }` + ); + } + + reportFileDone( + `Exported ${completedFiles}/${totalFiles}: ${ + item.node.metadata?.name ?? "" + }` + ); + } + + if (errors.length > 0) { + vscode.window.showWarningMessage( + `Export completed with ${errors.length} errors. Output: ${assemblyOutputDir.fsPath}` + ); + } else { + const choice = await vscode.window.showInformationMessage( + `Exported ${totalFiles} files to ${assemblyOutputDir.fsPath}`, + "Reveal in File Explorer" + ); + if (choice === "Reveal in File Explorer") { + vscode.commands.executeCommand( + "revealFileInOS", + assemblyOutputDir + ); + } + } + } + ); + } catch (e) { + if (e instanceof ExportCancelledError) { + return; + } + throw e; + } + } + ); +} + +type ExportItem = { + node: Node; + outputFile: vscode.Uri; + outputDir: vscode.Uri; +}; + +async function getOrPickAssemblyNode( + node: Node | undefined, + decompiledTreeProvider: DecompiledTreeProvider +) { + if (node?.metadata?.type === NodeType.Assembly) { + return node; + } + + const assemblies = (await decompiledTreeProvider.getChildNodes()).filter( + (n) => n.metadata?.type === NodeType.Assembly + ); + if (assemblies.length === 0) { + vscode.window.showWarningMessage("No assemblies loaded."); + return undefined; + } + + const pick = await vscode.window.showQuickPick( + assemblies.map((a) => ({ + label: a.displayName, + description: a.description, + node: a, + })), + { title: "Select assembly to export" } + ); + + return pick?.node; +} + +async function promptForExportDirectory(): Promise { + const dirs = await vscode.window.showOpenDialog({ + openLabel: "Export here", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + }); + return dirs?.[0]; +} + +function getAssemblyFolderName(assemblyNode: Node) { + const rawName = + assemblyNode.metadata?.name ?? + path.basename(assemblyNode.metadata?.assemblyPath ?? "assembly"); + return sanitizePathSegment(path.parse(rawName).name || rawName); +} + +function getNamespaceDirectory(baseDir: vscode.Uri, namespaceName: string) { + if (!namespaceName) { + return baseDir; + } + const parts = namespaceName.split(".").filter(Boolean); + const safeParts = parts.map(sanitizePathSegment).filter(Boolean); + return safeParts.reduce( + (uri, part) => vscode.Uri.joinPath(uri, part), + baseDir + ); +} + +async function createUniqueDirectory(dir: vscode.Uri) { + const base = dir; + const parent = base.with({ path: path.posix.dirname(base.path) }); + const baseName = path.posix.basename(base.path); + let candidate = base; + for (let i = 1; i < 1000; i++) { + try { + await vscode.workspace.fs.stat(candidate); + candidate = vscode.Uri.joinPath(parent, `${baseName}-${i}`); + } catch { + await vscode.workspace.fs.createDirectory(candidate); + return candidate; + } + } + + await vscode.workspace.fs.createDirectory(candidate); + return candidate; +} + +function throwIfCancelled(token: vscode.CancellationToken) { + if (token.isCancellationRequested) { + throw new ExportCancelledError("Export cancelled"); + } +} + +function ensureTrailingNewline(s: string) { + return s.endsWith("\n") ? s : `${s}\n`; +} + +function sanitizePathSegment(segment: string) { + const trimmed = segment.trim(); + const sanitized = trimmed + .replace(/[<>:"/\\\\|?*]/g, "_") + .replace(/[\u0000-\u001F\u007F]/g, "_") + .replace(/[. ]+$/g, ""); + + const safe = sanitized.length > 0 ? sanitized : "_"; + return isWindowsReservedName(safe) ? `_${safe}` : safe; +} + +function sanitizeFileBaseName(name: string) { + return sanitizePathSegment(name).replace(/\s+/g, " "); +} + +function isWindowsReservedName(name: string) { + const upper = name.toUpperCase(); + if (["CON", "PRN", "AUX", "NUL"].includes(upper)) { + return true; + } + if (/^COM[1-9]$/.test(upper)) { + return true; + } + if (/^LPT[1-9]$/.test(upper)) { + return true; + } + return false; +} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 13b2227..a020c05 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -46,6 +46,7 @@ import { registerAnalyzeCommand } from "./commands/analyze"; import { registerRefreshAssemblyListCommand } from "./commands/refreshAssemblyList"; import { AssemblyNodeDecorationProvider } from "./decompiler/AssemblyNodeDecorationProvider"; import { registerAddAssemblyByPathCommand } from "./commands/addAssemblyByPath"; +import { registerExportDecompiledAssemblyCommand } from "./commands/exportDecompiledAssembly"; let client: LanguageClient; @@ -176,6 +177,9 @@ export async function activate(context: ExtensionContext) { disposables.push(registerReloadAssemblyCommand(decompiledTreeProvider)); disposables.push(registerUnloadAssemblyCommand(decompiledTreeProvider)); disposables.push(registerRefreshAssemblyListCommand(decompiledTreeProvider)); + disposables.push( + registerExportDecompiledAssemblyCommand(decompiledTreeProvider, ilspyBackend) + ); disposables.push(registerSearchEditorSelectionCommand()); From 7e6ac9e192855f319cdd47f80a5e5fa4221c6d1d Mon Sep 17 00:00:00 2001 From: Raphael Martin Date: Wed, 7 Jan 2026 13:40:18 +0100 Subject: [PATCH 2/3] Implement backend-driven export using WholeProjectDecompiler --- .../Handlers/ExportAssemblyHandler.cs | 37 ++ backend/ILSpyX.Backend.LSP/Program.cs | 1 + .../ILSpyX.Backend.LSP/Protocol/Messages.cs | 20 + .../Decompiler/DecompilerBackend.cs | 382 +++++++++++++++++- .../Decompiler/ExportAssemblyResult.cs | 11 + .../src/commands/exportDecompiledAssembly.ts | 307 +++----------- .../src/decompiler/IILSpyBackend.ts | 7 + .../src/decompiler/ILSpyBackend.ts | 16 + .../src/protocol/ExportAssemblyResponse.ts | 13 + .../src/protocol/exportAssembly.ts | 38 ++ 10 files changed, 581 insertions(+), 251 deletions(-) create mode 100644 backend/ILSpyX.Backend.LSP/Handlers/ExportAssemblyHandler.cs create mode 100644 backend/ILSpyX.Backend/Decompiler/ExportAssemblyResult.cs create mode 100644 vscode-extension/src/protocol/ExportAssemblyResponse.ts create mode 100644 vscode-extension/src/protocol/exportAssembly.ts diff --git a/backend/ILSpyX.Backend.LSP/Handlers/ExportAssemblyHandler.cs b/backend/ILSpyX.Backend.LSP/Handlers/ExportAssemblyHandler.cs new file mode 100644 index 0000000..d5761fa --- /dev/null +++ b/backend/ILSpyX.Backend.LSP/Handlers/ExportAssemblyHandler.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2025 ICSharpCode +// Licensed under the MIT license. See the LICENSE file in the project root for more information. + +using ILSpyX.Backend.Decompiler; +using ILSpyX.Backend.LSP.Protocol; +using OmniSharp.Extensions.JsonRpc; +using System.Threading; +using System.Threading.Tasks; + +namespace ILSpyX.Backend.LSP.Handlers; + +[Serial, Method("ilspy/exportAssembly", Direction.ClientToServer)] +public class ExportAssemblyHandler(DecompilerBackend decompilerBackend) + : IJsonRpcRequestHandler +{ + public async Task Handle( + ExportAssemblyRequest request, + CancellationToken cancellationToken) + { + (var result, bool shouldUpdateAssemblyList) = + await decompilerBackend.DetectAutoLoadedAssemblies(() => + decompilerBackend.ExportAssemblyAsync( + request.NodeMetadata, + request.OutputLanguage, + request.OutputDirectory, + request.IncludeCompilerGenerated, + cancellationToken)); + + return new ExportAssemblyResponse( + result.Succeeded, + result.OutputDirectory, + result.FilesWritten, + result.ErrorCount, + result.ErrorMessage, + shouldUpdateAssemblyList); + } +} diff --git a/backend/ILSpyX.Backend.LSP/Program.cs b/backend/ILSpyX.Backend.LSP/Program.cs index 10c9535..0f016e6 100644 --- a/backend/ILSpyX.Backend.LSP/Program.cs +++ b/backend/ILSpyX.Backend.LSP/Program.cs @@ -41,6 +41,7 @@ static async Task Main(string[] args) .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .WithHandler() .WithHandler() .WithHandler() diff --git a/backend/ILSpyX.Backend.LSP/Protocol/Messages.cs b/backend/ILSpyX.Backend.LSP/Protocol/Messages.cs index f457da6..9ff0de5 100644 --- a/backend/ILSpyX.Backend.LSP/Protocol/Messages.cs +++ b/backend/ILSpyX.Backend.LSP/Protocol/Messages.cs @@ -60,6 +60,26 @@ public DecompileResponse(DecompileResult decompileResult, bool shouldUpdateAssem #endregion + #region exportAssembly + + [Serial, Method("ilspy/exportAssembly", Direction.ClientToServer)] + public record ExportAssemblyRequest( + NodeMetadata NodeMetadata, + string OutputLanguage, + string OutputDirectory, + bool IncludeCompilerGenerated) + : IRequest; + + public record ExportAssemblyResponse( + bool Succeeded, + string? OutputDirectory, + int FilesWritten, + int ErrorCount, + string? ErrorMessage, + bool ShouldUpdateAssemblyList); + + #endregion + #region getNodes [Serial, Method("ilspy/getNodes", Direction.ClientToServer)] diff --git a/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs b/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs index 349fefe..9d5c524 100644 --- a/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs +++ b/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs @@ -3,6 +3,7 @@ using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Disassembler; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; @@ -16,6 +17,7 @@ using System.Linq; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -464,4 +466,382 @@ public async Task> ListTypes(AssemblyFileIdentifier asse new MemberData(Name: t.TypeToString(includeNamespace: false), Token: MetadataTokens.GetToken(t.MetadataToken), SubKind: t.Kind)); } -} \ No newline at end of file + + public async Task ExportAssemblyAsync( + NodeMetadata nodeMetadata, + string outputLanguage, + string outputDirectory, + bool includeCompilerGenerated, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(outputDirectory)) + { + return new ExportAssemblyResult(false, null, 0, 1, "Output directory is required."); + } + + var assemblyFile = nodeMetadata.GetAssemblyFileIdentifier(); + var loadedAssembly = await assemblyList.FindAssembly(assemblyFile); + if (loadedAssembly is null) + { + return new ExportAssemblyResult(false, null, 0, 1, "Assembly is not loaded."); + } + + var metadataFile = await loadedAssembly.GetMetadataFileOrNullAsync(); + if (metadataFile is null) + { + return new ExportAssemblyResult(false, null, 0, 1, "Assembly metadata could not be loaded."); + } + + try + { + Directory.CreateDirectory(outputDirectory); + } + catch (Exception ex) + { + logger?.LogError( + ex, + "Failed to create output directory {outputDirectory}", + outputDirectory); + return new ExportAssemblyResult(false, outputDirectory, 0, 1, ex.Message); + } + + var assemblyName = nodeMetadata.Name; + if (string.IsNullOrWhiteSpace(assemblyName)) + { + assemblyName = Path.GetFileName(assemblyFile.BundledAssemblyFile ?? assemblyFile.File); + } + + var baseName = Path.GetFileNameWithoutExtension(assemblyName); + if (string.IsNullOrWhiteSpace(baseName)) + { + baseName = "assembly"; + } + + var targetDirectory = CreateUniqueDirectory(outputDirectory, baseName); + + try + { + return outputLanguage == LanguageName.IL + ? await ExportIlAsync( + metadataFile, + loadedAssembly, + assemblyFile, + targetDirectory, + includeCompilerGenerated, + cancellationToken) + : await ExportCSharpAsync( + metadataFile, + loadedAssembly, + targetDirectory, + outputLanguage, + includeCompilerGenerated, + cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + logger?.LogError( + ex, + "Failed to export assembly {assembly} to {outputDirectory}", + assemblyName, + targetDirectory); + return new ExportAssemblyResult(false, targetDirectory, 0, 1, ex.Message); + } + } + + private async Task ExportCSharpAsync( + MetadataFile metadataFile, + LoadedAssembly loadedAssembly, + string targetDirectory, + string outputLanguage, + bool includeCompilerGenerated, + CancellationToken cancellationToken) + { + var settings = ilspyBackendSettings.CreateDecompilerSettings(outputLanguage); + settings.UseNestedDirectoriesForNamespaces = true; + + var resolver = loadedAssembly.GetAssemblyResolver(true); + var projectDecompiler = new CodeOnlyProjectDecompiler( + settings, + resolver, + metadataFile, + includeCompilerGenerated); + await Task.Run( + () => projectDecompiler.DecompileProject(metadataFile, targetDirectory, cancellationToken), + cancellationToken); + + return new ExportAssemblyResult( + true, + targetDirectory, + projectDecompiler.FilesWritten, + 0, + null); + } + + private async Task ExportIlAsync( + MetadataFile metadataFile, + LoadedAssembly loadedAssembly, + AssemblyFileIdentifier assemblyFile, + string targetDirectory, + bool includeCompilerGenerated, + CancellationToken cancellationToken) + { + var settings = ilspyBackendSettings.CreateDecompilerSettings(LanguageName.CSharpLatest); + settings.UseNestedDirectoriesForNamespaces = true; + + var resolver = loadedAssembly.GetAssemblyResolver(true); + DecompilerTypeSystem? typeSystem = includeCompilerGenerated + ? null + : new DecompilerTypeSystem(metadataFile, resolver, settings); + + var metadata = metadataFile.Metadata; + var comparer = OperatingSystem.IsWindows() + ? StringComparer.OrdinalIgnoreCase + : StringComparer.Ordinal; + var usedPaths = new HashSet(comparer); + int filesWritten = 0; + int errorCount = 0; + + cancellationToken.ThrowIfCancellationRequested(); + var assemblyInfoPath = Path.Combine( + targetDirectory, + WholeProjectDecompiler.CleanUpFileName("AssemblyInfo", ".il")); + var assemblyInfoResult = await GetCode( + assemblyFile, + EntityHandle.AssemblyDefinition, + LanguageName.IL); + await WriteTextAsync( + assemblyInfoPath, + BuildExportContent(assemblyInfoResult, "assembly"), + cancellationToken); + usedPaths.Add(assemblyInfoPath); + filesWritten++; + if (assemblyInfoResult.IsError) + { + errorCount++; + } + + foreach (var typeHandle in metadata.GetTopLevelTypeDefinitions()) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!ShouldExportType(metadataFile, typeHandle, settings, includeCompilerGenerated, typeSystem)) + { + continue; + } + + var typeDef = metadata.GetTypeDefinition(typeHandle); + string name = metadata.GetString(typeDef.Name); + string ns = metadata.GetString(typeDef.Namespace); + string directoryPath = string.IsNullOrEmpty(ns) + ? targetDirectory + : Path.Combine( + targetDirectory, + settings.UseNestedDirectoriesForNamespaces + ? WholeProjectDecompiler.CleanUpPath(ns) + : WholeProjectDecompiler.CleanUpDirectoryName(ns)); + Directory.CreateDirectory(directoryPath); + + int symbolToken = MetadataTokens.GetToken(typeHandle); + string filePath = GetUniqueOutputPath( + directoryPath, + name, + ".il", + symbolToken, + usedPaths); + + var result = await GetCode( + assemblyFile, + MetadataTokens.EntityHandle(symbolToken), + LanguageName.IL); + await WriteTextAsync( + filePath, + BuildExportContent(result, name), + cancellationToken); + filesWritten++; + if (result.IsError) + { + errorCount++; + } + } + + return new ExportAssemblyResult(true, targetDirectory, filesWritten, errorCount, null); + } + + private static bool ShouldExportType( + MetadataFile metadataFile, + TypeDefinitionHandle typeHandle, + DecompilerSettings settings, + bool includeCompilerGenerated, + DecompilerTypeSystem? typeSystem) + { + var metadata = metadataFile.Metadata; + var typeDef = metadata.GetTypeDefinition(typeHandle); + string name = metadata.GetString(typeDef.Name); + string ns = metadata.GetString(typeDef.Namespace); + if (name == "" || CSharpDecompiler.MemberIsHidden(metadataFile, typeHandle, settings)) + { + return false; + } + + if (ns == "XamlGeneratedNamespace" && name == "GeneratedInternalTypeHelper") + { + return false; + } + + if (!includeCompilerGenerated && typeSystem is not null) + { + var definition = typeSystem.MainModule.GetDefinition(typeHandle); + if (definition?.IsCompilerGenerated() == true) + { + return false; + } + } + + return true; + } + + private static string GetUniqueOutputPath( + string directoryPath, + string baseName, + string extension, + int symbolToken, + HashSet usedPaths) + { + string baseFileName = WholeProjectDecompiler.CleanUpFileName(baseName, extension); + string basePath = Path.Combine(directoryPath, baseFileName); + if (usedPaths.Add(basePath)) + { + return basePath; + } + + string suffixBase = $"{baseName}_{symbolToken}"; + for (int attempt = 0; attempt < 1000; attempt++) + { + string suffix = attempt == 0 ? suffixBase : $"{suffixBase}_{attempt}"; + string fileName = WholeProjectDecompiler.CleanUpFileName(suffix, extension); + string candidate = Path.Combine(directoryPath, fileName); + if (usedPaths.Add(candidate)) + { + return candidate; + } + } + + string fallbackName = WholeProjectDecompiler.CleanUpFileName( + $"{baseName}_{symbolToken}_{DateTime.UtcNow.Ticks}", + extension); + string fallbackPath = Path.Combine(directoryPath, fallbackName); + usedPaths.Add(fallbackPath); + return fallbackPath; + } + + private static async Task WriteTextAsync( + string path, + string contents, + CancellationToken cancellationToken) + { + await File.WriteAllTextAsync(path, contents, Encoding.UTF8, cancellationToken); + } + + private static string BuildExportContent(DecompileResult result, string label) + { + if (!string.IsNullOrEmpty(result.DecompiledCode)) + { + return EnsureTrailingNewline(result.DecompiledCode); + } + + string error = result.ErrorMessage ?? "Unknown error."; + return EnsureTrailingNewline($"// Failed to decompile {label}.\n// {error}\n"); + } + + private static string EnsureTrailingNewline(string content) + { + return content.EndsWith('\n') ? content : $"{content}\n"; + } + + private static string CreateUniqueDirectory(string baseDirectory, string folderName) + { + string safeName = WholeProjectDecompiler.CleanUpDirectoryName(folderName); + string candidate = Path.Combine(baseDirectory, safeName); + if (!Directory.Exists(candidate) && !File.Exists(candidate)) + { + Directory.CreateDirectory(candidate); + return candidate; + } + + for (int i = 1; i < 1000; i++) + { + string next = Path.Combine(baseDirectory, $"{safeName}-{i}"); + if (!Directory.Exists(next) && !File.Exists(next)) + { + Directory.CreateDirectory(next); + return next; + } + } + + Directory.CreateDirectory(candidate); + return candidate; + } + + private sealed class CodeOnlyProjectDecompiler : WholeProjectDecompiler + { + private readonly bool includeCompilerGenerated; + private readonly DecompilerTypeSystem? typeSystem; + private int filesWritten; + + public int FilesWritten => filesWritten; + + public CodeOnlyProjectDecompiler( + DecompilerSettings settings, + IAssemblyResolver assemblyResolver, + MetadataFile metadataFile, + bool includeCompilerGenerated) + : base(settings, assemblyResolver, projectWriter: null, assemblyReferenceClassifier: null, debugInfoProvider: null) + { + this.includeCompilerGenerated = includeCompilerGenerated; + if (!includeCompilerGenerated) + { + typeSystem = new DecompilerTypeSystem(metadataFile, assemblyResolver, settings); + } + } + + protected override bool IncludeTypeWhenDecompilingProject(MetadataFile module, TypeDefinitionHandle type) + { + if (!base.IncludeTypeWhenDecompilingProject(module, type)) + { + return false; + } + + if (includeCompilerGenerated || typeSystem is null) + { + return true; + } + + var definition = typeSystem.MainModule.GetDefinition(type); + return definition?.IsCompilerGenerated() != true; + } + + protected override IEnumerable WriteResourceFilesInProject(MetadataFile module) + { + return []; + } + + protected override IEnumerable WriteMiscellaneousFilesInProject(PEFile module) + { + return []; + } + + protected override TextWriter CreateFile(string path) + { + if (path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) + { + return TextWriter.Null; + } + + Interlocked.Increment(ref filesWritten); + return base.CreateFile(path); + } + } +} diff --git a/backend/ILSpyX.Backend/Decompiler/ExportAssemblyResult.cs b/backend/ILSpyX.Backend/Decompiler/ExportAssemblyResult.cs new file mode 100644 index 0000000..74c14d6 --- /dev/null +++ b/backend/ILSpyX.Backend/Decompiler/ExportAssemblyResult.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2025 ICSharpCode +// Licensed under the MIT license. See the LICENSE file in the project root for more information. + +namespace ILSpyX.Backend.Decompiler; + +public record ExportAssemblyResult( + bool Succeeded, + string? OutputDirectory, + int FilesWritten, + int ErrorCount, + string? ErrorMessage); diff --git a/vscode-extension/src/commands/exportDecompiledAssembly.ts b/vscode-extension/src/commands/exportDecompiledAssembly.ts index de56e90..d836944 100644 --- a/vscode-extension/src/commands/exportDecompiledAssembly.ts +++ b/vscode-extension/src/commands/exportDecompiledAssembly.ts @@ -3,22 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from "path"; import * as vscode from "vscode"; import { DecompiledTreeProvider } from "../decompiler/DecompiledTreeProvider"; import { getDefaultOutputLanguage, getShowCompilerGeneratedSymbolsSetting, } from "../decompiler/settings"; -import { hasNodeFlag, isTypeNode } from "../decompiler/utils"; import IILSpyBackend from "../decompiler/IILSpyBackend"; -import { LanguageName } from "../protocol/LanguageName"; import Node from "../protocol/Node"; -import { NodeFlags } from "../protocol/NodeFlags"; import { NodeType } from "../protocol/NodeType"; -class ExportCancelledError extends Error {} - export function registerExportDecompiledAssemblyCommand( decompiledTreeProvider: DecompiledTreeProvider, backend: IILSpyBackend @@ -41,193 +35,83 @@ export function registerExportDecompiledAssemblyCommand( } const outputLanguage = getDefaultOutputLanguage(); - const fileExtension = outputLanguage === LanguageName.IL ? ".il" : ".cs"; - - const assemblyFolderName = getAssemblyFolderName(assemblyNode); - const assemblyOutputDir = await createUniqueDirectory( - vscode.Uri.joinPath(baseOutputDir, assemblyFolderName) - ); - - try { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: `ILSpy: Export decompiled code`, - cancellable: true, - }, - async (progress, token) => { - const errors: string[] = []; - - progress.report({ message: "Collecting types..." }); - const namespaces = (await decompiledTreeProvider.getChildNodes( - assemblyNode - )).filter((n) => n.metadata?.type === NodeType.Namespace); - - const exportItems: ExportItem[] = []; - const usedOutputPaths = new Set(); - const includeCompilerGenerated = - getShowCompilerGeneratedSymbolsSetting(); - - for (const nsNode of namespaces) { - throwIfCancelled(token); - - const nsName = nsNode.metadata?.name ?? ""; - const nsDir = getNamespaceDirectory(assemblyOutputDir, nsName); - - const typeNodes = (await decompiledTreeProvider.getChildNodes( - nsNode - )) - .filter((n) => isTypeNode(n.metadata?.type ?? NodeType.Unknown)) - .filter( - (n) => - includeCompilerGenerated || - !hasNodeFlag(n, NodeFlags.CompilerGenerated) - ); - - for (const typeNode of typeNodes) { - const symbolToken = typeNode.metadata?.symbolToken ?? 0; - const fileNameBase = sanitizeFileBaseName( - typeNode.metadata?.name ?? `type_${symbolToken}` - ); - - const baseFileName = `${fileNameBase}${fileExtension}`; - const baseUri = vscode.Uri.joinPath(nsDir, baseFileName); - const outputUri = usedOutputPaths.has(baseUri.toString()) - ? vscode.Uri.joinPath( - nsDir, - `${fileNameBase}_${symbolToken}${fileExtension}` - ) - : baseUri; - usedOutputPaths.add(outputUri.toString()); - - exportItems.push({ - node: typeNode, - outputFile: outputUri, - outputDir: nsDir, - }); - } - } - - const totalFiles = exportItems.length + 1; - let completedFiles = 0; - const reportFileDone = (message?: string) => { - completedFiles++; - progress.report({ - message, - increment: (1 / totalFiles) * 100, - }); - }; - - throwIfCancelled(token); - progress.report({ message: "Writing AssemblyInfo..." }); - - const assemblyInfo = await backend.sendDecompileNode({ - nodeMetadata: assemblyMetadata, - outputLanguage, - }); + const includeCompilerGenerated = + getShowCompilerGeneratedSymbolsSetting(); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `ILSpy: Export decompiled code`, + cancellable: true, + }, + async (progress, token) => { + progress.report({ message: "Exporting..." }); + try { + const response = await backend.sendExportAssembly( + { + nodeMetadata: assemblyMetadata, + outputLanguage, + outputDirectory: baseOutputDir.fsPath, + includeCompilerGenerated, + }, + token + ); - if (assemblyInfo?.shouldUpdateAssemblyList) { + if (response?.shouldUpdateAssemblyList) { vscode.commands.executeCommand("ilspy.refreshAssemblyList"); } - await vscode.workspace.fs.writeFile( - vscode.Uri.joinPath( - assemblyOutputDir, - `AssemblyInfo${fileExtension}` - ), - Buffer.from( - ensureTrailingNewline( - assemblyInfo?.decompiledCode ?? - `// Failed to decompile assembly.\n// ${ - assemblyInfo?.errorMessage ?? "" - }\n` - ), - "utf8" - ) - ); - if (assemblyInfo?.isError) { - errors.push( - `AssemblyInfo: ${assemblyInfo.errorMessage ?? "unknown error"}` - ); + if (!response) { + vscode.window.showErrorMessage("Export failed."); + return; } - reportFileDone(); - - const createdDirs = new Set(); - for (const item of exportItems) { - throwIfCancelled(token); - - const dirKey = item.outputDir.toString(); - if (!createdDirs.has(dirKey)) { - await vscode.workspace.fs.createDirectory(item.outputDir); - createdDirs.add(dirKey); - } - - const response = await backend.sendDecompileNode({ - nodeMetadata: item.node.metadata!, - outputLanguage, - }); - if (response?.shouldUpdateAssemblyList) { - vscode.commands.executeCommand("ilspy.refreshAssemblyList"); - } - - const code = - response?.decompiledCode ?? - `// Failed to decompile.\n// ${response?.errorMessage ?? ""}\n`; - await vscode.workspace.fs.writeFile( - item.outputFile, - Buffer.from(ensureTrailingNewline(code), "utf8") - ); - - if (response?.isError) { - errors.push( - `${item.node.metadata?.name ?? "type"}: ${ - response.errorMessage ?? "unknown error" - }` - ); - } - - reportFileDone( - `Exported ${completedFiles}/${totalFiles}: ${ - item.node.metadata?.name ?? "" - }` + if (!response.succeeded) { + vscode.window.showErrorMessage( + response.errorMessage + ? `Export failed: ${response.errorMessage}` + : "Export failed." ); + return; } - if (errors.length > 0) { - vscode.window.showWarningMessage( - `Export completed with ${errors.length} errors. Output: ${assemblyOutputDir.fsPath}` - ); - } else { - const choice = await vscode.window.showInformationMessage( - `Exported ${totalFiles} files to ${assemblyOutputDir.fsPath}`, + const outputDirectory = + response.outputDirectory ?? baseOutputDir.fsPath; + const outputUri = vscode.Uri.file(outputDirectory); + const fileCount = response.filesWritten ?? 0; + + if (response.errorCount > 0) { + const choice = await vscode.window.showWarningMessage( + `Exported ${fileCount} files to ${outputDirectory} with ${response.errorCount} errors.`, "Reveal in File Explorer" ); if (choice === "Reveal in File Explorer") { - vscode.commands.executeCommand( - "revealFileInOS", - assemblyOutputDir - ); + vscode.commands.executeCommand("revealFileInOS", outputUri); } + return; } + + const choice = await vscode.window.showInformationMessage( + `Exported ${fileCount} files to ${outputDirectory}`, + "Reveal in File Explorer" + ); + if (choice === "Reveal in File Explorer") { + vscode.commands.executeCommand("revealFileInOS", outputUri); + } + } catch (err) { + if (token.isCancellationRequested) { + return; + } + const message = + err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Export failed: ${message}`); } - ); - } catch (e) { - if (e instanceof ExportCancelledError) { - return; } - throw e; - } + ); } ); } -type ExportItem = { - node: Node; - outputFile: vscode.Uri; - outputDir: vscode.Uri; -}; - async function getOrPickAssemblyNode( node: Node | undefined, decompiledTreeProvider: DecompiledTreeProvider @@ -265,80 +149,3 @@ async function promptForExportDirectory(): Promise { }); return dirs?.[0]; } - -function getAssemblyFolderName(assemblyNode: Node) { - const rawName = - assemblyNode.metadata?.name ?? - path.basename(assemblyNode.metadata?.assemblyPath ?? "assembly"); - return sanitizePathSegment(path.parse(rawName).name || rawName); -} - -function getNamespaceDirectory(baseDir: vscode.Uri, namespaceName: string) { - if (!namespaceName) { - return baseDir; - } - const parts = namespaceName.split(".").filter(Boolean); - const safeParts = parts.map(sanitizePathSegment).filter(Boolean); - return safeParts.reduce( - (uri, part) => vscode.Uri.joinPath(uri, part), - baseDir - ); -} - -async function createUniqueDirectory(dir: vscode.Uri) { - const base = dir; - const parent = base.with({ path: path.posix.dirname(base.path) }); - const baseName = path.posix.basename(base.path); - let candidate = base; - for (let i = 1; i < 1000; i++) { - try { - await vscode.workspace.fs.stat(candidate); - candidate = vscode.Uri.joinPath(parent, `${baseName}-${i}`); - } catch { - await vscode.workspace.fs.createDirectory(candidate); - return candidate; - } - } - - await vscode.workspace.fs.createDirectory(candidate); - return candidate; -} - -function throwIfCancelled(token: vscode.CancellationToken) { - if (token.isCancellationRequested) { - throw new ExportCancelledError("Export cancelled"); - } -} - -function ensureTrailingNewline(s: string) { - return s.endsWith("\n") ? s : `${s}\n`; -} - -function sanitizePathSegment(segment: string) { - const trimmed = segment.trim(); - const sanitized = trimmed - .replace(/[<>:"/\\\\|?*]/g, "_") - .replace(/[\u0000-\u001F\u007F]/g, "_") - .replace(/[. ]+$/g, ""); - - const safe = sanitized.length > 0 ? sanitized : "_"; - return isWindowsReservedName(safe) ? `_${safe}` : safe; -} - -function sanitizeFileBaseName(name: string) { - return sanitizePathSegment(name).replace(/\s+/g, " "); -} - -function isWindowsReservedName(name: string) { - const upper = name.toUpperCase(); - if (["CON", "PRN", "AUX", "NUL"].includes(upper)) { - return true; - } - if (/^COM[1-9]$/.test(upper)) { - return true; - } - if (/^LPT[1-9]$/.test(upper)) { - return true; - } - return false; -} diff --git a/vscode-extension/src/decompiler/IILSpyBackend.ts b/vscode-extension/src/decompiler/IILSpyBackend.ts index 26ccfc8..c7fbe2d 100644 --- a/vscode-extension/src/decompiler/IILSpyBackend.ts +++ b/vscode-extension/src/decompiler/IILSpyBackend.ts @@ -20,6 +20,9 @@ import { InitWithAssembliesResponse, } from "../protocol/initWithAssemblies"; import { AnalyzeParams, AnalyzeResponse } from "../protocol/analyze"; +import { ExportAssemblyParams } from "../protocol/exportAssembly"; +import ExportAssemblyResponse from "../protocol/ExportAssemblyResponse"; +import { CancellationToken } from "vscode"; export default interface IILSpyBackend { sendInitWithAssemblies( @@ -42,4 +45,8 @@ export default interface IILSpyBackend { sendSearch(params: SearchParams): Promise; sendAnalyze(params: AnalyzeParams): Promise; + sendExportAssembly( + params: ExportAssemblyParams, + token?: CancellationToken + ): Promise; } diff --git a/vscode-extension/src/decompiler/ILSpyBackend.ts b/vscode-extension/src/decompiler/ILSpyBackend.ts index 668cd60..89c7458 100644 --- a/vscode-extension/src/decompiler/ILSpyBackend.ts +++ b/vscode-extension/src/decompiler/ILSpyBackend.ts @@ -42,6 +42,11 @@ import { AnalyzeRequest, AnalyzeResponse, } from "../protocol/analyze"; +import { + ExportAssemblyParams, + ExportAssemblyRequest, +} from "../protocol/exportAssembly"; +import ExportAssemblyResponse from "../protocol/ExportAssemblyResponse"; export default class ILSpyBackend implements IILSpyBackend { constructor(private languageClient: LanguageClient) {} @@ -92,4 +97,15 @@ export default class ILSpyBackend implements IILSpyBackend { sendAnalyze(params: AnalyzeParams): Promise { return this.languageClient.sendRequest(AnalyzeRequest.type, params); } + + sendExportAssembly( + params: ExportAssemblyParams, + token?: vscode.CancellationToken + ): Promise { + return this.languageClient.sendRequest( + ExportAssemblyRequest.type, + params, + token + ); + } } diff --git a/vscode-extension/src/protocol/ExportAssemblyResponse.ts b/vscode-extension/src/protocol/ExportAssemblyResponse.ts new file mode 100644 index 0000000..ef53e30 --- /dev/null +++ b/vscode-extension/src/protocol/ExportAssemblyResponse.ts @@ -0,0 +1,13 @@ +/*------------------------------------------------------------------------------------------------ + * Copyright (c) 2025 ICSharpCode + * Licensed under the MIT License. See LICENSE.TXT in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +export default interface ExportAssemblyResponse { + succeeded: boolean; + outputDirectory?: string; + filesWritten: number; + errorCount: number; + errorMessage?: string; + shouldUpdateAssemblyList: boolean; +} diff --git a/vscode-extension/src/protocol/exportAssembly.ts b/vscode-extension/src/protocol/exportAssembly.ts new file mode 100644 index 0000000..6e7fb90 --- /dev/null +++ b/vscode-extension/src/protocol/exportAssembly.ts @@ -0,0 +1,38 @@ +/*------------------------------------------------------------------------------------------------ + * Copyright (c) 2025 ICSharpCode + * Licensed under the MIT License. See LICENSE.TXT in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { + CancellationToken, + HandlerResult, + ParameterStructures, + RequestHandler, + RequestType, +} from "vscode-languageclient"; +import ExportAssemblyResponse from "./ExportAssemblyResponse"; +import NodeMetadata from "./NodeMetadata"; + +export interface ExportAssemblyParams { + nodeMetadata: NodeMetadata; + outputLanguage: string; + outputDirectory: string; + includeCompilerGenerated: boolean; +} + +export namespace ExportAssemblyRequest { + export const type = new RequestType< + ExportAssemblyParams, + ExportAssemblyResponse | null, + never + >("ilspy/exportAssembly", ParameterStructures.byName); + export type HandlerSignature = RequestHandler< + ExportAssemblyParams, + ExportAssemblyResponse | null, + void + >; + export type MiddlewareSignature = ( + token: CancellationToken, + next: HandlerSignature + ) => HandlerResult; +} From a0a95e936b039754996fac5e944ac2184093473e Mon Sep 17 00:00:00 2001 From: Raphael Martin Date: Fri, 9 Jan 2026 01:15:36 +0100 Subject: [PATCH 3/3] Enable project file generation when exporting decompiled assemblies, fix progress notification --- .../Decompiler/DecompilerBackend.cs | 11 +++------- vscode-extension/README.md | 4 +++- .../src/commands/exportDecompiledAssembly.ts | 22 ++++++++++--------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs b/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs index 9d5c524..5acab57 100644 --- a/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs +++ b/backend/ILSpyX.Backend/Decompiler/DecompilerBackend.cs @@ -564,7 +564,7 @@ private async Task ExportCSharpAsync( settings.UseNestedDirectoriesForNamespaces = true; var resolver = loadedAssembly.GetAssemblyResolver(true); - var projectDecompiler = new CodeOnlyProjectDecompiler( + var projectDecompiler = new SimpleProjectDecompiler( settings, resolver, metadataFile, @@ -785,7 +785,7 @@ private static string CreateUniqueDirectory(string baseDirectory, string folderN return candidate; } - private sealed class CodeOnlyProjectDecompiler : WholeProjectDecompiler + private sealed class SimpleProjectDecompiler : WholeProjectDecompiler { private readonly bool includeCompilerGenerated; private readonly DecompilerTypeSystem? typeSystem; @@ -793,7 +793,7 @@ private sealed class CodeOnlyProjectDecompiler : WholeProjectDecompiler public int FilesWritten => filesWritten; - public CodeOnlyProjectDecompiler( + public SimpleProjectDecompiler( DecompilerSettings settings, IAssemblyResolver assemblyResolver, MetadataFile metadataFile, @@ -835,11 +835,6 @@ protected override IEnumerable WriteMiscellaneousFilesInProject protected override TextWriter CreateFile(string path) { - if (path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) - { - return TextWriter.Null; - } - Interlocked.Increment(ref filesWritten); return base.CreateFile(path); } diff --git a/vscode-extension/README.md b/vscode-extension/README.md index ce1af14..38bb572 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -11,7 +11,9 @@ A description of the extension's features can be found in [Feature Tour](https:/ ## Export decompiled code -To export all decompiled code of a loaded assembly (keeping the namespace directory structure), right-click the assembly node in the `ILSpy: Assemblies` view and run `Export Decompiled Code...`. +To export all decompiled code of a loaded assembly, right-click the assembly node in the `ILSpy: Assemblies` view and run `Export Decompiled Code...`. This will generate a complete C# project including: +- All decompiled source files organized by namespace +- A .csproj project file for easy compilation ## Requirements diff --git a/vscode-extension/src/commands/exportDecompiledAssembly.ts b/vscode-extension/src/commands/exportDecompiledAssembly.ts index d836944..bd1d5d9 100644 --- a/vscode-extension/src/commands/exportDecompiledAssembly.ts +++ b/vscode-extension/src/commands/exportDecompiledAssembly.ts @@ -81,23 +81,25 @@ export function registerExportDecompiledAssemblyCommand( const fileCount = response.filesWritten ?? 0; if (response.errorCount > 0) { - const choice = await vscode.window.showWarningMessage( + vscode.window.showWarningMessage( `Exported ${fileCount} files to ${outputDirectory} with ${response.errorCount} errors.`, "Reveal in File Explorer" - ); - if (choice === "Reveal in File Explorer") { - vscode.commands.executeCommand("revealFileInOS", outputUri); - } + ).then((choice) => { + if (choice === "Reveal in File Explorer") { + vscode.commands.executeCommand("revealFileInOS", outputUri); + } + }); return; } - const choice = await vscode.window.showInformationMessage( + vscode.window.showInformationMessage( `Exported ${fileCount} files to ${outputDirectory}`, "Reveal in File Explorer" - ); - if (choice === "Reveal in File Explorer") { - vscode.commands.executeCommand("revealFileInOS", outputUri); - } + ).then((choice) => { + if (choice === "Reveal in File Explorer") { + vscode.commands.executeCommand("revealFileInOS", outputUri); + } + }); } catch (err) { if (token.isCancellationRequested) { return;