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
  • Loading branch information
DanTup committed Jul 26, 2023
1 parent 2398162 commit e667664
Show file tree
Hide file tree
Showing 9 changed files with 548 additions and 6 deletions.
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
Expand Up @@ -4,7 +4,9 @@

import 'package:flutter/material.dart';

import 'vs_code/api.dart';
import 'vs_code/flutter_panel.dart';
import 'vs_code/flutter_panel_mock.dart';

/// "Screens" that are intended for standalone use only, likely for embedding
/// directly in an IDE.
Expand All @@ -13,7 +15,8 @@ import 'vs_code/flutter_panel.dart';
/// meaning that this screen will not be part of DevTools' normal navigation.
/// The only way to access a standalone screen is directly from the url.
enum StandaloneScreenType {
vsCodeFlutterPanel;
vsCodeFlutterPanel,
vsCodeFlutterPanelMock;

static StandaloneScreenType? parse(String? id) {
if (id == null) return null;
Expand All @@ -26,7 +29,10 @@ enum StandaloneScreenType {

Widget get screen {
return switch (this) {
StandaloneScreenType.vsCodeFlutterPanel => const VsCodeFlutterPanel(),
StandaloneScreenType.vsCodeFlutterPanel =>
VsCodeFlutterPanel(DartApi.postMessage()),
StandaloneScreenType.vsCodeFlutterPanelMock =>
const VsCodeFlutterPanelMock(),
};
}
}
132 changes: 132 additions & 0 deletions packages/devtools_app/lib/src/standalone_ui/vs_code/api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2;
import 'package:stream_channel/stream_channel.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import '../../shared/config_specific/post_message/post_message.dart';

/// An API for interacting with Dart tooling.
class DartApi {
DartApi.rpc(this._rpc) : vsCode = VsCodeApi(_rpc) {
unawaited(_rpc.listen());
}

/// Connects the API using 'postMessage'. This is only available when running
/// on web and embedded inside VS Code.
factory DartApi.postMessage() {
final postMessageController = StreamController();
postMessageController.stream.listen((message) => postMessage(message, '*'));
final channel = StreamChannel(
onPostMessage.map((event) => event.data),
postMessageController,
);
return DartApi.rpc(json_rpc_2.Peer.withoutJson(channel));
}

/// Connects the API over the provided WebSocket.
factory DartApi.webSocket(WebSocketChannel socket) {
return DartApi.rpc(json_rpc_2.Peer(socket.cast<String>()));
}

final json_rpc_2.Peer _rpc;

/// Access to APIs related to VS Code, such as executing VS Code commands or
/// interacting with the Dart/Flutter extensions.
final VsCodeApi vsCode;

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

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

final json_rpc_2.Peer rpc;

String get apiName;

/// Checks whether this API is available.
///
/// Calls to any other API should only be made if and when this [Future]
/// completes with `true`.
late final Future<bool> isAvailable =
_sendRequest<bool>('checkAvailable').catchError((_) => false);

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.
Stream<Map<String, Object?>> events(String name) {
final streamController = StreamController<Map<String, Object?>>.broadcast();
rpc.registerMethod('$apiName.$name', (json_rpc_2.Parameters parameters) {
streamController.add(parameters.asMap.cast<String, Object?>());
});
return streamController.stream;
}
}

final class VsCodeApi extends ToolApi {
// TODO(dantup): Consider code-generation because Dart-Code and DevTools will
// both have implementations of this API (although in Dart + TypeScript).
VsCodeApi(super.rpc);

@override
final apiName = 'vsCode';

late final Stream<VsCodeDevicesEvent> devicesChanged =
events('devicesChanged').map(VsCodeDevicesEvent.fromJson);

Future<Object?> executeCommand(String command, [List<Object?>? arguments]) {
return _sendRequest(
'executeCommand',
{'command': command, 'arguments': arguments},
);
}

Future<void> selectDevice(String id) {
return _sendRequest(
'selectDevice',
{'id': id},
);
}
}

class VsCodeDevice {
VsCodeDevice({required this.id});

VsCodeDevice.fromJson(Map<String, Object?> json)
: this(id: json['id'] as String);

final String id;

Map<String, Object?> toJson() => {'id': id};
}

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

VsCodeDevicesEvent.fromJson(Map<String, Object?> json)
: this(
selectedDeviceId: json['selectedDeviceId'] as String?,
devices: (json['devices'] as List)
.cast<Map<String, Object?>>()
.map(VsCodeDevice.fromJson)
.toList(),
);

final String? selectedDeviceId;
final List<VsCodeDevice> devices;

Map<String, Object?> toJson() => {
'selectedDeviceId': selectedDeviceId,
'devices': devices.map((device) => device.toJson()).toList(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,102 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';

import '../../../devtools_app.dart';
import '../../shared/feature_flags.dart';
import 'api.dart';

/// A general Flutter sidebar panel for embedding inside IDEs.
///
/// Provides some basic functionality to improve discoverability of features
/// such as creation of new projects, device selection and DevTools features.
class VsCodeFlutterPanel extends StatefulWidget {
const VsCodeFlutterPanel(this.api, {super.key});

final DartApi api;

class VsCodeFlutterPanel extends StatelessWidget {
const VsCodeFlutterPanel({super.key});
@override
State<VsCodeFlutterPanel> createState() => _VsCodeFlutterPanelState();
}

class _VsCodeFlutterPanelState extends State<VsCodeFlutterPanel> {
@override
Widget build(BuildContext context) {
assert(FeatureFlags.vsCodeSidebarTooling);
return const Center(
child: Text('TODO: a panel for flutter actions in VS Code'),

final api = widget.api;

return Expanded(
child: Column(
children: [
const Text(''),
FutureBuilder(
future: api.vsCode.isAvailable,
builder: (context, snapshot) => switch (snapshot.data) {
true => _VsCodeConnectedPanel(api.vsCode),
false => const Text('Unable to connect to VS Code'),
null => const CenteredCircularProgressIndicator(),
},
),
],
),
);
}
}

/// The panel shown once we know VS Code is available (the host has responded to
/// the `vsCode.isAvailable` request).
class _VsCodeConnectedPanel extends StatefulWidget {
const _VsCodeConnectedPanel(this.api, {super.key});

final VsCodeApi api;

@override
State<_VsCodeConnectedPanel> createState() => _VsCodeConnectedPanelState();
}

class _VsCodeConnectedPanelState extends State<_VsCodeConnectedPanel> {
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () =>
unawaited(widget.api.executeCommand('flutter.createProject')),
child: const Text('New Project'),
),
StreamBuilder(
stream: widget.api.devicesChanged,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Text('');
}
final deviceEvent = snapshot.data!;
return Table(
children: [
for (final device in deviceEvent.devices)
TableRow(
children: [
TextButton(
child: Text(device.id),
onPressed: () =>
unawaited(widget.api.selectDevice(device.id)),
),
Text(
device.id == deviceEvent.selectedDeviceId
? '(selected)'
: '',
),
],
),
],
);
},
),
],
);
}
}
Loading

0 comments on commit e667664

Please sign in to comment.