-
Notifications
You must be signed in to change notification settings - Fork 337
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
Changes from 7 commits
34b5a8d
32bb16d
c1db587
9d90ccc
e560948
b32f765
7aaf25f
074eb47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added it to use It was mostly just to enforce everything going through methods (eg. don't call |
||
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should there also be a capability for 'selectDevice' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good spot, fixed! |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 withoutdispose
being called explicitly).