diff --git a/ILSpy.Tests.Windows/ImageList/ImageListResourceEntryNodeTests.cs b/ILSpy.Tests.Windows/ImageList/ImageListResourceEntryNodeTests.cs index 0841f4851..91cac5903 100644 --- a/ILSpy.Tests.Windows/ImageList/ImageListResourceEntryNodeTests.cs +++ b/ILSpy.Tests.Windows/ImageList/ImageListResourceEntryNodeTests.cs @@ -127,7 +127,7 @@ public class ImageListResourceEntryNodeTests var output = new AvaloniaEditTextOutput(); var language = AppComposition.Current.GetExport().CurrentLanguage; - node.Decompile(language, output, new DecompilationOptions()); + node.Decompile(language, output, new DecompilationOptions(new DecompilerSettings())); output.UIElements.Should().HaveCount(1, "the parent's Decompile contributes exactly one inline preview panel"); diff --git a/ILSpy.Tests/AssemblyList/AssemblyListDecompileTests.cs b/ILSpy.Tests/AssemblyList/AssemblyListDecompileTests.cs index eeaa5ca83..07273be44 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyListDecompileTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyListDecompileTests.cs @@ -53,7 +53,7 @@ public class AssemblyListDecompileTests var output = new PlainTextOutput(); // Decompiling every assembly is far too slow for a test, so cancel up front: the header // and the first rule are written before the first member decompilation observes the token. - var options = new DecompilationOptions { CancellationToken = new CancellationToken(true) }; + var options = new DecompilationOptions(new DecompilerSettings()) { CancellationToken = new CancellationToken(true) }; try { diff --git a/ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs b/ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs index f82fb397b..c227c43f3 100644 --- a/ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs +++ b/ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs @@ -23,6 +23,7 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; +using ICSharpCode.Decompiler; using ICSharpCode.ILSpy; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Languages; @@ -60,7 +61,7 @@ public class CSharpILMixedLanguageTests string Decompile(Language language) { var output = new AvaloniaEditTextOutput(); - language.DecompileMethod(method, output, new DecompilationOptions()); + language.DecompileMethod(method, output, new DecompilationOptions(new DecompilerSettings())); return output.GetText(); } diff --git a/ILSpy.Tests/Languages/ProjectExportTests.cs b/ILSpy.Tests/Languages/ProjectExportTests.cs index 44c6f9815..139dd5de0 100644 --- a/ILSpy.Tests/Languages/ProjectExportTests.cs +++ b/ILSpy.Tests/Languages/ProjectExportTests.cs @@ -79,7 +79,7 @@ public class ProjectExportTests Directory.CreateDirectory(tempDir); try { - var options = new ICSharpCode.ILSpy.DecompilationOptions { + var options = new ICSharpCode.ILSpy.DecompilationOptions(new DecompilerSettings()) { FullDecompilation = true, SaveAsProjectDirectory = tempDir, }; diff --git a/ILSpy.Tests/Languages/ShowMemberSettingsTests.cs b/ILSpy.Tests/Languages/ShowMemberSettingsTests.cs new file mode 100644 index 000000000..b0c3a4442 --- /dev/null +++ b/ILSpy.Tests/Languages/ShowMemberSettingsTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpy.AppEnv; +using ICSharpCode.ILSpy.Languages; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Languages; + +// CSharpLanguage.ShowMember drives which members the assembly tree shows at all: every member +// tree node consults it (unless the API-visibility filter is set to "All"). It must evaluate +// CSharpDecompiler.MemberIsHidden against the LIVE decompiler settings, not a default-constructed +// DecompilerSettings — otherwise toggling a decompiler option that unhides compiler-generated +// members (here: ArrayInitializers, which hides "") has no effect +// on the tree. +[TestFixture] +public class ShowMemberSettingsTests +{ + /// + /// Emits an assembly containing a top-level [CompilerGenerated] type named + /// "<PrivateImplementationDetails>" and returns its type definition. + /// MemberIsHidden hides that type iff DecompilerSettings.ArrayInitializers is set, + /// which makes it a minimal settings-sensitive probe for ShowMember. + /// + static ITypeDefinition EmitPrivateImplementationDetails() + { + const string name = "ShowMemberFixture"; + var ab = new PersistedAssemblyBuilder(new AssemblyName(name), typeof(object).Assembly); + var module = ab.DefineDynamicModule(name); + + var details = module.DefineType("", + TypeAttributes.NotPublic | TypeAttributes.Class | TypeAttributes.Sealed); + details.SetCustomAttribute(new CustomAttributeBuilder( + typeof(CompilerGeneratedAttribute).GetConstructor(Type.EmptyTypes)!, [])); + details.CreateType(); + + var dir = Path.Combine(Path.GetTempPath(), $"ILSpyShowMember_{Guid.NewGuid():N}"); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, $"{name}.dll"); + ab.Save(path); + + var file = new PEFile(path); + var resolver = new UniversalAssemblyResolver(path, throwOnError: false, + targetFramework: file.DetectTargetFrameworkId()); + var typeSystem = new DecompilerTypeSystem(file, resolver); + return typeSystem.MainModule.TypeDefinitions + .Single(t => t.Name == ""); + } + + [AvaloniaTest] + public void ShowMember_hides_PrivateImplementationDetails_under_default_settings() + { + var language = AppComposition.Current.GetExport().GetLanguage("C#"); + + language.ShowMember(EmitPrivateImplementationDetails()).Should().BeFalse( + "ArrayInitializers defaults to true, which hides "); + } + + [AvaloniaTest] + public void ShowMember_honours_the_live_decompiler_settings() + { + var settingsService = AppComposition.Current.GetExport(); + settingsService.DecompilerSettings.ArrayInitializers = false; + + var language = AppComposition.Current.GetExport().GetLanguage("C#"); + + language.ShowMember(EmitPrivateImplementationDetails()).Should().BeTrue( + "with ArrayInitializers off, MemberIsHidden no longer hides , " + + "so the tree must show it"); + } +} diff --git a/ILSpy.Tests/Languages/SolutionExportTests.cs b/ILSpy.Tests/Languages/SolutionExportTests.cs index 78a1e4c47..7a952bc5c 100644 --- a/ILSpy.Tests/Languages/SolutionExportTests.cs +++ b/ILSpy.Tests/Languages/SolutionExportTests.cs @@ -18,12 +18,14 @@ using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Headless.NUnit; using AwesomeAssertions; +using ICSharpCode.Decompiler; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Languages; @@ -60,7 +62,7 @@ public class SolutionExportTests var slnPath = Path.Combine(tempDir, "Solution.sln"); try { - var result = await ICSharpCode.ILSpy.SolutionWriter.CreateSolutionAsync(slnPath, language, assemblies); + var result = await ICSharpCode.ILSpy.SolutionWriter.CreateSolutionAsync(slnPath, language, assemblies, CancellationToken.None, new DecompilerSettings()); result.Success.Should().BeTrue( "the solution export should succeed for valid assemblies. Status:\n" + result.StatusText); @@ -106,7 +108,7 @@ public class SolutionExportTests // The same assembly twice collides on ShortName: the writer must refuse rather than // clobber one project directory with another. var result = await ICSharpCode.ILSpy.SolutionWriter.CreateSolutionAsync( - slnPath, language, new[] { assembly, assembly }); + slnPath, language, new[] { assembly, assembly }, CancellationToken.None, new DecompilerSettings()); result.Success.Should().BeFalse("duplicate assembly names cannot produce a valid solution"); result.StatusText.Should().Contain("Duplicate assembly names"); diff --git a/ILSpy.Tests/Resources/BamlResourceTests.cs b/ILSpy.Tests/Resources/BamlResourceTests.cs index 8a809eaaf..fcf1fe1ab 100644 --- a/ILSpy.Tests/Resources/BamlResourceTests.cs +++ b/ILSpy.Tests/Resources/BamlResourceTests.cs @@ -94,7 +94,7 @@ public class BamlResourceTests // (the handler doesn't touch the options for CanHandle). EnsureComposition(); var handler = new BamlResourceFileHandler(); - var context = new ResourceFileHandlerContext(new DecompilationOptions()); + var context = new ResourceFileHandlerContext(new DecompilationOptions(new DecompilerSettings())); // Act + Assert — three positive variants and two rejections. handler.CanHandle("MainWindow.baml", context).Should().BeTrue(); diff --git a/ILSpy.Tests/Resources/ResourceFactoryTests.cs b/ILSpy.Tests/Resources/ResourceFactoryTests.cs index 156f8dfd2..fb37ee720 100644 --- a/ILSpy.Tests/Resources/ResourceFactoryTests.cs +++ b/ILSpy.Tests/Resources/ResourceFactoryTests.cs @@ -125,7 +125,7 @@ public class ResourceFactoryTests var language = AppComposition.Current.GetExport().CurrentLanguage; // Act — decompile the resource node into the AvaloniaEdit text output. - node.Decompile(language, output, new DecompilationOptions()); + node.Decompile(language, output, new DecompilationOptions(new DecompilerSettings())); // Assert — exactly three UI elements (string grid + object grid + inherited Save button) // and they all materialise as Avalonia Controls. diff --git a/ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs b/ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs index 77a26598b..db295e12d 100644 --- a/ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs +++ b/ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs @@ -42,7 +42,7 @@ public class DisplaySettingsBridgeTests var display = new DisplaySettings { FoldBraces = true, ShowDebugInfo = true }; var settings = new DecompilerSettings { FoldBraces = false, ShowDebugInfo = false }; - DecompilerTabPageModel.ApplyDisplaySettings(settings, display); + SettingsService.ApplyDisplaySettings(settings, display); settings.FoldBraces.Should().BeTrue(); settings.ShowDebugInfo.Should().BeTrue(); @@ -54,7 +54,7 @@ public class DisplaySettingsBridgeTests var display = new DisplaySettings { IndentationUseTabs = false, IndentationSize = 2 }; var settings = new DecompilerSettings(); - DecompilerTabPageModel.ApplyDisplaySettings(settings, display); + SettingsService.ApplyDisplaySettings(settings, display); settings.CSharpFormattingOptions.IndentationString.Should().Be(" ", "two spaces"); } @@ -65,7 +65,7 @@ public class DisplaySettingsBridgeTests var display = new DisplaySettings { IndentationUseTabs = true, IndentationSize = 4, IndentationTabSize = 4 }; var settings = new DecompilerSettings(); - DecompilerTabPageModel.ApplyDisplaySettings(settings, display); + SettingsService.ApplyDisplaySettings(settings, display); settings.CSharpFormattingOptions.IndentationString.Should().Be("\t", "one tab"); } @@ -76,7 +76,7 @@ public class DisplaySettingsBridgeTests var display = new DisplaySettings { ExpandUsingDeclarations = true, ExpandMemberDefinitions = true }; var settings = new DecompilerSettings { ExpandUsingDeclarations = false, ExpandMemberDefinitions = false }; - DecompilerTabPageModel.ApplyDisplaySettings(settings, display); + SettingsService.ApplyDisplaySettings(settings, display); settings.ExpandUsingDeclarations.Should().BeTrue(); settings.ExpandMemberDefinitions.Should().BeTrue(); diff --git a/ILSpy/Commands/DecompileAllCommand.cs b/ILSpy/Commands/DecompileAllCommand.cs index d9da9463c..7cfa2e9bb 100644 --- a/ILSpy/Commands/DecompileAllCommand.cs +++ b/ILSpy/Commands/DecompileAllCommand.cs @@ -66,6 +66,8 @@ namespace ICSharpCode.ILSpy.Commands async Task ExecuteAsync() { + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new ICSharpCode.Decompiler.DecompilerSettings(); // Run in a dedicated frozen tab so navigation cannot cancel this long run. await dockWorkspace.RunInNewTabAsync("Decompiling all assemblies…", token => Task.Run(() => { var output = new AvaloniaEditTextOutput { Title = "Decompile All" }; @@ -82,7 +84,7 @@ namespace ICSharpCode.ILSpy.Commands try { using var writer = new StreamWriter(path); - var options = new DecompilationOptions { + var options = new DecompilationOptions(settings) { CancellationToken = token, FullDecompilation = true, }; @@ -133,6 +135,8 @@ namespace ICSharpCode.ILSpy.Commands async Task ExecuteAsync() { + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new ICSharpCode.Decompiler.DecompilerSettings(); // Run in a dedicated frozen tab so navigation cannot cancel this long run. await dockWorkspace.RunInNewTabAsync("Disassembling all assemblies…", token => Task.Run(() => { var output = new AvaloniaEditTextOutput { Title = "Disassemble All" }; @@ -150,7 +154,7 @@ namespace ICSharpCode.ILSpy.Commands try { using var writer = new StreamWriter(path); - var options = new DecompilationOptions { + var options = new DecompilationOptions(settings) { CancellationToken = token, FullDecompilation = true, }; @@ -204,10 +208,12 @@ namespace ICSharpCode.ILSpy.Commands var nodes = assemblyTreeModel.SelectedItems.OfType().ToArray(); if (nodes.Length == 0) return; + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new ICSharpCode.Decompiler.DecompilerSettings(); // Run in a dedicated frozen tab so navigation cannot cancel this long run. await dockWorkspace.RunInNewTabAsync("Decompiling 100×…", token => Task.Run(() => { var watch = Stopwatch.StartNew(); - var options = new DecompilationOptions { CancellationToken = token }; + var options = new DecompilationOptions(settings) { CancellationToken = token }; for (int i = 0; i < NumRuns; i++) { foreach (var node in nodes) diff --git a/ILSpy/Commands/PdbGenerator.cs b/ILSpy/Commands/PdbGenerator.cs index 6e7d25ff9..0e8d52d83 100644 --- a/ILSpy/Commands/PdbGenerator.cs +++ b/ILSpy/Commands/PdbGenerator.cs @@ -32,6 +32,7 @@ using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpyX; using ICSharpCode.ILSpyX.TreeView; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; @@ -106,6 +107,11 @@ namespace ICSharpCode.ILSpy.Commands ? string.Format(Resources.GeneratingPortablePDB, supported.Keys.First().ShortName) : string.Format(Resources.GeneratingPortablePDB, supported.Count + " assemblies"); + // The PDB embeds decompiled sources, so honor the user's current decompiler settings + // (snapshot once; the generation runs off the UI thread). + var settings = AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new DecompilerSettings(); + // Run in a dedicated frozen tab so browsing the tree while PDBs generate can't cancel it. await dockWorkspace.RunInNewTabAsync(title, token => Task.Run(() => { var output = new AvaloniaEditTextOutput { Title = "Generate Portable PDB" }; @@ -117,7 +123,6 @@ namespace ICSharpCode.ILSpy.Commands { using var stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write); var resolver = assembly.GetAssemblyResolver(); - var settings = new DecompilerSettings(); var decompiler = new CSharpDecompiler(file, resolver, settings) { CancellationToken = token, }; diff --git a/ILSpy/Commands/SaveCodeHelper.cs b/ILSpy/Commands/SaveCodeHelper.cs index 1119addae..b96e57024 100644 --- a/ILSpy/Commands/SaveCodeHelper.cs +++ b/ILSpy/Commands/SaveCodeHelper.cs @@ -107,7 +107,9 @@ namespace ICSharpCode.ILSpy.Commands ArgumentNullException.ThrowIfNull(node); ArgumentNullException.ThrowIfNull(language); - var options = new DecompilationOptions { + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new ICSharpCode.Decompiler.DecompilerSettings(); + var options = new DecompilationOptions(settings) { FullDecompilation = true, EscapeInvalidIdentifiers = true, CancellationToken = ct, diff --git a/ILSpy/Commands/SolutionExport.cs b/ILSpy/Commands/SolutionExport.cs index 6a91879c3..4dce21bd1 100644 --- a/ILSpy/Commands/SolutionExport.cs +++ b/ILSpy/Commands/SolutionExport.cs @@ -73,10 +73,15 @@ namespace ICSharpCode.ILSpy.Commands if (string.IsNullOrEmpty(path)) return; + // Snapshot the user's current decompiler settings for the whole export, like the + // per-project export path does. + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new ICSharpCode.Decompiler.DecompilerSettings(); + // Run in a dedicated frozen tab so browsing the tree while the export runs can't cancel it. await dockWorkspace.RunInNewTabAsync("Exporting solution", async (token, progress) => { var result = await SolutionWriter.CreateSolutionAsync(path, language, assemblies, token, - settings: null, strongNameKeyFile: null, progress: progress) + settings, strongNameKeyFile: null, progress: progress) .ConfigureAwait(false); var o = new AvaloniaEditTextOutput { Title = Resources._SaveCode }; o.Write(result.StatusText); diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs index 82d42b1ec..1c205b61c 100644 --- a/ILSpy/DecompilationOptions.cs +++ b/ILSpy/DecompilationOptions.cs @@ -75,11 +75,13 @@ namespace ICSharpCode.ILSpy /// public IProgress? ProgressIndicator { get; set; } + // Deliberately no parameterless constructor: every decompilation must make an explicit + // choice of settings. Callers inside the app want the user's current settings (see + // SettingsService.CreateEffectiveDecompilerSettings), and a silent new DecompilerSettings() + // default has repeatedly masked exactly that bug. public DecompilationOptions(DecompilerSettings settings) { DecompilerSettings = settings; } - - public DecompilationOptions() : this(new DecompilerSettings()) { } } } diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 342358cb7..0d8f877f8 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -257,7 +257,12 @@ namespace ICSharpCode.ILSpy.Languages var assembly = member.ParentModule?.MetadataFile; if (assembly == null) return true; - return !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, new DecompilerSettings()); + // Use the effective settings, not defaults: which members MemberIsHidden hides + // depends on the decompiler options (and language version), and the tree must agree + // with what the text view actually elides. + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new DecompilerSettings(); + return !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, settings); } public override RichText GetRichTextTooltip(IEntity entity) diff --git a/ILSpy/Options/DisplaySettingReactions.cs b/ILSpy/Options/DisplaySettingReactions.cs index 62637e552..b89ebf3ef 100644 --- a/ILSpy/Options/DisplaySettingReactions.cs +++ b/ILSpy/Options/DisplaySettingReactions.cs @@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy.Options [nameof(DisplaySettings.UseNestedNamespaceNodes)] = DisplaySettingReaction.TreeShape, [nameof(DisplaySettings.HideEmptyMetadataTables)] = DisplaySettingReaction.TreeShape, - // Decompiler / disassembler output (see DecompilerTabPageModel.ApplyDisplaySettings + + // Decompiler / disassembler output (see SettingsService.ApplyDisplaySettings + // GetIndentationString, and CSharpILMixedLanguage / ILLanguage for the IL-detail ones). [nameof(DisplaySettings.FoldBraces)] = DisplaySettingReaction.Redecompile, [nameof(DisplaySettings.ExpandMemberDefinitions)] = DisplaySettingReaction.Redecompile, diff --git a/ILSpy/SettingsService.cs b/ILSpy/SettingsService.cs index d2fe0a564..f084a8371 100644 --- a/ILSpy/SettingsService.cs +++ b/ILSpy/SettingsService.cs @@ -16,6 +16,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; using System.ComponentModel; using System.Composition; @@ -57,6 +58,52 @@ namespace ICSharpCode.ILSpy public MiscSettings MiscSettings => GetSettings(); + /// + /// Returns the effective decompiler settings a decompilation started right now would + /// use: a clone of with the Display options bridged in + /// and the language version selected in the toolbar applied. Mutating the returned + /// instance does not affect the persisted settings. + /// + public Decompiler.DecompilerSettings CreateEffectiveDecompilerSettings() + { + var settings = DecompilerSettings.Clone(); + ApplyDisplaySettings(settings, DisplaySettings); + // No LanguageService (non-C# language or minimal test host) leaves the version + // null, so the language version falls back to Latest below. + var version = AppEnv.AppComposition.TryGetExport()?.CurrentVersion; + if (Enum.TryParse(version?.Version, out var languageVersion)) + settings.SetLanguageVersion(languageVersion); + else + settings.SetLanguageVersion(Decompiler.CSharp.LanguageVersion.Latest); + return settings; + } + + /// + /// Bridges the Display options that affect decompiler output into : + /// the fold-expansion flags (TextTokenWriter reads them to set each fold's DefaultClosed), + /// brace folding, debug-symbol info, and the indentation string. Without this these Display + /// options would have no effect on the produced source. + /// + internal static void ApplyDisplaySettings(Decompiler.DecompilerSettings settings, DisplaySettings display) + { + settings.ExpandUsingDeclarations = display.ExpandUsingDeclarations; + settings.ExpandMemberDefinitions = display.ExpandMemberDefinitions; + settings.FoldBraces = display.FoldBraces; + settings.ShowDebugInfo = display.ShowDebugInfo; + settings.CSharpFormattingOptions.IndentationString = GetIndentationString(display); + } + + static string GetIndentationString(DisplaySettings display) + { + if (display.IndentationUseTabs) + { + int tabs = display.IndentationSize / display.IndentationTabSize; + int spaces = display.IndentationSize % display.IndentationTabSize; + return new string('\t', tabs) + new string(' ', spaces); + } + return new string(' ', display.IndentationSize); + } + public Updates.UpdateSettings UpdateSettings => GetSettings(); AssemblyListManager? assemblyListManager; diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 9e5c32d7f..81c931fde 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -56,20 +56,20 @@ namespace ICSharpCode.ILSpy /// or whitespace. /// Thrown when or /// is null. - /// Decompiler settings each project is decompiled with. When null, - /// each project uses the language's own defaults (preserves the quick "Save Code" path). + /// Decompiler settings each project is decompiled with. /// Optional .snk copied into every project and emitted /// as <AssemblyOriginatorKeyFile>. public static Task CreateSolutionAsync(string solutionFilePath, Language language, IReadOnlyList assemblies, - CancellationToken cancellationToken = default, - DecompilerSettings? settings = null, string? strongNameKeyFile = null, + CancellationToken cancellationToken, + DecompilerSettings settings, string? strongNameKeyFile = null, IProgress? progress = null) { if (string.IsNullOrWhiteSpace(solutionFilePath)) throw new ArgumentException("The solution file path cannot be null or empty.", nameof(solutionFilePath)); ArgumentNullException.ThrowIfNull(language); ArgumentNullException.ThrowIfNull(assemblies); + ArgumentNullException.ThrowIfNull(settings); return new SolutionWriter(solutionFilePath, settings, strongNameKeyFile, progress) .CreateSolutionAsync(assemblies, language, cancellationToken); @@ -77,14 +77,14 @@ namespace ICSharpCode.ILSpy readonly string solutionFilePath; readonly string solutionDirectory; - readonly DecompilerSettings? settings; + readonly DecompilerSettings settings; readonly string? strongNameKeyFile; readonly IProgress? progress; readonly ConcurrentBag projects; readonly ConcurrentBag statusOutput; int completedAssemblies; - SolutionWriter(string solutionFilePath, DecompilerSettings? settings, string? strongNameKeyFile, + SolutionWriter(string solutionFilePath, DecompilerSettings settings, string? strongNameKeyFile, IProgress? progress) { this.solutionFilePath = solutionFilePath; @@ -224,7 +224,7 @@ namespace ICSharpCode.ILSpy try { - var options = settings != null ? new DecompilationOptions(settings) : new DecompilationOptions(); + var options = new DecompilationOptions(settings); options.FullDecompilation = true; options.EscapeInvalidIdentifiers = true; options.CancellationToken = ct; diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 7936ac64f..4eff012cd 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -514,17 +514,14 @@ namespace ICSharpCode.ILSpy.TextView { (output, _) = await Task.Run(() => { var output = new AvaloniaEditTextOutput { LengthLimit = outputLengthLimit }; - var options = decompilerSettings != null - ? new DecompilationOptions(decompilerSettings) { - CancellationToken = cts.Token, - StepLimit = stepLimit, - IsDebug = isDebug, - } - : new DecompilationOptions { - CancellationToken = cts.Token, - StepLimit = stepLimit, - IsDebug = isDebug, - }; + // decompilerSettings is null only in design-time / minimal test hosts + // without composition; fall back to defaults there. + var options = new DecompilationOptions( + decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) { + CancellationToken = cts.Token, + StepLimit = stepLimit, + IsDebug = isDebug, + }; try { for (int i = 0; i < nodes.Count; i++) @@ -714,52 +711,10 @@ namespace ICSharpCode.ILSpy.TextView Text = text; } - // Pulls the live DecompilerSettings via MEF and returns a clone for this run. Also - // bakes the active LanguageService.CurrentVersion into the clone — without this the - // toolbar's Language-Version dropdown writes-through to LanguageSettings but never - // reaches the decompiler. Resolves go through TryGetExport so design-time / minimal test - // hosts that bypass composition fall back to default settings rather than throwing. + // Pulls the effective DecompilerSettings (clone + Display options + toolbar language + // version) for this run. Resolves via TryGetExport so design-time / minimal test hosts + // that bypass composition fall back to default settings rather than throwing. static ICSharpCode.Decompiler.DecompilerSettings? TryGetLiveDecompilerSettings() - { - var settingsService = AppEnv.AppComposition.TryGetExport(); - if (settingsService is null) - return null; - var settings = settingsService.DecompilerSettings.Clone(); - ApplyDisplaySettings(settings, settingsService.DisplaySettings); - // No LanguageService (non-C# language or minimal host) leaves version null, so the - // language version falls back to Latest below. - var version = AppEnv.AppComposition.TryGetExport()?.CurrentVersion; - if (Enum.TryParse(version?.Version, out var languageVersion)) - settings.SetLanguageVersion(languageVersion); - else - settings.SetLanguageVersion(ICSharpCode.Decompiler.CSharp.LanguageVersion.Latest); - return settings; - } - - /// - /// Bridges the Display options that affect decompiler output into : - /// the fold-expansion flags (TextTokenWriter reads them to set each fold's DefaultClosed), - /// brace folding, debug-symbol info, and the indentation string. Without this these Display - /// options would have no effect on the produced source. - /// - internal static void ApplyDisplaySettings(ICSharpCode.Decompiler.DecompilerSettings settings, DisplaySettings display) - { - settings.ExpandUsingDeclarations = display.ExpandUsingDeclarations; - settings.ExpandMemberDefinitions = display.ExpandMemberDefinitions; - settings.FoldBraces = display.FoldBraces; - settings.ShowDebugInfo = display.ShowDebugInfo; - settings.CSharpFormattingOptions.IndentationString = GetIndentationString(display); - } - - static string GetIndentationString(DisplaySettings display) - { - if (display.IndentationUseTabs) - { - int tabs = display.IndentationSize / display.IndentationTabSize; - int spaces = display.IndentationSize % display.IndentationTabSize; - return new string('\t', tabs) + new string(' ', spaces); - } - return new string(' ', display.IndentationSize); - } + => AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings(); } } diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 9e9eae875..a7df6c323 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -194,8 +194,10 @@ namespace ICSharpCode.ILSpy.TreeNodes var ext = Path.GetExtension(path); var isProject = string.Equals(ext, language.ProjectFileExtension, StringComparison.OrdinalIgnoreCase); + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new ICSharpCode.Decompiler.DecompilerSettings(); await Task.Run(() => { - var options = new DecompilationOptions { + var options = new DecompilationOptions(settings) { FullDecompilation = true, EscapeInvalidIdentifiers = true, };