diff --git a/backend/ILSpyX.Backend.LSP/Handlers/ExportAssemblyHandler.cs b/backend/ILSpyX.Backend.LSP/Handlers/ExportAssemblyHandler.cs new file mode 100644 index 00000000..d5761fa4 --- /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 10c9535b..0f016e66 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 f457da6d..9ff0de52 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 349fefe6..5acab57c 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,377 @@ 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 SimpleProjectDecompiler( + 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 SimpleProjectDecompiler : WholeProjectDecompiler + { + private readonly bool includeCompilerGenerated; + private readonly DecompilerTypeSystem? typeSystem; + private int filesWritten; + + public int FilesWritten => filesWritten; + + public SimpleProjectDecompiler( + 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) + { + 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 00000000..74c14d6a --- /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/README.md b/vscode-extension/README.md index 5c1dae92..38bb5721 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -9,6 +9,12 @@ 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, 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 - Visual Studio Code >= 1.101 @@ -34,4 +40,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 67781217..446ff558 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 00000000..bd1d5d92 --- /dev/null +++ b/vscode-extension/src/commands/exportDecompiledAssembly.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from "vscode"; +import { DecompiledTreeProvider } from "../decompiler/DecompiledTreeProvider"; +import { + getDefaultOutputLanguage, + getShowCompilerGeneratedSymbolsSetting, +} from "../decompiler/settings"; +import IILSpyBackend from "../decompiler/IILSpyBackend"; +import Node from "../protocol/Node"; +import { NodeType } from "../protocol/NodeType"; + +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 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 (response?.shouldUpdateAssemblyList) { + vscode.commands.executeCommand("ilspy.refreshAssemblyList"); + } + + if (!response) { + vscode.window.showErrorMessage("Export failed."); + return; + } + + if (!response.succeeded) { + vscode.window.showErrorMessage( + response.errorMessage + ? `Export failed: ${response.errorMessage}` + : "Export failed." + ); + return; + } + + const outputDirectory = + response.outputDirectory ?? baseOutputDir.fsPath; + const outputUri = vscode.Uri.file(outputDirectory); + const fileCount = response.filesWritten ?? 0; + + if (response.errorCount > 0) { + vscode.window.showWarningMessage( + `Exported ${fileCount} files to ${outputDirectory} with ${response.errorCount} errors.`, + "Reveal in File Explorer" + ).then((choice) => { + if (choice === "Reveal in File Explorer") { + vscode.commands.executeCommand("revealFileInOS", outputUri); + } + }); + return; + } + + vscode.window.showInformationMessage( + `Exported ${fileCount} files to ${outputDirectory}`, + "Reveal in File Explorer" + ).then((choice) => { + 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}`); + } + } + ); + } + ); +} + +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]; +} diff --git a/vscode-extension/src/decompiler/IILSpyBackend.ts b/vscode-extension/src/decompiler/IILSpyBackend.ts index 26ccfc8e..c7fbe2d3 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 668cd609..89c74586 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/extension.ts b/vscode-extension/src/extension.ts index 13b2227a..a020c057 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()); diff --git a/vscode-extension/src/protocol/ExportAssemblyResponse.ts b/vscode-extension/src/protocol/ExportAssemblyResponse.ts new file mode 100644 index 00000000..ef53e301 --- /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 00000000..6e7fb90e --- /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; +}