New VSCode fallback behaviour for copyToClipboard (#6598)

![](https://media.giphy.com/media/4URfklUToxk6A/giphy.gif)

Adds a fallback behaviour for copying which will allow copy buttons to actually work in VSCode.

NOTE: success toasts will no longer show in VSCode though.

I've also added some logging just in case a user is looking at their console when a copy fails. Hopefully this provides enough information that the unhide-able error doesn't look suspicious.
![Screenshot 2023-10-26 at 10 53 39 AM](https://github.com/flutter/devtools/assets/1386322/b4ebd139-f3f5-4dd6-8c62-794f7d6df80d)

Related to https://github.com/Dart-Code/Dart-Code/pull/4814
Fixes https://github.com/flutter/devtools/issues/5775

Mirror package:web version from Flutter SDK
diff --git a/packages/devtools_app/lib/src/framework/settings_dialog.dart b/packages/devtools_app/lib/src/framework/settings_dialog.dart
index 3705255..0e850ef 100644
--- a/packages/devtools_app/lib/src/framework/settings_dialog.dart
+++ b/packages/devtools_app/lib/src/framework/settings_dialog.dart
@@ -11,10 +11,10 @@
 import '../shared/analytics/analytics_controller.dart';
 import '../shared/analytics/constants.dart' as gac;
 import '../shared/common_widgets.dart';
+import '../shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart';
 import '../shared/config_specific/server/server.dart';
 import '../shared/globals.dart';
 import '../shared/log_storage.dart';
-import '../shared/utils.dart';
 
 class OpenSettingsAction extends ScaffoldAction {
   OpenSettingsAction({super.key, Color? color})
diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart
index f84ff58..d72610d 100644
--- a/packages/devtools_app/lib/src/screens/network/network_screen.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart
@@ -13,6 +13,7 @@
 import '../../shared/analytics/analytics.dart' as ga;
 import '../../shared/analytics/constants.dart' as gac;
 import '../../shared/common_widgets.dart';
+import '../../shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart';
 import '../../shared/globals.dart';
 import '../../shared/http/curl_command.dart';
 import '../../shared/http/http_request_data.dart';
diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart
index 3345b93..7a130f1 100644
--- a/packages/devtools_app/lib/src/shared/common_widgets.dart
+++ b/packages/devtools_app/lib/src/shared/common_widgets.dart
@@ -17,6 +17,7 @@
 import '../screens/inspector/layout_explorer/ui/theme.dart';
 import 'analytics/analytics.dart' as ga;
 import 'analytics/constants.dart' as gac;
+import 'config_specific/copy_to_clipboard/copy_to_clipboard.dart';
 import 'config_specific/launch_url/launch_url.dart';
 import 'console/widgets/expandable_variable.dart';
 import 'diagnostics/dart_object_node.dart';
diff --git a/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/_copy_to_clipboard_desktop.dart b/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/_copy_to_clipboard_desktop.dart
new file mode 100644
index 0000000..04bef78
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/_copy_to_clipboard_desktop.dart
@@ -0,0 +1,7 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+void copyToClipboardVSCode(String _) {
+  // Do nothing
+}
diff --git a/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/_copy_to_clipboard_web.dart b/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/_copy_to_clipboard_web.dart
new file mode 100644
index 0000000..e00b987
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/_copy_to_clipboard_web.dart
@@ -0,0 +1,19 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+import 'dart:js_interop';
+import 'package:web/web.dart';
+
+void copyToClipboardVSCode(String data) {
+  // This post message is only relevant in VSCode. If the parent frame is
+  // listening for this command, then it will attempt to copy the contents
+  // to the clipboard in the context of the parent frame.
+  window.parent?.postMessage(
+    {
+      'command': 'clipboard-write',
+      'data': data,
+    }.jsify(),
+    '*'.toJS,
+  );
+}
diff --git a/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart b/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart
new file mode 100644
index 0000000..455414e
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart
@@ -0,0 +1,52 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/services.dart';
+import 'package:logging/logging.dart';
+
+import '../../globals.dart';
+import '_copy_to_clipboard_desktop.dart'
+    if (dart.library.js_interop) '_copy_to_clipboard_web.dart';
+
+final _log = Logger('copy_to_clipboard');
+
+/// Attempts to copy a String of `data` to the clipboard.
+///
+/// Shows a `successMessage` [Notification] on the passed in `context`, if the
+/// copy is successfully done using the [Clipboard.setData] api. Otherwise it
+/// attempts to post the [data] to the parent frame where the parent frame will
+/// try to complete the copy (this fallback will only work in VSCode).
+Future<void> copyToClipboard(
+  String data,
+  String? successMessage,
+) async {
+  try {
+    await Clipboard.setData(
+      ClipboardData(
+        text: data,
+      ),
+    );
+
+    if (successMessage != null) notificationService.push(successMessage);
+  } catch (e) {
+    if (ideTheme.embed) {
+      _log.warning(
+        'DevTools copy failed. This may be as a result of a known bug in VSCode. '
+        'See https://github.com/Dart-Code/Dart-Code/issues/4540 for more '
+        'information. DevTools will now attempt to use a fallback method of '
+        'copying the contents.',
+      );
+      // Trying to use Clipboard.setData to copy in vscode will not work as a
+      // result of a bug. So we should fallback to `copyToClipboardVSCode` which
+      // delegates the copy to the frame above DevTools.
+      // Sending this message in other environments will just have no effect.
+      // See https://github.com/Dart-Code/Dart-Code/issues/4540 for more
+      // information.
+      copyToClipboardVSCode(data);
+    } else {
+      rethrow;
+    }
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/dialogs.dart b/packages/devtools_app/lib/src/shared/dialogs.dart
index 6b4abd2..f510379 100644
--- a/packages/devtools_app/lib/src/shared/dialogs.dart
+++ b/packages/devtools_app/lib/src/shared/dialogs.dart
@@ -8,8 +8,8 @@
 import 'package:flutter/material.dart';
 
 import '../shared/config_specific/launch_url/launch_url.dart';
+import 'config_specific/copy_to_clipboard/copy_to_clipboard.dart';
 import 'globals.dart';
-import 'utils.dart';
 
 /// A dialog, that reports unexpected error and allows to copy details and create issue.
 class UnexpectedErrorDialog extends StatelessWidget {
diff --git a/packages/devtools_app/lib/src/shared/editable_list.dart b/packages/devtools_app/lib/src/shared/editable_list.dart
index c6c08ba..6d01312 100644
--- a/packages/devtools_app/lib/src/shared/editable_list.dart
+++ b/packages/devtools_app/lib/src/shared/editable_list.dart
@@ -10,7 +10,7 @@
 import 'package:flutter/material.dart';
 
 import 'common_widgets.dart';
-import 'utils.dart';
+import 'config_specific/copy_to_clipboard/copy_to_clipboard.dart';
 
 /// A widget that displays the contents of [entries].
 ///
diff --git a/packages/devtools_app/lib/src/shared/utils.dart b/packages/devtools_app/lib/src/shared/utils.dart
index 3c691b9..29cd8d3 100644
--- a/packages/devtools_app/lib/src/shared/utils.dart
+++ b/packages/devtools_app/lib/src/shared/utils.dart
@@ -6,13 +6,10 @@
 // other libraries in this package.
 // Utils, that do not have dependencies, should go to primitives/utils.dart.
 
-import 'dart:async';
-
 import 'package:devtools_app_shared/service.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 import 'package:provider/provider.dart';
@@ -25,22 +22,6 @@
 
 final _log = Logger('lib/src/shared/utils');
 
-/// Attempts to copy a String of `data` to the clipboard.
-///
-/// Shows a `successMessage` [Notification] on the passed in `context`.
-Future<void> copyToClipboard(
-  String data,
-  String? successMessage,
-) async {
-  await Clipboard.setData(
-    ClipboardData(
-      text: data,
-    ),
-  );
-
-  if (successMessage != null) notificationService.push(successMessage);
-}
-
 /// Logging to debug console only in debug runs.
 void debugLogger(String message) {
   assert(
diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml
index a9a6252..4d052f0 100644
--- a/packages/devtools_app/pubspec.yaml
+++ b/packages/devtools_app/pubspec.yaml
@@ -66,7 +66,7 @@
   vm_service: ^11.10.0
   # TODO https://github.com/dart-lang/sdk/issues/52853 - unpin this version
   vm_snapshot_analysis: 0.7.2
-  web: ^0.3.0
+  web: ^0.2.1-beta
   web_socket_channel: ^2.1.0
   # widget_icons: ^0.0.1