Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@
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 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}";
Expand All @@ -33,7 +37,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();

Expand All @@ -45,9 +51,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)
Expand All @@ -62,11 +66,58 @@ public bool TrySetClipboardData(string key, string data)
return false;
}

/// <summary>
/// Similar to <see cref="Clipboard.GetDataObject"/> except that this will only block a max of 250ms, not a full second.
/// </summary>
private static IDataObject? GetDataObject()
=> GetDataObject(RetryTimes, RetryDelay);

/// <summary>
/// Copied from https://github.com/dotnet/winforms/blob/0f76e65878b1a0958175f17c4360b8198f8b36ba/src/System.Windows.Forms/src/System/Windows/Forms/Clipboard.cs#L139
/// </summary>
private static IDataObject? GetDataObject(int retryTimes, int retryDelay)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
IComDataObject? dataObject = null;
int hr;
var retry = retryTimes;
do
{
hr = OleGetClipboard(ref dataObject);
if (hr != 0)
{
if (retry == 0)
return null;
Copy link
Member

@JoeRobich JoeRobich Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Clipboard this threw. How will the behavior change affect the user experience?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We caught the throw and returned null. So this has the same effect. It means we don't support rich copy/paste semantics. But that's better than timing out for 1 second :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I should have looked further down.


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))
{
Expand Down
Loading