diff --git a/ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs b/ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs
new file mode 100644
index 000000000..76bdf39b4
--- /dev/null
+++ b/ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs
@@ -0,0 +1,84 @@
+// 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 AwesomeAssertions;
+
+using ICSharpCode.Decompiler;
+
+using ILSpy.Options;
+using ILSpy.TextView;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.TextView;
+
+///
+/// The Display options that affect generated source (brace folding, debug info, indentation, and the
+/// fold-expansion flags) must be bridged into the decompiler settings used for a decompile. WPF wired
+/// these in DecompilationOptions; the Avalonia port previously only bridged the two expand flags, so
+/// FoldBraces / ShowDebugInfo / indentation were silently dead.
+///
+[TestFixture]
+public class DisplaySettingsBridgeTests
+{
+ [Test]
+ public void Brace_Folding_And_Debug_Info_Are_Bridged()
+ {
+ var display = new DisplaySettings { FoldBraces = true, ShowDebugInfo = true };
+ var settings = new DecompilerSettings { FoldBraces = false, ShowDebugInfo = false };
+
+ DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
+
+ settings.FoldBraces.Should().BeTrue();
+ settings.ShowDebugInfo.Should().BeTrue();
+ }
+
+ [Test]
+ public void Indentation_Uses_Spaces_When_Tabs_Are_Off()
+ {
+ var display = new DisplaySettings { IndentationUseTabs = false, IndentationSize = 2 };
+ var settings = new DecompilerSettings();
+
+ DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
+
+ settings.CSharpFormattingOptions.IndentationString.Should().Be(" ", "two spaces");
+ }
+
+ [Test]
+ public void Indentation_Uses_Tabs_When_Enabled()
+ {
+ var display = new DisplaySettings { IndentationUseTabs = true, IndentationSize = 4, IndentationTabSize = 4 };
+ var settings = new DecompilerSettings();
+
+ DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
+
+ settings.CSharpFormattingOptions.IndentationString.Should().Be("\t", "one tab");
+ }
+
+ [Test]
+ public void Expand_Flags_Still_Bridged()
+ {
+ var display = new DisplaySettings { ExpandUsingDeclarations = true, ExpandMemberDefinitions = true };
+ var settings = new DecompilerSettings { ExpandUsingDeclarations = false, ExpandMemberDefinitions = false };
+
+ DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
+
+ settings.ExpandUsingDeclarations.Should().BeTrue();
+ settings.ExpandMemberDefinitions.Should().BeTrue();
+ }
+}
diff --git a/ILSpy.Tests/TextView/OutputLengthLimitTests.cs b/ILSpy.Tests/TextView/OutputLengthLimitTests.cs
new file mode 100644
index 000000000..62a33ded8
--- /dev/null
+++ b/ILSpy.Tests/TextView/OutputLengthLimitTests.cs
@@ -0,0 +1,68 @@
+// 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 AwesomeAssertions;
+
+using ILSpy.TextView;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.TextView;
+
+///
+/// The decompiler text output stops once it exceeds its LengthLimit, so a runaway decompile (a huge
+/// type/namespace) can't hang or OOM the UI thread. (Regression: the Avalonia port had no limit.)
+///
+[TestFixture]
+public class OutputLengthLimitTests
+{
+ [Test]
+ public void Write_Throws_Once_The_Length_Limit_Is_Exceeded()
+ {
+ var output = new AvaloniaEditTextOutput { LengthLimit = 10 };
+
+ var act = () => {
+ for (int i = 0; i < 100; i++)
+ output.Write("0123456789");
+ };
+
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void Write_Does_Not_Throw_Below_The_Limit()
+ {
+ var output = new AvaloniaEditTextOutput { LengthLimit = 1000 };
+
+ var act = () => {
+ output.Write("short output");
+ output.WriteLine();
+ };
+
+ act.Should().NotThrow();
+ }
+
+ [Test]
+ public void Default_Limit_Is_Unlimited()
+ {
+ var output = new AvaloniaEditTextOutput();
+ output.LengthLimit.Should().Be(int.MaxValue);
+ }
+}
diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs
index 9287e2138..db129d256 100644
--- a/ILSpy/TextView/AvaloniaEditTextOutput.cs
+++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs
@@ -45,6 +45,14 @@ namespace ILSpy.TextView
public sealed class AvaloniaEditTextOutput : ISmartTextOutput
{
readonly StringBuilder builder = new();
+
+ ///
+ /// When the accumulated text exceeds this many characters, the next write throws
+ /// so a runaway decompile is stopped before it
+ /// hangs/OOMs the UI thread. Defaults to unlimited; the decompiler tab sets a real limit.
+ ///
+ public int LengthLimit { get; set; } = int.MaxValue;
+
readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new();
readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new();
readonly List foldings = new();
@@ -127,12 +135,14 @@ namespace ILSpy.TextView
{
WriteIndentIfNeeded();
builder.Append(ch);
+ CheckLength();
}
public void Write(string text)
{
WriteIndentIfNeeded();
builder.Append(text);
+ CheckLength();
}
public void WriteLine()
@@ -140,6 +150,13 @@ namespace ILSpy.TextView
builder.Append('\n');
lineNumber++;
needsIndent = true;
+ CheckLength();
+ }
+
+ void CheckLength()
+ {
+ if (builder.Length > LengthLimit)
+ throw new OutputLengthExceededException();
}
public void WriteReference(OpCodeInfo opCode, bool omitSuffix = false)
diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs
index a93dd60b9..7fcaf18c8 100644
--- a/ILSpy/TextView/DecompilerTabPageModel.cs
+++ b/ILSpy/TextView/DecompilerTabPageModel.cs
@@ -37,6 +37,7 @@ using CommunityToolkit.Mvvm.Input;
using ICSharpCode.Decompiler;
using ILSpy.Languages;
+using ILSpy.Options;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
@@ -299,6 +300,15 @@ namespace ILSpy.TextView
int pendingStepLimit = int.MaxValue;
bool pendingIsDebug;
+ ///
+ /// Output-length safety limits (characters): a decompile that produces more than the active
+ /// limit is stopped and replaced with a "too much code" message rather than hanging/OOMing the
+ /// UI. The user can re-run at the extended limit or save to disk. Mirrors the previous version.
+ ///
+ public const int DefaultOutputLengthLimit = 5_000_000;
+ public const int ExtendedOutputLengthLimit = 75_000_000;
+ int pendingOutputLengthLimit = DefaultOutputLengthLimit;
+
///
/// Force a fresh decompile of the same nodes with the IL-transform pipeline halted
/// after steps. Used by the Debug Steps pane to render
@@ -313,6 +323,53 @@ namespace ILSpy.TextView
StartDecompile();
}
+ /// Re-runs the current decompile with a larger output-length limit (the "Display code
+ /// anyway" button on the too-much-code message).
+ public void RestartDecompileWithOutputLimit(int outputLengthLimit)
+ {
+ pendingOutputLengthLimit = outputLengthLimit;
+ StartDecompile();
+ }
+
+ // Built (off the UI thread) when a decompile blows past its output limit: a short message plus
+ // a "Display code anyway" button (retry at the extended limit, only offered for the normal
+ // limit) and a "Save code" button. The button click handlers run back on the UI thread.
+ AvaloniaEditTextOutput BuildOutputLengthExceededMessage(int outputLengthLimit)
+ {
+ var output = new AvaloniaEditTextOutput();
+ bool wasNormalLimit = outputLengthLimit == DefaultOutputLengthLimit;
+ output.WriteLine(wasNormalLimit
+ ? "You have selected too much code for it to be displayed automatically."
+ : "You have selected too much code; it cannot be displayed here.");
+ output.WriteLine();
+ if (wasNormalLimit)
+ {
+ output.AddButton(Images.Images.ViewCode, ICSharpCode.ILSpy.Properties.Resources.DisplayCode,
+ (_, _) => RestartDecompileWithOutputLimit(ExtendedOutputLengthLimit));
+ output.WriteLine();
+ }
+ output.AddButton(Images.Images.Save, ICSharpCode.ILSpy.Properties.Resources.SaveCode,
+ (_, _) => SaveCurrentNode());
+ output.WriteLine();
+ return output;
+ }
+
+ void SaveCurrentNode()
+ {
+ if (CurrentNode is not { } node)
+ return;
+ try
+ {
+ var languageService = AppEnv.AppComposition.Current.GetExport();
+ var dockWorkspace = AppEnv.AppComposition.Current.GetExport();
+ ILSpy.Commands.SaveCodeHelper.SaveNodeAsync(node, languageService, dockWorkspace).HandleExceptions();
+ }
+ catch
+ {
+ // Best-effort: no save path available (minimal host).
+ }
+ }
+
// Fire-and-forget wrapper around DecompileAsync that observes the resulting Task.
// Without this, exceptions raised by the dispatched property setters (e.g. the
// PropertyChanged subscribers in DecompilerTextView) become UnobservedTaskException
@@ -394,11 +451,13 @@ namespace ILSpy.TextView
var isDebug = pendingIsDebug;
pendingStepLimit = int.MaxValue;
pendingIsDebug = false;
+ var outputLengthLimit = pendingOutputLengthLimit;
+ pendingOutputLengthLimit = DefaultOutputLengthLimit;
AvaloniaEditTextOutput output;
using (ILSpy.AppEnv.AppLog.Phase($"DecompileAsync #{callNumber}: Task.Run decompile body ({nodes.Count} node(s), language={language.Name})"))
{
(output, _) = await Task.Run(() => {
- var output = new AvaloniaEditTextOutput();
+ var output = new AvaloniaEditTextOutput { LengthLimit = outputLengthLimit };
var options = decompilerSettings != null
? new DecompilationOptions(decompilerSettings) {
CancellationToken = cts.Token,
@@ -425,6 +484,12 @@ namespace ILSpy.TextView
{
// expected on cancel — just return whatever we got
}
+ catch (OutputLengthExceededException)
+ {
+ // The decompile produced more text than the limit allows -- replace it with
+ // a message offering to display anyway (extended limit) or save to disk.
+ output = BuildOutputLengthExceededMessage(outputLengthLimit);
+ }
catch (Exception ex)
{
output.WriteLine();
@@ -597,13 +662,7 @@ namespace ILSpy.TextView
{
var settingsService = AppEnv.AppComposition.Current.GetExport();
var settings = settingsService.DecompilerSettings.Clone();
- // Bridge the Display-options fold-expansion flags into the decompiler settings:
- // TextTokenWriter reads settings.ExpandUsingDeclarations / ExpandMemberDefinitions
- // to set each fold's DefaultClosed. Without this the "Expand using declarations /
- // member definitions after decompilation" options would have no effect.
- var display = settingsService.DisplaySettings;
- settings.ExpandUsingDeclarations = display.ExpandUsingDeclarations;
- settings.ExpandMemberDefinitions = display.ExpandMemberDefinitions;
+ ApplyDisplaySettings(settings, settingsService.DisplaySettings);
try
{
var version = AppEnv.AppComposition.Current.GetExport().CurrentVersion;
@@ -624,5 +683,31 @@ namespace ILSpy.TextView
return null;
}
}
+
+ ///
+ /// 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);
+ }
}
}
diff --git a/ILSpy/TextView/OutputLengthExceededException.cs b/ILSpy/TextView/OutputLengthExceededException.cs
new file mode 100644
index 000000000..6fa8304ca
--- /dev/null
+++ b/ILSpy/TextView/OutputLengthExceededException.cs
@@ -0,0 +1,31 @@
+// 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;
+
+namespace ILSpy.TextView
+{
+ ///
+ /// Thrown by once its accumulated text exceeds
+ /// , so a runaway decompile (a huge type or
+ /// namespace) is stopped before it can hang or exhaust memory on the UI thread.
+ ///
+ public sealed class OutputLengthExceededException : Exception
+ {
+ }
+}