From b153291cdffbd1168dda9cf42c112b949d286809 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 20 Jun 2026 10:43:03 +0200 Subject: [PATCH] Fix #3793: stop accidental text selection after clicking a reference Clicking a reference left AvaloniaEdit's selection-drag mode stuck, so a subsequent mouse move (with no button held) extended a selection. AvaloniaEdit's SelectionMouseHandler captures the pointer and enters selection mode on press, and extends the selection on every move while that mode is active -- it keys off the mode, not whether a button is down. Only its bubble-phase PointerReleased handler resets the mode and releases the capture, and that handler early-returns when the event is already handled. The reference-click handler ran in the tunnel phase and marked the release handled before AvaloniaEdit saw it, so AvaloniaEdit's cleanup never ran. Move the release handler to the bubble phase so it runs after AvaloniaEdit has reset its state. AvaloniaEdit subscribes from the TextArea constructor, before this view attaches its handlers, so the ordering is deterministic and navigation can stay synchronous. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/Editor/ReferenceClickTests.cs | 17 +++++++++++++++++ ILSpy/TextView/DecompilerTextView.axaml.cs | 20 ++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/ILSpy.Tests/Editor/ReferenceClickTests.cs b/ILSpy.Tests/Editor/ReferenceClickTests.cs index 0f5f1111a..809eba19b 100644 --- a/ILSpy.Tests/Editor/ReferenceClickTests.cs +++ b/ILSpy.Tests/Editor/ReferenceClickTests.cs @@ -115,4 +115,21 @@ public class ReferenceClickTests navigated.Should().BeTrue("a click without dragging follows the link"); } + + [AvaloniaTest] + public async Task Click_On_A_Link_Then_Moving_The_Mouse_Does_Not_Select_Text() + { + var (window, view, tab) = await SetupAsync(); + var (_, point) = FindVisibleReference(window, view, tab); + tab.NavigateRequested += _ => { }; + + // A stationary click navigates; the gesture must end cleanly so that afterwards moving + // the mouse with no button held does not extend a selection (issue #3793). + window.MouseDown(point, MouseButton.Left); + window.MouseUp(point, MouseButton.Left); + window.MouseMove(point + new Point(60, 0)); + + view.Editor.TextArea.Selection.IsEmpty.Should().BeTrue( + "moving the mouse after a click, with no button held, must not select text"); + } } diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 693a0777e..a55cfe04a 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -157,15 +157,22 @@ namespace ICSharpCode.ILSpy.TextView // Reference navigation fires on pointer-RELEASE without drag (WPF parity: the WPF // view used TextArea.PreviewMouseDown/Up the same way), so a press-and-drag over a - // link starts a text selection instead of navigating away. Tunnel routing mirrors - // WPF's Preview events and sees the gesture before AvaloniaEdit's own handlers. + // link starts a text selection instead of navigating away. The press handler only + // records the start position, so tunnel routing (before AvaloniaEdit consumes the + // press) is fine. Editor.TextArea.AddHandler(InputElement.PointerPressedEvent, OnTextAreaPointerPressedForReferenceClick, RoutingStrategies.Tunnel, handledEventsToo: true); + // The release handler must run AFTER AvaloniaEdit's own PointerReleased: AvaloniaEdit's + // SelectionMouseHandler captures the pointer and enters selection mode on press, and + // only its release handler resets that mode and releases the capture. AvaloniaEdit + // subscribes in the bubble phase from the TextArea constructor (before this view adds + // its handlers), so a bubble handler here is guaranteed to run after it. handledEventsToo + // keeps us running even though AvaloniaEdit marks the release handled. Editor.TextArea.AddHandler(InputElement.PointerReleasedEvent, OnTextAreaPointerReleasedForReferenceClick, - RoutingStrategies.Tunnel, + RoutingStrategies.Bubble, handledEventsToo: true); } @@ -200,9 +207,10 @@ namespace ICSharpCode.ILSpy.TextView ClearLocalReferenceMarks(); return; } - // Cancel the caret-click selection AvaloniaEdit started on press, and stop its - // release processing, so the navigation's caret move doesn't grow a selection - // between the old anchor and the new position. + // AvaloniaEdit's own release handler has already reset its selection mode and released + // the pointer capture (it runs first in the bubble phase). Clear the empty caret-click + // selection it leaves behind, then navigate; marking the release handled stops it from + // bubbling further now that it has been consumed as a link click. Editor.TextArea.ClearSelection(); e.Handled = true; OnReferenceClicked(segment);