Browse Source

Merge pull request #3756 from icsharpcode/siegfriedpammer-patch-1

Smaller Fixes for Avalonia Branch Merge
pull/3750/head
Christoph Wille 4 weeks ago committed by GitHub
parent
commit
232be30c06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      ILSpy.Tests/Editor/CaretHighlightAdornerTests.cs
  2. 80
      ILSpy.Tests/MainWindow/LanguageVersionPersistenceTests.cs
  3. 9
      ILSpy.Tests/Options/DisplaySettingsReactionTests.cs
  4. 9
      ILSpy.Tests/Options/OptionsTabTests.cs
  5. 13
      ILSpy/ILSpy.csproj
  6. 34
      ILSpy/TextView/CaretHighlightAdorner.cs
  7. 21
      ILSpy/Views/MainToolBar.axaml.cs
  8. 4
      README.md

15
ILSpy.Tests/Editor/CaretHighlightAdornerTests.cs

@ -57,8 +57,8 @@ public class CaretHighlightAdornerTests @@ -57,8 +57,8 @@ public class CaretHighlightAdornerTests
// AvaloniaEditTextOutput.AddReference), so `OnReferenceClicked` resolves the click
// to an in-document jump and DecompilerTextView fires the caret highlight.
//
// The lifecycle (register then unregister after ~1 s) is verified end-to-end —
// hand-tuned animation curve is left for eyeball validation.
// The lifecycle (register on click, then unregister via Dismiss) is verified end-to-end —
// the hand-tuned animation curve and the one-second timing are left for eyeball validation.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
@ -103,9 +103,12 @@ public class CaretHighlightAdornerTests @@ -103,9 +103,12 @@ public class CaretHighlightAdornerTests
"clicking a member definition must route through DecompilerTextView.OnReferenceClicked "
+ "and call CaretHighlightAdorner.DisplayCaretHighlightAnimation");
// Wait for the lifetime timer (~1 s) to tear it back down.
await Waiters.WaitForAsync(
() => !renderers.Any(r => r is CaretHighlightAdorner),
timeout: TimeSpan.FromSeconds(3));
// The 1 s lifetime is a real-time DispatcherTimer, which is unreliable to wait on in a
// headless run; invoke the same teardown the timer runs (Dismiss) directly so the
// unregister half is verified deterministically rather than against the wall clock.
var adorner = renderers.OfType<CaretHighlightAdorner>().Single();
adorner.Dismiss();
renderers.Should().NotContain(r => r is CaretHighlightAdorner,
"Dismiss unregisters the caret-highlight adorner from the text view");
}
}

80
ILSpy.Tests/MainWindow/LanguageVersionPersistenceTests.cs

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
// 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.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Headless.NUnit;
using Avalonia.Threading;
using Avalonia.VisualTree;
using AwesomeAssertions;
using ILSpy;
using ILSpy.AppEnv;
using ILSpy.Languages;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
[TestFixture]
public class LanguageVersionPersistenceTests
{
[AvaloniaTest]
public async Task Switching_Away_From_And_Back_To_A_Versioned_Language_Restores_The_Version()
{
// Regression: selecting C# x.y, switching to a language with no versions (IL), then back to
// C# must restore x.y in the version combo. The toolbar's version ComboBox is bound TwoWay
// to LanguageService.CurrentVersion; when the language flips, its ItemsSource swaps to the
// new (empty) language first and writes a null selection back, so the outgoing version has
// to be stashed before that happens or the restore reads back null.
var (window, _) = await TestHarness.BootAsync();
var toolbar = await window.WaitForComponent<MainToolBar>();
var languageCombo = toolbar.GetVisualDescendants().OfType<ComboBox>()
.Single(c => c.Name == "LanguageComboBox");
var versionCombo = toolbar.GetVisualDescendants().OfType<ComboBox>()
.Single(c => c.Name == "LanguageVersionComboBox");
var languageService = AppComposition.Current.GetExport<LanguageService>();
var csharp = languageService.Languages.Single(l => l.Name == "C#");
var noVersionLanguage = languageService.Languages.First(l => !l.HasLanguageVersions);
// Start on C# and pick a version that is NOT the default latest, so a reset is detectable.
languageCombo.SelectedItem = csharp;
Dispatcher.UIThread.RunJobs();
var chosen = csharp.LanguageVersions.First();
chosen.Should().NotBe(csharp.LanguageVersions.Last(), "the test needs a non-default version");
versionCombo.SelectedItem = chosen;
Dispatcher.UIThread.RunJobs();
languageService.CurrentVersion.Should().Be(chosen);
// Flip to a language with no versions, then back to C#.
languageCombo.SelectedItem = noVersionLanguage;
Dispatcher.UIThread.RunJobs();
languageCombo.SelectedItem = csharp;
Dispatcher.UIThread.RunJobs();
languageService.CurrentVersion.Should().Be(chosen,
"switching away from C# and back must restore the previously selected C# version");
versionCombo.SelectedItem.Should().Be(chosen,
"the version ComboBox must reflect the restored version");
}
}

9
ILSpy.Tests/Options/DisplaySettingsReactionTests.cs

@ -59,9 +59,16 @@ public class DisplaySettingsReactionTests @@ -59,9 +59,16 @@ public class DisplaySettingsReactionTests
public async Task Toggling_A_Decompiler_Output_Setting_Re_Decompiles_The_Active_Tab()
{
var (_, vm) = await TestHarness.BootAsync();
// Decompile a single small method rather than the whole Enumerable type: the full type
// takes >15 s in headless and times out WaitForDecompiledTextAsync under CI load. The
// re-decompile reaction under test fires on whatever the active tab shows, so one method
// exercises it just as well.
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
vm.AssemblyTreeModel.SelectedItem = typeNode;
typeNode.IsExpanded = true;
var method = typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "Empty");
vm.AssemblyTreeModel.SelectNode(method);
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
int decompileStarts = 0;

9
ILSpy.Tests/Options/OptionsTabTests.cs

@ -309,10 +309,15 @@ public class OptionsTabTests @@ -309,10 +309,15 @@ public class OptionsTabTests
// pull focus back to MainTab and yank the user off the Options page they are editing.
var (_, vm) = await TestHarness.BootAsync(3);
// Show some decompiled content first, so there is a decompiler tab to refresh.
// Show some decompiled content first, so there is a decompiler tab to refresh. Use a single
// small method, not the whole Enumerable type: the full type takes >15 s in headless and
// times out WaitForDecompiledTextAsync under CI load.
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
vm.AssemblyTreeModel.SelectNode(typeNode);
typeNode.IsExpanded = true;
var method = typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "Empty");
vm.AssemblyTreeModel.SelectNode(method);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
// Open Options and confirm it is the active document.

13
ILSpy/ILSpy.csproj

@ -6,6 +6,10 @@ @@ -6,6 +6,10 @@
file stays in sync for these targets without forcing per-RID restore onto
the ILSpyCmd tool (PackAsTool + GeneratePackageOnBuild) or the test projects. -->
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;osx-arm64</RuntimeIdentifiers>
<!-- Framework-dependent publishes target net10.0; roll forward to a newer major runtime when
the exact one isn't installed so the app still starts on machines that only have a later
.NET. -->
<RollForward>major</RollForward>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets/ILSpy-Large.ico</ApplicationIcon>
@ -33,6 +37,15 @@ @@ -33,6 +37,15 @@
stop-the-world for long. -->
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<!-- DATAS: let server GC scale its heap count to the live load instead of pinning one heap
per core, trading a little throughput for a much smaller idle working set. -->
<GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
</PropertyGroup>
<!-- Trap arithmetic overflow/underflow in Debug builds so off-by-one bugs surface as exceptions
during development instead of silently wrapping. -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
</PropertyGroup>
<ItemGroup>

34
ILSpy/TextView/CaretHighlightAdorner.cs

@ -47,9 +47,14 @@ namespace ILSpy.TextView @@ -47,9 +47,14 @@ namespace ILSpy.TextView
readonly Rect maxRect;
readonly IPen pen;
readonly Stopwatch elapsed = Stopwatch.StartNew();
readonly TextArea textArea;
readonly DispatcherTimer frameTimer;
readonly DispatcherTimer lifetimeTimer;
CaretHighlightAdorner(TextArea textArea)
{
this.textArea = textArea;
// Caret rect is in document coordinates; subtracting ScrollOffset converts to the
// viewport-relative space IBackgroundRenderer.Draw paints into.
var caretRect = textArea.Caret.CalculateCaretRectangle();
@ -64,6 +69,13 @@ namespace ILSpy.TextView @@ -64,6 +69,13 @@ namespace ILSpy.TextView
// black when unset (e.g. design-time / standalone-renderer tests).
var brush = textArea.TextView.GetValue(TextElement.ForegroundProperty) ?? Brushes.Black;
pen = new Pen(brush, 1).ToImmutable();
// The frame timer ticks at ~60 fps to invalidate the Caret layer so Draw re-runs with
// fresh elapsed time; the lifetime timer dismisses the adorner after one second.
frameTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) };
frameTimer.Tick += (_, _) => textArea.TextView.InvalidateLayer(KnownLayer.Caret);
lifetimeTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(LifetimeMs) };
lifetimeTimer.Tick += (_, _) => Dismiss();
}
public KnownLayer Layer => KnownLayer.Caret;
@ -107,7 +119,7 @@ namespace ILSpy.TextView @@ -107,7 +119,7 @@ namespace ILSpy.TextView
/// <summary>
/// Registers a one-shot caret-highlight animation on <paramref name="textArea"/>. Spins
/// up two timers: one ticks at ~60 fps to invalidate the Caret layer so <see cref="Draw"/>
/// re-runs with fresh elapsed time, the other unregisters the adorner after one second.
/// re-runs with fresh elapsed time, the other calls <see cref="Dismiss"/> after one second.
/// </summary>
public static void DisplayCaretHighlightAnimation(TextArea textArea)
{
@ -115,18 +127,20 @@ namespace ILSpy.TextView @@ -115,18 +127,20 @@ namespace ILSpy.TextView
var adorner = new CaretHighlightAdorner(textArea);
textArea.TextView.BackgroundRenderers.Add(adorner);
adorner.frameTimer.Start();
adorner.lifetimeTimer.Start();
}
var frameTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) };
frameTimer.Tick += (_, _) => textArea.TextView.InvalidateLayer(KnownLayer.Caret);
frameTimer.Start();
var lifetimeTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(LifetimeMs) };
lifetimeTimer.Tick += (_, _) => {
/// <summary>
/// Ends the animation immediately: stops both timers and unregisters the adorner from the
/// text view. Invoked by the one-second lifetime timer, and usable to tear the highlight
/// down on demand instead of waiting the lifetime out.
/// </summary>
public void Dismiss()
{
lifetimeTimer.Stop();
frameTimer.Stop();
textArea.TextView.BackgroundRenderers.Remove(adorner);
};
lifetimeTimer.Start();
textArea.TextView.BackgroundRenderers.Remove(this);
}
}
}

21
ILSpy/Views/MainToolBar.axaml.cs

@ -24,6 +24,7 @@ using System.Reflection; @@ -24,6 +24,7 @@ using System.Reflection;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using ILSpy.AppEnv;
using ILSpy.Commands;
@ -112,6 +113,26 @@ public partial class MainToolBar : UserControl @@ -112,6 +113,26 @@ public partial class MainToolBar : UserControl
if (menuRegistry != null)
WireManageAssemblyListsButton(menuRegistry);
WireLanguageVersionComboSync();
}
void WireLanguageVersionComboSync()
{
// The version ComboBox binds ItemsSource to CurrentLanguage.LanguageVersions and SelectedItem
// to CurrentVersion. On a language switch the model assigns CurrentVersion (pushing it onto
// SelectedItem) before the CurrentLanguage change propagates to ItemsSource, so SelectedItem
// is set against the previous language's list, rejected, and left null once ItemsSource
// repopulates. Re-assert the selection from the bound CurrentVersion after each ItemsSource
// swap has settled so flipping to a version-less language (e.g. IL) and back restores it.
LanguageVersionComboBox.PropertyChanged += (_, e) => {
if (e.Property != ItemsControl.ItemsSourceProperty)
return;
Dispatcher.UIThread.Post(() => {
if (DataContext is MainWindowViewModel vm)
LanguageVersionComboBox.SelectedItem = vm.LanguageService.CurrentVersion;
}, DispatcherPriority.Background);
};
}
void DispatchToolbarCommands(ToolbarCommandRegistry registry)

4
README.md

@ -48,7 +48,7 @@ How to build @@ -48,7 +48,7 @@ How to build
- Clone the ILSpy repository using git.
- Execute `git submodule update --init --recursive` to download the ILSpy-Tests submodule (used by some test cases).
- Install Visual Studio (documented version: 18.0/2026). You need the following workload components:
- Workload ".NET Desktop Development". This workload includes the .NET Framework 4.8 SDK and the .NET Framework 4.7.2 targeting pack, as well as the [.NET 11.0 SDK](https://dotnet.microsoft.com/download/dotnet/11.0) (ILSpy.csproj targets .NET 11.0, but we have net472 projects too).
- Workload ".NET Desktop Development". This workload includes the .NET Framework 4.8 SDK and the .NET Framework 4.7.2 targeting pack, as well as the [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (ILSpy.csproj targets .NET 10.0, but we have net472 projects too). Our unit tests require [.NET 11.0 SDK](https://dotnet.microsoft.com/download/dotnet/11.0).
- Workload "Visual Studio extension development" (Note: ILSpy.VSExtensions.sln is separate from ILSpy.sln and thus this workload is optional)
- Individual Component "MSVC v143 - VS 2022 C++ x64/x86 build tools" (or similar)
- _The VC++ toolset is optional_; if present it is used for `editbin.exe` to modify the stack size used by ILSpy.exe from 1MB to 16MB, because the decompiler makes heavy use of recursion, where small stack sizes lead to problems in very complex methods.
@ -67,7 +67,7 @@ If this problem occurs, please manually install the .NET 11.0 SDK from [here](ht @@ -67,7 +67,7 @@ If this problem occurs, please manually install the .NET 11.0 SDK from [here](ht
#### Unix / Mac:
- Make sure [.NET 11.0 SDK](https://dotnet.microsoft.com/download/dotnet/11.0) is installed.
- Make sure [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (for run) and [.NET 11.0 SDK](https://dotnet.microsoft.com/download/dotnet/11.0) (for tests) is installed.
- Make sure [PowerShell](https://github.com/PowerShell/PowerShell) is installed (formerly known as PowerShell Core)
- Clone the repository using git.
- Execute `git submodule update --init --recursive` to download the ILSpy-Tests submodule (used by some test cases).

Loading…
Cancel
Save