From d4908fe319ce887773a387b84a489473a0ce0798 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Thu, 7 Sep 2023 11:43:24 +0100 Subject: [PATCH] Add events to receive debug sessions into sidebar (#6237) * Add events to receive debug sessions into sidebar This adds some events for listing debug sessions in the sidebar and buttons to open DevTools features from them. As before, this is functional but the UI needs much work. * Rename DevToolSfeature -> DevToolsPage * Improve styling * Review tweaks - DartDocs - Update comments - Use named constants instead of literals - Add some TODOs * Review tweaks - Remove infinite SizedBoxes and use crossAxisAlignment on the parent - Tweak comments about opening DevTools --- packages/.vscode/launch.json | 1 - .../standalone_ui/api/impl/vs_code_api.dart | 93 ++++++++++++++ .../src/standalone_ui/api/vs_code_api.dart | 115 +++++++++++++++++ .../standalone_ui/vs_code/debug_sessions.dart | 116 ++++++++++++++++++ .../src/standalone_ui/vs_code/devices.dart | 73 +++++++++++ .../standalone_ui/vs_code/flutter_panel.dart | 88 ++++++------- .../scenes/standalone_ui/vs_code.dart | 2 + .../standalone_ui/vs_code_mock_editor.dart | 35 +++++- .../test_data/dart_tooling_api/mock_api.dart | 44 ++++++- 9 files changed, 521 insertions(+), 46 deletions(-) create mode 100644 packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart create mode 100644 packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart diff --git a/packages/.vscode/launch.json b/packages/.vscode/launch.json index a41186fca1d..758d88d285a 100644 --- a/packages/.vscode/launch.json +++ b/packages/.vscode/launch.json @@ -83,7 +83,6 @@ "request": "launch", "type": "dart", "program": "devtools_app/test/test_infra/scenes/standalone_ui/vs_code.stager_app.g.dart", - "deviceId": "chrome", }, { "name": "attach", diff --git a/packages/devtools_app/lib/src/standalone_ui/api/impl/vs_code_api.dart b/packages/devtools_app/lib/src/standalone_ui/api/impl/vs_code_api.dart index fa6603782d0..896f3b863e6 100644 --- a/packages/devtools_app/lib/src/standalone_ui/api/impl/vs_code_api.dart +++ b/packages/devtools_app/lib/src/standalone_ui/api/impl/vs_code_api.dart @@ -13,6 +13,9 @@ final class VsCodeApiImpl extends ToolApiImpl implements VsCodeApi { this.capabilities = VsCodeCapabilitiesImpl(capabilities); devicesChanged = events(VsCodeApi.jsonDevicesChangedEvent) .map(VsCodeDevicesEventImpl.fromJson); + + debugSessionsChanged = events(VsCodeApi.jsonDebugSessionsChangedEvent) + .map(VsCodeDebugSessionsEventImpl.fromJson); } static Future tryConnect(json_rpc_2.Peer rpc) async { @@ -31,6 +34,9 @@ final class VsCodeApiImpl extends ToolApiImpl implements VsCodeApi { @override late final Stream devicesChanged; + @override + late final Stream debugSessionsChanged; + @override late final VsCodeCapabilities capabilities; @@ -52,6 +58,17 @@ final class VsCodeApiImpl extends ToolApiImpl implements VsCodeApi { {VsCodeApi.jsonSelectDeviceIdParameter: id}, ); } + + @override + Future openDevToolsPage(String debugSessionId, String page) { + return sendRequest( + VsCodeApi.openDevToolsPageMethod, + { + VsCodeApi.openDevToolsPageDebugSessionIdParameter: debugSessionId, + VsCodeApi.openDevToolsPagePageParameter: page, + }, + ); + } } class VsCodeDeviceImpl implements VsCodeDevice { @@ -114,6 +131,57 @@ class VsCodeDeviceImpl implements VsCodeDevice { }; } +class VsCodeDebugSessionImpl implements VsCodeDebugSession { + VsCodeDebugSessionImpl({ + required this.id, + required this.name, + required this.vmServiceUri, + required this.flutterMode, + required this.flutterDeviceId, + required this.debuggerType, + }); + + VsCodeDebugSessionImpl.fromJson(Map json) + : this( + id: json[VsCodeDebugSession.jsonIdField] as String, + name: json[VsCodeDebugSession.jsonNameField] as String, + vmServiceUri: + json[VsCodeDebugSession.jsonVmServiceUriField] as String?, + flutterMode: json[VsCodeDebugSession.jsonFlutterModeField] as String?, + flutterDeviceId: + json[VsCodeDebugSession.jsonFlutterDeviceIdField] as String?, + debuggerType: + json[VsCodeDebugSession.jsonDebuggerTypeField] as String?, + ); + + @override + final String id; + + @override + final String name; + + @override + final String? vmServiceUri; + + @override + final String? flutterMode; + + @override + final String? flutterDeviceId; + + @override + final String? debuggerType; + + Map toJson() => { + VsCodeDebugSession.jsonIdField: id, + VsCodeDebugSession.jsonNameField: name, + VsCodeDebugSession.jsonVmServiceUriField: vmServiceUri, + VsCodeDebugSession.jsonFlutterModeField: flutterMode, + VsCodeDebugSession.jsonFlutterDeviceIdField: flutterDeviceId, + VsCodeDebugSession.jsonDebuggerTypeField: debuggerType, + }; +} + class VsCodeDevicesEventImpl implements VsCodeDevicesEvent { VsCodeDevicesEventImpl({ required this.selectedDeviceId, @@ -142,6 +210,27 @@ class VsCodeDevicesEventImpl implements VsCodeDevicesEvent { }; } +class VsCodeDebugSessionsEventImpl implements VsCodeDebugSessionsEvent { + VsCodeDebugSessionsEventImpl({ + required this.sessions, + }); + + VsCodeDebugSessionsEventImpl.fromJson(Map json) + : this( + sessions: (json[VsCodeDebugSessionsEvent.jsonSessionsField] as List) + .map((item) => Map.from(item)) + .map((map) => VsCodeDebugSessionImpl.fromJson(map)) + .toList(), + ); + + @override + final List sessions; + + Map toJson() => { + VsCodeDebugSessionsEvent.jsonSessionsField: sessions, + }; +} + class VsCodeCapabilitiesImpl implements VsCodeCapabilities { VsCodeCapabilitiesImpl(this._raw); @@ -154,4 +243,8 @@ class VsCodeCapabilitiesImpl implements VsCodeCapabilities { @override bool get selectDevice => _raw?[VsCodeCapabilities.jsonSelectDeviceField] == true; + + @override + bool get openDevToolsPage => + _raw?[VsCodeCapabilities.openDevToolsPageField] == true; } diff --git a/packages/devtools_app/lib/src/standalone_ui/api/vs_code_api.dart b/packages/devtools_app/lib/src/standalone_ui/api/vs_code_api.dart index d3d8d206700..2b426f21c65 100644 --- a/packages/devtools_app/lib/src/standalone_ui/api/vs_code_api.dart +++ b/packages/devtools_app/lib/src/standalone_ui/api/vs_code_api.dart @@ -9,12 +9,51 @@ /// [VsCodeCapabilities] to advertise which capabilities are available and /// handle any changes in behaviour. abstract interface class VsCodeApi { + /// The capabilities of the instance of VS Code / Dart VS Code extension that + /// we are connected to. + /// + /// All API calls should be guarded by checks of capabilities because the API + /// may change over time. VsCodeCapabilities get capabilities; + + /// Informs the VS Code extension we are initialized, allowing it to send + /// initial events to all streams with an initial set of data. Future initialize(); + + /// A stream of events for whenever the set of devices (or selected device) + /// change in VS Code. + /// + /// An event with initial devices is sent after [initialize] is called. Stream get devicesChanged; + + /// A stream of events for whenever the set of debug sessions change or are + /// updated in VS Code. + /// + /// An event with initial sessions is sent after [initialize] is called. + Stream get debugSessionsChanged; + + /// Executes a VS Code command. + /// + /// Commands can be native VS Code commands or commands registered by the + /// Dart/Flutter extensions. + /// + /// Which commands are available is not part of the API contract so callers + /// should take care when calling APIs that might evolve over time. Future executeCommand(String command, [List? arguments]); + + /// Changes the current Flutter device. + /// + /// The selected device is the same one shown in the status bar in VS Code. + /// Calling this API will update the device for the whole VS Code extension. Future selectDevice(String id); + /// Opens a specific DevTools [page] for the debug session with ID + /// [debugSessionId]. + /// + /// Depending on user settings, this may open embedded (the default) or in an + /// external browser window. + Future openDevToolsPage(String debugSessionId, String page); + static const jsonApiName = 'vsCode'; static const jsonInitializeMethod = 'initialize'; @@ -27,6 +66,12 @@ abstract interface class VsCodeApi { static const jsonSelectDeviceMethod = 'selectDevice'; static const jsonSelectDeviceIdParameter = 'id'; + + static const openDevToolsPageMethod = 'openDevToolsPage'; + static const openDevToolsPageDebugSessionIdParameter = 'debugSessionId'; + static const openDevToolsPagePageParameter = 'page'; + + static const jsonDebugSessionsChangedEvent = 'debugSessionsChanged'; } /// This class defines a device exposed by the Dart/Flutter extensions in VS @@ -55,6 +100,47 @@ abstract interface class VsCodeDevice { static const jsonPlatformTypeField = 'platformType'; } +/// This class defines a debug session exposed by the Dart/Flutter extensions in +/// VS Code (and must match the implementation there). +/// +/// All changes to this file should be backwards-compatible and use +/// [VsCodeCapabilities] to advertise which capabilities are available and +/// handle any changes in behaviour. +abstract interface class VsCodeDebugSession { + String get id; + String get name; + String? get vmServiceUri; + + /// The mode the app is running in. + /// + /// These values are defined by Flutter and at the time of writing can include + /// 'debug', 'profile', 'release' and 'jit_release'. + /// + /// This value may be unavailable (`null`) for Dart/Test sessions or those + /// that have not fully started yet. + String? get flutterMode; + + /// The ID of the device the Flutter app is running on, if available. + String? get flutterDeviceId; + + /// The type of debugger session. If available, this is usually one of: + /// + /// - Dart (dart run) + /// - DartTest (dart test) + /// - Flutter (flutter run) + /// - FlutterTest (flutter test) + /// - Web (webdev serve) + /// - WebTest (webdev test) + String? get debuggerType; + + static const jsonIdField = 'id'; + static const jsonNameField = 'name'; + static const jsonVmServiceUriField = 'vmServiceUri'; + static const jsonFlutterModeField = 'flutterMode'; + static const jsonFlutterDeviceIdField = 'flutterDeviceId'; + static const jsonDebuggerTypeField = 'debuggerType'; +} + /// This class defines a device event sent by the Dart/Flutter extensions in VS /// Code (and must match the implementation there). /// @@ -62,13 +148,32 @@ abstract interface class VsCodeDevice { /// [VsCodeCapabilities] to advertise which capabilities are available and /// handle any changes in behaviour. abstract interface class VsCodeDevicesEvent { + /// The ID of the selected Flutter device in VS Code. + /// + /// This device can be changed with the `selectDevice` method but can also + /// be changed by the VS Code extension (which will emit a new event). String? get selectedDeviceId; + + /// A list of the devices that are available to select. List get devices; static const jsonSelectedDeviceIdField = 'selectedDeviceId'; static const jsonDevicesField = 'devices'; } +/// This class defines a debug session event sent by the Dart/Flutter extensions +/// in VS Code (and must match the implementation there). +/// +/// All changes to this file should be backwards-compatible and use +/// [VsCodeCapabilities] to advertise which capabilities are available and +/// handle any changes in behaviour. +abstract interface class VsCodeDebugSessionsEvent { + /// A list of debug sessions that are currently active in VS Code. + List get sessions; + + static const jsonSessionsField = 'sessions'; +} + /// This class defines the capabilities provided by the current version of the /// Dart/Flutter extensions in VS Code. /// @@ -76,9 +181,19 @@ abstract interface class VsCodeDevicesEvent { /// [VsCodeCapabilities] to advertise which capabilities are available and /// handle any changes in behaviour. abstract interface class VsCodeCapabilities { + /// Whether the `executeCommand` method is available to call to execute VS + /// Code commands. bool get executeCommand; + + /// Whether the `selectDevice` method is available to call to change the + /// selected Flutter device. bool get selectDevice; + /// Whether the `openDevToolsPage` method is available call to open a specific + /// DevTools page. + bool get openDevToolsPage; + static const jsonExecuteCommandField = 'executeCommand'; static const jsonSelectDeviceField = 'selectDevice'; + static const openDevToolsPageField = 'openDevToolsPage'; } diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart new file mode 100644 index 00000000000..4dad863173a --- /dev/null +++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart @@ -0,0 +1,116 @@ +// 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:async'; + +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; + +import '../../shared/screen.dart'; +import '../api/vs_code_api.dart'; + +class DebugSessions extends StatelessWidget { + const DebugSessions(this.api, this.sessions, {super.key}); + + final VsCodeApi api; + final List sessions; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Debug Sessions', + style: Theme.of(context).textTheme.titleSmall, + ), + if (sessions.isEmpty) + const Text('Begin a debug session to use DevTools.') + else + Table( + columnWidths: const {0: FlexColumnWidth()}, + defaultColumnWidth: + FixedColumnWidth(defaultIconSize + defaultSpacing), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + for (final session in sessions) + _createSessionRow(context, session), + ], + ), + ], + ); + } + + TableRow _createSessionRow(BuildContext context, VsCodeDebugSession session) { + // TODO(dantup): What to show if mode is unknown (null)? + final name = session.name; + final mode = session.flutterMode; + final isDebug = mode == 'debug'; + final isProfile = mode == 'profile'; + // final isRelease = mode == 'release' || mode == 'jit_release'; + final isFlutter = session.debuggerType?.contains('Flutter') ?? false; + + return TableRow( + children: [ + Text( + '$name ($mode)', + style: Theme.of(context).textTheme.titleSmall, + ), + // TODO(dantup): Ensure the order matches the DevTools tab bar (if + // possible, share this order). + if (api.capabilities.openDevToolsPage) ...[ + // TODO(dantup): Make these conditions use the real screen + // conditions and/or verify if these conditions are correct. + _devToolsButton( + session, + ScreenMetaData.inspector, + enabled: isFlutter && isDebug, + ), + _devToolsButton( + session, + ScreenMetaData.cpuProfiler, + enabled: isDebug || isProfile, + ), + _devToolsButton( + session, + ScreenMetaData.memory, + enabled: isDebug || isProfile, + ), + _devToolsButton( + session, + ScreenMetaData.performance, + ), + _devToolsButton( + session, + ScreenMetaData.network, + enabled: isDebug, + ), + _devToolsButton( + session, + ScreenMetaData.logging, + ), + // TODO(dantup): Check other screens (like appSize) work embedded and + // add here. + ], + ], + ); + } + + Widget _devToolsButton( + VsCodeDebugSession session, + ScreenMetaData screen, { + bool enabled = true, + }) { + return DevToolsTooltip( + message: screen.title ?? screen.id, + padding: const EdgeInsets.all(denseSpacing), + child: TextButton( + onPressed: enabled + ? () => unawaited(api.openDevToolsPage(session.id, screen.id)) + : null, + child: Icon(screen.icon, size: actionsIconSize), + ), + ); + } +} diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart new file mode 100644 index 00000000000..e5b9ecaea1d --- /dev/null +++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart @@ -0,0 +1,73 @@ +// 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:async'; + +import 'package:flutter/material.dart'; + +import '../api/vs_code_api.dart'; + +class Devices extends StatelessWidget { + const Devices( + this.api, + this.devices, { + required this.selectedDeviceId, + super.key, + }); + + final VsCodeApi api; + final List devices; + final String? selectedDeviceId; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Devices', + style: Theme.of(context).textTheme.titleSmall, + ), + if (devices.isEmpty) + const Text('Connect a device or enable web/desktop platforms.') + else + Table( + columnWidths: const { + 0: FlexColumnWidth(3), + 1: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + for (final device in devices) + _createDeviceRow( + context, + device, + isSelected: device.id == selectedDeviceId, + ), + ], + ), + ], + ); + } + + TableRow _createDeviceRow( + BuildContext context, + VsCodeDevice device, { + required bool isSelected, + }) { + return TableRow( + children: [ + Align( + alignment: Alignment.centerLeft, + child: TextButton( + child: Text(device.name), + onPressed: () => unawaited(api.selectDevice(device.id)), + ), + ), + // TODO(dantup): Use a highlighted/select row for this instead of text. + Text(isSelected ? 'current device' : ''), + ], + ); + } +} diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart index 741ccd6c5a8..413c2ebcb02 100644 --- a/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart +++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart @@ -11,6 +11,8 @@ import '../../../devtools_app.dart'; import '../../shared/feature_flags.dart'; import '../api/dart_tooling_api.dart'; import '../api/vs_code_api.dart'; +import 'debug_sessions.dart'; +import 'devices.dart'; /// A general Flutter sidebar panel for embedding inside IDEs. /// @@ -64,52 +66,54 @@ class _VsCodeConnectedPanelState extends State<_VsCodeConnectedPanel> { @override Widget build(BuildContext context) { - return Column( - children: [ - const SizedBox(height: defaultSpacing), - if (widget.api.capabilities.executeCommand) - ElevatedButton( - onPressed: () => - unawaited(widget.api.executeCommand('flutter.createProject')), - child: const Text('New Flutter Project'), - ), - if (widget.api.capabilities.selectDevice) + return Padding( + padding: const EdgeInsets.all(denseSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: defaultSpacing), + if (widget.api.capabilities.executeCommand) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => unawaited( + widget.api.executeCommand('flutter.createProject'), + ), + child: const Text('New Flutter Project'), + ), + ElevatedButton( + onPressed: () => unawaited( + widget.api.executeCommand('flutter.doctor'), + ), + child: const Text('Run Flutter Doctor'), + ), + ], + ), + const SizedBox(height: defaultSpacing), StreamBuilder( - stream: widget.api.devicesChanged, + stream: widget.api.debugSessionsChanged, builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SizedBox.shrink(); - } - final deviceEvent = snapshot.data!; - return Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - for (final device in deviceEvent.devices) - TableRow( - children: [ - TextButton( - child: Text(device.name), - onPressed: () => - unawaited(widget.api.selectDevice(device.id)), - ), - Text( - device.id == deviceEvent.selectedDeviceId - ? '(selected)' - : '', - ), - ], - ), - ], - ); + final sessions = snapshot.data?.sessions ?? const []; + return DebugSessions(widget.api, sessions); }, ), - if (widget.api.capabilities.executeCommand) - ElevatedButton( - onPressed: () => - unawaited(widget.api.executeCommand('flutter.doctor')), - child: const Text('Run Flutter Doctor'), - ), - ], + const SizedBox(height: defaultSpacing), + if (widget.api.capabilities.selectDevice) + StreamBuilder( + stream: widget.api.devicesChanged, + builder: (context, snapshot) { + final devices = snapshot.data?.devices ?? const []; + final selectedDeviceId = snapshot.data?.selectedDeviceId; + return Devices( + widget.api, + devices, + selectedDeviceId: selectedDeviceId, + ); + }, + ), + ], + ), ); } } diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart index 50ed69336e9..b49d4a1754b 100644 --- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart +++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart @@ -6,6 +6,7 @@ import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/shared/feature_flags.dart'; import 'package:devtools_app/src/standalone_ui/vs_code/flutter_panel.dart'; import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; import 'package:stager/stager.dart'; @@ -58,5 +59,6 @@ class VsCodeScene extends Scene { @override Future setUp() async { FeatureFlags.vsCodeSidebarTooling = true; + setGlobal(IdeTheme, IdeTheme()); } } diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart index b71fd3924fb..1c136354bfc 100644 --- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart +++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart @@ -115,13 +115,44 @@ class _VsCodeFlutterPanelMockEditorState const Text(''), Row( children: [ + const Text('Devices: '), ElevatedButton( onPressed: api.connectDevices, - child: const Text('Connect Devices'), + child: const Text('Connect'), ), ElevatedButton( onPressed: api.disconnectDevices, - child: const Text('Disconnect Devices'), + child: const Text('Disconnect'), + ), + ], + ), + const Text(''), + Row( + children: [ + const Text('Debug Sessions: '), + ElevatedButton( + onPressed: () => api.startSession(null), + child: const Text('Start null'), + ), + ElevatedButton( + onPressed: () => api.startSession('debug'), + child: const Text('Start debug'), + ), + ElevatedButton( + onPressed: () => api.startSession('profile'), + child: const Text('Start profile'), + ), + ElevatedButton( + onPressed: () => api.startSession('release'), + child: const Text('Start release'), + ), + ElevatedButton( + onPressed: () => api.startSession('jit_release'), + child: const Text('Start jit_release'), + ), + ElevatedButton( + onPressed: () => api.endSessions(), + child: const Text('Stop All'), ), ], ), diff --git a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart index 24b781e8134..8adbfbdb295 100644 --- a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart +++ b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart @@ -65,11 +65,13 @@ class MockDartToolingApi extends DartToolingApiImpl { return { 'executeCommand': true, 'selectDevice': true, + 'openDevToolsPage': true, }; }); server.registerMethod('vsCode.initialize', initialize); server.registerMethod('vsCode.executeCommand', executeCommand); server.registerMethod('vsCode.selectDevice', selectDevice); + server.registerMethod('vsCode.openDevToolsPage', openDevToolsPage); } final json_rpc_2.Peer client; @@ -102,6 +104,12 @@ class MockDartToolingApi extends DartToolingApiImpl { /// The current set of devices being presented to the embedded panel. final _devices = []; + /// The current set of debug sessions that are running. + final _debugSessions = []; + + /// The number of the next debug session to start. + var _nextDebugSessionNumber = 1; + /// The currently selected device presented to the embedded panel. String? _selectedDeviceId; @@ -136,6 +144,9 @@ class MockDartToolingApi extends DartToolingApiImpl { return true; } + /// Simulates opening a DevTools feature. + Future openDevToolsPage(json_rpc_2.Parameters parameters) async {} + /// Simulates devices being connected in the IDE by notifying the embedded /// panel about a set of test devices. void connectDevices() { @@ -146,6 +157,28 @@ class MockDartToolingApi extends DartToolingApiImpl { _sendDevicesChanged(); } + /// Simulates starting a debug session. + void startSession(String? mode) { + final sessionNum = _nextDebugSessionNumber++; + _debugSessions.add( + VsCodeDebugSessionImpl( + id: 'debug-$sessionNum', + name: 'Session $sessionNum', + vmServiceUri: 'ws://127.0.0.1:1234/ws', + flutterMode: mode, + flutterDeviceId: 'flutter-tester', + debuggerType: 'Flutter', + ), + ); + _sendDebugSessionsChanged(); + } + + /// Simulates ending all debug sessions. + void endSessions() { + _debugSessions.clear(); + _sendDebugSessionsChanged(); + } + /// Simulates devices being disconnected in the IDE by notifying the embedded /// panel about a now-empty set of devices. void disconnectDevices() { @@ -156,11 +189,20 @@ class MockDartToolingApi extends DartToolingApiImpl { void _sendDevicesChanged() { server.sendNotification( - 'vsCode.devicesChanged', + '${VsCodeApi.jsonApiName}.${VsCodeApi.jsonDevicesChangedEvent}', VsCodeDevicesEventImpl( devices: _devices, selectedDeviceId: _selectedDeviceId, ).toJson(), ); } + + void _sendDebugSessionsChanged() { + server.sendNotification( + '${VsCodeApi.jsonApiName}.${VsCodeApi.jsonDebugSessionsChangedEvent}', + VsCodeDebugSessionsEventImpl( + sessions: _debugSessions, + ).toJson(), + ); + } }