From b969075d01a279ee2ef54f0e30b38e403dd11772 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 5 May 2026 09:21:25 +0200 Subject: [PATCH] Hyperlinks in decompiler view activate on plain click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flips Editor.Options.RequireControlModifierForHyperlinkClick to false in DecompilerTextView's constructor — that's the value AvaloniaEdit's built-in LinkElementGenerator forwards onto every VisualLineLinkText it constructs, so toggling it once at the editor level removes the Ctrl requirement for every hyperlink in the decompiled output (in-app references, About-page resource links, future external URLs alike). Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy.Tests/Commands/HelpCommandTests.cs | 31 ++++++++++++++++ ILSpy/Commands/AboutCommand.cs | 16 -------- ILSpy/Commands/ResourceLinkGenerator.cs | 43 ++++++++++++++++++++++ ILSpy/TextView/DecompilerTextView.axaml.cs | 7 ++++ 4 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 ILSpy/Commands/ResourceLinkGenerator.cs diff --git a/ILSpy.Tests/Commands/HelpCommandTests.cs b/ILSpy.Tests/Commands/HelpCommandTests.cs index f072d87a7..95f642079 100644 --- a/ILSpy.Tests/Commands/HelpCommandTests.cs +++ b/ILSpy.Tests/Commands/HelpCommandTests.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Headless.NUnit; +using Avalonia.VisualTree; using AwesomeAssertions; @@ -126,6 +127,36 @@ public class HelpCommandTests licenseTab.Text.Should().Contain("Permission is hereby granted"); } + [AvaloniaTest] + public async Task Decompiler_Editor_Activates_Hyperlinks_Without_Ctrl_Modifier() + { + // AvaloniaEdit defaults RequireControlModifierForHyperlinkClick=true on its + // TextEditorOptions; decompiler output uses hyperlinks for in-app navigation, so a + // plain click is the expected affordance. Verifies the option is flipped on the + // realised editor — the value AvaloniaEdit's LinkElementGenerator (and our own + // ResourceLinkGenerator) both consult when constructing VisualLineLinkText. + + // Arrange — boot, select a node so the decompiler tab is realised + the editor inside + // it is actually constructed (the view is data-templated lazily on first activation). + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var node = vm.AssemblyTreeModel.FindNode("System.Linq"); + vm.AssemblyTreeModel.SelectedItem = node; + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + await Waiters.WaitForAsync( + () => window.GetVisualDescendants().OfType().Any()); + + // Act — locate the editor inside the realised DecompilerTextView. + var view = window.GetVisualDescendants().OfType().First(); + var editor = view.GetVisualDescendants().OfType().Single(); + + // Assert — the editor's hyperlink-click option is off. + editor.Options.RequireControlModifierForHyperlinkClick.Should().BeFalse( + "hyperlinks in the decompiler view must activate on a plain click"); + } + [AvaloniaTest] public async Task About_Page_Lands_On_Back_History_And_Round_Trips_Through_Navigation() { diff --git a/ILSpy/Commands/AboutCommand.cs b/ILSpy/Commands/AboutCommand.cs index 9318bbed7..909a9c121 100644 --- a/ILSpy/Commands/AboutCommand.cs +++ b/ILSpy/Commands/AboutCommand.cs @@ -136,22 +136,6 @@ namespace ILSpy.Commands return typeof(object).Assembly.GetName().Version?.ToString() ?? "UNKNOWN"; } - // Specialised LinkElementGenerator that matches a single literal phrase and produces a - // VisualLineLinkText pointing at a fixed Uri. Mirrors the "MyLinkElementGenerator" - // pattern from the legacy WPF host. - sealed class ResourceLinkGenerator : LinkElementGenerator - { - readonly Uri target; - - public ResourceLinkGenerator(string phrase, Uri target) - : base(new Regex(Regex.Escape(phrase))) - { - this.target = target; - RequireControlModifierForClick = false; - } - - protected override Uri GetUriFromMatch(Match match) => target; - } } /// diff --git a/ILSpy/Commands/ResourceLinkGenerator.cs b/ILSpy/Commands/ResourceLinkGenerator.cs new file mode 100644 index 000000000..4ab920147 --- /dev/null +++ b/ILSpy/Commands/ResourceLinkGenerator.cs @@ -0,0 +1,43 @@ +// 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.Text.RegularExpressions; + +using AvaloniaEdit.Rendering; + +namespace ILSpy.Commands +{ + /// + /// LinkElementGenerator that matches a single literal phrase and produces a + /// pointing at a fixed . Mirrors the + /// "MyLinkElementGenerator" pattern from the legacy WPF host. + /// + internal sealed class ResourceLinkGenerator : LinkElementGenerator + { + readonly Uri target; + + public ResourceLinkGenerator(string phrase, Uri target) + : base(new Regex(Regex.Escape(phrase))) + { + this.target = target ?? throw new ArgumentNullException(nameof(target)); + } + + protected override Uri GetUriFromMatch(Match match) => target; + } +} diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 8fa4c090a..745e849a6 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -76,6 +76,13 @@ namespace ILSpy.TextView { InitializeComponent(); + // AvaloniaEdit defaults to "Ctrl+Click to follow hyperlink" on its built-in + // LinkElementGenerator and propagates that flag onto every VisualLineLinkText it + // constructs (including those produced by user-added LinkElementGenerator subclasses + // like the About-page resource generator). Decompiler output uses hyperlinks + // extensively for in-app navigation — a plain click is the expected affordance. + Editor.Options.RequireControlModifierForHyperlinkClick = false; + // One generator lives for the lifetime of the view; we only swap its References // collection per document. The predicate filters out anything that should not be // clickable — references with a null target slip through unconditionally otherwise.