Skip to content

Commit

Permalink
Add some simple side bar functionality with a mock editor environment (
Browse files Browse the repository at this point in the history
…#6104)

* Add some simple side bar functionality with a mock editor environment

* Add additional comments

* Minor review comments

- Add comments
- Use more constants
- StatelessWidget where state not required
- Fix typos
- Add `selectDevice` capability
- ...

* Move mock environment to test/stager scene

* Fix "Dark Theme" toggle in VS Code panel in stager scene

* Use defaultSpacing + remove custom split widgets

* Remove unused key parameter

* Update imports
  • Loading branch information
DanTup authored Aug 21, 2023
1 parent 15f07e7 commit 6440a75
Show file tree
Hide file tree
Showing 14 changed files with 948 additions and 4 deletions.
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();
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;
}
}
157 changes: 157 additions & 0 deletions packages/devtools_app/lib/src/standalone_ui/api/impl/vs_code_api.dart
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';

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 {
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;
}
Loading

0 comments on commit 6440a75

Please sign in to comment.