Skip to content

Commit

Permalink
Pull available extensions from the server
Browse files Browse the repository at this point in the history
  • Loading branch information
kenzieschmoll committed Nov 15, 2023
1 parent 3d26af5 commit c609e92
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 79 deletions.
103 changes: 65 additions & 38 deletions packages/devtools_app/lib/src/extensions/extension_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ import '../shared/server/server.dart' as server;

class ExtensionService extends DisposableController
with AutoDisposeControllerMixin {
ExtensionService({this.fixedAppRootPath});

/// The fixed (unchanging) root path for the application this
/// [ExtensionService] will manage DevTools extensions for.
///
/// When null, the root path will instead be calculated from the
/// [serviceManager]'s currently connected app. See [_initRootPath].
final String? fixedAppRootPath;

/// The root path for the Dart / Flutter application this [ExtensionService]
/// will manage DevTools extensions for.
String? _rootPath;

/// All the DevTools extensions that are available for the connected
/// application, regardless of whether they have been enabled or disabled
/// by the user.
Expand Down Expand Up @@ -45,31 +58,43 @@ class ExtensionService extends DisposableController
<String, ValueNotifier<ExtensionEnabledState>>{};

Future<void> initialize() async {
await _initRootPath();
await _maybeRefreshExtensions();
addAutoDisposeListener(
serviceConnection.serviceManager.connectedState,
() async {
if (serviceConnection.serviceManager.connectedState.value.connected) {
await _maybeRefreshExtensions();
} else {
_reset();
}
},
);

// TODO(https://github.com/flutter/flutter/issues/134470): refresh on
// hot reload and hot restart events instead.
addAutoDisposeListener(
serviceConnection.serviceManager.isolateManager.mainIsolate,
() async {
if (serviceConnection.serviceManager.isolateManager.mainIsolate.value !=
null) {
await _maybeRefreshExtensions();
} else {
_reset();
}
},
);
cancelListeners();

// We only need to add VM service manager related listeners when we are
// interacting with the currently connected app (i.e. when
// [fixedAppRootPath] is null).
if (fixedAppRootPath == null) {
addAutoDisposeListener(
serviceConnection.serviceManager.connectedState,
() async {
if (serviceConnection.serviceManager.connectedState.value.connected) {
await _initRootPath();
await _maybeRefreshExtensions();
} else {
_reset();
}
},
);

// TODO(https://github.com/flutter/flutter/issues/134470): refresh on
// hot reload and hot restart events instead.
addAutoDisposeListener(
serviceConnection.serviceManager.isolateManager.mainIsolate,
() async {
if (serviceConnection
.serviceManager.isolateManager.mainIsolate.value !=
null) {
await _initRootPath();
await _maybeRefreshExtensions();
} else {
_reset();
}
},
);
}

addAutoDisposeListener(
preferences.devToolsExtensions.showOnlyEnabledExtensions,
Expand All @@ -83,29 +108,31 @@ class ExtensionService extends DisposableController
// .dart_tool/package_config.json file for changes.
}

Future<void> _initRootPath() async {
_rootPath = fixedAppRootPath ?? await _connectedAppRootPath();
}

Future<void> _maybeRefreshExtensions() async {
final appRootPath = await _connectedAppRootPath();
if (appRootPath == null) return;
if (_rootPath == null) return;

_refreshInProgress.value = true;
_availableExtensions.value =
await server.refreshAvailableExtensions(appRootPath)
await server.refreshAvailableExtensions(_rootPath!)
..sort();
await _refreshExtensionEnabledStates();
_refreshInProgress.value = false;
}

Future<void> _refreshExtensionEnabledStates() async {
final appRootPath = await _connectedAppRootPath();
if (appRootPath == null) return;
if (_rootPath == null) return;

final onlyIncludeEnabled =
preferences.devToolsExtensions.showOnlyEnabledExtensions.value;

final visible = <DevToolsExtensionConfig>[];
for (final extension in _availableExtensions.value) {
final stateFromOptionsFile = await server.extensionEnabledState(
rootPath: appRootPath,
rootPath: _rootPath!,
extensionName: extension.name,
);
final stateNotifier = _extensionEnabledStates.putIfAbsent(
Expand Down Expand Up @@ -133,18 +160,18 @@ class ExtensionService extends DisposableController
DevToolsExtensionConfig extension, {
required bool enable,
}) async {
final appRootPath = await _connectedAppRootPath();
if (appRootPath != null) {
await server.extensionEnabledState(
rootPath: appRootPath,
extensionName: extension.name,
enable: enable,
);
await _refreshExtensionEnabledStates();
}
if (_rootPath == null) return;

await server.extensionEnabledState(
rootPath: _rootPath!,
extensionName: extension.name,
enable: enable,
);
await _refreshExtensionEnabledStates();
}

void _reset() {
_rootPath = null;
_availableExtensions.value = [];
_visibleExtensions.value = [];
_extensionEnabledStates.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ bool debugAnalytics = false;
///
/// This flag should never be checked in with a value of true - this is covered
/// by a test.
final debugDevToolsExtensions = _debugDevToolsExtensions || integrationTestMode;
final debugDevToolsExtensions =
_debugDevToolsExtensions || integrationTestMode || stagerMode;
const _debugDevToolsExtensions = false;

List<DevToolsExtensionConfig> debugHandleRefreshAvailableExtensions(
Expand Down
10 changes: 10 additions & 0 deletions packages/devtools_app/lib/src/shared/globals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/foundation.dart';

import '../extensions/extension_service.dart';
import '../screens/debugger/breakpoint_manager.dart';
Expand Down Expand Up @@ -67,3 +68,12 @@ bool _integrationTestMode = false;
void setIntegrationTestMode() {
_integrationTestMode = true;
}

/// Whether DevTools is being run as a stager app.
bool get stagerMode => _stagerMode;
bool _stagerMode = false;
void setStagerMode() {
if (!kReleaseMode) {
_stagerMode = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import 'dart:async';

import 'package:devtools_app_shared/ui.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/constants.dart';
import '../../shared/feature_flags.dart';
import '../../shared/globals.dart';
import '../../shared/screen.dart';
import '../api/vs_code_api.dart';

Expand Down Expand Up @@ -115,7 +116,7 @@ class DebugSessions extends StatelessWidget {
}
}

class _DevToolsMenu extends StatelessWidget {
class _DevToolsMenu extends StatefulWidget {
const _DevToolsMenu({
required this.api,
required this.session,
Expand All @@ -134,6 +135,46 @@ class _DevToolsMenu extends StatelessWidget {
final bool isRelease;
final bool isWeb;

@override
State<_DevToolsMenu> createState() => _DevToolsMenuState();
}

class _DevToolsMenuState extends State<_DevToolsMenu> {
ExtensionService? _extensionServiceForSession;

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

Future<void> _initExtensions() async {
final sessionRootPath = widget.session.projectRootPath;
if (sessionRootPath != null) {
setState(() {
_extensionServiceForSession =
ExtensionService(fixedAppRootPath: sessionRootPath);
unawaited(_extensionServiceForSession!.initialize());
});
}
}

@override
void didUpdateWidget(_DevToolsMenu oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.session != widget.session) {
_extensionServiceForSession?.dispose();
unawaited(_initExtensions());
}
}

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

@override
Widget build(BuildContext context) {
final normalDirection = Directionality.of(context);
Expand All @@ -144,13 +185,13 @@ class _DevToolsMenu extends StatelessWidget {
Widget devToolsButton(ScreenMetaData screen) {
final title = screen.title ?? screen.id;
String? disabledReason;
if (isRelease) {
if (widget.isRelease) {
disabledReason = 'Not available in release mode';
} else if (screen.requiresFlutter && !isFlutter) {
} else if (screen.requiresFlutter && !widget.isFlutter) {
disabledReason = 'Only available for Flutter applications';
} else if (screen.requiresDebugBuild && !isDebug) {
} else if (screen.requiresDebugBuild && !widget.isDebug) {
disabledReason = 'Only available in debug mode';
} else if (screen.requiresDartVm && isWeb) {
} else if (screen.requiresDartVm && widget.isWeb) {
disabledReason = 'Not available when running on the web';
}

Expand All @@ -163,7 +204,7 @@ class _DevToolsMenu extends StatelessWidget {
gac.VsCodeFlutterSidebar.id,
gac.VsCodeFlutterSidebar.openDevToolsScreen(screen.id),
);
unawaited(api.openDevToolsPage(session.id, screen.id));
unawaited(widget.api.openDevToolsPage(widget.session.id, screen.id));
},
);
}
Expand All @@ -181,7 +222,15 @@ class _DevToolsMenu extends StatelessWidget {
.where(_shouldIncludeScreen)
.map(devToolsButton)
.toList(),
const ExtensionScreenMenuItem(),
if (_extensionServiceForSession != null)
ValueListenableBuilder(
valueListenable: _extensionServiceForSession!.visibleExtensions,
builder: (context, extensions, _) {
return extensions.isEmpty
? const SizedBox.shrink()
: ExtensionScreenMenuItem(extensions: extensions);
},
),
],
builder: (context, controller, child) => IconButton(
onPressed: () {
Expand Down Expand Up @@ -252,34 +301,28 @@ class DevToolsScreenMenuItem extends StatelessWidget {
}

class ExtensionScreenMenuItem extends StatelessWidget {
const ExtensionScreenMenuItem({super.key});
const ExtensionScreenMenuItem({super.key, required this.extensions});

final List<DevToolsExtensionConfig> extensions;

@override
Widget build(BuildContext context) {
// TODO: we are not getting the proper list of visible extensions here.
// More than likely, we will need to as the DevTools server for the list of
// available extensions directly based on the debug session's root path.
return ValueListenableBuilder(
valueListenable: extensionService.visibleExtensions,
builder: (context, extensions, _) {
return SubmenuButton(
menuStyle: const MenuStyle(
alignment: Alignment.centerLeft,
),
menuChildren: extensions
.map(
(e) => DevToolsScreenMenuItem(
title: e.name,
icon: e.icon,
// TODO: this should open the extension screen in the browser,
// or if possible, in an embedded iFrame in VS code.
onPressed: () {},
),
)
.toList(),
child: const Text('Extensions'),
);
},
return SubmenuButton(
menuStyle: const MenuStyle(
alignment: Alignment.centerLeft,
),
menuChildren: extensions
.map(
(e) => DevToolsScreenMenuItem(
title: e.name,
icon: e.icon,
// TODO: this should open the extension screen in the browser,
// or if possible, in an embedded iFrame in VS code.
onPressed: () {},
),
)
.toList(),
child: const Text('Extensions'),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import 'package:devtools_app/devtools_app.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:devtools_test/devtools_test.dart';
import 'package:flutter/material.dart';
import 'package:stager/stager.dart';

import '../../../test_infra/test_data/dart_tooling_api/mock_api.dart';
import '../../test_data/extensions.dart';
import 'vs_code_mock_editor.dart';

final _api = MockDartToolingApi();
Expand Down Expand Up @@ -59,10 +57,8 @@ class VsCodeScene extends Scene {

@override
Future<void> setUp() async {
setStagerMode();
setGlobal(IdeTheme, IdeTheme());
setGlobal(
ExtensionService,
await createMockExtensionServiceWithDefaults(testExtensions),
);
setGlobal(PreferencesController, PreferencesController());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class MockDartToolingApi extends DartToolingApiImpl {
flutterMode: mode,
flutterDeviceId: deviceId,
debuggerType: 'Flutter',
projectRootPath: null,
projectRootPath: '/mock/root/path',
),
);
_sendDebugSessionsChanged();
Expand Down

0 comments on commit c609e92

Please sign in to comment.