| // 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); |
| } |
| } |
| } |