Browse Source

Braille spinner in decompiler tab title while decompiling

Assisted-by: Claude:claude-opus-4-7:Claude Code

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
0c87ba4f92
  1. 43
      ILSpy.Tests/Editor/DecompilerViewTests.cs
  2. 53
      ILSpy/TextView/DecompilerTabPageModel.cs

43
ILSpy.Tests/Editor/DecompilerViewTests.cs

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
@ -27,6 +29,7 @@ using ICSharpCode.ILSpy.Properties; @@ -27,6 +29,7 @@ using ICSharpCode.ILSpy.Properties;
using ILSpy.AppEnv;
using ILSpy.Commands;
using ILSpy.TextView;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
@ -220,6 +223,46 @@ public class DecompilerViewTests @@ -220,6 +223,46 @@ public class DecompilerViewTests
tab.Title.Should().Be(node.Text!.ToString());
}
[AvaloniaTest]
public async Task Decompile_Tab_Title_Cycles_Round_Spinner_While_Decompiling()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
var firstNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>("System.Linq");
vm.AssemblyTreeModel.SelectedItem = firstNode;
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
var titles = new List<string>();
var editorTexts = new List<string>();
tab.PropertyChanged += OnTabPropertyChanged;
var coreLib = typeof(object).Assembly.GetName().Name!;
var second = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLib, "System", "System.Version");
vm.AssemblyTreeModel.SelectedItem = second;
await vm.DockWorkspace.WaitForDecompiledTextAsync();
tab.PropertyChanged -= OnTabPropertyChanged;
var glyphs = new[] { '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' };
titles.Should().Contain(t => glyphs.Any(t.Contains),
"the tab title must show one of the Braille spinner frames while decompilation is in flight");
editorTexts.Should().NotContain(t => glyphs.Any(t.Contains),
"the editor's Text must not be overwritten with the spinner — title prefix only");
tab.Title.Should().NotContainAny("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
"the spinner glyph must clear once decompilation finishes");
void OnTabPropertyChanged(object? s, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DecompilerTabPageModel.Title))
titles.Add(tab.Title ?? string.Empty);
else if (e.PropertyName == nameof(DecompilerTabPageModel.Text))
editorTexts.Add(tab.Text ?? string.Empty);
}
}
[AvaloniaTest]
public async Task Selecting_Assembly_Node_Emits_Header_And_Assembly_Attributes()
{

53
ILSpy/TextView/DecompilerTabPageModel.cs

@ -134,9 +134,12 @@ namespace ILSpy.TextView @@ -134,9 +134,12 @@ namespace ILSpy.TextView
{
// Tree-node Text can change after we capture the title (e.g. AssemblyTreeNode swaps
// from ShortName to "ShortName (version, tfm)" once the assembly finishes loading).
// Re-read it whenever the node fires Text so the tab header stays in sync.
if (e.PropertyName == nameof(ILSpyTreeNode.Text) && sender is ILSpyTreeNode node)
Title = node.Text?.ToString() ?? "(unnamed)";
// While a decompile is running the spinner prefixes the title — keep the prefix and
// just refresh the suffix; otherwise replace the title outright.
if (e.PropertyName != nameof(ILSpyTreeNode.Text) || sender is not ILSpyTreeNode node)
return;
var text = node.Text?.ToString() ?? "(unnamed)";
Title = IsDecompiling ? ComposeSpinnerTitle(0, text) : text;
}
public DecompilerTabPageModel()
@ -162,6 +165,11 @@ namespace ILSpy.TextView @@ -162,6 +165,11 @@ namespace ILSpy.TextView
var newSyntaxExtension = language.FileExtension;
IsDecompiling = true;
// Spinner appears as a glyph prefix on the tab title while the decompile runs;
// editor state is left untouched so cancellation falls back cleanly.
Title = ComposeSpinnerTitle(0, currentNode?.Text?.ToString() ?? "(unnamed)");
_ = RunSpinnerAsync(cts.Token);
try
{
var (output, _) = await Task.Run(() => {
@ -220,12 +228,47 @@ namespace ILSpy.TextView @@ -220,12 +228,47 @@ namespace ILSpy.TextView
// at the top of DecompileAsync).
if (ReferenceEquals(activeCts, cts))
{
if (Dispatcher.UIThread.CheckAccess())
void StopSpinner()
{
IsDecompiling = false;
// If we cancelled before producing fresh output, drop the spinner glyph
// from the title — the editor still shows the previous decompile.
if (currentNode != null)
Title = currentNode.Text?.ToString() ?? "(unnamed)";
}
if (Dispatcher.UIThread.CheckAccess())
StopSpinner();
else
await Dispatcher.UIThread.InvokeAsync(() => IsDecompiling = false);
await Dispatcher.UIThread.InvokeAsync(StopSpinner);
}
}
}
// Braille round-spinner: the dot pattern walks around the cell so each frame reads as
// a different orientation of a small spinning circle.
static readonly char[] SpinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
static readonly TimeSpan SpinnerInterval = TimeSpan.FromMilliseconds(80);
static string ComposeSpinnerTitle(int frame, string baseTitle)
=> $"{SpinnerFrames[frame % SpinnerFrames.Length]} {baseTitle}";
async Task RunSpinnerAsync(CancellationToken token)
{
int frame = 1;
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(SpinnerInterval, token).ConfigureAwait(true);
}
catch (OperationCanceledException)
{
return;
}
if (token.IsCancellationRequested || !IsDecompiling)
return;
Title = ComposeSpinnerTitle(frame++, currentNode?.Text?.ToString() ?? "(unnamed)");
}
}
}
}

Loading…
Cancel
Save