Skip to content

Commit e667664

Browse files
committed
Add some simple side bar functionality with a mock editor environment
1 parent 2398162 commit e667664

File tree

9 files changed

+548
-6
lines changed

9 files changed

+548
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2023 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be found
3+
// in the LICENSE file.
4+
5+
export 'post_message_stub.dart' if (dart.library.html) 'post_message_web.dart';
6+
7+
class PostMessageEvent {
8+
PostMessageEvent({
9+
required this.origin,
10+
required this.data,
11+
});
12+
13+
final String origin;
14+
final Object? data;
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2023 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be found
3+
// in the LICENSE file.
4+
5+
import 'post_message.dart';
6+
7+
Stream<PostMessageEvent> get onPostMessage =>
8+
throw UnsupportedError('unsupported platform');
9+
10+
void postMessage(Object? _, String __) =>
11+
throw UnsupportedError('unsupported platform');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2023 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be found
3+
// in the LICENSE file.
4+
5+
import 'dart:html' as html;
6+
7+
import 'post_message.dart';
8+
9+
Stream<PostMessageEvent> get onPostMessage {
10+
return html.window.onMessage.map(
11+
(message) => PostMessageEvent(
12+
origin: message.origin,
13+
data: message.data,
14+
),
15+
);
16+
}
17+
18+
void postMessage(Object? message, String targetOrigin) =>
19+
html.window.parent?.postMessage(message, targetOrigin);

packages/devtools_app/lib/src/standalone_ui/standalone_screen.dart

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import 'package:flutter/material.dart';
66

7+
import 'vs_code/api.dart';
78
import 'vs_code/flutter_panel.dart';
9+
import 'vs_code/flutter_panel_mock.dart';
810

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

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

2730
Widget get screen {
2831
return switch (this) {
29-
StandaloneScreenType.vsCodeFlutterPanel => const VsCodeFlutterPanel(),
32+
StandaloneScreenType.vsCodeFlutterPanel =>
33+
VsCodeFlutterPanel(DartApi.postMessage()),
34+
StandaloneScreenType.vsCodeFlutterPanelMock =>
35+
const VsCodeFlutterPanelMock(),
3036
};
3137
}
3238
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2023 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2;
8+
import 'package:stream_channel/stream_channel.dart';
9+
import 'package:web_socket_channel/web_socket_channel.dart';
10+
11+
import '../../shared/config_specific/post_message/post_message.dart';
12+
13+
/// An API for interacting with Dart tooling.
14+
class DartApi {
15+
DartApi.rpc(this._rpc) : vsCode = VsCodeApi(_rpc) {
16+
unawaited(_rpc.listen());
17+
}
18+
19+
/// Connects the API using 'postMessage'. This is only available when running
20+
/// on web and embedded inside VS Code.
21+
factory DartApi.postMessage() {
22+
final postMessageController = StreamController();
23+
postMessageController.stream.listen((message) => postMessage(message, '*'));
24+
final channel = StreamChannel(
25+
onPostMessage.map((event) => event.data),
26+
postMessageController,
27+
);
28+
return DartApi.rpc(json_rpc_2.Peer.withoutJson(channel));
29+
}
30+
31+
/// Connects the API over the provided WebSocket.
32+
factory DartApi.webSocket(WebSocketChannel socket) {
33+
return DartApi.rpc(json_rpc_2.Peer(socket.cast<String>()));
34+
}
35+
36+
final json_rpc_2.Peer _rpc;
37+
38+
/// Access to APIs related to VS Code, such as executing VS Code commands or
39+
/// interacting with the Dart/Flutter extensions.
40+
final VsCodeApi vsCode;
41+
42+
void dispose() {
43+
unawaited(_rpc.close());
44+
}
45+
}
46+
47+
/// Base class for the different APIs that may be available.
48+
abstract base class ToolApi {
49+
ToolApi(this.rpc);
50+
51+
final json_rpc_2.Peer rpc;
52+
53+
String get apiName;
54+
55+
/// Checks whether this API is available.
56+
///
57+
/// Calls to any other API should only be made if and when this [Future]
58+
/// completes with `true`.
59+
late final Future<bool> isAvailable =
60+
_sendRequest<bool>('checkAvailable').catchError((_) => false);
61+
62+
Future<T> _sendRequest<T>(String method, [Object? parameters]) async {
63+
return (await rpc.sendRequest('$apiName.$method', parameters)) as T;
64+
}
65+
66+
/// Listens for an event '[apiName].[name]' that has a Map for parameters.
67+
Stream<Map<String, Object?>> events(String name) {
68+
final streamController = StreamController<Map<String, Object?>>.broadcast();
69+
rpc.registerMethod('$apiName.$name', (json_rpc_2.Parameters parameters) {
70+
streamController.add(parameters.asMap.cast<String, Object?>());
71+
});
72+
return streamController.stream;
73+
}
74+
}
75+
76+
final class VsCodeApi extends ToolApi {
77+
// TODO(dantup): Consider code-generation because Dart-Code and DevTools will
78+
// both have implementations of this API (although in Dart + TypeScript).
79+
VsCodeApi(super.rpc);
80+
81+
@override
82+
final apiName = 'vsCode';
83+
84+
late final Stream<VsCodeDevicesEvent> devicesChanged =
85+
events('devicesChanged').map(VsCodeDevicesEvent.fromJson);
86+
87+
Future<Object?> executeCommand(String command, [List<Object?>? arguments]) {
88+
return _sendRequest(
89+
'executeCommand',
90+
{'command': command, 'arguments': arguments},
91+
);
92+
}
93+
94+
Future<void> selectDevice(String id) {
95+
return _sendRequest(
96+
'selectDevice',
97+
{'id': id},
98+
);
99+
}
100+
}
101+
102+
class VsCodeDevice {
103+
VsCodeDevice({required this.id});
104+
105+
VsCodeDevice.fromJson(Map<String, Object?> json)
106+
: this(id: json['id'] as String);
107+
108+
final String id;
109+
110+
Map<String, Object?> toJson() => {'id': id};
111+
}
112+
113+
class VsCodeDevicesEvent {
114+
VsCodeDevicesEvent({required this.selectedDeviceId, required this.devices});
115+
116+
VsCodeDevicesEvent.fromJson(Map<String, Object?> json)
117+
: this(
118+
selectedDeviceId: json['selectedDeviceId'] as String?,
119+
devices: (json['devices'] as List)
120+
.cast<Map<String, Object?>>()
121+
.map(VsCodeDevice.fromJson)
122+
.toList(),
123+
);
124+
125+
final String? selectedDeviceId;
126+
final List<VsCodeDevice> devices;
127+
128+
Map<String, Object?> toJson() => {
129+
'selectedDeviceId': selectedDeviceId,
130+
'devices': devices.map((device) => device.toJson()).toList(),
131+
};
132+
}

packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart

+88-4
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,102 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'package:flutter/material.dart';
68

9+
import '../../../devtools_app.dart';
710
import '../../shared/feature_flags.dart';
11+
import 'api.dart';
12+
13+
/// A general Flutter sidebar panel for embedding inside IDEs.
14+
///
15+
/// Provides some basic functionality to improve discoverability of features
16+
/// such as creation of new projects, device selection and DevTools features.
17+
class VsCodeFlutterPanel extends StatefulWidget {
18+
const VsCodeFlutterPanel(this.api, {super.key});
19+
20+
final DartApi api;
821

9-
class VsCodeFlutterPanel extends StatelessWidget {
10-
const VsCodeFlutterPanel({super.key});
22+
@override
23+
State<VsCodeFlutterPanel> createState() => _VsCodeFlutterPanelState();
24+
}
1125

26+
class _VsCodeFlutterPanelState extends State<VsCodeFlutterPanel> {
1227
@override
1328
Widget build(BuildContext context) {
1429
assert(FeatureFlags.vsCodeSidebarTooling);
15-
return const Center(
16-
child: Text('TODO: a panel for flutter actions in VS Code'),
30+
31+
final api = widget.api;
32+
33+
return Expanded(
34+
child: Column(
35+
children: [
36+
const Text(''),
37+
FutureBuilder(
38+
future: api.vsCode.isAvailable,
39+
builder: (context, snapshot) => switch (snapshot.data) {
40+
true => _VsCodeConnectedPanel(api.vsCode),
41+
false => const Text('Unable to connect to VS Code'),
42+
null => const CenteredCircularProgressIndicator(),
43+
},
44+
),
45+
],
46+
),
47+
);
48+
}
49+
}
50+
51+
/// The panel shown once we know VS Code is available (the host has responded to
52+
/// the `vsCode.isAvailable` request).
53+
class _VsCodeConnectedPanel extends StatefulWidget {
54+
const _VsCodeConnectedPanel(this.api, {super.key});
55+
56+
final VsCodeApi api;
57+
58+
@override
59+
State<_VsCodeConnectedPanel> createState() => _VsCodeConnectedPanelState();
60+
}
61+
62+
class _VsCodeConnectedPanelState extends State<_VsCodeConnectedPanel> {
63+
@override
64+
Widget build(BuildContext context) {
65+
return Column(
66+
children: [
67+
ElevatedButton(
68+
onPressed: () =>
69+
unawaited(widget.api.executeCommand('flutter.createProject')),
70+
child: const Text('New Project'),
71+
),
72+
StreamBuilder(
73+
stream: widget.api.devicesChanged,
74+
builder: (context, snapshot) {
75+
if (!snapshot.hasData) {
76+
return const Text('');
77+
}
78+
final deviceEvent = snapshot.data!;
79+
return Table(
80+
children: [
81+
for (final device in deviceEvent.devices)
82+
TableRow(
83+
children: [
84+
TextButton(
85+
child: Text(device.id),
86+
onPressed: () =>
87+
unawaited(widget.api.selectDevice(device.id)),
88+
),
89+
Text(
90+
device.id == deviceEvent.selectedDeviceId
91+
? '(selected)'
92+
: '',
93+
),
94+
],
95+
),
96+
],
97+
);
98+
},
99+
),
100+
],
17101
);
18102
}
19103
}

0 commit comments

Comments
 (0)