From 3b303ad477417fe21fde7e55d1aa889b159c9ebe Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 19 Nov 2025 17:14:33 +0100 Subject: [PATCH 1/2] Add a shorter timeout for retrieving clipboard data --- .../WpfStringCopyPasteService.cs | 76 ++++++++++++++++--- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs b/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs index 3023c8f9c7e17..bf56502f5ba08 100644 --- a/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs +++ b/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs @@ -5,24 +5,29 @@ using System; using System.Composition; using System.Runtime.InteropServices; +using System.Threading; + // Use of System.Windows.Forms over System.Windows is intentional here. S.W.F has logic in its clipboard impl to help // with common errors. using System.Windows.Forms; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio; +using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; namespace Microsoft.CodeAnalysis.Editor.StringCopyPaste; [ExportWorkspaceService(typeof(IStringCopyPasteService), ServiceLayer.Host), Shared] -internal sealed class WpfStringCopyPasteService : IStringCopyPasteService +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WpfStringCopyPasteService() : IStringCopyPasteService { - private const string RoslynFormat = nameof(RoslynFormat); + // Similar to what WinForms does, except that instead of blocking for up to 1s, we only block for up to 250ms. + // https://github.com/dotnet/winforms/blob/0f76e65878b1a0958175f17c4360b8198f8b36ba/src/System.Windows.Forms/src/System/Windows/Forms/Clipboard.cs#L31 + private const int RetryTimes = 5; + private const int RetryDelay = 50; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public WpfStringCopyPasteService() - { - } + private const string RoslynFormat = nameof(RoslynFormat); private static string GetFormat(string key) => $"{RoslynFormat}-{key}"; @@ -33,7 +38,9 @@ public bool TrySetClipboardData(string key, string data) try { - var dataObject = Clipboard.GetDataObject(); + var dataObject = GetDataObject(); + if (dataObject is null) + return false; var copy = new DataObject(); @@ -45,9 +52,7 @@ public bool TrySetClipboardData(string key, string data) copy.SetData(GetFormat(key), data); - // Similar to what WinForms does, except that instead of blocking for up to 1s, we only block for up to 250ms. - // https://github.com/dotnet/winforms/blob/0f76e65878b1a0958175f17c4360b8198f8b36ba/src/System.Windows.Forms/src/System/Windows/Forms/Clipboard.cs#L31 - Clipboard.SetDataObject(copy, copy: false, retryTimes: 5, retryDelay: 50); + Clipboard.SetDataObject(copy, copy: false, RetryTimes, RetryDelay); return true; } catch (ExternalException ex) when ((uint)ex.ErrorCode == CLIPBRD_E_CANT_OPEN) @@ -62,11 +67,58 @@ public bool TrySetClipboardData(string key, string data) return false; } + /// + /// Similar to except that this will only block a max of 250ms, not a full second. + /// + private static IDataObject? GetDataObject() + => GetDataObject(RetryTimes, RetryDelay); + + /// + /// Copied from https://github.com/dotnet/winforms/blob/0f76e65878b1a0958175f17c4360b8198f8b36ba/src/System.Windows.Forms/src/System/Windows/Forms/Clipboard.cs#L139 + /// + private static IDataObject? GetDataObject(int retryTimes, int retryDelay) + { + IComDataObject? dataObject = null; + int hr; + var retry = retryTimes; + do + { + hr = OleGetClipboard(ref dataObject); + if (!ErrorHandler.Succeeded(hr)) + { + if (retry == 0) + return null; + + retry--; + Thread.Sleep(millisecondsTimeout: retryDelay); + } + } + while (hr != 0); + + if (dataObject is not null) + { + if (dataObject is IDataObject ido && !Marshal.IsComObject(dataObject)) + { + return ido; + } + + return new DataObject(dataObject); + } + + return null; + } + + [DllImport("ole32.dll", ExactSpelling = true)] + public static extern int OleGetClipboard(ref IComDataObject? data); + public string? TryGetClipboardData(string key) { try { - var dataObject = Clipboard.GetDataObject(); + var dataObject = GetDataObject(); + if (dataObject is null) + return null; + var format = GetFormat(key); if (dataObject.GetDataPresent(format)) { From 5e4c43f242ed810c699b0e0106a0299a2a1c3cec Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 19 Nov 2025 19:18:59 +0100 Subject: [PATCH 2/2] Switch to 0 --- .../Core/StringCopyPaste/WpfStringCopyPasteService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs b/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs index bf56502f5ba08..8d48fd759ee2a 100644 --- a/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs +++ b/src/EditorFeatures/Core/StringCopyPaste/WpfStringCopyPasteService.cs @@ -12,7 +12,6 @@ using System.Windows.Forms; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.VisualStudio; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; namespace Microsoft.CodeAnalysis.Editor.StringCopyPaste; @@ -84,7 +83,7 @@ public bool TrySetClipboardData(string key, string data) do { hr = OleGetClipboard(ref dataObject); - if (!ErrorHandler.Succeeded(hr)) + if (hr != 0) { if (retry == 0) return null;