Skip to content
Draft
Show file tree
Hide file tree
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
143 changes: 86 additions & 57 deletions packages/devtools_app/lib/src/shared/editor/editor_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,85 +4,54 @@

import 'dart:async';

import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_shared/devtools_shared.dart';
import 'package:dtd/dtd.dart';
import 'package:flutter/foundation.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:logging/logging.dart';

import '../analytics/constants.dart';
import '../framework/app_error_handling.dart';
import 'api_classes.dart';

final _log = Logger('editor_client');

/// A client wrapper that connects to an editor over DTD.
///
/// Changes made to the editor services/events should be considered carefully to
/// ensure they are not breaking changes to already-shipped editors.
class EditorClient extends DisposableController
with AutoDisposeControllerMixin {
EditorClient(this._dtd) {
EditorClient(this._dtdManager) {
unawaited(initialized); // Trigger async initialization.
}

final DartToolingDaemon _dtd;
final DTDManager _dtdManager;
late final initialized = _initialize();

DartToolingDaemon get _dtd => _dtdManager.connection.value!;

String get gaId => EditorSidebar.id;

Future<void> _initialize() async {
autoDisposeStreamSubscription(
_dtd.onEvent(CoreDtdServiceConstants.servicesStreamId).listen((data) {
final kind = data.kind;
if (kind != CoreDtdServiceConstants.serviceRegisteredKind &&
kind != CoreDtdServiceConstants.serviceUnregisteredKind) {
return;
}

_dtdManager.serviceRegistrationBroadcastStream.listen((data) {
final service = data.data[DtdParameters.service] as String?;
if (service == null ||
(service != editorServiceName && service != lspServiceName)) {
return;
}

if (service == null) return;
final isRegistered =
kind == CoreDtdServiceConstants.serviceRegisteredKind;
data.kind == CoreDtdServiceConstants.serviceRegisteredKind;
final method = data.data[DtdParameters.method] as String;
final capabilities =
data.data[DtdParameters.capabilities] as Map<String, Object?>?;
final lspMethod = LspMethod.fromMethodName(method);
if (lspMethod != null) {
lspMethod.isRegistered = isRegistered;
if (lspMethod == LspMethod.editableArguments) {
// Update the notifier so that the Property Editor is aware that the
// editableArguments API is registered.
_editableArgumentsApiIsRegistered.value = isRegistered;
}
} else if (method == EditorMethod.getDevices.name) {
_supportsGetDevices = isRegistered;
} else if (method == EditorMethod.getDebugSessions.name) {
_supportsGetDebugSessions = isRegistered;
} else if (method == EditorMethod.selectDevice.name) {
_supportsSelectDevice = isRegistered;
} else if (method == EditorMethod.hotReload.name) {
_supportsHotReload = isRegistered;
} else if (method == EditorMethod.hotRestart.name) {
_supportsHotRestart = isRegistered;
} else if (method == EditorMethod.openDevToolsPage.name) {
_supportsOpenDevToolsPage = isRegistered;
_supportsOpenDevToolsForceExternal =
capabilities?[Field.supportsForceExternal] == true;
} else {
return;
}

final info = isRegistered
? ServiceRegistered(
service: service,
method: method,
capabilities: capabilities,
)
: ServiceUnregistered(service: service, method: method);
_editorServiceChangedController.add(info);
_handleServiceRegistration(
service: service,
method: method,
capabilities: capabilities,
isRegistered: isRegistered,
);
}),
);

Expand Down Expand Up @@ -126,15 +95,75 @@ class EditorClient extends DisposableController
}
}),
);
await [
_dtd.streamListen(CoreDtdServiceConstants.servicesStreamId),
_dtd.streamListen(editorStreamName).catchError((_) {
// Because we currently call streamListen in two places (here and
// ThemeManager) this can fail. It doesn't matter if this happens,
// however we should refactor this code to better support using the DTD
// connection in multiple places without them having to coordinate.
}),
].wait;

await _dtd.streamListen(editorStreamName).catchError((_) {
// Because we currently call streamListen in two places (here and
// ThemeManager) this can fail. It doesn't matter if this happens,
// however we should refactor this code to better support using the DTD
// connection in multiple places without them having to coordinate.
});

// Check if any client services have already been registered against DTD.
try {
final response = await _dtd.getRegisteredServices();
for (final service in response.clientServices) {
for (final method in service.methods.values) {
_handleServiceRegistration(
service: service.name,
method: method.name,
capabilities: method.capabilities,
);
}
}
} catch (e) {
_log.warning('Failed to fetch registered services: $e');
}
}

void _handleServiceRegistration({
required String service,
required String method,
Map<String, Object?>? capabilities,
bool isRegistered = true,
}) {
if (service != editorServiceName && service != lspServiceName) {
return;
}

final lspMethod = LspMethod.fromMethodName(method);
if (lspMethod != null) {
lspMethod.isRegistered = isRegistered;
if (lspMethod == LspMethod.editableArguments) {
// Update the notifier so that the Property Editor is aware that the
// editableArguments API is registered.
_editableArgumentsApiIsRegistered.value = isRegistered;
}
} else if (method == EditorMethod.getDevices.name) {
_supportsGetDevices = isRegistered;
} else if (method == EditorMethod.getDebugSessions.name) {
_supportsGetDebugSessions = isRegistered;
} else if (method == EditorMethod.selectDevice.name) {
_supportsSelectDevice = isRegistered;
} else if (method == EditorMethod.hotReload.name) {
_supportsHotReload = isRegistered;
} else if (method == EditorMethod.hotRestart.name) {
_supportsHotRestart = isRegistered;
} else if (method == EditorMethod.openDevToolsPage.name) {
_supportsOpenDevToolsPage = isRegistered;
_supportsOpenDevToolsForceExternal =
capabilities?[Field.supportsForceExternal] == true;
} else {
return;
}

final info = isRegistered
? ServiceRegistered(
service: service,
method: method,
capabilities: capabilities,
)
: ServiceUnregistered(service: service, method: method);
_editorServiceChangedController.add(info);
}

/// Close the connection to DTD.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

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:dtd/dtd.dart';
import 'package:flutter/material.dart';

import '../../../framework/scaffold/report_feedback_button.dart';
Expand All @@ -20,9 +20,9 @@ import 'property_editor_view.dart';

/// The side panel for the Property Editor.
class PropertyEditorPanel extends StatefulWidget {
const PropertyEditorPanel(this.dtd, {super.key});
const PropertyEditorPanel(this.dtdManager, {super.key});

final DartToolingDaemon dtd;
final DTDManager dtdManager;

@override
State<PropertyEditorPanel> createState() => _PropertyEditorPanelState();
Expand All @@ -38,7 +38,7 @@ class _PropertyEditorPanelState extends State<PropertyEditorPanel> {
void initState() {
super.initState();

final editor = EditorClient(widget.dtd);
final editor = EditorClient(widget.dtdManager);
ga.screen(gac.PropertyEditorSidebar.id);
unawaited(
_editor = editor.initialized.then((_) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ enum StandaloneScreenType {
),
StandaloneScreenType.editorSidebar => _DtdConnectedScreen(
dtdManager: dtdManager,
builder: (dtd) => EditorSidebarPanel(dtd),
builder: EditorSidebarPanel.new,
),
StandaloneScreenType.propertyEditor => _DtdConnectedScreen(
dtdManager: dtdManager,
builder: (dtd) => PropertyEditorPanel(dtd),
builder: PropertyEditorPanel.new,
),
};
}
Expand All @@ -58,7 +58,7 @@ class _DtdConnectedScreen extends StatelessWidget {
const _DtdConnectedScreen({required this.dtdManager, required this.builder});

final DTDManager dtdManager;
final Widget Function(DartToolingDaemon) builder;
final Widget Function(DTDManager) builder;

@override
Widget build(BuildContext context) {
Expand All @@ -80,7 +80,7 @@ class _DtdConnectedScreen extends StatelessWidget {
// reconnect occurs.
KeyedSubtree(
key: ValueKey(connection),
child: builder(connection),
child: builder(dtdManager),
),
if (connectionState is! ConnectedDTDState)
NotConnectedOverlay(connectionState),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

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:dtd/dtd.dart';
import 'package:flutter/material.dart';

import '../../shared/analytics/analytics.dart' as ga;
Expand All @@ -22,9 +22,9 @@ import 'devtools/devtools_view.dart';
/// Provides some basic functionality to improve discoverability of features
/// such as creation of new projects, device selection and DevTools features.
class EditorSidebarPanel extends StatefulWidget {
const EditorSidebarPanel(this.dtd, {super.key});
const EditorSidebarPanel(this.dtdManager, {super.key});

final DartToolingDaemon dtd;
final DTDManager dtdManager;

@override
State<EditorSidebarPanel> createState() => _EditorSidebarPanelState();
Expand All @@ -39,7 +39,7 @@ class _EditorSidebarPanelState extends State<EditorSidebarPanel> {
void initState() {
super.initState();

final editor = EditorClient(widget.dtd);
final editor = EditorClient(widget.dtdManager);
ga.screen(editor.gaId);
unawaited(_editor = editor.initialized.then((_) => editor));
}
Expand Down
Loading
Loading