-
Notifications
You must be signed in to change notification settings - Fork 326
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Connect to VS Code using a JSON-RPC API over postMessage of WebSocket
- Loading branch information
Showing
10 changed files
with
338 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
packages/devtools_app/lib/src/shared/config_specific/post_message/post_message.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 dynamic data; | ||
} |
11 changes: 11 additions & 0 deletions
11
packages/devtools_app/lib/src/shared/config_specific/post_message/post_message_stub.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(dynamic message, String targetOrigin) => | ||
throw UnsupportedError('unsupported platform'); |
19 changes: 19 additions & 0 deletions
19
packages/devtools_app/lib/src/shared/config_specific/post_message/post_message_web.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(dynamic message, String targetOrigin) => | ||
html.window.parent?.postMessage(message, targetOrigin); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
packages/devtools_app/lib/src/standalone_ui/vs_code/device_info.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// 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:flutter/material.dart'; | ||
|
||
import 'temp_api.dart'; | ||
|
||
/// Widget showing the current selected device for Flutter apps, and a button | ||
/// to change. | ||
class DeviceInfo extends StatefulWidget { | ||
const DeviceInfo(this.api, {super.key}); | ||
|
||
final VsCodeApi api; | ||
|
||
@override | ||
State<DeviceInfo> createState() => _DeviceInfoState(); | ||
} | ||
|
||
class _DeviceInfoState extends State<DeviceInfo> { | ||
Object? _currentDevice; | ||
late Stream<Object?> _deviceChangedStream; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
|
||
_deviceChangedStream = widget.api.selectedDeviceChanged | ||
.map((newDevice) => _currentDevice = newDevice['device']); | ||
|
||
unawaited( | ||
widget.api.getSelectedDevice().then((newDevice) { | ||
setState(() { | ||
_currentDevice = newDevice; | ||
}); | ||
}), | ||
); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return StreamBuilder( | ||
stream: _deviceChangedStream, | ||
initialData: _currentDevice, | ||
builder: (context, snapshot) { | ||
return Column( | ||
children: [ | ||
Text('Current device is $_currentDevice'), | ||
TextButton( | ||
onPressed: widget.api.showDeviceSelector, | ||
child: const Text('Change Device'), | ||
) | ||
], | ||
); | ||
}, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
packages/devtools_app/lib/src/standalone_ui/vs_code/temp_api.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// 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'; | ||
|
||
// TODO(dantup): This should live in a package so it can be used by tools | ||
// hosted outside of DevTools. | ||
|
||
/// An API for interacting with Dart tooling. | ||
class DartApi { | ||
DartApi._(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._(json_rpc_2.Peer.withoutJson(channel)); | ||
} | ||
|
||
/// Connects the API over the provided WebSocket. | ||
factory DartApi.webSocket(WebSocketChannel socket) { | ||
return DartApi._(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 { | ||
VsCodeApi(super.rpc); | ||
|
||
@override | ||
final apiName = 'vsCode'; | ||
|
||
Future<Object?> getSelectedDevice() => _sendRequest('getSelectedDevice'); | ||
|
||
Future<Object?> showDeviceSelector() => | ||
executeCommand('flutter.selectDevice'); | ||
|
||
late final Stream<Map<String, Object?>> selectedDeviceChanged = | ||
events('selectedDeviceChanged'); | ||
|
||
Future<Object?> executeCommand(String command, [List<Object?>? arguments]) => | ||
_sendRequest( | ||
'executeCommand', | ||
{'command': command, 'arguments': arguments}, | ||
); | ||
} |
52 changes: 52 additions & 0 deletions
52
packages/devtools_app/lib/src/standalone_ui/vs_code/web_socket_connection_screen.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// 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:flutter/material.dart'; | ||
import 'package:web_socket_channel/web_socket_channel.dart'; | ||
|
||
/// A connection screen shown when running outside of VS Code to accept a | ||
/// WebSocket URL to connect to an instance of VS Code. | ||
class WebSocketConnectionScreen extends StatefulWidget { | ||
const WebSocketConnectionScreen({required this.onConnected, super.key}); | ||
|
||
final Function(WebSocketChannel) onConnected; | ||
|
||
@override | ||
State<WebSocketConnectionScreen> createState() => | ||
_WebSocketConnectionScreenState(); | ||
} | ||
|
||
class _WebSocketConnectionScreenState extends State<WebSocketConnectionScreen> { | ||
final _controller = TextEditingController(); | ||
String? _errorText; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Center( | ||
child: Column( | ||
children: [ | ||
const Text('Connect to VS Code'), | ||
const Text('Enter WebSocket URL from VS Code'), | ||
TextField(controller: _controller), | ||
TextButton( | ||
onPressed: () async { | ||
try { | ||
final socket = | ||
WebSocketChannel.connect(Uri.parse(_controller.text)); | ||
await socket.ready; | ||
widget.onConnected(socket); | ||
} catch (e) { | ||
setState(() { | ||
_errorText = '$e'; | ||
}); | ||
} | ||
}, | ||
child: const Text('Connect'), | ||
), | ||
if (_errorText != null) Text(_errorText!), | ||
], | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters