blob: 4fa359d5df5b54f8d5c7721bf1a502a6b23e8ff8 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. 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 'dart:typed_data';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:stream_channel/stream_channel.dart';
import 'exceptions.dart' show rethrowAsDartPadException;
import 'shared.dart';
export 'exceptions.dart' hide rethrowAsDartPadException;
/// Client for talking to `shared_worker.dart`.
class WorkerClient {
final rpc.Peer _peer;
final _languageServers = <int, LanguageServer>{};
final _hotReloadCompilers = <int, HotReloadCompiler>{};
/// Creates a client that communicates over [channel].
///
/// The [channel] usually connects to a `Worker` instance (in tests) or a
/// `MessagePort` (in the browser).
WorkerClient(StreamChannel<String> channel) : _peer = rpc.Peer(channel) {
_peer.registerMethod('workspace/languageServer/message', _handleLsMessage);
_peer.registerMethod('workspace/languageServer/exited', _handleLsExited);
_peer.listen();
}
Future<void> get done => _peer.done;
/// Closes the connection to the worker.
Future<void> dispose() async {
await _peer.close();
}
/// Creates a workspace in the worker.
///
/// A [Workspace] is allocated a unique folder [Workspace.workspaceFolder].
/// Disposing of a workspace using [Workspace.dispose] deletes the
/// _workspace folder_ and any [LanguageServer] and [HotReloadCompiler]
/// started within said workspace.
///
/// Workspaces are not isolated, and file operations may interfere with other
/// _workspace folders_. This may change in the future.
Future<Workspace> createWorkspace() async {
final result = await _peer.request<Map>('createWorkspace', {});
return Workspace._(
this,
(result['workspaceId'] as num).toInt(),
Uri.parse(result['workspaceFolder'] as String),
);
}
void _handleLsMessage(rpc.Parameters params) {
final id = (params['languageServerId'].value as num).toInt();
final message = params['message'].asMap;
_languageServers[id]?._incomingMessages.add(message);
}
void _handleLsExited(rpc.Parameters params) {
final id = (params['languageServerId'].value as num).toInt();
_languageServers[id]?._handleExited();
}
}
/// Representation of a _workspace_ inside the worker with methods wrapping
/// the RPC interface.
///
/// All URIs and paths passed to methods will be resolved relative to the the
/// [workspaceFolder].
class Workspace {
final WorkerClient _client;
final int id;
final Uri workspaceFolder;
Workspace._(this._client, this.id, this.workspaceFolder);
/// Helper to attach the workspaceId to every request.
Future<T> _request<T>(String method, [Map<String, Object?>? params]) async {
return await _client._peer.request<T>(method, {
...?params,
'workspaceId': id,
});
}
Future<void> writeFileFromText(String uri, String text) =>
_request('workspace/writeFileFromText', {'uri': uri, 'text': text});
Future<void> writeFileFromBytes(String uri, Uint8List bytes) => _request(
'workspace/writeFileFromBytes',
{'uri': uri, 'base64': base64.encode(bytes)},
);
Future<String> readFileAsText(String uri) async {
final result = await _request<Map>('workspace/readFileAsText', {
'uri': uri,
});
return result['text'] as String;
}
Future<Uint8List> readFileAsBytes(String uri) async {
final result = await _request<Map>('workspace/readFileAsBytes', {
'uri': uri,
});
return base64.decode(result['base64'] as String);
}
Future<void> importTarArchive(String uri, Uint8List tarArchive) => _request(
'workspace/importTarArchive',
{'uri': uri, 'base64': base64.encode(tarArchive)},
);
Future<Uint8List> exportTarArchive(String uri) async {
final result = await _request<Map>('workspace/exportTarArchive', {
'uri': uri,
});
return base64.decode(result['base64'] as String);
}
Future<void> deleteFileSystemEntity(String uri) =>
_request('workspace/deleteFileSystemEntity', {'uri': uri});
/// Get information about a file or folder in this workspace.
Future<({String type, int? size})> stat(String uri) async {
final result = await _request<Map>('workspace/stat', {'uri': uri});
return (
type: result['type'] as String,
size: (result['size'] as num?)?.toInt(),
);
}
/// Returns true if a file exists at [uri] in this workspace.
Future<bool> fileExist(String uri) async {
try {
final s = await stat(uri);
return s.type == 'file';
} on FileNotFoundException {
return false;
}
}
/// Returns true if a folder exists at [uri] in this workspace.
Future<bool> folderExist(String uri) async {
try {
final s = await stat(uri);
return s.type == 'folder';
} on FileNotFoundException {
return false;
}
}
Future<void> createFolder(String uri) =>
_request('workspace/createFolder', {'uri': uri});
Future<List<({String path, String type})>> listDirectory({
required String uri,
bool recursive = false,
bool ignoreHidden = false,
}) async {
final result = await _request<Map>('workspace/listDirectory', {
'uri': uri,
'recursive': recursive,
'ignoreHidden': ignoreHidden,
});
return (result['entries'] as List).map((e) {
final map = e as Map;
return (path: map['path'] as String, type: map['type'] as String);
}).toList();
}
Future<CompileResult> compile(Uri entrypoint) async {
final c = await startHotReloadCompiler(entrypoint);
try {
return await c.compile();
} finally {
await c.close();
}
}
Future<({String log})> pub({
String uri = '',
required String command,
List<String> args = const <String>[],
}) async {
final result = await _request<Map>('workspace/pub', {
'uri': uri,
'command': command,
'args': args,
});
return (log: result['log'] as String);
}
Future<HotReloadCompiler> startHotReloadCompiler(Uri uri) async {
final result = await _request<Map>('workspace/startHotReloadCompiler', {
'uri': uri.toString(),
});
final id = (result['hotReloadCompilerId'] as num).toInt();
final c = HotReloadCompiler._(this, id);
_client._hotReloadCompilers[id] = c;
return c;
}
Future<LanguageServer> startLanguageServer() async {
final result = await _request<Map>('workspace/startLanguageServer');
final lsId = (result['languageServerId'] as num).toInt();
final ls = LanguageServer._(_client, this, lsId);
_client._languageServers[lsId] = ls;
return ls;
}
Future<void> dispose() async {
await _client._peer.request<void>('workspace/dispose', {'workspaceId': id});
}
}
/// A client for the language server running within a workspace.
final class LanguageServer {
final WorkerClient _client;
final Workspace workspace;
final int id;
final _incomingMessages = StreamController<Object?>();
final _outgoingMessages = StreamController<Object?>();
late final StreamChannel<Object?> _channel;
LanguageServer._(this._client, this.workspace, this.id) {
_channel = StreamChannel(_incomingMessages.stream, _outgoingMessages.sink);
// Forward outgoing LSP messages to the worker tunnel
_outgoingMessages.stream.listen((message) {
_client._peer.sendNotification('workspace/languageServer/message', {
'workspaceId': workspace.id,
'languageServerId': id,
'message': message,
});
});
}
/// Communication channel over which standard LSP JSON-RPC 2.0 messages
/// travel.
///
/// These are not encoded as JSON Strings, but instead travels as the kind of
/// JSON values returned by [json] codec from `dart:convert`.
StreamChannel<Object?> get languageServerChannel => _channel;
/// Stops the language server.
Future<void> stop() async {
try {
await _client._peer.request<void>('workspace/languageServer/stop', {
'workspaceId': workspace.id,
'languageServerId': id,
});
} catch (_) {
// Ignore if already closed
} finally {
_cleanup();
}
}
void _handleExited() {
_cleanup();
}
void _cleanup() {
_client._languageServers.remove(id);
_incomingMessages.close();
_outgoingMessages.close();
}
}
final class HotReloadCompiler {
final Workspace workspace;
final int id;
HotReloadCompiler._(this.workspace, this.id);
/// Compile the _entrypoint_ this [HotReloadCompiler] was started with.
///
/// Calling compile a second time may throw [HotReloadRejectedException], if
/// code changes are such that a hot-reload is not possible.
Future<CompileResult> compile() async {
final result = await workspace._request<Map>(
'workspace/hotReloadCompiler/compile',
{'hotReloadCompilerId': id},
);
return (
code: result['code'] as String,
compiledLibraryUris: (result['compiledLibraryUris'] as List)
.cast<String>(),
log: result['log'] as String,
);
}
/// Release resources associated with this [HotReloadCompiler].
Future<void> close() async {
try {
await workspace._request<Map>('workspace/hotReloadCompiler/close', {
'hotReloadCompilerId': id,
});
} catch (_) {
// Ignore if already closed
} finally {
_cleanup();
}
}
void _cleanup() {
workspace._client._hotReloadCompilers.remove(id);
}
}
extension on rpc.Peer {
/// Wrap [sendRequest] with casting the return to [T]
Future<T> request<T>(String method, [Object? parameters]) async {
try {
return await sendRequest(method, parameters) as T;
} on rpc.RpcException catch (e) {
rethrowAsDartPadException(e);
}
}
}