Skip to content

Commit

Permalink
Include static DevTools extensions in the Flutter sidebar (flutter#7604)
Browse files Browse the repository at this point in the history
  • Loading branch information
kenzieschmoll committed Apr 25, 2024
1 parent 564ee86 commit e15c472
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,10 @@ class ExtensionService extends DisposableController
_appRoot = null;
if (fixedAppRoot != null) {
_appRoot = fixedAppRoot;
} else if (serviceConnection
.serviceManager.connectedState.value.connected &&
} else if (!ignoreServiceConnection &&
serviceConnection.serviceManager.connectedState.value.connected &&
serviceConnection.serviceManager.isolateManager.mainIsolate.value !=
null &&
!ignoreServiceConnection) {
null) {
_appRoot = await _connectedAppRoot();
}

Expand Down
137 changes: 131 additions & 6 deletions packages/devtools_app/lib/src/standalone_ui/vs_code/devtools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@
import 'dart:async';

import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_shared/devtools_extensions.dart';
import 'package:flutter/material.dart';

import '../../extensions/extension_screen.dart';
import '../../extensions/extension_service.dart';
import '../../shared/analytics/analytics.dart' as ga;
import '../../shared/analytics/constants.dart' as gac;
import '../../shared/screen.dart';
import '../api/vs_code_api.dart';

/// A widget that displays DevTools options, including buttons to open static
/// screens, and a list of static DevTools extensions available for the IDE
/// workspace.
class DevToolsSidebarOptions extends StatelessWidget {
const DevToolsSidebarOptions({
required this.api,
required this.hasDebugSessions,
super.key,
});

final VsCodeApi api;
final bool hasDebugSessions;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -59,12 +68,14 @@ class DevToolsSidebarOptions extends StatelessWidget {
],
),
const PaddedDivider.thin(),
const Padding(
padding: EdgeInsets.only(left: borderPadding),
child: Text(
'Begin a debug session to use tools that require a running '
'application.',
),
_RuntimeToolInstructions(
hasDebugSessions: hasDebugSessions,
toolDescription: 'tools',
),
const SizedBox(height: denseSpacing),
_DevToolsExtensions(
api: api,
hasDebugSessions: hasDebugSessions,
),
],
);
Expand All @@ -84,6 +95,97 @@ class DevToolsSidebarOptions extends StatelessWidget {
}
}

class _DevToolsExtensions extends StatefulWidget {
const _DevToolsExtensions({
required this.api,
required this.hasDebugSessions,
});

final VsCodeApi api;
final bool hasDebugSessions;

@override
State<_DevToolsExtensions> createState() => _DevToolsExtensionsState();
}

class _DevToolsExtensionsState extends State<_DevToolsExtensions>
with AutoDisposeMixin {
ExtensionService? _extensionService;

var extensions = <DevToolsExtensionConfig>[];

@override
void initState() {
super.initState();
_initExtensions();
}

void _initExtensions() {
_extensionService = ExtensionService(ignoreServiceConnection: true);

cancelListeners();
extensions = _extensionService!.visibleExtensions.value;
addAutoDisposeListener(_extensionService!.visibleExtensions, () {
setState(() {
extensions = _extensionService!.visibleExtensions.value;
});
});

unawaited(_extensionService!.initialize());
}

@override
void dispose() {
_extensionService?.dispose();
_extensionService = null;
super.dispose();
}

@override
Widget build(BuildContext context) {
if (extensions.isEmpty) return const SizedBox();

final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'DevTools Extensions',
style: theme.textTheme.titleMedium,
),
Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
for (final ext in extensions)
_createDevToolsScreenRow(
label: ext.name,
icon: ext.icon,
api: widget.api,
theme: theme,
onPressed: () {
ga.select(
gac.VsCodeFlutterSidebar.id,
gac.VsCodeFlutterSidebar.openDevToolsScreen(
gac.DevToolsExtensionEvents.extensionScreenName(ext),
),
);
unawaited(
widget.api.openDevToolsPage(null, page: ext.screenId),
);
},
),
],
),
const PaddedDivider.thin(),
_RuntimeToolInstructions(
hasDebugSessions: widget.hasDebugSessions,
toolDescription: 'extensions',
),
],
);
}
}

TableRow _createDevToolsScreenRow({
required String label,
required IconData icon,
Expand Down Expand Up @@ -129,3 +231,26 @@ TableRow _createDevToolsScreenRow({
],
);
}

class _RuntimeToolInstructions extends StatelessWidget {
const _RuntimeToolInstructions({
required this.hasDebugSessions,
required this.toolDescription,
});

final bool hasDebugSessions;
final String toolDescription;

@override
Widget build(BuildContext context) {
final instruction = hasDebugSessions
? 'Open the tools menu for a debug session to access'
: 'Begin a debug session to use';
return Padding(
padding: const EdgeInsets.only(left: borderPadding),
child: Text(
'$instruction $toolDescription that require a running application.',
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';

import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/material.dart';

import '../../shared/analytics/analytics.dart' as ga;
Expand Down Expand Up @@ -67,12 +68,24 @@ class _VsCodeConnectedPanel extends StatefulWidget {
State<_VsCodeConnectedPanel> createState() => _VsCodeConnectedPanelState();
}

class _VsCodeConnectedPanelState extends State<_VsCodeConnectedPanel> {
class _VsCodeConnectedPanelState extends State<_VsCodeConnectedPanel>
with AutoDisposeMixin {
var debugSessions = <VsCodeDebugSession>[];

@override
void initState() {
super.initState();

unawaited(widget.api.initialize());

cancelStreamSubscriptions();
autoDisposeStreamSubscription(
widget.api.debugSessionsChanged.listen((event) {
setState(() {
debugSessions = event.sessions;
});
}),
);
}

@override
Expand All @@ -95,17 +108,10 @@ class _VsCodeConnectedPanelState extends State<_VsCodeConnectedPanel> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StreamBuilder(
stream: widget.api.debugSessionsChanged,
builder: (context, debugSessionsSnapshot) {
final sessions =
debugSessionsSnapshot.data?.sessions ?? const [];
return DebugSessions(
api: widget.api,
sessions: sessions,
deviceMap: deviceMap,
);
},
DebugSessions(
api: widget.api,
sessions: debugSessions,
deviceMap: deviceMap,
),
const SizedBox(height: defaultSpacing),
if (widget.api.capabilities.selectDevice) ...[
Expand All @@ -117,7 +123,10 @@ class _VsCodeConnectedPanelState extends State<_VsCodeConnectedPanel> {
),
const SizedBox(height: denseSpacing),
],
DevToolsSidebarOptions(api: widget.api),
DevToolsSidebarOptions(
api: widget.api,
hasDebugSessions: debugSessions.isNotEmpty,
),
],
);
},
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ TODO: Remove this section if there are not any general updates.

## VS Code Sidebar updates

* Added a DevTools section with a list of tools that are available without a debug
session. - [#7598](https://github.com/flutter/devtools/pull/7598)
* Added a DevTools section with a list of tools and extensions that are available without
a debug session. - [#7598](https://github.com/flutter/devtools/pull/7598), [#7604](https://github.com/flutter/devtools/pull/7604)

## DevTools Extension updates

Expand Down
117 changes: 117 additions & 0 deletions packages/devtools_app/test/standalone_ui/vs_code/devtools_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2024 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/devtools_app.dart';
import 'package:devtools_app/src/standalone_ui/api/impl/vs_code_api.dart';
import 'package:devtools_app/src/standalone_ui/vs_code/devtools.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_test/devtools_test.dart';
import 'package:devtools_test/helpers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

void main() {
const windowSize = Size(2000.0, 2000.0);

late MockVsCodeApi mockVsCodeApi;

setUpAll(() {
// Set test mode so that the debug list of extensions will be used.
setTestMode();
});

setUp(() {
mockVsCodeApi = MockVsCodeApi();
when(mockVsCodeApi.capabilities).thenReturn(
VsCodeCapabilitiesImpl({
'executeCommand': true,
'selectDevice': true,
'openDevToolsPage': true,
'openDevToolsExternally': true,
'hotReload': true,
'hotRestart': true,
}),
);
setGlobal(IdeTheme, IdeTheme());
setGlobal(PreferencesController, PreferencesController());
});

Future<void> pumpDevToolsSidebarOptions(
WidgetTester tester, {
bool hasDebugSessions = false,
}) async {
await tester.pumpWidget(
wrap(
DevToolsSidebarOptions(
api: mockVsCodeApi,
hasDebugSessions: hasDebugSessions,
),
),
);
// Additional pump to allow for initializing the extensions service.
await tester.pumpAndSettle();
}

group('$DevToolsSidebarOptions', () {
testWidgetsWithWindowSize(
'includes static DevTools screens',
windowSize,
(tester) async {
await pumpDevToolsSidebarOptions(tester);
expect(find.text('DevTools'), findsOneWidget);
expect(find.text(ScreenMetaData.appSize.title!), findsOneWidget);
expect(find.text(ScreenMetaData.deepLinks.title!), findsOneWidget);
expect(find.text('Open in Browser'), findsOneWidget);
expect(
find.text(
'Begin a debug session to use tools that require a running '
'application.',
),
findsOneWidget,
);
},
);

testWidgetsWithWindowSize(
'includes DevTools extensions',
windowSize,
(tester) async {
await pumpDevToolsSidebarOptions(tester);
expect(find.text('DevTools Extensions'), findsOneWidget);
expect(find.text('bar'), findsOneWidget);
expect(
find.text(
'Begin a debug session to use extensions that require a running '
'application.',
),
findsOneWidget,
);
},
);

testWidgetsWithWindowSize(
'changes runtime tool instructions with non-empty debug sessions',
windowSize,
(tester) async {
await pumpDevToolsSidebarOptions(tester, hasDebugSessions: true);
expect(
find.text(
'Open the tools menu for a debug session to access tools that '
'require a running application.',
),
findsOneWidget,
);
expect(
find.text(
'Open the tools menu for a debug session to access extensions that '
'require a running application.',
),
findsOneWidget,
);
},
);
});
}

0 comments on commit e15c472

Please sign in to comment.