blob: c075b2050281b34e0c6f8fdabed03f399a602419 [file] [log] [blame]
// Copyright (c) 2024, 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 'package:path/path.dart' as path;
/// The file suffix used for virtual macro files in the analyzer.
const macroClientFileSuffix = '.macro.dart';
/// The URI scheme used for virtual macro files on the client.
const macroClientUriScheme = 'dart-macro+file';
/// A class for converting between internal analyzer file paths/references and
/// URIs used by clients.
///
/// The simplest form of this class simple translates between file paths and
/// `file://` URIs but depending on client capabilities some paths/URIs may be
/// re-written to support features like virtual files for macros.
abstract class ClientUriConverter {
final path.Context _context;
/// The URI schemes that are supported by this converter.
///
/// This always includes 'file' and may optionally include others like
/// 'dart-macro+file'.
final Set<String> supportedSchemes;
/// The URI schemes that are supported by this converter except 'file'.
final Set<String> supportedNonFileSchemes;
/// Creates a converter that does nothing besides translation between file
/// paths and `file://` URIs.
factory ClientUriConverter.noop(path.Context context) =>
_NoOpConverter(context);
/// Creates a converter that translates paths/URIs for virtual files such as
/// those created by macros.
factory ClientUriConverter.withVirtualFileSupport(path.Context context) =>
_VirtualFileClientUriConverter(context);
ClientUriConverter._(this._context, [this.supportedNonFileSchemes = const {}])
: supportedSchemes = {'file', ...supportedNonFileSchemes};
/// Converts client FilePath (which may be a URI or a file path depending on
/// client capbilities) into a file path/reference from the analyzer.
///
/// This is the legacy protocol equiv of [fromClientUri].
String fromClientFilePath(String filePathOrUri);
/// Converts a URI provided by the client into a file path/reference that can
/// be used by the analyzer.
///
/// This is the LSP equiv of [fromClientFilePath].
String fromClientUri(Uri uri);
/// Converts a file path/reference from the analyzer into a client FilePath
/// (which may be a URI or a file path depending on client capbilities).
///
/// This is the legacy protocol equiv of [toClientUri].
String toClientFilePath(String filePath);
/// Converts a file path/reference from the analyzer into a URI to be sent to
/// the client.
///
/// This is the LSP equiv of [toClientFilePath].
Uri toClientUri(String filePath);
}
class _NoOpConverter extends ClientUriConverter {
_NoOpConverter(super.context) : super._();
@override
String fromClientFilePath(String filePathOrUri) => filePathOrUri;
@override
String fromClientUri(Uri uri) => _context.fromUri(uri);
@override
String toClientFilePath(String filePath) => filePath;
@override
Uri toClientUri(String filePath) => _context.toUri(filePath);
}
class _VirtualFileClientUriConverter extends ClientUriConverter {
_VirtualFileClientUriConverter(path.Context context)
: super._(context, {macroClientUriScheme});
@override
String fromClientFilePath(String filePathOrUri) =>
fromClientUri(Uri.parse(filePathOrUri));
@override
String fromClientUri(Uri uri) {
// For URIs with no scheme, assume it was a relative path and provide a
// better message than "scheme '' is not supported".
if (uri.scheme.isEmpty) {
throw ArgumentError.value(
uri.toString(), 'uri', 'URI is not a valid file:// URI');
}
if (!supportedSchemes.contains(uri.scheme)) {
var supportedSchemesString = supportedSchemes.isEmpty
? '(none)'
: supportedSchemes.map((scheme) => "'$scheme'").join(', ');
throw ArgumentError.value(
uri.toString(),
'uri',
"URI scheme '${uri.scheme}' is not supported. "
'Allowed schemes are $supportedSchemesString.',
);
}
switch (uri.scheme) {
// Map macro scheme back to 'file:///.../x.macro.dart'.
case macroClientUriScheme:
var pathWithoutExtension =
uri.path.substring(0, uri.path.length - '.dart'.length);
var newPath = '$pathWithoutExtension$macroClientFileSuffix';
return _context.fromUri(uri.replace(scheme: 'file', path: newPath));
default:
return _context.fromUri(uri);
}
}
@override
String toClientFilePath(String filePath) => toClientUri(filePath).toString();
@override
Uri toClientUri(String filePath) {
// Map '/.../x.macro.dart' onto macro scheme.
if (filePath.endsWith(macroClientFileSuffix) &&
supportedSchemes.contains(macroClientUriScheme)) {
var pathWithoutSuffix =
filePath.substring(0, filePath.length - macroClientFileSuffix.length);
var newPath = '$pathWithoutSuffix.dart';
return _context.toUri(newPath).replace(scheme: macroClientUriScheme);
}
return _context.toUri(filePath);
}
}