Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some simple side bar functionality with a mock editor environment #6104

Merged
merged 8 commits into from
Aug 21, 2023
Merged
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
7 changes: 7 additions & 0 deletions packages/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
"type": "dart",
"program": "devtools_app/test/test_infra/fixtures/memory_app/lib/main.dart",
},
{
"name": "standalone_ui/vs_code",
"request": "launch",
"type": "dart",
"program": "devtools_app/test/test_infra/scenes/standalone_ui/vs_code.stager_app.g.dart",
"deviceId": "chrome",
},
{
"name": "attach",
"type": "dart",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.

export 'post_message_stub.dart' if (dart.library.html) 'post_message_web.dart';

class PostMessageEvent {
PostMessageEvent({
required this.origin,
required this.data,
});

final String origin;
final Object? data;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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 'post_message.dart';

Stream<PostMessageEvent> get onPostMessage =>
throw UnsupportedError('unsupported platform');

void postMessage(Object? _, String __) =>
throw UnsupportedError('unsupported platform');
Original file line number Diff line number Diff line change
@@ -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:html' as html;

import 'post_message.dart';

Stream<PostMessageEvent> get onPostMessage {
return html.window.onMessage.map(
(message) => PostMessageEvent(
origin: message.origin,
data: message.data,
),
);
}

void postMessage(Object? message, String targetOrigin) =>
html.window.parent?.postMessage(message, targetOrigin);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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 'vs_code_api.dart';

/// An API exposed to Dart tooling surfaces.
///
/// APIs are grouped into child APIs that are exposed as fields. Each field is a
/// `Future` that will return null if the requested API is unavailable (for
/// example the VS Code APIs if not running inside VS Code, or the LSP APIs if
/// no LSP server is available).
abstract interface class DartToolingApi {
/// Access to APIs provided by VS Code and/or the Dart/Flutter VS Code
/// extensions.
Future<VsCodeApi?> get vsCode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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 'dart:convert';

import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:stream_channel/stream_channel.dart';

import '../../../shared/config_specific/logger/logger_helpers.dart';
import '../../../shared/config_specific/post_message/post_message.dart';
import '../../../shared/constants.dart';
import '../dart_tooling_api.dart';
import '../vs_code_api.dart';
import 'vs_code_api.dart';

/// Whether to enable verbose logging for postMessage communication.
///
/// This is useful for debugging when running inside VS Code.
///
/// TODO(dantup): Make a way for this to be enabled by users at runtime for
/// troubleshooting. This could be via a message from VS Code, or something
/// that passes a query param.
const _enablePostMessageVerboseLogging = false;

final _log = Logger('tooling_api');

/// An API used by Dart tooling surfaces to interact with Dart tools that expose
/// APIs such as Dart-Code and the LSP server.
class DartToolingApiImpl implements DartToolingApi {
DartToolingApiImpl.rpc(this._rpc) {
unawaited(_rpc.listen());
}

/// Connects the API using 'postMessage'. This is only available when running
/// on web and hosted inside an iframe (such as inside a VS Code webview).
factory DartToolingApiImpl.postMessage() {
if (_enablePostMessageVerboseLogging) {
setDevToolsLoggingLevel(verboseLoggingLevel);
}
final postMessageController = StreamController();
postMessageController.stream.listen((message) {
_log.info('==> $message');
postMessage(message, '*');
});
final channel = StreamChannel(
onPostMessage.map((event) {
_log.info('<== ${jsonEncode(event.data)}');
return event.data;
}),
postMessageController,
);
return DartToolingApiImpl.rpc(json_rpc_2.Peer.withoutJson(channel));
}

final json_rpc_2.Peer _rpc;

/// An API that provides Access to APIs related to VS Code, such as executing
/// VS Code commands or interacting with the Dart/Flutter extensions.
///
/// Lazy-initialized and completes with `null` if VS Code is not available.
@override
late final Future<VsCodeApi?> vsCode = VsCodeApiImpl.tryConnect(_rpc);

void dispose() {
unawaited(_rpc.close());
}
}

/// Base class for the different APIs that may be available.
abstract base class ToolApiImpl {
ToolApiImpl(this.rpc);

static Future<Map<String, Object?>?> tryGetCapabilities(
json_rpc_2.Peer rpc,
String apiName,
) async {
try {
final response = await rpc.sendRequest('$apiName.getCapabilities')
as Map<Object?, Object?>;
return response.cast<String, Object?>();
} catch (_) {
// Any error initializing should disable this functionality.
return null;
}
}

@protected
final json_rpc_2.Peer rpc;

@protected
String get apiName;

@protected
Future<T> sendRequest<T>(String method, [Object? parameters]) async {
return (await rpc.sendRequest('$apiName.$method', parameters)) as T;
}

/// Listens for an event '[apiName].[name]' that has a Map for parameters.
@protected
Stream<Map<String, Object?>> events(String name) {
final streamController = StreamController<Map<String, Object?>>.broadcast();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this stream controller need to be created in the context of the class so that it can be disposed once it is done being used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a line here to automatically close it when rpc closes which I think does what you wanted (but also handles the connection going away without dispose being called explicitly).

unawaited(rpc.done.then((_) => streamController.close()));
rpc.registerMethod('$apiName.$name', (json_rpc_2.Parameters parameters) {
streamController.add(parameters.asMap.cast<String, Object?>());
});
return streamController.stream;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// 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:json_rpc_2/json_rpc_2.dart' as json_rpc_2;
import 'package:meta/meta.dart';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to add a dep on meta? I remember an effort to remove this dependency from devtools a while back, though I can't recall why. Probably because we were using it for @required and some other signatures that are now provided by flutter out of the box.

Copy link
Contributor Author

@DanTup DanTup Aug 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it to use @protected to try and avoid some of the base methods in the API classes showing up to the widget code. Unfortunately code completion still shows them (dart-lang/sdk#35449), though it does generate a warning if you use them.

It was mostly just to enforce everything going through methods (eg. don't call sendRequest('someString') directly from a widget), but it's a fairly small benefit. If there's a reason to avoid the dependency (and it wasn't just removed because it was unnecessary), we could remove it. I tracked the change back to #3484 but I don't understand the reason - wdyt?


import '../vs_code_api.dart';
import 'dart_tooling_api.dart';

final class VsCodeApiImpl extends ToolApiImpl implements VsCodeApi {
VsCodeApiImpl._(super.rpc, Map<String, Object?> capabilities) {
this.capabilities = VsCodeCapabilitiesImpl(capabilities);
devicesChanged = events(VsCodeApi.jsonDevicesChangedEvent)
.map(VsCodeDevicesEventImpl.fromJson);
}

static Future<VsCodeApi?> tryConnect(json_rpc_2.Peer rpc) async {
final capabilities =
await ToolApiImpl.tryGetCapabilities(rpc, VsCodeApi.jsonApiName);
return capabilities != null ? VsCodeApiImpl._(rpc, capabilities) : null;
}

@override
Future<void> initialize() => sendRequest(VsCodeApi.jsonInitializeMethod);

@override
@protected
String get apiName => VsCodeApi.jsonApiName;

@override
late final Stream<VsCodeDevicesEvent> devicesChanged;

@override
late final VsCodeCapabilities capabilities;

@override
Future<Object?> executeCommand(String command, [List<Object?>? arguments]) {
return sendRequest(
VsCodeApi.jsonExecuteCommandMethod,
{
VsCodeApi.jsonExecuteCommandCommandParameter: command,
VsCodeApi.jsonExecuteCommandArgumentsParameter: arguments,
},
);
}

@override
Future<bool> selectDevice(String id) {
return sendRequest(
VsCodeApi.jsonSelectDeviceMethod,
{VsCodeApi.jsonSelectDeviceIdParameter: id},
);
}
}

class VsCodeDeviceImpl implements VsCodeDevice {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the benefit of having an interface for all these classes? Can we just have one class instead of having an interface and an impl?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal was to make the API much clearer so it can more easily be mirrored/compared to VS Code. Without interfaces it might also be quite easy to make an accidental breaking change to the interface (and the mock environment) without VS Code.

I added some more comments to the interface classes to make this clearer, although I'm not married to the idea - if you'd prefer not to have them (and aren't concerned about accidental breakage), I'm happy to remove :-)

VsCodeDeviceImpl({
required this.id,
required this.name,
required this.category,
required this.emulator,
required this.emulatorId,
required this.ephemeral,
required this.platform,
required this.platformType,
});

VsCodeDeviceImpl.fromJson(Map<String, Object?> json)
: this(
id: json[VsCodeDevice.jsonIdField] as String,
name: json[VsCodeDevice.jsonNameField] as String,
category: json[VsCodeDevice.jsonCategoryField] as String?,
emulator: json[VsCodeDevice.jsonEmulatorField] as bool,
emulatorId: json[VsCodeDevice.jsonEmulatorIdField] as String?,
ephemeral: json[VsCodeDevice.jsonEphemeralField] as bool,
platform: json[VsCodeDevice.jsonPlatformField] as String,
platformType: json[VsCodeDevice.jsonPlatformTypeField] as String?,
);

@override
final String id;

@override
final String name;

@override
final String? category;

@override
final bool emulator;

@override
final String? emulatorId;

@override
final bool ephemeral;

@override
final String platform;

@override
final String? platformType;

Map<String, Object?> toJson() => {
VsCodeDevice.jsonIdField: id,
VsCodeDevice.jsonNameField: name,
VsCodeDevice.jsonCategoryField: category,
VsCodeDevice.jsonEmulatorField: emulator,
VsCodeDevice.jsonEmulatorIdField: emulatorId,
VsCodeDevice.jsonEphemeralField: ephemeral,
VsCodeDevice.jsonPlatformField: platform,
VsCodeDevice.jsonPlatformTypeField: platformType,
};
}

class VsCodeDevicesEventImpl implements VsCodeDevicesEvent {
VsCodeDevicesEventImpl({
required this.selectedDeviceId,
required this.devices,
});

VsCodeDevicesEventImpl.fromJson(Map<String, Object?> json)
: this(
selectedDeviceId:
json[VsCodeDevicesEvent.jsonSelectedDeviceIdField] as String?,
devices: (json[VsCodeDevicesEvent.jsonDevicesField] as List)
.map((item) => Map<String, Object?>.from(item))
.map((map) => VsCodeDeviceImpl.fromJson(map))
.toList(),
);

@override
final String? selectedDeviceId;

@override
final List<VsCodeDevice> devices;

Map<String, Object?> toJson() => {
VsCodeDevicesEvent.jsonSelectedDeviceIdField: selectedDeviceId,
VsCodeDevicesEvent.jsonDevicesField: devices,
};
}

class VsCodeCapabilitiesImpl implements VsCodeCapabilities {
VsCodeCapabilitiesImpl(this._raw);

final Map<String, Object?>? _raw;

@override
bool get executeCommand =>
_raw?[VsCodeCapabilities.jsonExecuteCommandField] == true;

@override
bool get selectDevice =>
_raw?[VsCodeCapabilities.jsonSelectDeviceField] == true;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there also be a capability for 'selectDevice'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot, fixed!

Loading